Skip to content

Reference

liionpack

liionpack is a tool for simulating battery packs with pybamm. It can design the pack with a combination of batteries connected in series and parallel or can read a netlist.

netlist_utils

make_lcapy_circuit(netlist)

Generate a circuit that can be used with lcapy

Parameters:

Name Type Description Default
netlist pandas.DataFrame

A netlist of circuit elements with format. desc, node1, node2, value.

required

Returns:

Type Description
lcapy.Circuit

The Circuit class is used for describing networks using netlists. Despite the name, it does not require a closed path.

Source code in liionpack/netlist_utils.py
def make_lcapy_circuit(netlist):
    """
    Generate a circuit that can be used with lcapy

    Args:
        netlist (pandas.DataFrame):
            A netlist of circuit elements with format. desc, node1, node2, value.

    Returns:
        lcapy.Circuit:
            The Circuit class is used for describing networks using netlists.
            Despite the name, it does not require a closed path.

    """
    cct = Circuit()
    I_map = netlist["desc"].str.find("I") > -1
    net2 = netlist.copy()
    net2.loc[I_map, ("node1")] = netlist["node2"][I_map]
    net2.loc[I_map, ("node2")] = netlist["node1"][I_map]
    d1 = "down"
    d2 = "up"
    I_xs = [net2[I_map]["node1_x"].values[0], net2[I_map]["node2_x"].values[0]]
    I_left = np.any(np.array(I_xs) == -1)
    all_desc = netlist["desc"].values
    for index, row in net2.iterrows():
        color = "black"
        desc, n1, n2, value, n1x, n1y, n2x, n2y = row[:8]
        if desc[0] == "V":
            direction = d1
        elif desc[0] == "I":
            direction = d2
        elif desc[0] == "R":
            if desc[1] == "b":
                direction = "right"
            elif desc[1] == "t":
                # These are the terminal nodes and require special attention
                if desc[2] == "p":
                    # positive
                    color = "red"
                else:
                    # negative
                    color = "blue"
                # If terminals are not both at the same end then the netlist
                # has two resistors with half the value to make a nice circuit
                # diagram. Convert into 1 resistor + 1 wire
                if desc[3] == "0":
                    # The wires have the zero suffix
                    direction = d2
                    desc = "W"
                else:
                    # The reistors have the 1 suffix
                    # Convert the value to the total reistance if a wire element
                    # is in the netlist
                    w_desc = desc[:3] + "0"
                    if w_desc in all_desc:
                        value *= 2
                    desc = desc[:3]
                    # Terminal loop is C shaped with positive at the top so
                    # order is left-vertical-right if we're on the left side
                    # and right-vertical-left if we're on the right side
                    if desc[2] == "p":
                        if I_left:
                            direction = "left"
                            # if the terminal connection is not at the end then
                            # extend the element connections
                            if n1x > 0:
                                direction += "=" + str(1 + n1x)
                        else:
                            direction = "right"
                            if n1x < I_xs[0] - 1:
                                direction += "=" + str(1 + I_xs[0] - n1x)
                    else:
                        if I_left:
                            direction = "right"
                        else:
                            direction = "left"
            else:
                direction = d1
        if desc == "W":
            string = desc + " " + str(n1) + " " + str(n2)
        else:
            string = desc + " " + str(n1) + " " + str(n2) + " " + str(value)
        string = string + "; " + direction
        string = string + ", color=" + color
        cct.add(string)
    # Add ground node
    cct.add("W 0 00; down, sground")
    return cct

power_loss(netlist, include_Ri=False)

Calculate the power loss through joule heating of all the resistors in the circuit

Parameters:

Name Type Description Default
netlist pandas.DataFrame

A netlist of circuit elements with format desc, node1, node2, value.

required
include_Ri bool

Default is False. If True the internal resistance of the batteries is included

False

Returns:

Type Description

None

Source code in liionpack/netlist_utils.py
def power_loss(netlist, include_Ri=False):
    """
    Calculate the power loss through joule heating of all the resistors in the
    circuit

    Args:
        netlist (pandas.DataFrame):
            A netlist of circuit elements with format desc, node1, node2, value.
        include_Ri (bool):
            Default is False. If True the internal resistance of the batteries
            is included

    Returns:
        None

    """
    V_node, I_batt = lp.solve_circuit_vectorized(netlist)
    R_map = netlist["desc"].str.find("R") > -1
    R_map = R_map.values
    if not include_Ri:
        Ri_map = netlist["desc"].str.find("Ri") > -1
        Ri_map = Ri_map.values
        R_map *= ~Ri_map
    R_value = netlist[R_map].value.values
    R_node1 = netlist[R_map].node1.values
    R_node2 = netlist[R_map].node2.values
    R_node1_V = V_node[R_node1]
    R_node2_V = V_node[R_node2]
    V_diff = np.abs(R_node1_V - R_node2_V)
    P_loss = V_diff**2 / R_value
    netlist["power_loss"] = 0
    netlist.loc[R_map, ("power_loss")] = P_loss

read_netlist(filepath, Ri=None, Rc=None, Rb=None, Rt=None, I=None, V=None)

Assumes netlist has been saved by LTSpice with format Descriptor Node1 Node2 Value Any lines starting with * are comments and . are commands so ignore them Nodes begin with N so remove that Open ended components are not allowed and their nodes start with NC (no-connection)

Parameters:

Name Type Description Default
filepath str

Path to netlist circuit file '.cir' or '.txt'.

required
Ri float

Internal resistance (\Omega).

None
Rc float

Connection resistance (\Omega).

None
Rb float

Busbar resistance (\Omega).

None
Rt float

Terminal connection resistance (\Omega).

None
I float

Current (A).

None
V float

Initial battery voltage (V).

None

Returns:

Type Description
pandas.DataFrame

A netlist of circuit elements with format desc, node1, node2, value.

Source code in liionpack/netlist_utils.py
def read_netlist(
    filepath,
    Ri=None,
    Rc=None,
    Rb=None,
    Rt=None,
    I=None,
    V=None,
):
    """
    Assumes netlist has been saved by LTSpice with format Descriptor Node1 Node2 Value
    Any lines starting with * are comments and . are commands so ignore them
    Nodes begin with N so remove that
    Open ended components are not allowed and their nodes start with NC (no-connection)

    Args:
        filepath (str): Path to netlist circuit file '.cir' or '.txt'.
        Ri (float): Internal resistance ($\Omega$).
        Rc (float): Connection resistance ($\Omega$).
        Rb (float): Busbar resistance ($\Omega$).
        Rt (float): Terminal connection resistance ($\Omega$).
        I (float): Current (A).
        V (float): Initial battery voltage (V).

    Returns:
        pandas.DataFrame:
            A netlist of circuit elements with format desc, node1, node2, value.
    """

    # Read in the netlist
    if "." not in filepath:
        filepath += ".cir"
    if not os.path.isfile(filepath):
        temp = os.path.join(lp.CIRCUIT_DIR, filepath)
        if not os.path.isfile(temp):
            pass
        else:
            filepath = temp
    if ".cir" in filepath:
        with codecs.open(filepath, "r", "utf-16LE") as fd:
            Lines = fd.readlines()
    elif ".txt" in filepath:
        with open(filepath, "r") as f:
            Lines = f.readlines()
    else:
        raise FileNotFoundError(
            'Please supply a valid file with extension ".cir" or ".txt"'
        )
    # Ignore lines starting with * or .
    Lines = [l.strip("\n").split(" ") for l in Lines if l[0] not in ["*", "."]]
    Lines = np.array(Lines, dtype="<U16")

    # Read descriptions and nodes, strip N from nodes
    # Lines is desc | node1 | node2
    desc = Lines[:, 0]
    node1 = Lines[:, 1]
    node2 = Lines[:, 2]
    value = Lines[:, 3]
    try:
        value = value.astype(float)
    except ValueError:
        pass
    node1 = np.array([x.strip("N") for x in node1], dtype=int)
    node2 = np.array([x.strip("N") for x in node2], dtype=int)
    netlist = pd.DataFrame(
        {"desc": desc, "node1": node1, "node2": node2, "value": value}
    )

    # Populate the values based on the descriptions (element types)
    for name, val in [
        ("Ri", Ri),
        ("Rc", Rc),
        ("Rb", Rb),
        ("Rl", Rb),
        ("Rt", Rt),
        ("I", I),
        ("V", V),
    ]:
        if val is not None:
            # netlist["desc"] consists of entries like 'Ri13'
            # this map finds all the entries that start with (e.g.) 'Ri'
            name_map = netlist["desc"].str.find(name) > -1
            # then allocates the value to the corresponding indices
            netlist.loc[name_map, ("value")] = val

    lp.logger.notice("netlist " + filepath + " loaded")
    return netlist

setup_circuit(Np=1, Ns=1, Ri=0.01, Rc=0.01, Rb=0.0001, Rt=1e-05, I=80.0, V=4.2, plot=False, terminals='left', configuration='parallel-strings')

Define a netlist from a number of batteries in parallel and series

Parameters:

Name Type Description Default
Np int

Number of batteries in parallel.

1
Ns int

Number of batteries in series.

1
Ri float

Internal resistance (\Omega).

0.01
Rc float

Connection resistance (\Omega).

0.01
Rb float

Busbar resistance (\Omega).

0.0001
Rt float

Terminal connection resistance (\Omega).

1e-05
I float

Current (A).

80.0
V float

Initial battery voltage (V).

4.2
plot bool

Plot the circuit.

False
terminals string

The location of the terminals. Can be "left", "right", "left-right", "right-left" or a list or array of node integers.

'left'
configuration string

The pack circuit configuration to use. Can be "parallel-strings" (default) or "series-groups"

'parallel-strings'

Returns:

Type Description
pandas.DataFrame

A netlist of circuit elements with format desc, node1, node2, value.

Source code in liionpack/netlist_utils.py
def setup_circuit(
    Np=1,
    Ns=1,
    Ri=1e-2,
    Rc=1e-2,
    Rb=1e-4,
    Rt=1e-5,
    I=80.0,
    V=4.2,
    plot=False,
    terminals="left",
    configuration="parallel-strings",
):
    """
    Define a netlist from a number of batteries in parallel and series

    Args:
        Np (int): Number of batteries in parallel.
        Ns (int): Number of batteries in series.
        Ri (float): Internal resistance ($\Omega$).
        Rc (float): Connection resistance ($\Omega$).
        Rb (float): Busbar resistance ($\Omega$).
        Rt (float): Terminal connection resistance ($\Omega$).
        I (float): Current (A).
        V (float): Initial battery voltage (V).
        plot (bool): Plot the circuit.
        terminals (string): The location of the terminals. Can be "left", "right",
            "left-right", "right-left" or a list or array of node integers.
        configuration (string): The pack circuit configuration to use. Can be
            "parallel-strings" (default) or "series-groups"

    Returns:
        pandas.DataFrame:
            A netlist of circuit elements with format desc, node1, node2, value.

    """
    Nc = Np
    Nr = Ns * 3 + 1

    grid = np.arange(Nc * Nr).reshape([Nr, Nc])
    coords = np.indices(grid.shape)
    y = coords[0, :, :]
    x = coords[1, :, :]
    # make contiguous now instead of later when netlist is done as very slow
    mask = np.ones([Nr, Nc], dtype=bool)
    # This is no longer needed as terminals connect directly to battery
    # Guess could also add a terminal connection resistor though
    # mask[1:-1, 0] = False
    grid[mask] = np.arange(np.sum(mask)) + 1
    x = x[mask].flatten()
    y = y[mask].flatten()
    grid[~mask] = -2  # These should never be used

    # grid is a Nr x Nc matrix
    # 1st column is terminals only
    # 1st and last rows are busbars
    # Other rows alternate between series resistor and voltage source
    # For example if Np=1 and Nc=2,
    # grid = array([[ 0,  1], # busbar
    #                         # Rs
    #               [ 2,  3],
    #                         # V
    #               [ 4,  5],
    #                         # Ri
    #               [ 6,  7],
    #                         # Rs
    #               [ 8,  9],
    #                         # V
    #               [10, 11],
    #                         # Ri
    #               [12, 13]] # busbar)
    # Connections are across busbars in first and last rows, and down each column
    # See "01 Getting Started.ipynb"

    # Build data  with ['element type', node1, node2, value]
    netlist = []

    num_Rb = 0
    num_V = 0

    desc = []
    node1 = []
    node2 = []
    value = []

    # -ve busbars (bottom row of the grid)
    bus_nodes = [grid[0, :]]
    for nodes in bus_nodes:
        for i in range(len(nodes) - 1):
            # netline = []
            desc.append("Rbn" + str(num_Rb))
            num_Rb += 1
            node1.append(nodes[i])
            node2.append(nodes[i + 1])
            value.append(Rb)
    num_Rs = 0
    num_Ri = 0
    # Series resistors and voltage sources
    cols = np.arange(Nc)
    rows = np.arange(Nr)[:-1]
    rtype = ["Rc", "V", "Ri"] * Ns
    for col in cols:
        # Go down the column alternating Rs, V, Ri connections between nodes
        nodes = grid[:, col]
        for row in rows:
            if rtype[row] == "Rc":
                # Inter(c)onnection / weld
                desc.append(rtype[row] + str(num_Rs))
                num_Rs += 1
                val = Rc
            elif rtype[row] == "Ri":
                # Internal resistor
                desc.append(rtype[row] + str(num_Ri))
                num_Ri += 1
                val = Ri
            else:
                # Voltage source
                desc.append("V" + str(num_V))
                num_V += 1
                val = V
            node1.append(nodes[row + 1])
            node2.append(nodes[row])
            value.append(val)
            # netlist.append(netline)

    # +ve busbar (top row of the grid)
    if configuration == "parallel-strings":
        bus_nodes = [grid[-1, :]]
    elif configuration == "series-groups":
        bus_nodes = grid[3::3, :]
    else:
        raise ValueError("configuration must be parallel-strings or series-groups")

    for nodes in bus_nodes:
        for i in range(len(nodes) - 1):
            # netline = []
            desc.append("Rbp" + str(num_Rb))
            num_Rb += 1
            node1.append(nodes[i])
            node2.append(nodes[i + 1])
            value.append(Rb)

    desc = np.asarray(desc)
    node1 = np.asarray(node1)
    node2 = np.asarray(node2)
    value = np.asarray(value)
    main_grid = {
        "desc": desc,
        "node1": node1,
        "node2": node2,
        "value": value,
        "node1_x": x[node1 - 1],
        "node1_y": y[node1 - 1],
        "node2_x": x[node2 - 1],
        "node2_y": y[node2 - 1],
    }

    # Current source - spans the entire pack
    if (terminals == "left") or (terminals is None):
        t_nodes = [0, 0]
    elif terminals == "right":
        t_nodes = [-1, -1]
    elif terminals == "left-right":
        t_nodes = [0, -1]
    elif terminals == "right-left":
        t_nodes = [-1, 0]
    elif isinstance(terminals, (list, np.ndarray)):
        t_nodes = terminals
    else:
        raise ValueError(
            'Please specify a valid terminals argument: "left", '
            + '"right", "left-right" or "right-left" or a list or '
            + "array of nodes"
        )
    # terminal nodes
    t1 = grid[-1, t_nodes[0]]
    t2 = grid[0, t_nodes[1]]
    # terminal coords
    x1 = x[t1 - 1]
    x2 = x[t2 - 1]
    y1 = y[t1 - 1]
    y2 = y[t2 - 1]
    nn = grid.max() + 1  # next node
    # coords of nodes forming current source loop
    if terminals == "left" or (
        isinstance(terminals, (list, np.ndarray)) and np.all(np.array(terminals) == 0)
    ):
        ix = x1 - 1
        dy = 0
    elif terminals == "right" or (
        isinstance(terminals, (list, np.ndarray)) and np.all(np.array(terminals) == -1)
    ):
        ix = x1 + 1
        dy = 0
    else:
        ix = -1
        dy = 1
    if dy == 0:
        desc = ["Rtp1", "I0", "Rtn1"]
        xs = np.array([x1, ix, ix, x2])
        ys = np.array([y1, y1, y2, y2])
        node1 = [t1, nn, 0]
        node2 = [nn, 0, t2]
        value = [Rt, I, Rt]
        num_elem = 3
    else:
        desc = ["Rtp0", "Rtp1", "I0", "Rtn1", "Rtn0"]
        xs = np.array([x1, x1, ix, ix, x2, x2])
        ys = np.array([y1, y1 + dy, y1 + dy, 0 - dy, 0 - dy, y2])
        node1 = [t1, nn, nn + 1, 0, nn + 2]
        node2 = [nn, nn + 1, 0, nn + 2, t2]
        hRt = Rt / 2
        value = [hRt, hRt, I, hRt, hRt]
        num_elem = 5

    desc = np.asarray(desc)
    node1 = np.asarray(node1)
    node2 = np.asarray(node2)
    value = np.asarray(value)
    current_loop = {
        "desc": desc,
        "node1": node1,
        "node2": node2,
        "value": value,
        "node1_x": xs[:num_elem],
        "node1_y": ys[:num_elem],
        "node2_x": xs[1:],
        "node2_y": ys[1:],
    }

    for key in main_grid.keys():
        main_grid[key] = np.concatenate((main_grid[key], current_loop[key]))
    netlist = pd.DataFrame(main_grid)

    if plot:
        lp.simple_netlist_plot(netlist)
    lp.logger.notice("Circuit created")
    return netlist

solve_circuit(netlist)

Generate and solve the Modified Nodal Analysis (MNA) equations for the circuit. The MNA equations are a linear system Ax = z. See http://lpsa.swarthmore.edu/Systems/Electrical/mna/MNA3.html

Parameters:

Name Type Description Default
netlist pandas.DataFrame

A netlist of circuit elements with format desc, node1, node2, value.

required

Returns:

Type Description
(np.ndarray, np.ndarray)
  • V_node: Voltages of the voltage elements
  • I_batt: Currents of the current elements
Source code in liionpack/netlist_utils.py
def solve_circuit(netlist):
    """
    Generate and solve the Modified Nodal Analysis (MNA) equations for the circuit.
    The MNA equations are a linear system Ax = z.
    See http://lpsa.swarthmore.edu/Systems/Electrical/mna/MNA3.html

    Args:
        netlist (pandas.DataFrame):
            A netlist of circuit elements with format desc, node1, node2, value.

    Returns:
        (np.ndarray, np.ndarray):
        - V_node: Voltages of the voltage elements
        - I_batt: Currents of the current elements

    """
    timer = pybamm.Timer()

    desc = np.array(netlist["desc"]).astype("<U16")
    node1 = np.array(netlist["node1"])
    node2 = np.array(netlist["node2"])
    value = np.array(netlist["value"])
    nLines = netlist.shape[0]

    n = np.concatenate((node1, node2)).max()  # Number of nodes (highest node number)

    m = 0  # "m" is the number of voltage sources, determined below.
    V_elem = ["V", "O", "E", "H"]
    for nm in desc:
        if nm[0] in V_elem:
            m += 1

    # Construct the A matrix, which will be a (n+m) x (n+m) matrix
    # A = [G    B]
    #     [B.T  D]
    # G matrix tracks the conductance between nodes (consists of floats)
    # B matrix tracks voltage sources between nodes (consists of -1, 0, 1)
    # D matrix is always zero for non-dependent sources
    # Construct the z vector with length (n+m)
    # z = [i]
    #     [e]
    # i is currents and e is voltages
    # Use lil matrices to construct the A array
    G = sp.sparse.lil_matrix((n, n))
    B = sp.sparse.lil_matrix((n, m))
    D = sp.sparse.lil_matrix((m, m))
    i = np.zeros([n, 1])
    e = np.zeros([m, 1])

    """
    % We need to keep track of the number of voltage sources we've parsed
    % so far as we go through file.  We start with zero.
    """
    vsCnt = 0
    """
    % This loop does the bulk of filling in the arrays.  It scans line by line
    % and fills in the arrays depending on the type of element found on the
    % current line.
    % See http://lpsa.swarthmore.edu/Systems/Electrical/mna/MNA3.html
    """

    for k1 in range(nLines):
        n1 = node1[k1] - 1  # get the two node numbers in python index format
        n2 = node2[k1] - 1
        elem = desc[k1][0]
        if elem == "R":
            # Resistance elements: fill the G matrix only
            g = 1 / value[k1]  # conductance = 1 / R
            """
            % Here we fill in G array by adding conductance.
            % The procedure is slightly different if one of the nodes is
            % ground, so check for those accordingly.
            """
            if n1 == -1:  # -1 is the ground node
                G[n2, n2] = G[n2, n2] + g
            elif n2 == -1:
                G[n1, n1] = G[n1, n1] + g
            else:
                G[n1, n1] = G[n1, n1] + g
                G[n2, n2] = G[n2, n2] + g
                G[n1, n2] = G[n1, n2] - g
                G[n2, n1] = G[n2, n1] - g
        elif elem == "V":
            # Voltage elements: fill the B matrix and the e vector
            if n1 >= 0:
                B[n1, vsCnt] = B[n1, vsCnt] + 1
            if n2 >= 0:
                B[n2, vsCnt] = B[n2, vsCnt] - 1
            e[vsCnt] = value[k1]
            vsCnt += 1

        elif elem == "I":
            # Current elements: fill the i vector only
            if n1 >= 0:
                i[n1] = i[n1] - value[k1]
            if n2 >= 0:
                i[n2] = i[n2] + value[k1]

    # Construct final matrices from sub-matrices
    upper = sp.sparse.hstack((G, B))
    lower = sp.sparse.hstack((B.T, D))
    A = sp.sparse.vstack((upper, lower))
    # Convert a to csr sparse format for more efficient solving of the linear system
    # csr works slighhtly more robustly than csc
    A_csr = sp.sparse.csr_matrix(A)
    z = np.vstack((i, e))

    toc_setup = timer.time()
    lp.logger.debug(f"Circuit set up in {toc_setup}")

    # Scipy
    # X = solve(A, z).flatten()
    X = sp.sparse.linalg.spsolve(A_csr, z).flatten()
    # Pypardiso
    # X = pypardiso.spsolve(Aspr, z).flatten()

    # amg
    # ml = pyamg.smoothed_aggregation_solver(Aspr)
    # X = ml.solve(b=z, tol=1e-6, maxiter=10, accel="bicgstab")

    # include ground node (0V)
    # it is counter-intuitive that z is [i,e] while X is [V,I], but this is correct
    V_node = np.zeros(n + 1)
    V_node[1:] = X[:n]
    I_batt = X[n:]

    toc = timer.time()
    lp.logger.debug(f"Circuit solved in {toc - toc_setup}")
    lp.logger.info(f"Circuit set up and solved in {toc}")

    return V_node, I_batt

solve_circuit_vectorized(netlist)

Generate and solve the Modified Nodal Analysis (MNA) equations for the circuit. The MNA equations are a linear system Ax = z. See http://lpsa.swarthmore.edu/Systems/Electrical/mna/MNA3.html

Parameters:

Name Type Description Default
netlist pandas.DataFrame

A netlist of circuit elements with format desc, node1, node2, value.

required

Returns:

Type Description
(np.ndarray, np.ndarray)
  • V_node: Voltages of the voltage elements
  • I_batt: Currents of the current elements
Source code in liionpack/netlist_utils.py
def solve_circuit_vectorized(netlist):
    """
    Generate and solve the Modified Nodal Analysis (MNA) equations for the circuit.
    The MNA equations are a linear system Ax = z.
    See http://lpsa.swarthmore.edu/Systems/Electrical/mna/MNA3.html

    Args:
        netlist (pandas.DataFrame):
            A netlist of circuit elements with format desc, node1, node2, value.

    Returns:
        (np.ndarray, np.ndarray):
        - V_node: Voltages of the voltage elements
        - I_batt: Currents of the current elements
    """
    timer = pybamm.Timer()

    desc = np.array(netlist["desc"]).astype("<U1")  # just take first character
    node1 = np.array(netlist["node1"])
    node2 = np.array(netlist["node2"])
    value = np.array(netlist["value"])
    n = np.concatenate((node1, node2)).max()  # Number of nodes (highest node number)

    m = np.sum(desc == "V")  # we only use V in liionpack

    # Construct the A matrix, which will be a (n+m) x (n+m) matrix
    # A = [G    B]
    #     [B.T  D]
    # G matrix tracks the conductance between nodes (consists of floats)
    # B matrix tracks voltage sources between nodes (consists of -1, 0, 1)
    # D matrix is always zero for non-dependent sources
    # Construct the z vector with length (n+m)
    # z = [i]
    #     [e]
    # i is currents and e is voltages
    # Use lil matrices to construct the A array
    G = sp.sparse.lil_matrix((n, n))
    B = sp.sparse.lil_matrix((n, m))
    D = sp.sparse.lil_matrix((m, m))
    i = np.zeros([n, 1])
    e = np.zeros([m, 1])

    """
    % This old loop is now vectorized
    """

    node1 = node1 - 1  # get the two node numbers in python index format
    node2 = node2 - 1
    # Resistance elements: fill the G matrix only
    g = np.ones(len(value)) * np.nan
    n1_ground = node1 == -1
    n2_ground = node2 == -1
    # Resistors
    R_map = desc == "R"
    g[R_map] = 1 / value[R_map]  # conductance = 1 / R
    R_map_n1_ground = np.logical_and(R_map, n1_ground)
    R_map_n2_ground = np.logical_and(R_map, n2_ground)
    R_map_ok = np.logical_and(R_map, ~np.logical_or(n1_ground, n2_ground))

    """
    % Here we fill in G array by adding conductance.
    % The procedure is slightly different if one of the nodes is
    % ground, so check for those accordingly.
    """
    if np.any(R_map_n1_ground):  # -1 is the ground node
        n2 = node2[R_map_n1_ground]
        G[n2, n2] = G[n2, n2] + g[R_map_n1_ground]
    if np.any(R_map_n2_ground):
        n1 = node1[R_map_n2_ground]
        G[n1, n1] = G[n1, n1] + g[R_map_n2_ground]

    # No longer needs unique nodes
    # We can take advantage of the fact that coo style inputs sum
    # duplicates with converted to csr
    n1 = node1[R_map_ok]
    n2 = node2[R_map_ok]
    g_change = g[R_map_ok]
    gn1n1 = sp.sparse.csr_matrix((g_change, (n1, n1)), shape=G.shape)
    gn2n2 = sp.sparse.csr_matrix((g_change, (n2, n2)), shape=G.shape)
    gn1n2 = sp.sparse.csr_matrix((g_change, (n1, n2)), shape=G.shape)
    gn2n1 = sp.sparse.csr_matrix((g_change, (n2, n1)), shape=G.shape)
    G += gn1n1
    G += gn2n2
    G -= gn1n2
    G -= gn2n1

    # Assume Voltage sources do not connect directly to ground
    V_map = desc == "V"
    V_map_not_n1_ground = np.logical_and(V_map, ~n1_ground)
    V_map_not_n2_ground = np.logical_and(V_map, ~n2_ground)
    n1 = node1[V_map_not_n1_ground]
    n2 = node2[V_map_not_n2_ground]
    vsCnt = np.ones(len(V_map)) * -1
    vsCnt[V_map] = np.arange(m)
    # Voltage elements: fill the B matrix and the e vector
    B[n1, vsCnt[V_map_not_n1_ground]] = 1
    B[n2, vsCnt[V_map_not_n2_ground]] = -1
    e[np.arange(m), 0] = value[V_map]
    # Current Sources
    I_map = desc == "I"
    n1 = node1[I_map]
    n2 = node2[I_map]
    # Current elements: fill the i vector only
    if n1 >= 0:
        i[n1] = i[n1] - value[I_map]
    if n2 >= 0:
        i[n2] = i[n2] + value[I_map]

    # Construct final matrices from sub-matrices
    upper = sp.sparse.hstack((G, B))
    lower = sp.sparse.hstack((B.T, D))
    A = sp.sparse.vstack((upper, lower))
    # Convert a to csr sparse format for more efficient solving of the linear system
    # csr works slighhtly more robustly than csc
    A_csr = sp.sparse.csr_matrix(A)
    z = np.vstack((i, e))

    toc_setup = timer.time()
    lp.logger.debug(f"Circuit set up in {toc_setup}")

    # Scipy
    X = sp.sparse.linalg.spsolve(A_csr, z).flatten()

    # include ground node (0V)
    # it is counter-intuitive that z is [i,e] while X is [V,I], but this is correct
    V_node = np.zeros(n + 1)
    V_node[1:] = X[:n]
    I_batt = X[n:]

    toc = timer.time()
    lp.logger.debug(f"Circuit solved in {toc - toc_setup}")
    lp.logger.info(f"Circuit set up and solved in {toc}")

    return V_node, I_batt

write_netlist(netlist, filename)

Write netlist to file

Parameters:

Name Type Description Default
netlist pandas.DataFrame

A netlist of circuit elements with format desc, node1, node2, value.

required

Returns:

Type Description

None

Source code in liionpack/netlist_utils.py
def write_netlist(netlist, filename):
    """
    Write netlist to file

    Args:
        netlist (pandas.DataFrame):
            A netlist of circuit elements with format desc, node1, node2, value.

    Returns:
        None


    """
    lines = ["* " + filename]
    for i, r in netlist.iterrows():
        line = r.desc + " " + _fn(r.node1) + " " + _fn(r.node2) + " " + str(r.value)
        lines.append(line)
    lines.append(".op")
    lines.append(".backanno")
    lines.append(".end")
    with open(filename, "w") as f:
        for line in lines:
            f.write(line)
            f.write("\n")

plots

compare_solution_output(a, b)

Compare two solutions Terminal Voltage [V] and Current [A]. Solutions can be PyBaMM.Solution or dict output from Liionpack solve.

Parameters:

Name Type Description Default
a dict / PyBaMM.Solution

Output from solve.

required
b dict / PyBaMM.Solution

Output from solve.

required
Source code in liionpack/plots.py
def compare_solution_output(a, b):
    r"""
    Compare two solutions Terminal Voltage [V] and Current [A]. Solutions can
    be PyBaMM.Solution or dict output from Liionpack solve.

    Args:
        a (dict / PyBaMM.Solution):
            Output from solve.
        b (dict / PyBaMM.Solution):
            Output from solve.

    """
    # Get pack level results
    if a.__class__ is dict:
        time_a = a["Time [s]"]
        v_a = a["Pack terminal voltage [V]"]
        i_a = a["Pack current [A]"]
        title_a = "a) Liionpack Simulation"
    else:
        time_a = a["Time [s]"].entries
        v_a = a["Terminal voltage [V]"].entries
        i_a = a["Current [A]"].entries
        title_a = "a) PyBaMM Simulation"
    if b.__class__ is dict:
        time_b = b["Time [s]"]
        v_b = b["Pack terminal voltage [V]"]
        i_b = b["Pack current [A]"]
        title_b = "b) Liionpack Simulation"
    else:
        time_b = b["Time [s]"].entries
        v_b = b["Terminal voltage [V]"].entries
        i_b = b["Current [A]"].entries
        title_b = "b) PyBaMM Simulation"
    cmap = lp_cmap()
    colors = cmap(np.linspace(0, 1, 4))
    with plt.rc_context(lp_context()):
        # Plot pack voltage and current
        _, (axl, axr) = plt.subplots(
            1, 2, tight_layout=True, figsize=(15, 10), sharex=True, sharey=True
        )
        axl.plot(time_a, v_a, color=colors[0], label="simulation")
        axl.set_xlabel("Time [s]")
        axl.set_ylabel("Terminal voltage [V]", color=colors[0])
        axl2 = axl.twinx()
        axl2.plot(time_a, i_a, color=colors[1], label="simulation")
        axl2.set_ylabel("Current [A]", color=colors[1])
        axl2.set_title(title_a)
        axr.plot(time_b, v_b, color=colors[2], label="simulation")
        axr.set_xlabel("Time [s]")
        axr.set_ylabel("Terminal voltage [V]", color=colors[2])
        axr2 = axr.twinx()
        axr2.plot(time_b, i_b, color=colors[3], label="simulation")
        axr2.set_ylabel("Current [A]", color=colors[3])
        axr2.set_title(title_b)

draw_circuit(netlist, cpt_size=1.0, dpi=300, node_spacing=2.0, scale=1.0, help_lines=0.0, font='\\scriptsize', label_ids=True, label_values=True, draw_nodes=True, label_nodes='primary', style='american')

Draw a latex version of netlist circuit N.B only works with generated netlists not imported ones.

Parameters:

Name Type Description Default
netlist pandas.DataFrame

A netlist of circuit elements with format. desc, node1, node2, value.

required
cpt_size float

component size, default 1.0

1.0
dpi int

dots per inch, default 300

300
node_spacing float

spacing between component nodes, default 2.0

2.0
scale float

schematic scale factor, default 1.0

1.0
help_lines float

distance between lines in grid, default 0.0 (disabled)

0.0
font string

LaTex font size, default \scriptsize

'\\scriptsize'
label_ids bool

Show component ids, default True

True
label_values bool

Display component values, default True

True
draw_nodes bool

True to show all nodes (default), False to show no nodes,'primary' to show primary nodes, 'connections' to show nodes that connect more than two components, 'all' to show all nodes.

True
label_nodes bool

True to label all nodes, False to label no nodes, 'primary' to label primary nodes (default), 'alpha' to label nodes starting with a letter, 'pins' to label nodes that are pins on a chip, 'all' to label all nodes

'primary'
style string

'american', 'british', or 'european'

'american'

Examples:

>>> import liionpack as lp
>>> net = lp.setup_circuit(Np=3, Ns=1, Rb=1e-4, Rc=1e-2, Ri=5e-2, V=3.2, I=80.0)
>>> lp.draw_circuit(net)
Source code in liionpack/plots.py
def draw_circuit(
    netlist,
    cpt_size=1.0,
    dpi=300,
    node_spacing=2.0,
    scale=1.0,
    help_lines=0.0,
    font="\scriptsize",
    label_ids=True,
    label_values=True,
    draw_nodes=True,
    label_nodes="primary",
    style="american",
):
    """
    Draw a latex version of netlist circuit
    N.B only works with generated netlists not imported ones.

    Args:
        netlist (pandas.DataFrame):
            A netlist of circuit elements with format. desc, node1, node2, value.
        cpt_size (float):
            component size, default 1.0
        dpi (int):
            dots per inch, default 300
        node_spacing (float):
            spacing between component nodes, default 2.0
        scale (float):
            schematic scale factor, default 1.0
        help_lines (float):
            distance between lines in grid, default 0.0 (disabled)
        font (string):
            LaTex font size, default \scriptsize
        label_ids (bool):
            Show component ids, default True
        label_values (bool):
            Display component values, default True
        draw_nodes (bool):
            True to show all nodes (default), False to show no nodes,'primary' to show
            primary nodes, 'connections' to show nodes that connect more than
            two components, 'all' to show all nodes.
        label_nodes (bool):
            True to label all nodes, False to label no nodes, 'primary' to label
            primary nodes (default), 'alpha' to label nodes starting with a letter,
            'pins' to label nodes that are pins on a chip, 'all' to label all nodes
        style (string):
            'american', 'british', or 'european'

    Example:
        >>> import liionpack as lp
        >>> net = lp.setup_circuit(Np=3, Ns=1, Rb=1e-4, Rc=1e-2, Ri=5e-2, V=3.2, I=80.0)
        >>> lp.draw_circuit(net)
    """
    cct = lp.make_lcapy_circuit(netlist)
    kwargs = {
        "cpt_size": cpt_size,
        "dpi": dpi,
        "node_spacing": node_spacing,
        "scale": scale,
        "help_lines": help_lines,
        "font": font,
        "label_ids": label_ids,
        "label_values": label_values,
        "draw_nodes": draw_nodes,
        "label_nodes": label_nodes,
        "style": style,
    }
    cct.draw(**kwargs)

lp_cmap(color='dark')

Return the colormap to use in plots

Parameters:

Name Type Description Default
color string

The color-scheme for plotting, default="dark".

'dark'

Returns:

Type Description
cmap (matplotlib.cm)

The colormap for matplotlib to plot

Source code in liionpack/plots.py
def lp_cmap(color="dark"):
    """
    Return the colormap to use in plots

    Args:
        color (string):
            The color-scheme for plotting, default="dark".

    Returns:
        cmap (matplotlib.cm):
            The colormap for matplotlib to plot

    """
    if color == "dark":
        return plt.cm.cool
    else:
        return plt.cm.coolwarm

lp_context(color='dark')

Return the liionpack matplotlib rc_context for plotting

Parameters:

Name Type Description Default
color string

The color-scheme for plotting, default="dark"

'dark'

Returns:

Type Description
context (dict)

The options to pass to matplotlib.pyplot.rc_context

Source code in liionpack/plots.py
def lp_context(color="dark"):
    """
    Return the liionpack matplotlib rc_context for plotting

    Args:
        color (string):
            The color-scheme for plotting, default="dark"

    Returns:
        context (dict):
            The options to pass to matplotlib.pyplot.rc_context

    """
    if color == "dark":
        context = {
            "text.color": "white",
            "axes.edgecolor": "white",
            "axes.titlecolor": "white",
            "axes.labelcolor": "white",
            "xtick.color": "white",
            "ytick.color": "white",
            "figure.facecolor": "#323232",
            "axes.facecolor": "#323232",
            "axes.grid": False,
            "axes.labelsize": "large",
            "figure.figsize": (8, 6),
        }
    else:
        context = {
            "text.color": "black",
            "axes.edgecolor": "black",
            "axes.titlecolor": "black",
            "axes.labelcolor": "black",
            "xtick.color": "black",
            "ytick.color": "black",
            "figure.facecolor": "white",
            "axes.facecolor": "white",
            "axes.grid": False,
            "axes.labelsize": "large",
            "figure.figsize": (8, 6),
        }
    return context

plot_cell_data_image(netlist, data, tick_labels=True, figsize=(8, 6))

Plot the cell data for all cells at a particular point in time in an image format using the node coordinates in the netlist to arrange the cells.

Parameters:

Name Type Description Default
netlist pandas.DataFrame

A netlist of circuit elements with format desc, node1, node2, value.

required
data numpy.array

The data to be plotted for each cell.

required
tick_labels boolean

Show the Np and Ns cell indices.

True
figsize tuple

The figzise in inches.

(8, 6)
Source code in liionpack/plots.py
def plot_cell_data_image(netlist, data, tick_labels=True, figsize=(8, 6)):
    r"""
    Plot the cell data for all cells at a particular point in time in an image
    format using the node coordinates in the netlist to arrange the cells.

    Args:
        netlist (pandas.DataFrame):
            A netlist of circuit elements with format desc, node1, node2, value.
        data (numpy.array):
            The data to be plotted for each cell.
        tick_labels boolean:
            Show the Np and Ns cell indices.
        figsize (tuple):
            The figzise in inches.

    """
    V_map = netlist["desc"].str.find("V") > -1
    vlist = netlist[V_map]
    n1x = np.unique(vlist["node1_x"])
    n1y = np.unique(vlist["node1_y"])
    Nx = len(n1x)
    Ny = len(n1y)
    for ix in range(Nx):
        vlist.loc[vlist["node1_x"] == n1x[ix], ("node1_x")] = ix
    for iy in range(Ny):
        vlist.loc[vlist["node1_y"] == n1y[iy], ("node1_y")] = iy

    im = np.ones([Nx, Ny])
    im[np.array(vlist["node1_x"]), np.array(vlist["node1_y"])] = data

    cmap = lp_cmap()
    with plt.rc_context(lp_context()):
        fig, ax = plt.subplots(figsize=figsize)
        mappable = ax.imshow(im.T, cmap=cmap)
        # Major ticks
        ax.set_xticks(np.arange(0, Nx, 1))
        ax.set_yticks(np.arange(0, Ny, 1))
        if tick_labels:
            # Labels for major ticks
            ax.set_xticklabels(np.arange(0, Nx, 1))
            ax.set_yticklabels(np.arange(0, Ny, 1))
        else:
            ax.set_xticklabels([])
            ax.set_yticklabels([])
        # Minor ticks
        ax.set_xticks(np.arange(-0.5, Nx, 1), minor=True)
        ax.set_yticks(np.arange(-0.5, Ny, 1), minor=True)
        # Gridlines based on minor ticks
        ax.grid(which="minor", color="w", linestyle="-", linewidth=1)
        plt.colorbar(mappable)
        plt.tight_layout()

plot_cells(output, color='dark')

Plot results for the battery cells.

Parameters:

Name Type Description Default
output dict

Output from liionpack.solve which contains pack and cell variables.

required
color string

The color-scheme for plotting, default="dark"

'dark'
Source code in liionpack/plots.py
def plot_cells(output, color="dark"):
    """
    Plot results for the battery cells.

    Args:
        output (dict):
            Output from liionpack.solve which contains pack and cell variables.
        color (string):
            The color-scheme for plotting, default="dark"
    """

    # Get time and results for battery cells
    time = output["Time [s]"]
    cell_vars = [k for k in output.keys() if len(output[k].shape) > 1]

    context = lp_context(color)
    cmap = lp_cmap(context)

    # Get number of cells and setup colormap
    n = output[cell_vars[0]].shape[-1]
    colors = cmap(np.linspace(0, 1, n))

    # Create plot figures for cell variables
    with plt.rc_context(context):
        for var in cell_vars:
            _, ax = plt.subplots(tight_layout=True)
            for i in range(n):
                ax.plot(time, output[var][:, i], color=colors[i])
            ax.set_xlabel("Time [s]")
            ax.set_ylabel(textwrap.fill(var, 45))
            ax.ticklabel_format(axis="y", scilimits=[-5, 5])

plot_output(output, color='dark')

Plot all results for pack and cells

Parameters:

Name Type Description Default
output dict

Output from liionpack.solve which contains pack and cell variables.

required
color string

The color-scheme for plotting, default="dark"

'dark'
Source code in liionpack/plots.py
def plot_output(output, color="dark"):
    """
    Plot all results for pack and cells

    Args:
        output (dict):
            Output from liionpack.solve which contains pack and cell variables.
        color (string):
            The color-scheme for plotting, default="dark"

    """
    plot_pack(output, color)
    plot_cells(output, color)

plot_pack(output, color='dark')

Plot the battery pack voltage and current.

Parameters:

Name Type Description Default
output dict

Output from liionpack.solve which contains pack and cell variables.

required
color string

The color-scheme for plotting, default="dark"

'dark'
Source code in liionpack/plots.py
def plot_pack(output, color="dark"):
    """
    Plot the battery pack voltage and current.

    Args:
        output (dict):
            Output from liionpack.solve which contains pack and cell variables.
        color (string):
            The color-scheme for plotting, default="dark"
    """

    # Get pack level results
    time = output["Time [s]"]
    v_pack = output["Pack terminal voltage [V]"]
    i_pack = output["Pack current [A]"]

    context = lp_context(color)
    cmap = lp_cmap(context)

    colors = cmap(np.linspace(0, 1, 2))
    with plt.rc_context(context):
        # Plot pack voltage and current
        _, ax = plt.subplots(tight_layout=True)
        ax.plot(time, v_pack, color=colors[0], label="simulation")
        ax.set_xlabel("Time [s]")
        ax.set_ylabel("Pack terminal voltage [V]", color=colors[0])
        ax.grid(False)
        ax2 = ax.twinx()
        ax2.plot(time, i_pack, color=colors[1], label="simulation")
        ax2.set_ylabel("Pack current [A]", color=colors[1])
        ax2.set_title("Pack Summary")

show_plots()

Wrapper function for the Matplotlib show() function.

Source code in liionpack/plots.py
def show_plots():  # pragma: no cover
    """
    Wrapper function for the Matplotlib show() function.
    """
    plt.show()

simple_netlist_plot(netlist)

Simple matplotlib netlist plot with colored lines for different elements

Parameters:

Name Type Description Default
netlist TYPE

DESCRIPTION.

required
Source code in liionpack/plots.py
def simple_netlist_plot(netlist):
    """
    Simple matplotlib netlist plot with colored lines for different elements

    Args:
        netlist (TYPE): DESCRIPTION.

    """
    plt.figure()
    for row in netlist.iterrows():
        elem, node1, node2, value, x1, y1, x2, y2 = row[1]
        if elem[0] == "I":
            color = "g"
        elif elem[:2] == "Rs":
            color = "r"
        elif elem[:2] == "Rb":
            color = "k"
        elif elem[:2] == "Ri":
            color = "y"
        elif elem[:2] == "Rt":
            color = "pink"
        elif elem[0] == "V":
            color = "b"
        else:
            color = "k"
        plt.scatter([x1, x2], [y1, y2], c="k")
        plt.plot([x1, x2], [y1, y2], c=color)

protocols

generate_protocol_from_experiment(experiment, flatten=True)

Parameters:

Name Type Description Default
experiment pybamm.Experiment

The experiment to generate the protocol from.

required
flatten bool

Default is True: return all steps in one list otherwise return a list of lists for each operating command.

True

Returns:

Type Description
list

a sequence of terminal currents to apply at each timestep

Source code in liionpack/protocols.py
def generate_protocol_from_experiment(experiment, flatten=True):
    """

    Args:
        experiment (pybamm.Experiment):
            The experiment to generate the protocol from.
        flatten (bool):
            Default is True: return all steps in one list otherwise return a
            list of lists for each operating command.

    Returns:
        list:
            a sequence of terminal currents to apply at each timestep

    """
    protocol = []
    for i, step in enumerate(experiment.operating_conditions_steps):
        proto = []
        t = step.duration
        dt = step.period
        if t % dt != 0:
            raise ValueError("Time must be an integer multiple of the period")
        typ = step.type
        if typ not in ["current"]:
            raise ValueError("Only constant current operations are supported")
        else:
            if typ == "current":
                if not step.is_drive_cycle:
                    I = step.value
                    proto.extend([I] * int(t / dt))
                    if i == 0:
                        # Include initial state when not drive cycle, first op
                        proto = [proto[0]] + proto
                else:
                    proto.extend(step.value.y.tolist())

        if flatten:
            protocol.extend(proto)
        else:
            protocol.append(proto)

    return protocol

sim_utils

get_initial_stoichiometries(initial_soc, parameter_values)

Calculate initial stoichiometries to start off the simulation at a particular state of charge, given voltage limits, open-circuit potentials, etc defined by parameter_values

Parameters:

Name Type Description Default
initial_soc float

Target initial SOC. Must be between 0 and 1.

required
parameter_values pybamm.ParameterValues

The parameter values class that will be used for the simulation. Required for calculating appropriate initial stoichiometries.

required

Returns:

Type Description
x, y (float)

The initial stoichiometries that give the desired initial state of charge

Source code in liionpack/sim_utils.py
def get_initial_stoichiometries(initial_soc, parameter_values):
    """
    Calculate initial stoichiometries to start off the simulation at a particular
    state of charge, given voltage limits, open-circuit potentials, etc defined by
    parameter_values

    Args:
        initial_soc (float):
            Target initial SOC. Must be between 0 and 1.
        parameter_values (pybamm.ParameterValues):
            The parameter values class that will be used for the simulation.
            Required for calculating appropriate initial stoichiometries.

    Returns:
        x, y (float):
            The initial stoichiometries that give the desired initial state of charge
    """
    if np.any(initial_soc < 0) or np.any(initial_soc > 1):
        raise ValueError("Initial SOC should be between 0 and 1")

    param = pybamm.LithiumIonParameters()
    esoh_solver = pybamm.lithium_ion.ElectrodeSOHSolver(parameter_values, param)
    return esoh_solver.get_initial_stoichiometries(initial_soc)

update_init_conc(param, SoC=None, update=True)

Update initial concentration parameters

Parameters:

Name Type Description Default
param pybamm.ParameterValues

The battery simulation parameters.

required
SoC float

Target initial SoC. Must be between 0 and 1. Default is -1, in which case the initial concentrations are set using the target OCV.

None
update bool

Update the initial concentrations in place if True

True

Returns:

Type Description
c_s_n_init, c_s_p_init (float)

initial concentrations in negative and positive particles

Source code in liionpack/sim_utils.py
def update_init_conc(param, SoC=None, update=True):
    """
    Update initial concentration parameters

    Args:
        param (pybamm.ParameterValues):
            The battery simulation parameters.
        SoC (float):
            Target initial SoC. Must be between 0 and 1. Default is -1, in which
            case the initial concentrations are set using the target OCV.
        update (bool):
            Update the initial concentrations in place if True

    Returns:
        c_s_n_init, c_s_p_init (float):
            initial concentrations in negative and positive particles
    """
    c_n_max = param["Maximum concentration in negative electrode [mol.m-3]"]
    c_p_max = param["Maximum concentration in positive electrode [mol.m-3]"]
    x, y = lp.get_initial_stoichiometries(SoC, param)
    if x is not None:
        c_s_n_init, c_s_p_init = x * c_n_max, y * c_p_max
    else:
        return x, y
    if update:
        param.update(
            {
                "Initial concentration in negative electrode [mol.m-3]": c_s_n_init,
                "Initial concentration in positive electrode [mol.m-3]": c_s_p_init,
            }
        )
    return c_s_n_init, c_s_p_init

simulations

basic_simulation(parameter_values=None)

Create a Basic PyBaMM simulation set up for integration with liionpack

Parameters:

Name Type Description Default
parameter_values pybamm.ParameterValues

The default is None.

None

Returns:

Type Description
pybamm.Simulation

A simulation that can be solved individually or passed into the liionpack solve method

Source code in liionpack/simulations.py
def basic_simulation(parameter_values=None):
    """
    Create a Basic PyBaMM simulation set up for integration with liionpack

    Args:
        parameter_values (pybamm.ParameterValues):
            The default is None.

    Returns:
        pybamm.Simulation:
            A simulation that can be solved individually or passed into the
            liionpack solve method

    """
    # Create the pybamm model
    model = pybamm.lithium_ion.SPM()

    # Add events to the model
    model = lp.add_events_to_model(model)

    # Set up parameter values
    if parameter_values is None:
        param = pybamm.ParameterValues("Chen2020")
    else:
        param = parameter_values.copy()

    # Set up solver and simulation
    solver = pybamm.CasadiSolver(mode="safe")
    sim = pybamm.Simulation(
        model=model,
        parameter_values=param,
        solver=solver,
    )
    return sim

thermal_external(parameter_values=None)

Create a PyBaMM simulation set up for integration with liionpack. External thermal option is used so that temperature dependence can be included in models but temperature supplied by another algorithm. This is useful for packs and cells where thermal connections are seperate or distinct from electrical connections.

Parameters:

Name Type Description Default
parameter_values pybamm.ParameterValues

The default is None.

None

Returns:

Type Description
pybamm.Simulation

A simulation that can be solved individually or passed into the liionpack solve method

Source code in liionpack/simulations.py
def thermal_external(parameter_values=None):
    """
    Create a PyBaMM simulation set up for integration with liionpack.
    External thermal option is used so that temperature dependence can be
    included in models but temperature supplied by another algorithm. This
    is useful for packs and cells where thermal connections are seperate or
    distinct from electrical connections.

    Args:
        parameter_values (pybamm.ParameterValues):
            The default is None.

    Returns:
        pybamm.Simulation:
            A simulation that can be solved individually or passed into the
            liionpack solve method

    """
    # Create the pybamm model
    model = pybamm.lithium_ion.SPMe(
        options={
            "calculate heat source for isothermal models": "true",
            "cell geometry": "arbitrary",
            "dimensionality": 0,
            "thermal": "isothermal",
        }
    )

    # Add events to the model
    model = lp.add_events_to_model(model)

    # Set up parameter values
    if parameter_values is None:
        parameter_values = pybamm.ParameterValues("Chen2020")

    # Change the ambient temperature to be an input controlled by the
    # external circuit
    parameter_values["Ambient temperature [K]"] = pybamm.InputParameter(
        "Input temperature [K]"
    )
    parameter_values["Initial temperature [K]"] = pybamm.InputParameter(
        "Input temperature [K]"
    )

    # Set up solver and simulation
    solver = pybamm.CasadiSolver(mode="safe")
    sim = pybamm.Simulation(
        model=model,
        parameter_values=parameter_values,
        solver=solver,
    )
    return sim

thermal_simulation(parameter_values=None)

Create a PyBaMM simulation set up for integration with liionpack

Parameters:

Name Type Description Default
parameter_values pybamm.ParameterValues

The default is None.

None

Returns:

Type Description
pybamm.Simulation

A simulation that can be solved individually or passed into the liionpack solve method

Source code in liionpack/simulations.py
def thermal_simulation(parameter_values=None):
    """
    Create a PyBaMM simulation set up for integration with liionpack

    Args:
        parameter_values (pybamm.ParameterValues):
            The default is None.

    Returns:
        pybamm.Simulation:
            A simulation that can be solved individually or passed into the
            liionpack solve method

    """
    # Create the pybamm model
    model = pybamm.lithium_ion.SPMe(
        options={
            "thermal": "lumped",
        }
    )

    # Add events to the model
    model = lp.add_events_to_model(model)

    # Set up parameter values
    if parameter_values is None:
        parameter_values = pybamm.ParameterValues("Chen2020")

    # Change the heat transfer coefficient to be an input controlled by the
    # external circuit
    parameter_values.update(
        {
            "Total heat transfer coefficient [W.m-2.K-1]": "[input]",
        },
    )

    # Set up solver and simulation
    solver = pybamm.CasadiSolver(mode="safe")
    sim = pybamm.Simulation(
        model=model,
        parameter_values=parameter_values,
        solver=solver,
    )
    return sim

solver_utils

solve(netlist=None, sim_func=None, parameter_values=None, experiment=None, inputs=None, initial_soc=None, nproc=1, output_variables=None, manager='casadi')

Solves a pack simulation

Parameters:

Name Type Description Default
netlist pandas.DataFrame

A netlist of circuit elements with format. desc, node1, node2, value. Produced by liionpack.read_netlist or liionpack.setup_circuit

None
sim_func function

A function containing model and solver definitions that accepts parameter_values and returns a simulation.

None
parameter_values pybamm.ParameterValues

A dictionary of all the model parameters

None
experiment pybamm.Experiment

The experiment to be simulated. experiment.period is used to determine the length of each timestep.

None
inputs dict

Dictionary for every model input with value for each battery

None
initial_soc float

The initial state of charge for every battery. The default is None in which case concentrations set in the parameter_values are used.

None
nproc int

Number of processes to start in parallel for mapping. The default is 1.

1
output_variables list

Variables to evaluate during solve. Must be a valid key in the model.variables

None
manager string, can be - ["casadi", "ray"]

The solver manager to use for solving the electrochemical problem.

'casadi'

Returns:

Type Description
output (dict)

simulation output with keys including those specified in output variables, values are arrays of shape - [# steps, # batteries])

Source code in liionpack/solver_utils.py
def solve(
    netlist=None,
    sim_func=None,
    parameter_values=None,
    experiment=None,
    inputs=None,
    initial_soc=None,
    nproc=1,
    output_variables=None,
    manager="casadi",
):
    """
    Solves a pack simulation

    Args:
        netlist (pandas.DataFrame):
            A netlist of circuit elements with format. desc, node1, node2, value.
            Produced by liionpack.read_netlist or liionpack.setup_circuit
        sim_func (function):
            A function containing model and solver definitions that accepts
            parameter_values and returns a simulation.
        parameter_values (pybamm.ParameterValues):
            A dictionary of all the model parameters
        experiment (pybamm.Experiment):
            The experiment to be simulated. experiment.period is used to
            determine the length of each timestep.
        inputs (dict):
            Dictionary for every model input with value for each battery
        initial_soc (float):
            The initial state of charge for every battery. The default is None
            in which case concentrations set in the parameter_values are used.
        nproc (int):
            Number of processes to start in parallel for mapping. The default is 1.
        output_variables (list):
            Variables to evaluate during solve. Must be a valid key in the
            model.variables
        manager (string, can be - ["casadi", "ray"]):
            The solver manager to use for solving the electrochemical problem.

    Returns:
        output (dict):
            simulation output with keys including those specified in output
            variables, values are arrays of shape - [# steps, # batteries])

    """

    if netlist is None or parameter_values is None or experiment is None:
        raise Exception("Please supply a netlist, paramater_values, and experiment")

    if manager == "casadi":
        rm = lp.CasadiManager()
    elif manager == "ray":
        rm = lp.RayManager()
    else:
        rm = lp.CasadiManager()
        lp.logger.notice("manager instruction not supported, using default")

    output = rm.solve(
        netlist=netlist,
        sim_func=sim_func,
        parameter_values=parameter_values,
        experiment=experiment,
        output_variables=output_variables,
        inputs=inputs,
        nproc=nproc,
        initial_soc=initial_soc,
        setup_only=False,
    )
    return output

utils

add_events_to_model(model)

Convert model events into variables to be evaluated in the solver step.

Parameters:

Name Type Description Default
model pybamm.lithium_ion.BaseModel

The PyBaMM model to solve.

required

Returns:

Type Description
pybamm.lithium_ion.BaseModel

The PyBaMM model to solve with events added as variables.

Source code in liionpack/utils.py
def add_events_to_model(model):
    """
    Convert model events into variables to be evaluated in the solver step.

    Args:
        model (pybamm.lithium_ion.BaseModel):
            The PyBaMM model to solve.

    Returns:
        pybamm.lithium_ion.BaseModel:
            The PyBaMM model to solve with events added as variables.

    """
    for event in model.events:
        model.variables.update({"Event: " + event.name: event.expression})
    return model

build_inputs_dict(I_batt, inputs, updated_inputs)

Function to convert inputs and external_variable arrays to list of dicts As expected by the casadi solver. These are then converted back for mapped solving but stored individually on each returned solution. Can probably remove this process later

Parameters:

Name Type Description Default
I_batt np.ndarray

The input current for each battery.

required
inputs dict

A dictionary with key of each input and value an array of input values for each battery.

required
updated_inputs dict

A dictionary with key of each updated input and value an array of variable values for each battery.

required

Returns:

Type Description
list

each element of the list is an inputs dictionary corresponding to each battery.

Source code in liionpack/utils.py
def build_inputs_dict(I_batt, inputs, updated_inputs):
    """
    Function to convert inputs and external_variable arrays to list of dicts
    As expected by the casadi solver. These are then converted back for mapped
    solving but stored individually on each returned solution.
    Can probably remove this process later

    Args:
        I_batt (np.ndarray):
            The input current for each battery.
        inputs (dict):
            A dictionary with key of each input and value an array of input
            values for each battery.
        updated_inputs (dict):
            A dictionary with key of each updated input and value an array
            of variable values for each battery.

    Returns:
        list:
            each element of the list is an inputs dictionary corresponding to each
            battery.


    """
    inputs_dict = {}
    current_dict = {"Current function [A]": I_batt}
    inputs_dict.update(current_dict)
    if inputs is not None:
        inputs_dict.update(inputs)
    if updated_inputs is not None:
        inputs_dict.update(updated_inputs)
    inputs_dict = _convert_dict_to_list_of_dict(inputs_dict)
    return inputs_dict

interp_current(df)

Returns an interpolation function for current w.r.t time

Parameters:

Name Type Description Default
df pandas.DataFrame or Dict

Contains data for 'Time' and 'Cells Total Current' from which to construct an interpolant function

required

Returns:

Type Description
function

interpolant function of total cell current with time.

Source code in liionpack/utils.py
def interp_current(df):
    """
    Returns an interpolation function for current w.r.t time

    Args:
        df (pandas.DataFrame or Dict):
            Contains data for 'Time' and 'Cells Total Current' from which to
            construct an interpolant function

    Returns:
        function:
            interpolant function of total cell current with time.

    """
    t = df["Time"]
    I = df["Cells Total Current"]
    f = interp1d(t, I)
    return f

save_to_csv(output, path='./csv-results')

Save simulation output to a CSV file for each output variable.

Parameters

output : dict Simulation output dictionary. path : str Folder path for saving the CSV files. Default path is a folder named csv-results in the current directory.

Returns
CSV files written to the specified path. Each file represents a single
output variable.
Source code in liionpack/utils.py
def save_to_csv(output, path="./csv-results"):
    """
    Save simulation output to a CSV file for each output variable.

    Parameters
    ----------
    output : dict
        Simulation output dictionary.
    path : str
        Folder path for saving the CSV files. Default path is a folder named
        `csv-results` in the current directory.

    Returns
    -------
        CSV files written to the specified path. Each file represents a single
        output variable.
    """

    # Create folder path for saving files
    path = pathlib.Path(path)
    path.mkdir(exist_ok=True)

    # Save simulation output to CSV files
    for k, v in output.items():
        filename = k.replace(" ", "_") + ".csv"
        np.savetxt(path / filename, v, delimiter=", ")

save_to_npy(output, path='./npy-results')

Save simulation output to NumPy .npy files where each file represents an output variable.

Parameters

output : dict Simulation output dictionary. path : str Folder path where the .npy files are saved. Default path is a folder named npy-results located in the current directory.

Returns
NumPy `.npy` files written to the specified path. Each file represents
a single output variable.
Source code in liionpack/utils.py
def save_to_npy(output, path="./npy-results"):
    """
    Save simulation output to NumPy `.npy` files where each file represents an
    output variable.

    Parameters
    ----------
    output : dict
        Simulation output dictionary.
    path : str
        Folder path where the `.npy` files are saved. Default path is a folder
        named `npy-results` located in the current directory.

    Returns
    -------
        NumPy `.npy` files written to the specified path. Each file represents
        a single output variable.
    """

    # Create folder path for saving files
    path = pathlib.Path(path)
    path.mkdir(exist_ok=True)

    # Save simulation output to npy files
    for k, v in output.items():
        filename = k.replace(" ", "_") + ".npy"
        np.save(path / filename, v)

save_to_npzcomp(output, path='.')

Save simulation output to a compressed NumPy output.npz file. The saved file is a dictionary-like object where each key represents a simulation output variable.

Parameters

output : dict Simulation output dictionary. path : str Path where the output.npz file is saved. Default path is the current directory.

Returns
A compressed NumPy `.npz` file named `output.npz` written to the
specified path. The file is a dictionary-like object where each key
has the same name as the simulation output variable.
Source code in liionpack/utils.py
def save_to_npzcomp(output, path="."):
    """
    Save simulation output to a compressed NumPy `output.npz` file. The saved
    file is a dictionary-like object where each key represents a simulation
    output variable.

    Parameters
    ----------
    output : dict
        Simulation output dictionary.
    path : str
        Path where the `output.npz` file is saved. Default path is the current
        directory.

    Returns
    -------
        A compressed NumPy `.npz` file named `output.npz` written to the
        specified path. The file is a dictionary-like object where each key
        has the same name as the simulation output variable.
    """

    # Create a path for saving the file
    path = pathlib.Path(path)
    path.mkdir(exist_ok=True)

    # Save simulation output to a compressed npz file
    filename = "output.npz"
    np.savez_compressed(path / filename, **output)