|
| 1 | +# |
| 2 | +# A model to calculate electrode-specific SOH, adapted for composite electrodes |
| 3 | +# |
| 4 | +import pybamm |
| 5 | + |
| 6 | +from .util import check_if_composite |
| 7 | + |
| 8 | + |
| 9 | +def _get_stoich_variables(options): |
| 10 | + variables = { |
| 11 | + "x_100_1": pybamm.Variable("x_100_1"), |
| 12 | + "y_100_1": pybamm.Variable("y_100_1"), |
| 13 | + "x_0_1": pybamm.Variable("x_0_1"), |
| 14 | + "y_0_1": pybamm.Variable("y_0_1"), |
| 15 | + "x_init_1": pybamm.Variable("x_init_1"), |
| 16 | + "y_init_1": pybamm.Variable("y_init_1"), |
| 17 | + } |
| 18 | + is_positive_composite = check_if_composite(options, "positive") |
| 19 | + is_negative_composite = check_if_composite(options, "negative") |
| 20 | + if is_positive_composite: |
| 21 | + variables["y_100_2"] = pybamm.Variable("y_100_2") |
| 22 | + variables["y_0_2"] = pybamm.Variable("y_0_2") |
| 23 | + variables["y_init_2"] = pybamm.Variable("y_init_2") |
| 24 | + if is_negative_composite: |
| 25 | + variables["x_100_2"] = pybamm.Variable("x_100_2") |
| 26 | + variables["x_0_2"] = pybamm.Variable("x_0_2") |
| 27 | + variables["x_init_2"] = pybamm.Variable("x_init_2") |
| 28 | + return variables |
| 29 | + |
| 30 | + |
| 31 | +def _get_initial_conditions(options, soc_init): |
| 32 | + variables = _get_stoich_variables(options) |
| 33 | + ics = {} |
| 34 | + for name, var in variables.items(): |
| 35 | + if "100" in name and "x" in name: |
| 36 | + ics[var] = 0.8 |
| 37 | + elif "0" in name and "x" in name: |
| 38 | + ics[var] = 0.2 |
| 39 | + elif "100" in name and "y" in name: |
| 40 | + ics[var] = 0.8 |
| 41 | + elif "0" in name and "y" in name: |
| 42 | + ics[var] = 0.2 |
| 43 | + elif "init" in name and "x" in name: |
| 44 | + ics[var] = soc_init |
| 45 | + elif "init" in name and "y" in name: |
| 46 | + ics[var] = 1 - soc_init |
| 47 | + return ics |
| 48 | + |
| 49 | + |
| 50 | +def _get_direction(electrode): |
| 51 | + if electrode == "positive": |
| 52 | + return pybamm.Scalar(-1) |
| 53 | + else: |
| 54 | + return pybamm.Scalar(1) |
| 55 | + |
| 56 | + |
| 57 | +def _get_prefix(electrode): |
| 58 | + if electrode == "positive": |
| 59 | + return "y" |
| 60 | + else: |
| 61 | + return "x" |
| 62 | + |
| 63 | + |
| 64 | +def _get_electrode_capacity_equation(options, electrode): |
| 65 | + prefix = _get_prefix(electrode) |
| 66 | + e = electrode[0] |
| 67 | + i_am_composite = check_if_composite(options, electrode) |
| 68 | + stoich_variables = _get_stoich_variables(options) |
| 69 | + direction = _get_direction(electrode) |
| 70 | + Q_1 = pybamm.InputParameter(f"Q_{e}_1") |
| 71 | + Q = ( |
| 72 | + direction |
| 73 | + * (stoich_variables[f"{prefix}_100_1"] - stoich_variables[f"{prefix}_0_1"]) |
| 74 | + * Q_1 |
| 75 | + ) |
| 76 | + if i_am_composite: |
| 77 | + Q_2 = pybamm.InputParameter(f"Q_{e}_2") |
| 78 | + Q += ( |
| 79 | + direction |
| 80 | + * (stoich_variables[f"{prefix}_100_2"] - stoich_variables[f"{prefix}_0_2"]) |
| 81 | + * Q_2 |
| 82 | + ) |
| 83 | + return Q |
| 84 | + |
| 85 | + |
| 86 | +def _get_cyclable_lithium_equation(options, soc="100"): |
| 87 | + x_soc_1 = pybamm.Variable(f"x_{soc}_1") |
| 88 | + y_soc_1 = pybamm.Variable(f"y_{soc}_1") |
| 89 | + Q_n_1 = pybamm.InputParameter("Q_n_1") |
| 90 | + Q_p_1 = pybamm.InputParameter("Q_p_1") |
| 91 | + lithium_primary_phases = Q_n_1 * x_soc_1 + Q_p_1 * y_soc_1 |
| 92 | + lithium_secondary_phases = 0.0 |
| 93 | + is_positive_composite = check_if_composite(options, "positive") |
| 94 | + is_negative_composite = check_if_composite(options, "negative") |
| 95 | + if is_positive_composite: |
| 96 | + Q_p_2 = pybamm.InputParameter("Q_p_2") |
| 97 | + y_soc_2 = pybamm.Variable(f"y_{soc}_2") |
| 98 | + lithium_secondary_phases += Q_p_2 * y_soc_2 |
| 99 | + if is_negative_composite: |
| 100 | + Q_n_2 = pybamm.InputParameter("Q_n_2") |
| 101 | + x_soc_2 = pybamm.Variable(f"x_{soc}_2") |
| 102 | + lithium_secondary_phases += Q_n_2 * x_soc_2 |
| 103 | + return lithium_primary_phases + lithium_secondary_phases |
| 104 | + |
| 105 | + |
| 106 | +class ElectrodeSOHComposite(pybamm.BaseModel): |
| 107 | + """Model to calculate electrode-specific SOH for a cell with composite electrodes, adapted from |
| 108 | + :footcite:t:`Mohtat2019`. |
| 109 | + This model is mainly for internal use, to calculate summary variables in a |
| 110 | + simulation. |
| 111 | +
|
| 112 | + Subscript w indicates working electrode and subscript c indicates counter electrode. |
| 113 | + """ |
| 114 | + |
| 115 | + def __init__( |
| 116 | + self, options, name="ElectrodeSOH model", initialization_method="voltage" |
| 117 | + ): |
| 118 | + pybamm.citations.register("Mohtat2019") |
| 119 | + super().__init__(name) |
| 120 | + param = pybamm.LithiumIonParameters(options) |
| 121 | + |
| 122 | + # Start by just assuming known value is cyclable lithium (and solve all at once) |
| 123 | + Q_Li = pybamm.InputParameter("Q_Li") |
| 124 | + is_negative_composite = check_if_composite(options, "negative") |
| 125 | + is_positive_composite = check_if_composite(options, "positive") |
| 126 | + variables = _get_stoich_variables(options) |
| 127 | + x_100_1 = variables["x_100_1"] |
| 128 | + y_100_1 = variables["y_100_1"] |
| 129 | + x_0_1 = variables["x_0_1"] |
| 130 | + y_0_1 = variables["y_0_1"] |
| 131 | + V_max = param.voltage_high_cut |
| 132 | + V_min = param.voltage_low_cut |
| 133 | + if is_negative_composite: |
| 134 | + x_100_2 = variables["x_100_2"] |
| 135 | + x_0_2 = variables["x_0_2"] |
| 136 | + self.algebraic[x_100_2] = param.n.prim.U( |
| 137 | + x_100_2, param.T_ref |
| 138 | + ) - param.n.sec.U(x_100_1, param.T_ref) |
| 139 | + self.algebraic[x_0_2] = param.n.prim.U(x_0_2, param.T_ref) - param.n.sec.U( |
| 140 | + x_0_1, param.T_ref |
| 141 | + ) |
| 142 | + if is_positive_composite: |
| 143 | + y_100_2 = variables["y_100_2"] |
| 144 | + y_0_2 = variables["y_0_2"] |
| 145 | + self.algebraic[y_100_2] = param.p.prim.U( |
| 146 | + y_100_2, param.T_ref |
| 147 | + ) - param.p.sec.U(y_100_1, param.T_ref) |
| 148 | + self.algebraic[y_0_2] = param.p.prim.U(y_0_2, param.T_ref) - param.p.sec.U( |
| 149 | + y_0_1, param.T_ref |
| 150 | + ) |
| 151 | + self.algebraic[x_100_1] = ( |
| 152 | + param.p.prim.U(y_100_1, param.T_ref) |
| 153 | + - param.n.prim.U(x_100_1, param.T_ref) |
| 154 | + - V_max |
| 155 | + ) |
| 156 | + self.algebraic[x_0_1] = ( |
| 157 | + param.p.prim.U(y_0_1, param.T_ref) |
| 158 | + - param.n.prim.U(x_0_1, param.T_ref) |
| 159 | + - V_min |
| 160 | + ) |
| 161 | + # arbitrary choice: use y_0_1 for the capacity equation |
| 162 | + self.algebraic[y_0_1] = _get_electrode_capacity_equation( |
| 163 | + options, "positive" |
| 164 | + ) - _get_electrode_capacity_equation(options, "negative") |
| 165 | + self.algebraic[y_100_1] = Q_Li - _get_cyclable_lithium_equation(options) |
| 166 | + |
| 167 | + x_init_1 = variables["x_init_1"] |
| 168 | + y_init_1 = variables["y_init_1"] |
| 169 | + if initialization_method == "voltage": |
| 170 | + V_init = pybamm.InputParameter("V_init") |
| 171 | + self.algebraic[x_init_1] = ( |
| 172 | + param.p.prim.U(y_init_1, param.T_ref) |
| 173 | + - param.n.prim.U(x_init_1, param.T_ref) |
| 174 | + - V_init |
| 175 | + ) |
| 176 | + self.algebraic[y_init_1] = ( |
| 177 | + _get_cyclable_lithium_equation(options, "init") - Q_Li |
| 178 | + ) |
| 179 | + elif initialization_method == "SOC": |
| 180 | + soc_init = pybamm.InputParameter("SOC_init") |
| 181 | + negative_soc = x_init_1 * pybamm.InputParameter("Q_n_1") |
| 182 | + if is_negative_composite: |
| 183 | + x_init_2 = variables["x_init_2"] |
| 184 | + negative_soc += x_init_2 * pybamm.InputParameter("Q_n_2") |
| 185 | + |
| 186 | + negative_0_soc = x_0_1 * pybamm.InputParameter("Q_n_1") |
| 187 | + if is_negative_composite: |
| 188 | + negative_0_soc += x_0_2 * pybamm.InputParameter("Q_n_2") |
| 189 | + |
| 190 | + negative_100_soc = x_100_1 * pybamm.InputParameter("Q_n_1") |
| 191 | + if is_negative_composite: |
| 192 | + negative_100_soc += x_100_2 * pybamm.InputParameter("Q_n_2") |
| 193 | + self.algebraic[x_init_1] = ( |
| 194 | + (negative_soc - negative_0_soc) / (negative_100_soc - negative_0_soc) |
| 195 | + ) - soc_init |
| 196 | + self.algebraic[y_init_1] = ( |
| 197 | + _get_cyclable_lithium_equation(options, "init") - Q_Li |
| 198 | + ) |
| 199 | + else: |
| 200 | + raise ValueError("Invalid initialization method") |
| 201 | + # Add voltage equations for secondary phases (init) |
| 202 | + if is_positive_composite: |
| 203 | + y_init_2 = variables["y_init_2"] |
| 204 | + self.algebraic[y_init_2] = param.p.prim.U( |
| 205 | + y_init_1, param.T_ref |
| 206 | + ) - param.p.sec.U(y_init_2, param.T_ref) |
| 207 | + if is_negative_composite: |
| 208 | + x_init_2 = variables["x_init_2"] |
| 209 | + self.algebraic[x_init_2] = param.n.prim.U( |
| 210 | + x_init_1, param.T_ref |
| 211 | + ) - param.n.sec.U(x_init_2, param.T_ref) |
| 212 | + |
| 213 | + self.variables.update(variables) |
| 214 | + if initialization_method == "SOC": |
| 215 | + soc_init = pybamm.InputParameter("SOC_init") |
| 216 | + else: |
| 217 | + soc_init = 0.5 |
| 218 | + self.initial_conditions.update(_get_initial_conditions(options, soc_init)) |
| 219 | + |
| 220 | + @property |
| 221 | + def default_solver(self): |
| 222 | + # Use AlgebraicSolver as CasadiAlgebraicSolver gives unnecessary warnings |
| 223 | + return pybamm.AlgebraicSolver(tol=1e-7) |
| 224 | + |
| 225 | + |
| 226 | +def get_initial_stoichiometries_composite( |
| 227 | + initial_value, |
| 228 | + parameter_values, |
| 229 | + param=None, |
| 230 | + options=None, |
| 231 | + tol=1e-6, |
| 232 | + inputs=None, |
| 233 | + known_value="cyclable lithium capacity", |
| 234 | + **kwargs, |
| 235 | +): |
| 236 | + """ |
| 237 | + Get the minimum and maximum stoichiometries from the parameter values |
| 238 | +
|
| 239 | + Parameters |
| 240 | + ---------- |
| 241 | + parameter_values : pybamm.ParameterValues |
| 242 | + The parameter values to use in the calculation |
| 243 | + options : dict, optional |
| 244 | + A dictionary of options to be passed to the parameters, see |
| 245 | + :class:`pybamm.BatteryModelOptions`. |
| 246 | + If None, the default is used: {"working electrode": "positive"} |
| 247 | + """ |
| 248 | + inputs = inputs or {} |
| 249 | + if known_value != "cyclable lithium capacity": |
| 250 | + raise ValueError( |
| 251 | + "Only `cyclable lithium capacity` is supported for composite electrodes" |
| 252 | + ) |
| 253 | + |
| 254 | + Q_n_1 = parameter_values.evaluate(param.n.prim.Q_init, inputs=inputs) |
| 255 | + Q_p_1 = parameter_values.evaluate(param.p.prim.Q_init, inputs=inputs) |
| 256 | + is_positive_composite = check_if_composite(options, "positive") |
| 257 | + is_negative_composite = check_if_composite(options, "negative") |
| 258 | + Qs = { |
| 259 | + "Q_n_1": Q_n_1, |
| 260 | + "Q_p_1": Q_p_1, |
| 261 | + } |
| 262 | + if is_positive_composite: |
| 263 | + Q_p_2 = parameter_values.evaluate(param.p.sec.Q_init, inputs=inputs) |
| 264 | + Qs["Q_p_2"] = Q_p_2 |
| 265 | + if is_negative_composite: |
| 266 | + Q_n_2 = parameter_values.evaluate(param.n.sec.Q_init, inputs=inputs) |
| 267 | + Qs["Q_n_2"] = Q_n_2 |
| 268 | + |
| 269 | + Q_Li = parameter_values.evaluate(param.Q_Li_particles_init, inputs=inputs) |
| 270 | + all_inputs = {**inputs, **Qs, "Q_Li": Q_Li} |
| 271 | + # Solve the model and check outputs |
| 272 | + if isinstance(initial_value, str) and initial_value.endswith("V"): |
| 273 | + all_inputs["V_init"] = float(initial_value[:-1]) |
| 274 | + initialization_method = "voltage" |
| 275 | + elif isinstance(initial_value, float) and initial_value >= 0 and initial_value <= 1: |
| 276 | + initialization_method = "SOC" |
| 277 | + all_inputs["SOC_init"] = initial_value |
| 278 | + else: |
| 279 | + raise ValueError("Invalid initial value") |
| 280 | + model = ElectrodeSOHComposite(options, initialization_method=initialization_method) |
| 281 | + sim = pybamm.Simulation(model, parameter_values=parameter_values) |
| 282 | + sol = sim.solve([0, 1], inputs=all_inputs) |
| 283 | + return {var: sol[var].entries[0] for var in model.variables.keys()} |
0 commit comments