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) |
|
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) |
|
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)