• 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

89.43
/mpet/mod_cell.py
1
"""The model defining the macroscopic cell.
2

3
This includes the equations defining
4
 - transport in the electrolyte
5
 - overall quantities such as current and voltage and their specification
6
 - overall filling fraction of each electrode
7
 - potential drop along the electrodes
8
 - potential drop between simulated particles
9
"""
10
import daetools.pyDAE as dae
4✔
11
from pyUnits import s
4✔
12

13
import numpy as np
4✔
14

15
import mpet.extern_funcs as extern_funcs
4✔
16
import mpet.geometry as geom
4✔
17
import mpet.mod_CCCVCPcycle as mod_CCCVCPcycle
4✔
18
import mpet.mod_electrodes as mod_electrodes
4✔
19
from mpet.mod_interface import InterfaceRegion
4✔
20
import mpet.ports as ports
4✔
21
import mpet.utils as utils
4✔
22
from mpet.config import constants
4✔
23
from mpet.daeVariableTypes import mole_frac_t, elec_pot_t, conc_t
4✔
24

25
# Dictionary of end conditions
26
endConditions = {
4✔
27
    1:"Vmax reached",
28
    2:"Vmin reached",
29
    3:"End condition for CCCVCPcycle reached"}
30

31

32
class ModCell(dae.daeModel):
4✔
33
    def __init__(self, config, Name, Parent=None, Description=""):
4✔
34
        dae.daeModel.__init__(self, Name, Parent, Description)
4✔
35

36
        self.config = config
4✔
37
        self.profileType = config['profileType']
4✔
38
        Nvol = config["Nvol"]
4✔
39
        Npart = config["Npart"]
4✔
40
        self.trodes = trodes = config["trodes"]
4✔
41

42
        # Domains where variables are distributed
43
        self.DmnCell = {}  # domains over full cell dimensions
4✔
44
        self.DmnPart = {}  # domains over particles in each cell volume
4✔
45
        if config['Nvol']['s']:  # If we have a separator
4✔
46
            self.DmnCell["s"] = dae.daeDomain(
4✔
47
                "DmnCell_s", self, dae.unit(),
48
                "Simulated volumes in the separator")
49
        for trode in trodes:
4✔
50
            self.DmnCell[trode] = dae.daeDomain(
4✔
51
                "DmnCell_{trode}".format(trode=trode), self, dae.unit(),
52
                "Simulated volumes in electrode {trode}".format(trode=trode))
53
            self.DmnPart[trode] = dae.daeDomain(
4✔
54
                "Npart_{trode}".format(trode=trode), self, dae.unit(),
55
                "Particles sampled in each control "
56
                + "volume in electrode {trode}".format(trode=trode))
57

58
        # Variables
59
        self.c_lyte = {}
4✔
60
        self.phi_lyte = {}
4✔
61
        self.phi_bulk = {}
4✔
62
        self.phi_part = {}
4✔
63
        self.R_Vp = {}
4✔
64
        self.R_Vi = {}
4✔
65
        self.ffrac = {}
4✔
66
        for trode in trodes:
4✔
67
            # Concentration/potential in electrode regions of elyte
68
            self.c_lyte[trode] = dae.daeVariable(
4✔
69
                "c_lyte_{trode}".format(trode=trode), conc_t, self,
70
                "Concentration in the elyte in electrode {trode}".format(trode=trode),
71
                [self.DmnCell[trode]])
72
            self.phi_lyte[trode] = dae.daeVariable(
4✔
73
                "phi_lyte_{trode}".format(trode=trode), elec_pot_t, self,
74
                "Electric potential in elyte in electrode {trode}".format(trode=trode),
75
                [self.DmnCell[trode]])
76
            self.phi_bulk[trode] = dae.daeVariable(
4✔
77
                "phi_bulk_{trode}".format(trode=trode), elec_pot_t, self,
78
                "Electrostatic potential in the bulk solid",
79
                [self.DmnCell[trode]])
80
            self.phi_part[trode] = dae.daeVariable(
4✔
81
                "phi_part_{trode}".format(trode=trode), elec_pot_t, self,
82
                "Electrostatic potential at each particle",
83
                [self.DmnCell[trode], self.DmnPart[trode]])
84
            self.R_Vp[trode] = dae.daeVariable(
4✔
85
                "R_Vp_{trode}".format(trode=trode), dae.no_t, self,
86
                "Rate of reaction of positives per electrode volume",
87
                [self.DmnCell[trode]])
88
            if self.config[f'simInterface_{trode}']:
4✔
89
                self.R_Vi[trode] = dae.daeVariable(
4✔
90
                    "R_Vi_{trode}".format(trode=trode), dae.no_t, self,
91
                    "Rate of reaction of positives per electrode volume with interface region",
92
                    [self.DmnCell[trode]])
93
            self.ffrac[trode] = dae.daeVariable(
4✔
94
                "ffrac_{trode}".format(trode=trode), mole_frac_t, self,
95
                "Overall filling fraction of solids in electrodes")
96
        if config['Nvol']['s']:  # If we have a separator
4✔
97
            self.c_lyte["s"] = dae.daeVariable(
4✔
98
                "c_lyte_s", conc_t, self,
99
                "Concentration in the electrolyte in the separator",
100
                [self.DmnCell["s"]])
101
            self.phi_lyte["s"] = dae.daeVariable(
4✔
102
                "phi_lyte_s", elec_pot_t, self,
103
                "Electrostatic potential in electrolyte in separator",
104
                [self.DmnCell["s"]])
105
        # Note if we're doing a single electrode volume simulation
106
        # It will be in a perfect bath of electrolyte at the applied
107
        # potential.
108
        if ('a' not in config['trodes']) and (not config['Nvol']['s']) and Nvol["c"] == 1:
4✔
109
            self.SVsim = True
4✔
110
        else:
111
            self.SVsim = False
4✔
112
            # Ghost points (GP) to aid in boundary condition (BC) implemenation
113
            self.c_lyteGP_L = dae.daeVariable("c_lyteGP_L", conc_t, self, "c_lyte left BC GP")
4✔
114
            self.phi_lyteGP_L = dae.daeVariable(
4✔
115
                "phi_lyteGP_L", elec_pot_t, self, "phi_lyte left BC GP")
116
        self.phi_applied = dae.daeVariable(
4✔
117
            "phi_applied", elec_pot_t, self,
118
            "Overall battery voltage (at anode current collector)")
119
        self.phi_cell = dae.daeVariable(
4✔
120
            "phi_cell", elec_pot_t, self,
121
            "Voltage between electrodes (phi_applied less series resistance)")
122
        self.current = dae.daeVariable(
4✔
123
            "current", dae.no_t, self, "Total current of the cell")
124
        self.endCondition = dae.daeVariable(
4✔
125
            "endCondition", dae.no_t, self, "A nonzero value halts the simulation")
126

127
        # Create models for representative particles within electrode
128
        # volumes and ports with which to talk to them.
129
        self.portsOutLyte = {}
4✔
130
        self.portsOutBulk = {}
4✔
131
        self.portsInInterface = {}
4✔
132
        self.particles = {}
4✔
133
        self.interfaces = {}
4✔
134
        for trode in trodes:
4✔
135
            Nv = Nvol[trode]
4✔
136
            Np = Npart[trode]
4✔
137
            self.portsOutLyte[trode] = np.empty(Nv, dtype=object)
4✔
138
            self.portsOutBulk[trode] = np.empty((Nv, Np), dtype=object)
4✔
139
            self.portsInInterface[trode] = np.empty((Nv, Np), dtype=object)
4✔
140
            self.particles[trode] = np.empty((Nv, Np), dtype=object)
4✔
141
            self.interfaces[trode] = np.empty((Nv, Np), dtype=object)
4✔
142
            for vInd in range(Nv):
4✔
143
                self.portsOutLyte[trode][vInd] = ports.portFromElyte(
4✔
144
                    "portTrode{trode}vol{vInd}".format(trode=trode, vInd=vInd), dae.eOutletPort,
145
                    self, "Electrolyte port to particles")
146
                for pInd in range(Np):
4✔
147
                    self.portsOutBulk[trode][vInd,pInd] = ports.portFromBulk(
4✔
148
                        "portTrode{trode}vol{vInd}part{pInd}".format(
149
                            trode=trode, vInd=vInd, pInd=pInd),
150
                        dae.eOutletPort, self,
151
                        "Bulk electrode port to particles")
152
                    solidType = config[trode, "type"]
4✔
153
                    if solidType in constants.two_var_types:
4✔
154
                        pMod = mod_electrodes.Mod2var
4✔
155
                    elif solidType in constants.one_var_types:
4✔
156
                        pMod = mod_electrodes.Mod1var
4✔
157
                    else:
158
                        raise NotImplementedError("unknown solid type")
×
159
                    self.particles[trode][vInd,pInd] = pMod(
4✔
160
                        config, trode, vInd, pInd,
161
                        Name="partTrode{trode}vol{vInd}part{pInd}".format(
162
                            trode=trode, vInd=vInd, pInd=pInd),
163
                        Parent=self)
164

165
                    if config[f"simInterface_{trode}"]:
4✔
166
                        # instantiate interfaces between particle and electrolyte per particle
167
                        self.interfaces[trode][vInd,pInd] = InterfaceRegion(
4✔
168
                            Name="interfaceTrode{trode}vol{vInd}part{pInd}".format(
169
                                trode=trode, vInd=vInd, pInd=pInd),
170
                            Parent=self, config=config, cell=self,
171
                            particle=self.particles[trode][vInd,pInd],
172
                            vInd=vInd,pInd=pInd,trode=trode)
173

174
                        self.portsInInterface[trode][vInd,pInd] = ports.portFromInterface(
4✔
175
                            "portIface{trode}vol{vInd}part{pInd}".format(
176
                                trode=trode, vInd=vInd, pInd=pInd),
177
                            dae.eInletPort, self,
178
                            "Interface region port to elyte")
179

180
                        # connect elyte to interface, then interface to particle
181
                        self.ConnectPorts(self.portsOutLyte[trode][vInd],
4✔
182
                                          self.interfaces[trode][vInd,pInd].portInLyte)
183

184
                        self.ConnectPorts(
4✔
185
                            self.interfaces[trode][vInd,pInd].portOutInterfaceParticle,
186
                            self.particles[trode][vInd,pInd].portInLyte)
187

188
                        # connect interface to elyte
189
                        self.ConnectPorts(self.interfaces[trode][vInd,pInd].portOutInterfaceElyte,
4✔
190
                                          self.portsInInterface[trode][vInd,pInd])
191

192
                        # connect particle to interface
193
                        self.ConnectPorts(self.particles[trode][vInd,pInd].portOutParticle,
4✔
194
                                          self.interfaces[trode][vInd,pInd].portInParticle)
195
                    else:
196
                        # connect elyte to particle
197
                        self.ConnectPorts(self.portsOutLyte[trode][vInd],
4✔
198
                                          self.particles[trode][vInd,pInd].portInLyte)
199

200
                    self.ConnectPorts(self.portsOutBulk[trode][vInd,pInd],
4✔
201
                                      self.particles[trode][vInd,pInd].portInBulk)
202

203
        # if cycling, set current port to cycling module
204
        if self.profileType == "CCCVCPcycle":
4✔
205
            pCycle = mod_CCCVCPcycle.CCCVCPcycle
4✔
206
            self.cycle = pCycle(config, Name="CCCVCPcycle", Parent=self)
4✔
207

208
    def DeclareEquations(self):
4✔
209
        dae.daeModel.DeclareEquations(self)
4✔
210

211
        # Some values of domain lengths
212
        trodes = self.trodes
4✔
213
        config = self.config
4✔
214
        Nvol = config["Nvol"]
4✔
215
        Npart = config["Npart"]
4✔
216
        Nlyte = np.sum(list(Nvol.values()))
4✔
217

218
        # Define the overall filling fraction in the electrodes
219
        for trode in trodes:
4✔
220
            eq = self.CreateEquation("ffrac_{trode}".format(trode=trode))
4✔
221
            eq.Residual = self.ffrac[trode]()
4✔
222
            dx = 1./Nvol[trode]
4✔
223
            # Make a float of Vtot, total particle volume in electrode
224
            # Note: for some reason, even when "factored out", it's a bit
225
            # slower to use Sum(self.psd_vol_ac[l].array([], [])
226
            tmp = 0
4✔
227
            for vInd in range(Nvol[trode]):
4✔
228
                for pInd in range(Npart[trode]):
4✔
229
                    Vj = config["psd_vol_FracVol"][trode][vInd,pInd]
4✔
230
                    tmp += self.particles[trode][vInd,pInd].cbar() * Vj * dx
4✔
231
            eq.Residual -= tmp
4✔
232

233
        # Define dimensionless R_Vp for each electrode volume
234
        for trode in trodes:
4✔
235
            for vInd in range(Nvol[trode]):
4✔
236
                eq = self.CreateEquation(
4✔
237
                    "R_Vp_trode{trode}vol{vInd}".format(vInd=vInd, trode=trode))
238
                # Start with no reaction, then add reactions for each
239
                # particle in the volume.
240
                RHS = 0
4✔
241
                # interface region has separate reaction rate
242
                if config[f"simInterface_{trode}"]:
4✔
243
                    eq_i = self.CreateEquation(
4✔
244
                        "R_Vi_trode{trode}vol{vInd}".format(vInd=vInd, trode=trode))
245
                    RHS_i = 0
4✔
246
                # sum over particle volumes in given electrode volume
247
                for pInd in range(Npart[trode]):
4✔
248
                    # The volume of this particular particle
249
                    Vj = config["psd_vol_FracVol"][trode][vInd,pInd]
4✔
250
                    RHS += -(config["beta"][trode] * (1-config["poros"][trode])
4✔
251
                             * config["P_L"][trode] * Vj
252
                             * self.particles[trode][vInd,pInd].dcbardt())
253
                    if config[f"simInterface_{trode}"]:
4✔
254
                        # Nm0 = self.portsInInterface[trode][vInd,pInd].Nm0()
255
                        i0 = self.portsInInterface[trode][vInd,pInd].i0()
4✔
256
                        # TODO: what is the reaction rate?
257
                        RHS_i += -i0
4✔
258
                eq.Residual = self.R_Vp[trode](vInd) - RHS
4✔
259
                if config[f"simInterface_{trode}"]:
4✔
260
                    eq_i.Residual = self.R_Vi[trode](vInd) - RHS_i
4✔
261

262
        # Define output port variables
263
        for trode in trodes:
4✔
264
            for vInd in range(Nvol[trode]):
4✔
265
                eq = self.CreateEquation(
4✔
266
                    "portout_c_trode{trode}vol{vInd}".format(vInd=vInd, trode=trode))
267
                eq.Residual = (self.c_lyte[trode](vInd)
4✔
268
                               - self.portsOutLyte[trode][vInd].c_lyte())
269
                eq = self.CreateEquation(
4✔
270
                    "portout_p_trode{trode}vol{vInd}".format(vInd=vInd, trode=trode))
271
                phi_lyte = self.phi_lyte[trode](vInd)
4✔
272
                eq.Residual = (phi_lyte - self.portsOutLyte[trode][vInd].phi_lyte())
4✔
273
                for pInd in range(Npart[trode]):
4✔
274
                    eq = self.CreateEquation(
4✔
275
                        "portout_pm_trode{trode}v{vInd}p{pInd}".format(
276
                            vInd=vInd, pInd=pInd, trode=trode))
277
                    eq.Residual = (self.phi_part[trode](vInd, pInd)
4✔
278
                                   - self.portsOutBulk[trode][vInd,pInd].phi_m())
279

280
            # Simulate the potential drop along the bulk electrode
281
            # solid phase
282
            simBulkCond = config['simBulkCond'][trode]
4✔
283
            if simBulkCond:
4✔
284
                # Calculate the RHS for electrode conductivity
285
                phi_tmp = utils.add_gp_to_vec(utils.get_var_vec(self.phi_bulk[trode], Nvol[trode]))
4✔
286
                porosvec = utils.pad_vec(utils.get_const_vec(
4✔
287
                    (1-self.config["poros"][trode])**(1-config["BruggExp"][trode]), Nvol[trode]))
288
                if np.all(self.config['specified_poros'][trode]):
4✔
289
                    specified_por = self.config['specified_poros'][trode]
×
290
                    porosvec = (
×
291
                        (np.ones(Nvol[trode]) - specified_por))**(
292
                            (1 - self.config["BruggExp"][trode]))
293
                    porosvec = utils.pad_vec(porosvec)
×
294
                poros_walls = utils.mean_harmonic(porosvec)
4✔
295
                if trode == "a":  # anode
4✔
296
                    # Potential at the current collector is from
297
                    # simulation
298
                    phi_tmp[0] = self.phi_cell()
4✔
299
                    # No current passes into the electrolyte
300
                    phi_tmp[-1] = phi_tmp[-2]
4✔
301
                else:  # cathode
302
                    phi_tmp[0] = phi_tmp[1]
4✔
303
                    # Potential at current at current collector is
304
                    # reference (set)
305
                    phi_tmp[-1] = config["phi_cathode"]
4✔
306
                dx = config["L"][trode]/Nvol[trode]
4✔
307
                dvg_curr_dens = np.diff(-poros_walls*config["sigma_s"][trode]
4✔
308
                                        * np.diff(phi_tmp)/dx)/dx
309

310
            # Actually set up the equations for bulk solid phi
311
            for vInd in range(Nvol[trode]):
4✔
312
                eq = self.CreateEquation(
4✔
313
                    "phi_ac_trode{trode}vol{vInd}".format(vInd=vInd, trode=trode))
314
                if simBulkCond:
4✔
315
                    # select reaction rate with interface region or particle
316
                    if config[f"simInterface_{trode}"]:
4✔
317
                        R_V = self.R_Vi
×
318
                    else:
319
                        R_V = self.R_Vp
4✔
320
                    eq.Residual = -dvg_curr_dens[vInd] - R_V[trode](vInd)
4✔
321
                else:
322
                    if trode == "a":  # anode
4✔
323
                        eq.Residual = self.phi_bulk[trode](vInd) - self.phi_cell()
4✔
324
                    else:  # cathode
325
                        eq.Residual = self.phi_bulk[trode](vInd) - config["phi_cathode"]
4✔
326

327
            # Simulate the potential drop along the connected
328
            # particles
329
            simPartCond = config['simPartCond'][trode]
4✔
330
            for vInd in range(Nvol[trode]):
4✔
331
                phi_bulk = self.phi_bulk[trode](vInd)
4✔
332
                for pInd in range(Npart[trode]):
4✔
333
                    G_l = config["G"][trode][vInd,pInd]
4✔
334
                    phi_n = self.phi_part[trode](vInd, pInd)
4✔
335
                    if pInd == 0:  # reference bulk phi
4✔
336
                        phi_l = phi_bulk
4✔
337
                    else:
338
                        phi_l = self.phi_part[trode](vInd, pInd-1)
4✔
339
                    if pInd == (Npart[trode] - 1):  # No particle at end of "chain"
4✔
340
                        G_r = 0
4✔
341
                        phi_r = phi_n
4✔
342
                    else:
343
                        G_r = config["G"][trode][vInd,pInd+1]
4✔
344
                        phi_r = self.phi_part[trode](vInd, pInd+1)
4✔
345
                    # charge conservation equation around this particle
346
                    eq = self.CreateEquation(
4✔
347
                        "phi_ac_trode{trode}vol{vInd}part{pInd}".format(
348
                            vInd=vInd, trode=trode, pInd=pInd))
349
                    if simPartCond:
4✔
350
                        # -dcsbar/dt = I_l - I_r
351
                        eq.Residual = (
4✔
352
                            self.particles[trode][vInd,pInd].dcbardt()
353
                            + ((-G_l * (phi_n - phi_l))
354
                               - (-G_r * (phi_r - phi_n))))
355
                    else:
356
                        eq.Residual = self.phi_part[trode](vInd, pInd) - phi_bulk
4✔
357

358
        # If we have a single electrode volume (in a perfect bath),
359
        # electrolyte equations are simple
360
        if self.SVsim:
4✔
361
            eq = self.CreateEquation("c_lyte")
4✔
362
            eq.Residual = self.c_lyte["c"].dt(0) - 0
4✔
363
            eq = self.CreateEquation("phi_lyte")
4✔
364
            eq.Residual = self.phi_lyte["c"](0) - self.phi_cell()
4✔
365
        else:
366
            if np.all(config["specified_poros"]["c"]):
4✔
367
                config_poros = config["specified_poros"]
×
368
            else:
369
                config_poros = config["poros"]
4✔
370
            disc = geom.get_elyte_disc(Nvol, config["L"], config_poros, config["BruggExp"])
4✔
371
            cvec = utils.get_asc_vec(self.c_lyte, Nvol)
4✔
372
            dcdtvec = utils.get_asc_vec(self.c_lyte, Nvol, dt=True)
4✔
373
            phivec = utils.get_asc_vec(self.phi_lyte, Nvol)
4✔
374
            if config[f"simInterface_{trode}"]:
4✔
375
                Rvvec = utils.get_asc_vec(self.R_Vi, Nvol)
4✔
376
            else:
377
                Rvvec = utils.get_asc_vec(self.R_Vp, Nvol)
4✔
378
            # Apply concentration and potential boundary conditions
379
            # Ghost points on the left and no-gradients on the right
380
            ctmp = np.hstack((self.c_lyteGP_L(), cvec, cvec[-1]))
4✔
381
            phitmp = np.hstack((self.phi_lyteGP_L(), phivec, phivec[-1]))
4✔
382

383
            Nm_edges, i_edges = get_lyte_internal_fluxes(ctmp, phitmp, disc, config)
4✔
384

385
            # If we don't have a porous anode:
386
            # 1) the total current flowing into the electrolyte is set
387
            # 2) assume we have a Li foil with BV kinetics and the specified rate constant
388
            eqC = self.CreateEquation("GhostPointC_L")
4✔
389
            eqP = self.CreateEquation("GhostPointP_L")
4✔
390
            if 'a' not in config["trodes"]:
4✔
391
                # Concentration BC from mass flux
392
                eqC.Residual = Nm_edges[0]
4✔
393

394
                # Phi BC from BV at the foil
395
                # We assume BV kinetics with alpha = 0.5,
396
                # exchange current density, ecd = k0_foil * c_lyte**(0.5)
397
                cWall = .5*(ctmp[0] + ctmp[1])
4✔
398
                ecd = config["k0_foil"]*cWall**0.5
4✔
399
                # Concentration is fixed for solid
400
                if config["elyteModelType"] == 'solid':
4✔
401
                    ecd = config["k0_foil"]*1**0.5
4✔
402
                # note negative current because positive current is
403
                # oxidation here
404
                eta = self.phi_cell() - self.current()*config["Rfilm_foil"] \
4✔
405
                    - .5*(phitmp[0] + phitmp[1])
406
                if config["elyteModelType"] == "dilute":
4✔
407
                    eta -= config["T"]*np.log(cWall)
4✔
408
                eqP.Residual = self.current() - ecd*2*np.sinh(eta/2)
4✔
409

410
            # We have a porous anode -- no flux of charge or anions through current collector
411
            else:
412
                eqC.Residual = ctmp[0] - ctmp[1]
4✔
413
                eqP.Residual = phitmp[0] - phitmp[1]
4✔
414

415
            dvgNm = np.diff(Nm_edges)/disc["dxvec"]
4✔
416
            dvgi = np.diff(i_edges)/disc["dxvec"]
4✔
417
            for vInd in range(Nlyte):
4✔
418
                # Mass Conservation (done with the anion, although "c" is neutral salt conc)
419
                eq = self.CreateEquation("lyte_mass_cons_vol{vInd}".format(vInd=vInd))
4✔
420
                eq.Residual = disc["porosvec"][vInd]*dcdtvec[vInd] + (1./config["num"])*dvgNm[vInd]
4✔
421
                # Charge Conservation
422
                eq = self.CreateEquation("lyte_charge_cons_vol{vInd}".format(vInd=vInd))
4✔
423
                eq.Residual = -dvgi[vInd] + config["zp"]*Rvvec[vInd]
4✔
424

425
        # Define the total current. This must be done at the capacity
426
        # limiting electrode because currents are specified in
427
        # C-rates.
428
        eq = self.CreateEquation("Total_Current")
4✔
429
        eq.Residual = self.current()
4✔
430
        limtrode = config["limtrode"]
4✔
431
        dx = 1./Nvol[limtrode]
4✔
432
        rxn_scl = config["beta"][limtrode] * (1-config["poros"][limtrode]) \
4✔
433
            * config["P_L"][limtrode]
434
        for vInd in range(Nvol[limtrode]):
4✔
435
            if limtrode == "a":
4✔
436
                eq.Residual -= dx * self.R_Vp[limtrode](vInd)/rxn_scl
4✔
437
            else:
438
                eq.Residual += dx * self.R_Vp[limtrode](vInd)/rxn_scl
4✔
439

440
        # Define the measured voltage, offset by the "applied" voltage
441
        # by any series resistance.
442
        # phi_cell = phi_applied - I*R
443
        eq = self.CreateEquation("Measured_Voltage")
4✔
444
        eq.Residual = self.phi_cell() - (
4✔
445
            self.phi_applied() - config["Rser"]*self.current())
446

447
        if self.profileType == "CC":
4✔
448
            # Total Current Constraint Equation
449
            eq = self.CreateEquation("Total_Current_Constraint")
4✔
450
            if config["tramp"] > 0:
4✔
451
                eq.Residual = self.current() - (
4✔
452
                    config["currPrev"] + (config["currset"] - config["currPrev"])
453
                    * (1 - np.exp(-dae.Time()/(config["tend"]*config["tramp"]))))
454
            else:
455
                eq.Residual = self.current() - config["currset"]
4✔
456
        elif self.profileType == "CV":
4✔
457
            # Keep applied potential constant
458
            eq = self.CreateEquation("applied_potential")
4✔
459
            if config["tramp"] > 0:
4✔
460
                eq.Residual = self.phi_applied() - (
4✔
461
                    config["phiPrev"] + (config["Vset"] - config["phiPrev"])
462
                    * (1 - np.exp(-dae.Time()/(config["tend"]*config["tramp"])))
463
                    )
464
            else:
465
                eq.Residual = self.phi_applied() - config["Vset"]
×
466
        elif self.profileType == "CP":
4✔
467
            # constant power constraint
468
            ndDVref = config["c", "phiRef"]
4✔
469
            if 'a' in config["trodes"]:
4✔
470
                ndDVref = config["c", "phiRef"] - config["a", "phiRef"]
×
471
            eq = self.CreateEquation("Total_Power_Constraint")
4✔
472
            # adding Vref since P = V*I
473
            if config["tramp"] > 0:
4✔
474
                eq.Residual = self.current()*(self.phi_applied() + ndDVref) - (
4✔
475
                    config["currPrev"]*(config["phiPrev"] + ndDVref)
476
                    + (config["power"] - (config["currPrev"]*(config["phiPrev"]
477
                                                              + ndDVref)))
478
                    * (1 - np.exp(-dae.Time()/(config["tend"]*config["tramp"])))
479
                    )
480
            else:
481
                eq.Residual = self.current()*(self.phi_applied() + ndDVref) - config["power"]
×
482
        elif self.profileType == "CCsegments":
4✔
483
            if config["tramp"] > 0:
4✔
484
                config["segments_setvec"][0] = config["currPrev"]
4✔
485
                self.segSet = extern_funcs.InterpTimeScalar(
4✔
486
                    "segSet", self, dae.unit(), dae.Time(),
487
                    config["segments_tvec"], config["segments_setvec"])
488
                eq = self.CreateEquation("Total_Current_Constraint")
4✔
489
                eq.Residual = self.current() - self.segSet()
4✔
490

491
            # CCsegments implemented as discontinuous equations
492
            else:
493
                # First segment
494
                time = config["segments"][0][1]
×
495
                self.IF(dae.Time() < dae.Constant(time*s), 1.e-3)
×
496
                eq = self.CreateEquation("Total_Current_Constraint")
×
497
                eq.Residual = self.current() - config["segments"][0][0]
×
498

499
                # Middle segments
500
                for i in range(1,len(config["segments"])-1):
×
501
                    time = time+config["segments"][i][1]
×
502
                    self.ELSE_IF(dae.Time() < dae.Constant(time*s), 1.e-3)
×
503
                    eq = self.CreateEquation("Total_Current_Constraint")
×
504
                    eq.Residual = self.current() - config["segments"][i][0]
×
505

506
                # Last segment
507
                self.ELSE()
×
508
                eq = self.CreateEquation("Total_Current_Constraint")
×
509
                eq.Residual = self.current() - config["segments"][-1][0]
×
510
                self.END_IF()
×
511

512
        elif self.profileType == "CVsegments":
4✔
513
            if config["tramp"] > 0:
4✔
514
                config["segments_setvec"][0] = config["phiPrev"]
4✔
515
                self.segSet = extern_funcs.InterpTimeScalar(
4✔
516
                    "segSet", self, dae.unit(), dae.Time(),
517
                    config["segments_tvec"], config["segments_setvec"])
518
                eq = self.CreateEquation("applied_potential")
4✔
519
                eq.Residual = self.phi_applied() - self.segSet()
4✔
520

521
            # CVsegments implemented as discontinuous equations
522
            else:
523
                # First segment
524
                time = config["segments"][0][1]
×
525
                self.IF(dae.Time() < dae.Constant(time*s), 1.e-3)
×
526
                eq = self.CreateEquation("applied_potential")
×
527
                eq.Residual = self.phi_applied() - config["segments"][0][0]
×
528

529
                # Middle segments
530
                for i in range(1,len(config["segments"])-1):
×
531
                    time = time+config["segments"][i][1]
×
532
                    self.ELSE_IF(dae.Time() < dae.Constant(time*s), 1.e-3)
×
533
                    eq = self.CreateEquation("applied_potential")
×
534
                    eq.Residual = self.phi_applied() - config["segments"][i][0]
×
535

536
                # Last segment
537
                self.ELSE()
×
538
                eq = self.CreateEquation("applied_potential")
×
539
                eq.Residual = self.phi_applied() - config["segments"][-1][0]
×
540
                self.END_IF()
×
541

542
        for eq in self.Equations:
4✔
543
            eq.CheckUnitsConsistency = False
4✔
544

545
        # Ending conditions for the simulation
546
        if self.profileType in ["CC", "CCsegments", "CV", "CVsegments", "CCCVCPcycle"]:
4✔
547
            # Vmax reached
548
            self.ON_CONDITION((self.phi_applied() <= config["phimin"])
4✔
549
                              & (self.endCondition() < 1),
550
                              setVariableValues=[(self.endCondition, 1)])
551

552
            # Vmin reached
553
            self.ON_CONDITION((self.phi_applied() >= config["phimax"])
4✔
554
                              & (self.endCondition() < 1),
555
                              setVariableValues=[(self.endCondition, 2)])
556

557
            if self.profileType == "CCCVCPcycle":
4✔
558
                # we need to set the end condition outside for some reason
559
                self.ON_CONDITION((self.cycle.cycle_number() >= config["totalCycle"]+1)
4✔
560
                                  & (self.endCondition() < 1),
561
                                  setVariableValues=[(self.endCondition, 3)])
562

563

564
def get_lyte_internal_fluxes(c_lyte, phi_lyte, disc, config):
4✔
565
    zp, zm, nup, num = config["zp"], config["zm"], config["nup"], config["num"]
4✔
566
    nu = nup + num
4✔
567
    T = config["T"]
4✔
568
    dxd1 = disc["dxd1"]
4✔
569
    eps_o_tau = disc["eps_o_tau"]
4✔
570

571
    # Get concentration at cell edges using weighted mean
572
    wt = utils.pad_vec(disc["dxvec"])
4✔
573

574
    c_edges_int = utils.weighted_linear_mean(c_lyte, wt)
4✔
575

576
    if config["elyteModelType"] == "dilute":
4✔
577
        # Get porosity at cell edges using weighted harmonic mean
578
        eps_o_tau_edges = utils.weighted_linear_mean(eps_o_tau, wt)
4✔
579
        Dp = eps_o_tau_edges * config["Dp"]
4✔
580
        Dm = eps_o_tau_edges * config["Dm"]
4✔
581
        Nm_edges_int = num*(-Dm*np.diff(c_lyte)/dxd1
4✔
582
                            - Dm/T*zm*c_edges_int*np.diff(phi_lyte)/dxd1)
583
        i_edges_int = (-((nup*zp*Dp + num*zm*Dm)*np.diff(c_lyte)/dxd1)
4✔
584
                       - (nup*zp**2*Dp + num*zm**2*Dm)/T*c_edges_int*np.diff(phi_lyte)/dxd1)
585
    elif config["elyteModelType"] == "SM":
4✔
586
        SMset = config["SMset"]
4✔
587
        elyte_function = utils.import_function(config["SMset_filename"], SMset,
4✔
588
                                               mpet_module=f"mpet.electrolyte.{SMset}")
589
        D_fs, sigma_fs, thermFac, tp0 = elyte_function()[:-1]
4✔
590

591
        # Get diffusivity and conductivity at cell edges using weighted harmonic mean
592
        D_edges = utils.weighted_harmonic_mean(eps_o_tau*D_fs(c_lyte, T), wt)
4✔
593
        sigma_edges = utils.weighted_harmonic_mean(eps_o_tau*sigma_fs(c_lyte, T), wt)
4✔
594

595
        sp, n = config["sp"], config["n"]
4✔
596
        # there is an error in the MPET paper, temperature dependence should be
597
        # in sigma and not outside of sigma
598
        i_edges_int = -sigma_edges * (
4✔
599
            np.diff(phi_lyte)/dxd1
600
            + nu*T*(sp/(n*nup)+tp0(c_edges_int, T)/(zp*nup))
601
            * thermFac(c_edges_int, T)
602
            * np.diff(np.log(c_lyte))/dxd1
603
            )
604
        Nm_edges_int = num*(-D_edges*np.diff(c_lyte)/dxd1
4✔
605
                            + (1./(num*zm)*(1-tp0(c_edges_int, T))*i_edges_int))
606
    elif config["elyteModelType"] == "solid":
4✔
607
        SMset = config["SMset"]
4✔
608
        elyte_function = utils.import_function(config["SMset_filename"], SMset,
4✔
609
                                               mpet_module=f"mpet.electrolyte.{SMset}")
610
        D_fs, sigma_fs, thermFac, tp0 = elyte_function()[:-1]
4✔
611
        # sigma_fs and thermFac not used bc the solid system is considered linear
612
        a_slyte = config["a_slyte"]
4✔
613
        c_edges_int_norm = c_edges_int / config["cmax"]
4✔
614

615
        # Get diffusivity at cell edges using weighted harmonic mean
616
        eps_o_tau_edges = utils.weighted_linear_mean(eps_o_tau, wt)
4✔
617
        Dp = eps_o_tau_edges * config["Dp"]
4✔
618
        Dm = (zp * Dp - zp * Dp * tp0) / (tp0 * zm)
4✔
619
        Dp0 = Dp / (1-c_edges_int_norm)  # should be c0/cmax
4✔
620
        Dchemp = Dp0 * (1 - 2 * a_slyte * c_edges_int_norm + 2 * a_slyte * c_edges_int_norm**2)
4✔
621
        Dchemm = Dm
4✔
622
        Damb = (zp * Dp * Dchemm + zm * Dm * Dchemp) / (zp * Dp - zm * Dm)
4✔
623
        i_edges_int = (-((nup*zp*Dchemp + num*zm*Dchemm)*np.diff(c_lyte)/dxd1)
4✔
624
                       - (nup * zp ** 2 * Dp0 * (1 - c_edges_int_norm) + num * zm ** 2 * Dm) / T
625
                       * c_edges_int * np.diff(phi_lyte) / dxd1)
626
        Nm_edges_int = num * (-Damb * np.diff(c_lyte) / dxd1
4✔
627
                              + (1. / (num * zm) * (1 - tp0) * i_edges_int))
628
    return Nm_edges_int, i_edges_int
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