• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TRI-AMDD / mpet / 8476080312

29 Mar 2024 01:46AM UTC coverage: 55.461% (-7.2%) from 62.648%
8476080312

Pull #126

github

d-cogswell
Adds a benchmark section to docs.
Pull Request #126: v1.0.0

2351 of 4239 relevant lines covered (55.46%)

2.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.53
/mpet/mod_electrodes.py
1
"""These models define individual particles of active material.
2

3
This includes the equations for both 1-parameter models and 2-parameters models defining
4
 - mass conservation (concentration evolution)
5
 - reaction rate at the surface of the particles
6
In each model class it has options for different types of particles:
7
 - homogeneous
8
 - Fick-like diffusion
9
 - Cahn-Hilliard (with reaction boundary condition)
10
 - Allen-Cahn (with reaction throughout the particle)
11

12
These models can be instantiated from the mod_cell module to simulate various types of active
13
materials within a battery electrode.
14
"""
15

16

17
import daetools.pyDAE as dae
4✔
18

19
import numpy as np
4✔
20
import scipy.sparse as sprs
4✔
21
import scipy.interpolate as sintrp
4✔
22

23
import mpet.geometry as geo
4✔
24
import mpet.ports as ports
4✔
25
import mpet.props_am as props_am
4✔
26
import mpet.utils as utils
4✔
27
from mpet.daeVariableTypes import mole_frac_t
4✔
28

29

30
class Mod2var(dae.daeModel):
4✔
31
    def __init__(self, config, trode, vInd, pInd,
4✔
32
                 Name, Parent=None, Description=""):
33
        super().__init__(Name, Parent, Description)
4✔
34

35
        self.config = config
4✔
36
        self.trode = trode
4✔
37
        self.ind = (vInd, pInd)
4✔
38

39
        # Domain
40
        self.Dmn = dae.daeDomain("discretizationDomain", self, dae.unit(),
4✔
41
                                 "discretization domain")
42

43
        # Variables
44
        self.c1 = dae.daeVariable(
4✔
45
            "c1", mole_frac_t, self,
46
            "Concentration in 'layer' 1 of active particle", [self.Dmn])
47
        self.c2 = dae.daeVariable(
4✔
48
            "c2", mole_frac_t, self,
49
            "Concentration in 'layer' 2 of active particle", [self.Dmn])
50
        self.cbar = dae.daeVariable(
4✔
51
            "cbar", mole_frac_t, self,
52
            "Average concentration in active particle")
53
        self.c1bar = dae.daeVariable(
4✔
54
            "c1bar", mole_frac_t, self,
55
            "Average concentration in 'layer' 1 of active particle")
56
        self.c2bar = dae.daeVariable(
4✔
57
            "c2bar", mole_frac_t, self,
58
            "Average concentration in 'layer' 2 of active particle")
59
        self.dcbardt = dae.daeVariable("dcbardt", dae.no_t, self, "Rate of particle filling")
4✔
60
        if self.get_trode_param("type") not in ["ACR2"]:
4✔
61
            self.Rxn1 = dae.daeVariable("Rxn1", dae.no_t, self, "Rate of reaction 1")
4✔
62
            self.Rxn2 = dae.daeVariable("Rxn2", dae.no_t, self, "Rate of reaction 2")
4✔
63
        else:
64
            self.Rxn1 = dae.daeVariable("Rxn1", dae.no_t, self, "Rate of reaction 1", [self.Dmn])
×
65
            self.Rxn2 = dae.daeVariable("Rxn2", dae.no_t, self, "Rate of reaction 2", [self.Dmn])
×
66

67
        # Get reaction rate function
68
        rxnType = config[trode, "rxnType"]
4✔
69
        self.calc_rxn_rate = utils.import_function(config[trode, "rxnType_filename"],
4✔
70
                                                   rxnType,
71
                                                   f"mpet.electrode.reactions.{rxnType}")
72

73
        # Ports
74
        self.portInLyte = ports.portFromElyte(
4✔
75
            "portInLyte", dae.eInletPort, self, "Inlet port from electrolyte")
76
        self.portInBulk = ports.portFromBulk(
4✔
77
            "portInBulk", dae.eInletPort, self,
78
            "Inlet port from e- conducting phase")
79
        self.phi_lyte = self.portInLyte.phi_lyte
4✔
80
        self.c_lyte = self.portInLyte.c_lyte
4✔
81
        self.phi_m = self.portInBulk.phi_m
4✔
82
        if self.config[f"simInterface_{trode}"]:
4✔
83
            self.portOutParticle = ports.portFromParticle(
×
84
                "portOutParticle", dae.eOutletPort, self,
85
                "Outlet port from particle")
86

87
    def get_trode_param(self, item):
4✔
88
        """
89
        Shorthand to retrieve electrode-specific value
90
        """
91
        value = self.config[self.trode, item]
4✔
92
        # check if it is a particle-specific parameter
93
        if item in self.config.params_per_particle:
4✔
94
            value = value[self.ind]
4✔
95
        return value
4✔
96

97
    def DeclareEquations(self):
4✔
98
        dae.daeModel.DeclareEquations(self)
4✔
99
        N = self.get_trode_param("N")  # number of grid points in particle
4✔
100
        T = self.config["T"]  # nondimensional temperature
4✔
101
        r_vec, volfrac_vec = geo.get_unit_solid_discr(self.get_trode_param('shape'), N)
4✔
102

103
        # Prepare noise
104
        self.noise1 = self.noise2 = None
4✔
105
        if self.get_trode_param("noise"):
4✔
106
            numnoise = self.get_trode_param("numnoise")
4✔
107
            noise_prefac = self.get_trode_param("noise_prefac")
4✔
108
            tvec = np.linspace(0., 1.05*self.config["tend"], numnoise)
4✔
109
            noise_data1 = noise_prefac*np.random.randn(numnoise, N)
4✔
110
            noise_data2 = noise_prefac*np.random.randn(numnoise, N)
4✔
111
            self.noise1 = sintrp.interp1d(tvec, noise_data1, axis=0,
4✔
112
                                          bounds_error=False, fill_value=0.)
113
            self.noise2 = sintrp.interp1d(tvec, noise_data2, axis=0,
4✔
114
                                          bounds_error=False, fill_value=0.)
115
        noises = (self.noise1, self.noise2)
4✔
116

117
        # Figure out mu_O, mu of the oxidized state
118
        mu_O, act_lyte = calc_mu_O(
4✔
119
            self.c_lyte(), self.phi_lyte(), self.phi_m(), T,
120
            self.config, self.trode)
121

122
        # Define average filling fractions in particle
123
        eq1 = self.CreateEquation("c1bar")
4✔
124
        eq2 = self.CreateEquation("c2bar")
4✔
125
        eq1.Residual = self.c1bar()
4✔
126
        eq2.Residual = self.c2bar()
4✔
127
        for k in range(N):
4✔
128
            eq1.Residual -= self.c1(k) * volfrac_vec[k]
4✔
129
            eq2.Residual -= self.c2(k) * volfrac_vec[k]
4✔
130
        eq = self.CreateEquation("cbar")
4✔
131
        eq.Residual = self.cbar() - (0.5*self.c1bar() + 0.5*self.c2bar())
4✔
132

133
        # Define average rate of filling of particle
134
        eq = self.CreateEquation("dcbardt")
4✔
135
        eq.Residual = self.dcbardt()
4✔
136
        for k in range(N):
4✔
137
            eq.Residual -= (0.5*self.c1.dt(k) + 0.5*self.c2.dt(k)) * volfrac_vec[k]
4✔
138

139
        c1 = np.empty(N, dtype=object)
4✔
140
        c2 = np.empty(N, dtype=object)
4✔
141
        c1[:] = [self.c1(k) for k in range(N)]
4✔
142
        c2[:] = [self.c2(k) for k in range(N)]
4✔
143
        if self.get_trode_param("type") in ["diffn2", "CHR2", "ACR2"]:
4✔
144
            # Equations for 1D particles of 2 field variables
145
            self.sld_dynamics_1D2var(c1, c2, mu_O, act_lyte, noises)
4✔
146
        elif self.get_trode_param("type") in ["homog2", "homog2_sdn"]:
4✔
147
            # Equations for 0D particles of 2 field variables
148
            self.sld_dynamics_0D2var(c1, c2, mu_O, act_lyte, noises)
4✔
149

150
        for eq in self.Equations:
4✔
151
            eq.CheckUnitsConsistency = False
4✔
152

153
        if self.config[f"simInterface_{self.trode}"]:
4✔
154
            # Output dcbardt to interface region
155
            eq = self.CreateEquation("particle_to_interface_dcbardt")
×
156
            eq.Residual = self.portOutParticle.dcbardt() - self.dcbardt()
×
157

158
    def sld_dynamics_0D2var(self, c1, c2, muO, act_lyte, noises):
4✔
159
        T = self.config["T"]
4✔
160
        c1_surf = c1
4✔
161
        c2_surf = c2
4✔
162
        (mu1R_surf, mu2R_surf), (act1R_surf, act2R_surf) = calc_muR(
4✔
163
            (c1_surf, c2_surf), (self.c1bar(), self.c2bar()), self.config,
164
            self.trode, self.ind)
165
        eta1 = calc_eta(mu1R_surf, muO)
4✔
166
        eta2 = calc_eta(mu2R_surf, muO)
4✔
167
        eta1_eff = eta1 + self.Rxn1()*self.get_trode_param("Rfilm")
4✔
168
        eta2_eff = eta2 + self.Rxn2()*self.get_trode_param("Rfilm")
4✔
169
        noise1, noise2 = noises
4✔
170
        if self.get_trode_param("noise"):
4✔
171
            eta1_eff += noise1(dae.Time().Value)
4✔
172
            eta2_eff += noise2(dae.Time().Value)
4✔
173
        Rxn1 = self.calc_rxn_rate(
4✔
174
            eta1_eff, c1_surf, self.c_lyte(), self.get_trode_param("k0"),
175
            self.get_trode_param("E_A"), T, act1R_surf, act_lyte,
176
            self.get_trode_param("lambda"), self.get_trode_param("alpha"))
177
        Rxn2 = self.calc_rxn_rate(
4✔
178
            eta2_eff, c2_surf, self.c_lyte(), self.get_trode_param("k0"),
179
            self.get_trode_param("E_A"), T, act2R_surf, act_lyte,
180
            self.get_trode_param("lambda"), self.get_trode_param("alpha"))
181
        eq1 = self.CreateEquation("Rxn1")
4✔
182
        eq2 = self.CreateEquation("Rxn2")
4✔
183
        eq1.Residual = self.Rxn1() - Rxn1[0]
4✔
184
        eq2.Residual = self.Rxn2() - Rxn2[0]
4✔
185

186
        eq1 = self.CreateEquation("dc1sdt")
4✔
187
        eq2 = self.CreateEquation("dc2sdt")
4✔
188
        eq1.Residual = self.c1.dt(0) - self.get_trode_param("delta_L")*Rxn1[0]
4✔
189
        eq2.Residual = self.c2.dt(0) - self.get_trode_param("delta_L")*Rxn2[0]
4✔
190

191
    def sld_dynamics_1D2var(self, c1, c2, muO, act_lyte, noises):
4✔
192
        N = self.get_trode_param("N")
4✔
193
        T = self.config["T"]
4✔
194
        # Equations for concentration evolution
195
        # Mass matrix, M, where M*dcdt = RHS, where c and RHS are vectors
196
        Mmat = get_Mmat(self.get_trode_param('shape'), N)
4✔
197
        dr, edges = geo.get_dr_edges(self.get_trode_param('shape'), N)
4✔
198

199
        # Get solid particle chemical potential, overpotential, reaction rate
200
        if self.get_trode_param("type") in ["diffn2", "CHR2"]:
4✔
201
            (mu1R, mu2R), (act1R, act2R) = calc_muR(
4✔
202
                (c1, c2), (self.c1bar(), self.c2bar()), self.config, self.trode, self.ind)
203
            c1_surf = c1[-1]
4✔
204
            c2_surf = c2[-1]
4✔
205
            mu1R_surf, act1R_surf = mu1R[-1], act1R[-1]
4✔
206
            mu2R_surf, act2R_surf = mu2R[-1], act2R[-1]
4✔
207
            eta1 = calc_eta(mu1R_surf, muO)
4✔
208
            eta2 = calc_eta(mu2R_surf, muO)
4✔
209
        if self.get_trode_param("type") in ["ACR2"]:
4✔
210
            c1_surf = c1
×
211
            c2_surf = c2
×
212
            (mu1R, mu2R), (act1R, act2R) = calc_muR(
×
213
                (c1, c2), (self.c1bar(), self.c2bar()), self.config, self.trode, self.ind)
214
            mu1R_surf, act1R_surf = mu1R, act1R
×
215
            mu2R_surf, act2R_surf = mu2R, act2R
×
216
            eta1 = calc_eta(mu1R, muO)
×
217
            eta2 = calc_eta(mu2R, muO)
×
218
            eta1_eff = np.array([eta1[i]
×
219
                                 + self.Rxn1(i)*self.get_trode_param("Rfilm") for i in range(N)])
220
            eta2_eff = np.array([eta2[i]
×
221
                                 + self.Rxn2(i)*self.get_trode_param("Rfilm") for i in range(N)])
222
        else:
223
            eta1_eff = eta1 + self.Rxn1()*self.get_trode_param("Rfilm")
4✔
224
            eta2_eff = eta2 + self.Rxn2()*self.get_trode_param("Rfilm")
4✔
225
        Rxn1 = self.calc_rxn_rate(
4✔
226
            eta1_eff, c1_surf, self.c_lyte(), self.get_trode_param("k0"),
227
            self.get_trode_param("E_A"), T, act1R_surf, act_lyte,
228
            self.get_trode_param("lambda"), self.get_trode_param("alpha"))
229
        Rxn2 = self.calc_rxn_rate(
4✔
230
            eta2_eff, c2_surf, self.c_lyte(), self.get_trode_param("k0"),
231
            self.get_trode_param("E_A"), T, act2R_surf, act_lyte,
232
            self.get_trode_param("lambda"), self.get_trode_param("alpha"))
233
        if self.get_trode_param("type") in ["ACR2"]:
4✔
234
            for i in range(N):
×
235
                eq1 = self.CreateEquation("Rxn1_{i}".format(i=i))
×
236
                eq2 = self.CreateEquation("Rxn2_{i}".format(i=i))
×
237
                eq1.Residual = self.Rxn1(i) - Rxn1[i]
×
238
                eq2.Residual = self.Rxn2(i) - Rxn2[i]
×
239
        else:
240
            eq1 = self.CreateEquation("Rxn1")
4✔
241
            eq2 = self.CreateEquation("Rxn2")
4✔
242
            eq1.Residual = self.Rxn1() - Rxn1
4✔
243
            eq2.Residual = self.Rxn2() - Rxn2
4✔
244

245
        # Get solid particle fluxes (if any) and RHS
246
        if self.get_trode_param("type") in ["ACR2"]:
4✔
247
            RHS1 = 0.5*np.array([self.get_trode_param("delta_L")*self.Rxn1(i) for i in range(N)])
×
248
            RHS2 = 0.5*np.array([self.get_trode_param("delta_L")*self.Rxn2(i) for i in range(N)])
×
249
        elif self.get_trode_param("type") in ["diffn2", "CHR2"]:
4✔
250
            # Positive reaction (reduction, intercalation) is negative
251
            # flux of Li at the surface.
252
            Flux1_bc = -0.5 * self.Rxn1()
4✔
253
            Flux2_bc = -0.5 * self.Rxn2()
4✔
254
            Dfunc_name = self.get_trode_param("Dfunc")
4✔
255
            Dfunc = utils.import_function(self.get_trode_param("Dfunc_filename"),
4✔
256
                                          Dfunc_name,
257
                                          f"mpet.electrode.diffusion.{Dfunc_name}")
258
            if self.get_trode_param("type") == "CHR2":
4✔
259
                noise1, noise2 = noises
4✔
260
                Flux1_vec, Flux2_vec = calc_flux_CHR2(
4✔
261
                    c1, c2, mu1R, mu2R, self.get_trode_param("D"), Dfunc,
262
                    self.get_trode_param("E_D"), Flux1_bc, Flux2_bc, dr, T, noise1, noise2)
263
            if self.get_trode_param("shape") == "sphere":
4✔
264
                area_vec = 4*np.pi*edges**2
4✔
265
            elif self.get_trode_param("shape") == "cylinder":
4✔
266
                area_vec = 2*np.pi*edges  # per unit height
4✔
267
            RHS1 = -np.diff(Flux1_vec * area_vec)
4✔
268
            RHS2 = -np.diff(Flux2_vec * area_vec)
4✔
269
#            kinterlayer = 1e-3
270
#            interLayerRxn = (kinterlayer * (1 - c1) * (1 - c2) * (act1R - act2R))
271
#            RxnTerm1 = -interLayerRxn
272
#            RxnTerm2 = interLayerRxn
273
            RxnTerm1 = 0
4✔
274
            RxnTerm2 = 0
4✔
275
            RHS1 += RxnTerm1
4✔
276
            RHS2 += RxnTerm2
4✔
277

278
        dc1dt_vec = np.empty(N, dtype=object)
4✔
279
        dc2dt_vec = np.empty(N, dtype=object)
4✔
280
        dc1dt_vec[0:N] = [self.c1.dt(k) for k in range(N)]
4✔
281
        dc2dt_vec[0:N] = [self.c2.dt(k) for k in range(N)]
4✔
282
        LHS1_vec = MX(Mmat, dc1dt_vec)
4✔
283
        LHS2_vec = MX(Mmat, dc2dt_vec)
4✔
284
        for k in range(N):
4✔
285
            eq1 = self.CreateEquation("dc1sdt_discr{k}".format(k=k))
4✔
286
            eq2 = self.CreateEquation("dc2sdt_discr{k}".format(k=k))
4✔
287
            eq1.Residual = LHS1_vec[k] - RHS1[k]
4✔
288
            eq2.Residual = LHS2_vec[k] - RHS2[k]
4✔
289

290

291
class Mod1var(dae.daeModel):
4✔
292
    def __init__(self, config, trode, vInd, pInd,
4✔
293
                 Name, Parent=None, Description=""):
294
        super().__init__(Name, Parent, Description)
4✔
295

296
        self.config = config
4✔
297
        self.trode = trode
4✔
298
        self.ind = (vInd, pInd)
4✔
299

300
        # Domain
301
        self.Dmn = dae.daeDomain("discretizationDomain", self, dae.unit(),
4✔
302
                                 "discretization domain")
303
        # Variables
304
        self.c = dae.daeVariable("c", mole_frac_t, self,
4✔
305
                                 "Concentration in active particle",
306
                                 [self.Dmn])
307

308
        # Creation of the ghost points to assit BC
309

310
        if self.get_trode_param("type") in ["ACR_Diff"]:
4✔
311
            self.c_left_GP = dae.daeVariable("c_left", mole_frac_t, self,
×
312
                                             "Concentration on the left side of the particle")
313
            self.c_right_GP = dae.daeVariable("c_right", mole_frac_t, self,
×
314
                                              "Concentration on the right side of the particle")
315

316
        self.cbar = dae.daeVariable(
4✔
317
            "cbar", mole_frac_t, self,
318
            "Average concentration in active particle")
319
        self.dcbardt = dae.daeVariable("dcbardt", dae.no_t, self, "Rate of particle filling")
4✔
320
        if config[trode, "type"] not in ["ACR", "ACR_Diff"]:
4✔
321
            self.Rxn = dae.daeVariable("Rxn", dae.no_t, self, "Rate of reaction")
4✔
322
        else:
323
            self.Rxn = dae.daeVariable("Rxn", dae.no_t, self, "Rate of reaction", [self.Dmn])
4✔
324

325
        # Get reaction rate function
326
        rxnType = config[trode, "rxnType"]
4✔
327
        self.calc_rxn_rate = utils.import_function(config[trode, "rxnType_filename"],
4✔
328
                                                   rxnType,
329
                                                   f"mpet.electrode.reactions.{rxnType}")
330

331
        # Ports
332
        self.portInLyte = ports.portFromElyte(
4✔
333
            "portInLyte", dae.eInletPort, self,
334
            "Inlet port from electrolyte")
335
        self.portInBulk = ports.portFromBulk(
4✔
336
            "portInBulk", dae.eInletPort, self,
337
            "Inlet port from e- conducting phase")
338
        self.phi_lyte = self.portInLyte.phi_lyte
4✔
339
        self.c_lyte = self.portInLyte.c_lyte
4✔
340
        self.phi_m = self.portInBulk.phi_m
4✔
341
        if config[f"simInterface_{trode}"]:
4✔
342
            self.portOutParticle = ports.portFromParticle(
4✔
343
                "portOutParticle", dae.eOutletPort, self,
344
                "Outlet port from particle")
345

346
    def get_trode_param(self, item):
4✔
347
        """
348
        Shorthand to retrieve electrode-specific value
349
        """
350
        value = self.config[self.trode, item]
4✔
351
        # check if it is a particle-specific parameter
352
        if item in self.config.params_per_particle:
4✔
353
            value = value[self.ind]
4✔
354
        return value
4✔
355

356
    def DeclareEquations(self):
4✔
357
        dae.daeModel.DeclareEquations(self)
4✔
358
        N = self.get_trode_param("N")  # number of grid points in particle
4✔
359
        T = self.config["T"]  # nondimensional temperature
4✔
360
        r_vec, volfrac_vec = geo.get_unit_solid_discr(self.get_trode_param('shape'), N)
4✔
361

362
        # Prepare noise
363
        self.noise = None
4✔
364
        if self.get_trode_param("noise"):
4✔
365
            numnoise = self.get_trode_param("numnoise")
4✔
366
            noise_prefac = self.get_trode_param("noise_prefac")
4✔
367
            tvec = np.linspace(0., 1.05*self.config["tend"], numnoise)
4✔
368
            noise_data = noise_prefac*np.random.randn(numnoise, N)
4✔
369
            self.noise = sintrp.interp1d(tvec, noise_data, axis=0,
4✔
370
                                         bounds_error=False, fill_value=0.)
371

372
        # Figure out mu_O, mu of the oxidized state
373
        mu_O, act_lyte = calc_mu_O(self.c_lyte(), self.phi_lyte(), self.phi_m(), T,
4✔
374
                                   self.config, self.trode)
375

376
        # Define average filling fraction in particle
377
        eq = self.CreateEquation("cbar")
4✔
378
        eq.Residual = self.cbar()
4✔
379
        for k in range(N):
4✔
380
            eq.Residual -= self.c(k) * volfrac_vec[k]
4✔
381

382
        # Define average rate of filling of particle
383
        eq = self.CreateEquation("dcbardt")
4✔
384
        eq.Residual = self.dcbardt()
4✔
385
        for k in range(N):
4✔
386
            eq.Residual -= self.c.dt(k) * volfrac_vec[k]
4✔
387

388
        c = np.empty(N, dtype=object)
4✔
389
        if self.get_trode_param("type") in ["ACR_Diff"]:
4✔
390
            ctmp = utils.get_var_vec(self.c,N)
×
391
            c = np.hstack((self.c_left_GP(),ctmp,self.c_right_GP()))
×
392
        else:
393
            c[:] = [self.c(k) for k in range(N)]
4✔
394
        if self.get_trode_param("type") in ["ACR", "ACR_Diff", "diffn", "CHR"]:
4✔
395
            # Equations for 1D particles of 1 field varible
396
            self.sld_dynamics_1D1var(c, mu_O, act_lyte, self.noise)
4✔
397
        elif self.get_trode_param("type") in ["homog", "homog_sdn"]:
4✔
398
            # Equations for 0D particles of 1 field variables
399
            self.sld_dynamics_0D1var(c, mu_O, act_lyte, self.noise)
4✔
400

401
        for eq in self.Equations:
4✔
402
            eq.CheckUnitsConsistency = False
4✔
403

404
        if self.config[f"simInterface_{self.trode}"]:
4✔
405
            # Output dcbardt to interface region
406
            eq = self.CreateEquation("particle_to_interface_dcbardt")
4✔
407
            eq.Residual = self.portOutParticle.dcbardt() - self.dcbardt()
4✔
408

409
    def sld_dynamics_0D1var(self, c, muO, act_lyte, noise):
4✔
410
        T = self.config["T"]
4✔
411
        c_surf = c
4✔
412
        muR_surf, actR_surf = calc_muR(c_surf, self.cbar(), self.config,
4✔
413
                                       self.trode, self.ind)
414
        eta = calc_eta(muR_surf, muO)
4✔
415
        eta_eff = eta + self.Rxn()*self.get_trode_param("Rfilm")
4✔
416
        if self.get_trode_param("noise"):
4✔
417
            eta_eff += noise(dae.Time().Value)
×
418
        Rxn = self.calc_rxn_rate(
4✔
419
            eta_eff, c_surf, self.c_lyte(), self.get_trode_param("k0"),
420
            self.get_trode_param("E_A"), T, actR_surf, act_lyte,
421
            self.get_trode_param("lambda"), self.get_trode_param("alpha"))
422
        eq = self.CreateEquation("Rxn")
4✔
423
        eq.Residual = self.Rxn() - Rxn[0]
4✔
424

425
        eq = self.CreateEquation("dcsdt")
4✔
426
        eq.Residual = self.c.dt(0) - self.get_trode_param("delta_L")*self.Rxn()
4✔
427

428
    def sld_dynamics_1D1var(self, c, muO, act_lyte, noise):
4✔
429
        N = self.get_trode_param("N")
4✔
430
        T = self.config["T"]
4✔
431
        # Equations for concentration evolution
432
        # Mass matrix, M, where M*dcdt = RHS, where c and RHS are vectors
433
        Mmat = get_Mmat(self.get_trode_param('shape'), N)
4✔
434
        dr, edges = geo.get_dr_edges(self.get_trode_param('shape'), N)
4✔
435

436
        # Get solid particle chemical potential, overpotential, reaction rate
437
        if self.get_trode_param("type") in ["ACR", "ACR_Diff"]:
4✔
438
            c_surf = c
4✔
439
            # surface diffusion in the ACR C3 model
440
            if self.get_trode_param("type") in ["ACR_Diff"]:
4✔
441
                dx = 1/np.size(c)
×
442
                beta_s = self.get_trode_param("beta_s")
×
443
                eqL = self.CreateEquation("leftBC")
×
444
                eqL.Residual = (c_surf[0] - c_surf[1] +
×
445
                                - dx*beta_s*(c_surf[1]+0.008)*(1-c_surf[1]-0.008))
446
                eqR = self.CreateEquation("rightBC")
×
447
                eqR.Residual = (c_surf[-1] - c_surf[-2] +
×
448
                                - dx*beta_s*(c_surf[-2]+0.008)*(1-c_surf[-2]-0.008))
449

450
        if self.get_trode_param("type") in ["ACR", "ACR_Diff"]:
4✔
451
            muR_surf, actR_surf = calc_muR(
4✔
452
                c_surf, self.cbar(), self.config, self.trode, self.ind)
453
        elif self.get_trode_param("type") in ["diffn", "CHR"]:
4✔
454
            muR, actR = calc_muR(c, self.cbar(), self.config, self.trode, self.ind)
4✔
455
            c_surf = c[-1]
4✔
456
            muR_surf = muR[-1]
4✔
457
            if actR is None:
4✔
458
                actR_surf = None
4✔
459
            else:
460
                actR_surf = actR[-1]
4✔
461
        # surface diffusion in the ACR C3 model
462
        if self.get_trode_param("type") in ["ACR_Diff"]:
4✔
463
            eta = calc_eta(muR_surf[1:-1], muO)
×
464
        else:
465
            eta = calc_eta(muR_surf, muO)
4✔
466
        if self.get_trode_param("type") in ["ACR", "ACR_Diff"]:
4✔
467
            eta_eff = np.array([eta[i] + self.Rxn(i)*self.get_trode_param("Rfilm")
4✔
468
                                for i in range(N)])
469
        else:
470
            eta_eff = eta + self.Rxn()*self.get_trode_param("Rfilm")
4✔
471
        if self.get_trode_param("type") in ["ACR_Diff"]:
4✔
472
            Rxn = self.calc_rxn_rate(
×
473
                eta_eff, c_surf[1:-1], self.c_lyte(), self.get_trode_param("k0"),
474
                self.get_trode_param("E_A"), T, actR_surf[1:-1], act_lyte,
475
                self.get_trode_param("lambda"), self.get_trode_param("alpha"))
476
        else:
477
            Rxn = self.calc_rxn_rate(
4✔
478
                eta_eff, c_surf, self.c_lyte(), self.get_trode_param("k0"),
479
                self.get_trode_param("E_A"), T, actR_surf, act_lyte,
480
                self.get_trode_param("lambda"), self.get_trode_param("alpha"))
481
        if self.get_trode_param("type") in ["ACR", "ACR_Diff"]:
4✔
482
            for i in range(N):
4✔
483
                eq = self.CreateEquation("Rxn_{i}".format(i=i))
4✔
484
                eq.Residual = self.Rxn(i) - Rxn[i]
4✔
485
        else:
486
            eq = self.CreateEquation("Rxn")
4✔
487
            eq.Residual = self.Rxn() - Rxn
4✔
488

489
        # Get solid particle fluxes (if any) and RHS
490
        if self.get_trode_param("type") in ["ACR", "ACR_Diff"]:
4✔
491
            # For ACR model localized contact loss.
492
            if self.config['localized_losses']:
4✔
493
                gamma_con = self.get_trode_param('gamma_con')
×
494
                if gamma_con == 1:
×
495
                    RHS = np.array([self.get_trode_param("delta_L")*self.Rxn(i) for i in range(N)])
×
496
                else:
497
                    N_cont = int((gamma_con)*N)-12
×
498
                    RHS = np.zeros(N, dtype=object)
×
499
                    position = int(np.random.uniform()*N)
×
500
                    # random position of contact + 6 points on the sides to facilitate wetting
501
                    if 6+position+N_cont < N-6:
×
502
                        for i in range(0,6):
×
503
                            RHS[i] = self.get_trode_param("delta_L")*self.Rxn(i)
×
504
                        for i in range(N-6,N):
×
505
                            RHS[i] = self.get_trode_param("delta_L")*self.Rxn(i)
×
506
                        for i in range(position+6,position+N_cont,1):
×
507
                            RHS[i] = self.get_trode_param("delta_L")*self.Rxn(i)
×
508
                    else:
509
                        for i in range(0,6):
×
510
                            RHS[i] = self.get_trode_param("delta_L")*self.Rxn(i)
×
511
                        for i in range(position, N):
×
512
                            RHS[i] = self.get_trode_param("delta_L")*self.Rxn(i)
×
513
                        for i in range(6,N_cont-(N-position)):
×
514
                            RHS[i] = self.get_trode_param("delta_L")*self.Rxn(i)
×
515
            else:
516
                RHS = np.array([self.get_trode_param("delta_L")*self.Rxn(i) for i in range(N)])
4✔
517

518
        elif self.get_trode_param("type") in ["diffn", "CHR"]:
4✔
519
            # Positive reaction (reduction, intercalation) is negative
520
            # flux of Li at the surface.
521
            Flux_bc = -self.Rxn()
4✔
522
            Dfunc_name = self.get_trode_param("Dfunc")
4✔
523
            Dfunc = utils.import_function(self.get_trode_param("Dfunc_filename"),
4✔
524
                                          Dfunc_name,
525
                                          f"mpet.electrode.diffusion.{Dfunc_name}")
526
            if self.get_trode_param("type") == "diffn":
4✔
527
                Flux_vec = calc_flux_diffn(c, self.get_trode_param("D"), Dfunc,
4✔
528
                                           self.get_trode_param("E_D"), Flux_bc, dr, T, noise)
529
            elif self.get_trode_param("type") == "CHR":
4✔
530
                Flux_vec = calc_flux_CHR(c, muR, self.get_trode_param("D"), Dfunc,
4✔
531
                                         self.get_trode_param("E_D"), Flux_bc, dr, T, noise)
532
            if self.get_trode_param("shape") == "sphere":
4✔
533
                area_vec = 4*np.pi*edges**2
4✔
534
            elif self.get_trode_param("shape") == "cylinder":
4✔
535
                area_vec = 2*np.pi*edges  # per unit height
4✔
536
            RHS = -np.diff(Flux_vec * area_vec)
4✔
537

538
        dcdt_vec = np.empty(N, dtype=object)
4✔
539
        dcdt_vec[0:N] = [self.c.dt(k) for k in range(N)]
4✔
540
        LHS_vec = MX(Mmat, dcdt_vec)
4✔
541
        if self.get_trode_param("type") in ["ACR_Diff"]:
4✔
542
            # surface diffusion in the ACR C3 model
543
            surf_diff_vec = calc_surf_diff(c_surf, muR_surf, self.get_trode_param("D"))
×
544
        for k in range(N):
4✔
545
            eq = self.CreateEquation("dcsdt_discr{k}".format(k=k))
4✔
546
            if self.get_trode_param("type") in ["ACR_Diff"]:
4✔
547
                eq.Residual = LHS_vec[k] - RHS[k] - surf_diff_vec[k]
×
548
            else:
549
                eq.Residual = LHS_vec[k] - RHS[k]
4✔
550

551

552
# surface diffusion in the ACR C3 model
553
def calc_surf_diff(c_surf, muR_surf, D):
4✔
554
    N_2 = np.size(c_surf)
×
555
    dxs = 1./N_2
×
556
    c_surf_long = c_surf
×
557
    c_surf_short = (c_surf_long[0:-1] + c_surf_long[1:])/2
×
558
    surf_diff = D*(np.diff(c_surf_short*(1-c_surf_short)*np.diff(muR_surf)))/(dxs**2)
×
559
    return surf_diff
×
560

561

562
def calc_eta(muR, muO):
4✔
563
    return muR - muO
4✔
564

565

566
def get_Mmat(shape, N):
4✔
567
    r_vec, volfrac_vec = geo.get_unit_solid_discr(shape, N)
4✔
568
    if shape == "C3":
4✔
569
        Mmat = sprs.eye(N, N, format="csr")
4✔
570
    elif shape in ["sphere", "cylinder"]:
4✔
571
        Rs = 1.
4✔
572
        # For discretization background, see Zeng & Bazant 2013
573
        # Mass matrix is common for each shape, diffn or CHR
574
        if shape == "sphere":
4✔
575
            Vp = 4./3. * np.pi * Rs**3
4✔
576
        elif shape == "cylinder":
4✔
577
            Vp = np.pi * Rs**2  # per unit height
4✔
578
        vol_vec = Vp * volfrac_vec
4✔
579
        M1 = sprs.diags([1./8, 3./4, 1./8], [-1, 0, 1],
4✔
580
                        shape=(N, N), format="csr")
581
        M1[1,0] = M1[-2,-1] = 1./4
4✔
582
        M2 = sprs.diags(vol_vec, 0, format="csr")
4✔
583
        Mmat = M1*M2
4✔
584
    return Mmat
4✔
585

586

587
def calc_flux_diffn(c, D, Dfunc, E_D, Flux_bc, dr, T, noise):
4✔
588
    N = len(c)
4✔
589
    Flux_vec = np.empty(N+1, dtype=object)
4✔
590
    Flux_vec[0] = 0  # Symmetry at r=0
4✔
591
    Flux_vec[-1] = Flux_bc
4✔
592
    c_edges = utils.mean_linear(c)
4✔
593
    if noise is None:
4✔
594
        Flux_vec[1:N] = -D * Dfunc(c_edges) * np.exp(-E_D/T + E_D/1) * np.diff(c)/dr
4✔
595
    else:
596
        Flux_vec[1:N] = -D * Dfunc(c_edges) * np.exp(-E_D/T + E_D/1) * \
×
597
            np.diff(c + noise(dae.Time().Value))/dr
598
    return Flux_vec
4✔
599

600

601
def calc_flux_CHR(c, mu, D, Dfunc, E_D, Flux_bc, dr, T, noise):
4✔
602
    N = len(c)
4✔
603
    Flux_vec = np.empty(N+1, dtype=object)
4✔
604
    Flux_vec[0] = 0  # Symmetry at r=0
4✔
605
    Flux_vec[-1] = Flux_bc
4✔
606
    c_edges = utils.mean_linear(c)
4✔
607
    if noise is None:
4✔
608
        Flux_vec[1:N] = -D/T * Dfunc(c_edges) * np.exp(-E_D/T + E_D/1) * np.diff(mu)/dr
4✔
609
    else:
610
        Flux_vec[1:N] = -D/T * Dfunc(c_edges) * np.exp(-E_D/T + E_D/1) * \
4✔
611
            np.diff(mu + noise(dae.Time().Value))/dr
612
    return Flux_vec
4✔
613

614

615
def calc_flux_CHR2(c1, c2, mu1_R, mu2_R, D, Dfunc, E_D, Flux1_bc, Flux2_bc, dr, T, noise1, noise2):
4✔
616
    N = len(c1)
4✔
617
    Flux1_vec = np.empty(N+1, dtype=object)
4✔
618
    Flux2_vec = np.empty(N+1, dtype=object)
4✔
619
    Flux1_vec[0] = 0.  # symmetry at r=0
4✔
620
    Flux2_vec[0] = 0.  # symmetry at r=0
4✔
621
    Flux1_vec[-1] = Flux1_bc
4✔
622
    Flux2_vec[-1] = Flux2_bc
4✔
623
    c1_edges = utils.mean_linear(c1)
4✔
624
    c2_edges = utils.mean_linear(c2)
4✔
625
    if noise1 is None:
4✔
626
        Flux1_vec[1:N] = -D/T * Dfunc(c1_edges) * np.exp(-E_D/T + E_D/1) * np.diff(mu1_R)/dr
×
627
        Flux2_vec[1:N] = -D/T * Dfunc(c2_edges) * np.exp(-E_D/T + E_D/1) * np.diff(mu2_R)/dr
×
628
    else:
629
        Flux1_vec[1:N] = -D/T * Dfunc(c1_edges) * np.exp(-E_D/T + E_D/1) * \
4✔
630
            np.diff(mu1_R+noise1(dae.Time().Value))/dr
631
        Flux2_vec[1:N] = -D/T * Dfunc(c2_edges) * np.exp(-E_D/T + E_D/1) * \
4✔
632
            np.diff(mu2_R+noise2(dae.Time().Value))/dr
633
    return Flux1_vec, Flux2_vec
4✔
634

635

636
def calc_mu_O(c_lyte, phi_lyte, phi_sld, T, config, trode):
4✔
637
    elyteModelType = config["elyteModelType"]
4✔
638

639
    if config[f"simInterface_{trode}"]:
4✔
640
        elyteModelType = config["interfaceModelType"]
4✔
641

642
    if elyteModelType == "SM":
4✔
643
        mu_lyte = phi_lyte
4✔
644
        act_lyte = c_lyte
4✔
645
    elif elyteModelType == "dilute":
4✔
646
        act_lyte = c_lyte
4✔
647
        mu_lyte = T*np.log(act_lyte) + phi_lyte
4✔
648
    elif elyteModelType == "solid":
4✔
649
        a_slyte = config['a_slyte']
4✔
650
        cmax = config['cmax']
4✔
651
        act_lyte = (c_lyte / cmax) / (1 - c_lyte / cmax)*np.exp(a_slyte*(1 - 2*c_lyte))
4✔
652
        mu_lyte = phi_lyte
4✔
653
    mu_O = mu_lyte - phi_sld
4✔
654
    return mu_O, act_lyte
4✔
655

656

657
def calc_muR(c, cbar, config, trode, ind):
4✔
658
    muRfunc = props_am.muRfuncs(config, trode, ind).muRfunc
4✔
659
    muR_ref = config[trode, "muR_ref"]
4✔
660
    muR, actR = muRfunc(c, cbar, muR_ref)
4✔
661
    return muR, actR
4✔
662

663

664
def MX(mat, objvec):
4✔
665
    if not isinstance(mat, sprs.csr.csr_matrix):
4✔
666
        raise Exception("MX function designed for csr mult")
×
667
    n = objvec.shape[0]
4✔
668
    if isinstance(objvec[0], dae.pyCore.adouble):
4✔
669
        out = np.empty(n, dtype=object)
4✔
670
    else:
671
        out = np.zeros(n, dtype=float)
×
672
    # Loop through the rows
673
    for i in range(n):
4✔
674
        low = mat.indptr[i]
4✔
675
        up = mat.indptr[i+1]
4✔
676
        if up > low:
4✔
677
            out[i] = np.sum(
4✔
678
                mat.data[low:up] * objvec[mat.indices[low:up]])
679
        else:
680
            out[i] = 0.0
×
681
    return out
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc