def get_lyte_internal_fluxes(c_lyte, phi_lyte, dxd1, eps_o_tau, ndD): zp, zm, nup, num = ndD["zp"], ndD["zm"], ndD["nup"], ndD["num"] nu = nup + num T = ndD["T"] c_edges_int = utils.mean_harmonic(c_lyte) if ndD["elyteModelType"] == "dilute": Dp = eps_o_tau * ndD["Dp"] Dm = eps_o_tau * ndD["Dm"] # Np_edges_int = nup*(-Dp*np.diff(c_lyte)/dxd1 # - Dp*zp*c_edges_int*np.diff(phi_lyte)/dxd1) Nm_edges_int = num*(-Dm*np.diff(c_lyte)/dxd1 - Dm/T*zm*c_edges_int*np.diff(phi_lyte)/dxd1) i_edges_int = (-((nup*zp*Dp + num*zm*Dm)*np.diff(c_lyte)/dxd1) - (nup*zp**2*Dp + num*zm**2*Dm)/T*c_edges_int*np.diff(phi_lyte)/dxd1) # i_edges_int = zp*Np_edges_int + zm*Nm_edges_int elif ndD["elyteModelType"] == "SM": D_fs, sigma_fs, thermFac, tp0 = props_elyte.get_props(ndD["SMset"])[:-1] # modify the free solution transport properties for porous media def D(c): return eps_o_tau*D_fs(c) def sigma(c): return eps_o_tau*sigma_fs(c) sp, n = ndD["sp"], ndD["n_refTrode"] i_edges_int = -sigma(c_edges_int)/T * ( np.diff(phi_lyte)/dxd1 + nu*T*(sp/(n*nup)+tp0(c_edges_int)/(zp*nup)) * thermFac(c_edges_int) * np.diff(np.log(c_lyte))/dxd1 ) Nm_edges_int = num*(-D(c_edges_int)*np.diff(c_lyte)/dxd1 + (1./(num*zm)*(1-tp0(c_edges_int))*i_edges_int)) return Nm_edges_int, i_edges_int
def calc_flux_diffn(c, D, Dfunc, Flux_bc, dr, T): N = len(c) Flux_vec = np.empty(N + 1, dtype=object) Flux_vec[0] = 0 # Symmetry at r=0 Flux_vec[-1] = Flux_bc c_edges = utils.mean_harmonic(c) Flux_vec[1:N] = -D / T * Dfunc(c_edges) * np.diff(c) / dr return Flux_vec
def calc_flux_CHR2(c1, c2, mu1_R, mu2_R, D, Dfunc, Flux1_bc, Flux2_bc, dr, T): if isinstance(c1[0], dae.pyCore.adouble): MIN, MAX = dae.Min, dae.Max else: MIN, MAX = min, max N = len(c1) Flux1_vec = np.empty(N + 1, dtype=object) Flux2_vec = np.empty(N + 1, dtype=object) Flux1_vec[0] = 0. # symmetry at r=0 Flux2_vec[0] = 0. # symmetry at r=0 Flux1_vec[-1] = Flux1_bc Flux2_vec[-1] = Flux2_bc c1_edges = utils.mean_harmonic(c1) c2_edges = utils.mean_harmonic(c2) # keep the concentrations between 0 and 1 c1_edges = np.array([MAX(1e-6, c1_edges[i]) for i in range(len(c1_edges))]) c1_edges = np.array( [MIN((1 - 1e-6), c1_edges[i]) for i in range(len(c1_edges))]) c2_edges = np.array([MAX(1e-6, c2_edges[i]) for i in range(len(c1_edges))]) c2_edges = np.array( [MIN((1 - 1e-6), c2_edges[i]) for i in range(len(c1_edges))]) Flux1_vec[1:N] = -D / T * Dfunc(c1_edges) * np.diff(mu1_R) / dr Flux2_vec[1:N] = -D / T * Dfunc(c2_edges) * np.diff(mu2_R) / dr return Flux1_vec, Flux2_vec
def calc_flux_CHR(c, mu, D, Dfunc, Flux_bc, dr, T): if isinstance(c[0], dae.pyCore.adouble): MIN, MAX = dae.Min, dae.Max else: MIN, MAX = min, max N = len(c) Flux_vec = np.empty(N + 1, dtype=object) Flux_vec[0] = 0 # Symmetry at r=0 Flux_vec[-1] = Flux_bc c_edges = utils.mean_harmonic(c) # Keep the concentration between 0 and 1 c_edges = np.array([MAX(1e-6, c_edges[i]) for i in range(N - 1)]) c_edges = np.array([MIN(1 - 1e-6, c_edges[i]) for i in range(N - 1)]) Flux_vec[1:N] = -D / T * Dfunc(c_edges) * np.diff(mu) / dr return Flux_vec
def get_elyte_disc(Nvol, L, poros, BruggExp): out = {} # Discretization out["dxvec"] = utils.get_dxvec(L, Nvol) dxtmp = np.hstack((out["dxvec"][0], out["dxvec"], out["dxvec"][-1])) out["dxd1"] = utils.mean_linear(dxtmp) out["dxd2"] = out["dxvec"] # The porosity vector out["porosvec"] = utils.get_asc_vec(poros, Nvol) porosvec_pad = utils.pad_vec(out["porosvec"]) # Vector of Bruggeman exponents Brugg_pad = utils.pad_vec(utils.get_asc_vec(BruggExp, Nvol)) # Vector of posority/tortuosity (assuming Bruggeman) porostortvec_pad = porosvec_pad/porosvec_pad**(Brugg_pad) out["eps_o_tau_edges"] = utils.mean_harmonic(porostortvec_pad) return out
def DeclareEquations(self): dae.daeModel.DeclareEquations(self) # Some values of domain lengths trodes = self.trodes ndD = self.ndD Nvol = ndD["Nvol"] Npart = ndD["Npart"] Nlyte = np.sum(list(Nvol.values())) # Define the overall filling fraction in the electrodes for trode in trodes: eq = self.CreateEquation("ffrac_{trode}".format(trode=trode)) eq.Residual = self.ffrac[trode]() dx = 1./Nvol[trode] # Make a float of Vtot, total particle volume in electrode # Note: for some reason, even when "factored out", it's a bit # slower to use Sum(self.psd_vol_ac[l].array([], []) tmp = 0 for vInd in range(Nvol[trode]): for pInd in range(Npart[trode]): Vj = ndD["psd_vol_FracVol"][trode][vInd,pInd] tmp += self.particles[trode][vInd,pInd].cbar() * Vj * dx eq.Residual -= tmp # Define dimensionless R_Vp for each electrode volume for trode in trodes: for vInd in range(Nvol[trode]): eq = self.CreateEquation( "R_Vp_trode{trode}vol{vInd}".format(vInd=vInd, trode=trode)) # Start with no reaction, then add reactions for each # particle in the volume. RHS = 0 # sum over particle volumes in given electrode volume for pInd in range(Npart[trode]): # The volume of this particular particle Vj = ndD["psd_vol_FracVol"][trode][vInd,pInd] RHS += -(ndD["beta"][trode] * (1-ndD["poros"][trode]) * ndD["P_L"][trode] * Vj * self.particles[trode][vInd,pInd].dcbardt()) eq.Residual = self.R_Vp[trode](vInd) - RHS # Define output port variables for trode in trodes: for vInd in range(Nvol[trode]): eq = self.CreateEquation( "portout_c_trode{trode}vol{vInd}".format(vInd=vInd, trode=trode)) eq.Residual = (self.c_lyte[trode](vInd) - self.portsOutLyte[trode][vInd].c_lyte()) eq = self.CreateEquation( "portout_p_trode{trode}vol{vInd}".format(vInd=vInd, trode=trode)) phi_lyte = self.phi_lyte[trode](vInd) eq.Residual = (phi_lyte - self.portsOutLyte[trode][vInd].phi_lyte()) for pInd in range(Npart[trode]): eq = self.CreateEquation( "portout_pm_trode{trode}v{vInd}p{pInd}".format( vInd=vInd, pInd=pInd, trode=trode)) eq.Residual = (self.phi_part[trode](vInd, pInd) - self.portsOutBulk[trode][vInd,pInd].phi_m()) # Simulate the potential drop along the bulk electrode # solid phase simBulkCond = ndD['simBulkCond'][trode] if simBulkCond: # Calculate the RHS for electrode conductivity phi_tmp = utils.add_gp_to_vec(utils.get_var_vec(self.phi_bulk[trode], Nvol[trode])) porosvec = utils.pad_vec(utils.get_const_vec( (1-self.ndD["poros"][trode])**(1-ndD["BruggExp"][trode]), Nvol[trode])) poros_walls = utils.mean_harmonic(porosvec) if trode == "a": # anode # Potential at the current collector is from # simulation phi_tmp[0] = self.phi_cell() # No current passes into the electrolyte phi_tmp[-1] = phi_tmp[-2] else: # cathode phi_tmp[0] = phi_tmp[1] # Potential at current at current collector is # reference (set) phi_tmp[-1] = ndD["phi_cathode"] dx = ndD["L"][trode]/Nvol[trode] dvg_curr_dens = np.diff(-poros_walls*ndD["sigma_s"][trode]*np.diff(phi_tmp)/dx)/dx # Actually set up the equations for bulk solid phi for vInd in range(Nvol[trode]): eq = self.CreateEquation( "phi_ac_trode{trode}vol{vInd}".format(vInd=vInd, trode=trode)) if simBulkCond: eq.Residual = -dvg_curr_dens[vInd] - self.R_Vp[trode](vInd) else: if trode == "a": # anode eq.Residual = self.phi_bulk[trode](vInd) - self.phi_cell() else: # cathode eq.Residual = self.phi_bulk[trode](vInd) - ndD["phi_cathode"] # Simulate the potential drop along the connected # particles simPartCond = ndD['simPartCond'][trode] for vInd in range(Nvol[trode]): phi_bulk = self.phi_bulk[trode](vInd) for pInd in range(Npart[trode]): G_l = ndD["G"][trode][vInd,pInd] phi_n = self.phi_part[trode](vInd, pInd) if pInd == 0: # reference bulk phi phi_l = phi_bulk else: phi_l = self.phi_part[trode](vInd, pInd-1) if pInd == (Npart[trode] - 1): # No particle at end of "chain" G_r = 0 phi_r = phi_n else: G_r = ndD["G"][trode][vInd,pInd+1] phi_r = self.phi_part[trode](vInd, pInd+1) # charge conservation equation around this particle eq = self.CreateEquation( "phi_ac_trode{trode}vol{vInd}part{pInd}".format( vInd=vInd, trode=trode, pInd=pInd)) if simPartCond: # -dcsbar/dt = I_l - I_r eq.Residual = ( self.particles[trode][vInd,pInd].dcbardt() + ((-G_l * (phi_n - phi_l)) - (-G_r * (phi_r - phi_n)))) else: eq.Residual = self.phi_part[trode](vInd, pInd) - phi_bulk # If we have a single electrode volume (in a perfect bath), # electrolyte equations are simple if self.SVsim: eq = self.CreateEquation("c_lyte") eq.Residual = self.c_lyte["c"].dt(0) - 0 eq = self.CreateEquation("phi_lyte") eq.Residual = self.phi_lyte["c"](0) - self.phi_cell() else: disc = geom.get_elyte_disc(Nvol, ndD["L"], ndD["poros"], ndD["BruggExp"]) cvec = utils.get_asc_vec(self.c_lyte, Nvol) dcdtvec = utils.get_asc_vec(self.c_lyte, Nvol, dt=True) phivec = utils.get_asc_vec(self.phi_lyte, Nvol) Rvvec = utils.get_asc_vec(self.R_Vp, Nvol) # Apply concentration and potential boundary conditions # Ghost points on the left and no-gradients on the right ctmp = np.hstack((self.c_lyteGP_L(), cvec, cvec[-1])) phitmp = np.hstack((self.phi_lyteGP_L(), phivec, phivec[-1])) # If we don't have a porous anode: # 1) the total current flowing into the electrolyte is set # 2) assume we have a Li foil with BV kinetics and the specified rate constant eqC = self.CreateEquation("GhostPointC_L") eqP = self.CreateEquation("GhostPointP_L") if Nvol["a"] == 0: # Concentration BC from mass flux Nm_foil = get_lyte_internal_fluxes( ctmp[0:2], phitmp[0:2], disc["dxd1"][0], disc["eps_o_tau_edges"][0], ndD)[0] eqC.Residual = Nm_foil[0] # Phi BC from BV at the foil # We assume BV kinetics with alpha = 0.5, # exchange current density, ecd = k0_foil * c_lyte**(0.5) cWall = utils.mean_harmonic(ctmp[0], ctmp[1]) ecd = ndD["k0_foil"]*cWall**0.5 # -current = ecd*(exp(-eta/2) - exp(eta/2)) # note negative current because positive current is # oxidation here # -current = ecd*(-2*sinh(eta/2)) # eta = 2*arcsinh(-current/(-2*ecd)) BVfunc = -self.current() / ecd eta_eff = 2*np.arcsinh(-BVfunc/2.) eta = eta_eff + self.current()*ndD["Rfilm_foil"] # # Infinitely fast anode kinetics # eta = 0. # eta = mu_R - mu_O = -mu_O (evaluated at interface) # mu_O = [T*ln(c) +] phiWall - phi_cell = -eta # phiWall = -eta + phi_cell [- T*ln(c)] phiWall = -eta + self.phi_cell() if ndD["elyteModelType"] == "dilute": phiWall -= ndD["T"]*np.log(cWall) # phiWall = 0.5 * (phitmp[0] + phitmp[1]) eqP.Residual = phiWall - utils.mean_linear(phitmp[0], phitmp[1]) # We have a porous anode -- no flux of charge or anions through current collector else: eqC.Residual = ctmp[0] - ctmp[1] eqP.Residual = phitmp[0] - phitmp[1] Nm_edges, i_edges = get_lyte_internal_fluxes( ctmp, phitmp, disc["dxd1"], disc["eps_o_tau_edges"], ndD) dvgNm = np.diff(Nm_edges)/disc["dxd2"] dvgi = np.diff(i_edges)/disc["dxd2"] for vInd in range(Nlyte): # Mass Conservation (done with the anion, although "c" is neutral salt conc) eq = self.CreateEquation("lyte_mass_cons_vol{vInd}".format(vInd=vInd)) eq.Residual = disc["porosvec"][vInd]*dcdtvec[vInd] + (1./ndD["num"])*dvgNm[vInd] # Charge Conservation eq = self.CreateEquation("lyte_charge_cons_vol{vInd}".format(vInd=vInd)) eq.Residual = -dvgi[vInd] + ndD["zp"]*Rvvec[vInd] # Define the total current. This must be done at the capacity # limiting electrode because currents are specified in # C-rates. eq = self.CreateEquation("Total_Current") eq.Residual = self.current() limtrode = ("c" if ndD["z"] < 1 else "a") dx = 1./Nvol[limtrode] rxn_scl = ndD["beta"][limtrode] * (1-ndD["poros"][limtrode]) * ndD["P_L"][limtrode] for vInd in range(Nvol[limtrode]): if limtrode == "a": eq.Residual -= dx * self.R_Vp[limtrode](vInd)/rxn_scl else: eq.Residual += dx * self.R_Vp[limtrode](vInd)/rxn_scl # Define the measured voltage, offset by the "applied" voltage # by any series resistance. # phi_cell = phi_applied - I*R eq = self.CreateEquation("Measured_Voltage") eq.Residual = self.phi_cell() - ( self.phi_applied() - ndD["Rser"]*self.current()) if self.profileType == "CC": # Total Current Constraint Equation eq = self.CreateEquation("Total_Current_Constraint") eq.Residual = self.current() - ( ndD["currPrev"] + (ndD["currset"] - ndD["currPrev"]) * (1 - np.exp(-dae.Time()/(ndD["tend"]*ndD["tramp"])))) elif self.profileType == "CV": # Keep applied potential constant eq = self.CreateEquation("applied_potential") eq.Residual = self.phi_applied() - ( ndD["phiPrev"] + (ndD["Vset"] - ndD["phiPrev"]) * (1 - np.exp(-dae.Time()/(ndD["tend"]*ndD["tramp"]))) ) elif "segments" in self.profileType: if self.profileType == "CCsegments": ndD["segments_setvec"][0] = ndD["currPrev"] elif self.profileType == "CVsegments": ndD["segments_setvec"][0] = ndD["phiPrev"] self.segSet = extern_funcs.InterpTimeScalar( "segSet", self, dae.unit(), dae.Time(), ndD["segments_tvec"], ndD["segments_setvec"]) if self.profileType == "CCsegments": eq = self.CreateEquation("Total_Current_Constraint") eq.Residual = self.current() - self.segSet() elif self.profileType == "CVsegments": eq = self.CreateEquation("applied_potential") eq.Residual = self.phi_applied() - self.segSet() for eq in self.Equations: eq.CheckUnitsConsistency = False if self.profileType in ["CC", "CCsegments"]: # Set the condition to terminate the simulation upon reaching # a cutoff voltage. self.stopCondition = ( ((self.phi_applied() <= ndD["phimin"]) | (self.phi_applied() >= ndD["phimax"])) & (self.dummyVar() < 1)) self.ON_CONDITION(self.stopCondition, setVariableValues=[(self.dummyVar, 2)])