def DeclareEquations(self): dae.daeModel.DeclareEquations(self) ndD = self.ndD N = ndD["N"] # number of grid points in particle T = self.ndD_s["T"] # nondimensional temperature r_vec, volfrac_vec = geo.get_unit_solid_discr(ndD['shape'], N) # Prepare the Ideal Solution log ratio terms self.ISfuncs = None if ndD["logPad"]: self.ISfuncs = np.array([ extern_funcs.LogRatio("LR", self, dae.unit(), self.c(k)) for k in range(N) ]) # Prepare noise self.noise = None if ndD["noise"]: numnoise = ndD["numnoise"] noise_prefac = ndD["noise_prefac"] tvec = np.linspace(0., 1.05 * self.ndD_s["tend"], numnoise) noise_data = noise_prefac * np.random.randn(numnoise, N) # Previous_output is common for all external functions previous_output = [] self.noise = [ extern_funcs.InterpTimeVector("noise", self, dae.unit(), dae.Time(), tvec, noise_data, previous_output, _position_) for _position_ in range(N) ] # Figure out mu_O, mu of the oxidized state mu_O, act_lyte = calc_mu_O(self.c_lyte, self.phi_lyte, self.phi_m, T, self.ndD_s["elyteModelType"]) # Define average filling fraction in particle eq = self.CreateEquation("cbar") eq.Residual = self.cbar() for k in range(N): eq.Residual -= self.c(k) * volfrac_vec[k] # Define average rate of filling of particle eq = self.CreateEquation("dcbardt") eq.Residual = self.dcbardt() for k in range(N): eq.Residual -= self.c.dt(k) * volfrac_vec[k] c = np.empty(N, dtype=object) c[:] = [self.c(k) for k in range(N)] if ndD["type"] in ["ACR", "diffn", "CHR"]: # Equations for 1D particles of 1 field varible self.sld_dynamics_1D1var(c, mu_O, act_lyte, self.ISfuncs, self.noise) elif ndD["type"] in ["homog", "homog_sdn"]: # Equations for 0D particles of 1 field variables self.sld_dynamics_0D1var(c, mu_O, act_lyte, self.ISfuncs, self.noise) for eq in self.Equations: eq.CheckUnitsConsistency = False
def DeclareEquations(self): self.stnSpikeSource = self.STN("SpikeSource") for i, t in enumerate(self.spiketimes): self.STATE('State_{0}'.format(i)) eq = self.CreateEquation("event") eq.Residual = self.event() - t self.ON_CONDITION(daet.Time() >= t, switchTo='State_{0}'.format(i + 1), triggerEvents=[(self.spikeoutput, daet.Time())]) self.STATE('State_{0}'.format(len(self.spiketimes))) eq = self.CreateEquation("event") eq.Residual = self.event() self.END_STN()
def getEquationsExpressionParserIdentifiers(model): dictIdentifiers = {} dictIdentifiers['pi'] = daet.Constant(math.pi) dictIdentifiers['e'] = daet.Constant(math.e) dictIdentifiers['t'] = daet.Time() if model: dictIdentifiers = addIdentifiers(model, model, dictIdentifiers) return dictIdentifiers
def DeclareEquations(self): dae.daeModel.DeclareEquations(self) ndD = self.ndD N = ndD["N"] # number of grid points in particle T = self.ndD_s["T"] # nondimensional temperature r_vec, volfrac_vec = geo.get_unit_solid_discr(ndD['shape'], N) # Prepare the Ideal Solution log ratio terms self.ISfuncs1 = self.ISfuncs2 = None if ndD["logPad"]: self.ISfuncs1 = np.array([ extern_funcs.LogRatio("LR1", self, dae.unit(), self.c1(k)) for k in range(N) ]) self.ISfuncs2 = np.array([ extern_funcs.LogRatio("LR2", self, dae.unit(), self.c2(k)) for k in range(N) ]) ISfuncs = (self.ISfuncs1, self.ISfuncs2) # Prepare noise self.noise1 = self.noise2 = None if ndD["noise"]: numnoise = ndD["numnoise"] noise_prefac = ndD["noise_prefac"] tvec = np.linspace(0., 1.05 * self.ndD_s["tend"], numnoise) noise_data1 = noise_prefac * np.random.randn(numnoise, N) noise_data2 = noise_prefac * np.random.randn(numnoise, N) # Previous_output is common for all external functions previous_output1 = [] previous_output2 = [] self.noise1 = [ extern_funcs.InterpTimeVector("noise1", self, dae.unit(), dae.Time(), tvec, noise_data1, previous_output1, _position_) for _position_ in range(N) ] self.noise2 = [ extern_funcs.InterpTimeVector("noise2", self, dae.unit(), dae.Time(), tvec, noise_data2, previous_output2, _position_) for _position_ in range(N) ] noises = (self.noise1, self.noise2) # Figure out mu_O, mu of the oxidized state mu_O, act_lyte = calc_mu_O(self.c_lyte(), self.phi_lyte(), self.phi_m(), T, self.ndD_s["elyteModelType"]) # Define average filling fractions in particle eq1 = self.CreateEquation("c1bar") eq2 = self.CreateEquation("c2bar") eq1.Residual = self.c1bar() eq2.Residual = self.c2bar() for k in range(N): eq1.Residual -= self.c1(k) * volfrac_vec[k] eq2.Residual -= self.c2(k) * volfrac_vec[k] eq = self.CreateEquation("cbar") eq.Residual = self.cbar() - utils.mean_linear(self.c1bar(), self.c2bar()) # Define average rate of filling of particle eq = self.CreateEquation("dcbardt") eq.Residual = self.dcbardt() for k in range(N): eq.Residual -= utils.mean_linear(self.c1.dt(k), self.c2.dt(k)) * volfrac_vec[k] c1 = np.empty(N, dtype=object) c2 = np.empty(N, dtype=object) c1[:] = [self.c1(k) for k in range(N)] c2[:] = [self.c2(k) for k in range(N)] if ndD["type"] in ["diffn2", "CHR2"]: # Equations for 1D particles of 1 field varible self.sld_dynamics_1D2var(c1, c2, mu_O, act_lyte, ISfuncs, noises) elif ndD["type"] in ["homog2", "homog2_sdn"]: # Equations for 0D particles of 1 field variables self.sld_dynamics_0D2var(c1, c2, mu_O, act_lyte, ISfuncs, noises) for eq in self.Equations: eq.CheckUnitsConsistency = False
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") if ndD["tramp"] > 0: eq.Residual = self.current() - ( ndD["currPrev"] + (ndD["currset"] - ndD["currPrev"]) * (1 - np.exp(-dae.Time() / (ndD["tend"] * ndD["tramp"])))) else: eq.Residual = self.current() - ndD["currset"] elif self.profileType == "CV": # Keep applied potential constant eq = self.CreateEquation("applied_potential") if ndD["tramp"] > 0: eq.Residual = self.phi_applied() - ( ndD["phiPrev"] + (ndD["Vset"] - ndD["phiPrev"]) * (1 - np.exp(-dae.Time() / (ndD["tend"] * ndD["tramp"])))) else: eq.Residual = self.phi_applied() - ndD["Vset"] elif self.profileType == "CCsegments": if ndD["tramp"] > 0: ndD["segments_setvec"][0] = ndD["currPrev"] self.segSet = extern_funcs.InterpTimeScalar( "segSet", self, dae.unit(), dae.Time(), ndD["segments_tvec"], ndD["segments_setvec"]) eq = self.CreateEquation("Total_Current_Constraint") eq.Residual = self.current() - self.segSet() #CCsegments implemented as discontinuous equations else: #First segment time = ndD["segments"][0][1] self.IF(dae.Time() < dae.Constant(time * s), 1.e-3) eq = self.CreateEquation("Total_Current_Constraint") eq.Residual = self.current() - ndD["segments"][0][0] #Middle segments for i in range(1, len(ndD["segments"]) - 1): time = time + ndD["segments"][i][1] self.ELSE_IF(dae.Time() < dae.Constant(time * s), 1.e-3) eq = self.CreateEquation("Total_Current_Constraint") eq.Residual = self.current() - ndD["segments"][i][0] #Last segment self.ELSE() eq = self.CreateEquation("Total_Current_Constraint") eq.Residual = self.current() - ndD["segments"][-1][0] self.END_IF() elif self.profileType == "CVsegments": if ndD["tramp"] > 0: ndD["segments_setvec"][0] = ndD["phiPrev"] self.segSet = extern_funcs.InterpTimeScalar( "segSet", self, dae.unit(), dae.Time(), ndD["segments_tvec"], ndD["segments_setvec"]) eq = self.CreateEquation("applied_potential") eq.Residual = self.phi_applied() - self.segSet() #CVsegments implemented as discontinuous equations else: #First segment time = ndD["segments"][0][1] self.IF(dae.Time() < dae.Constant(time * s), 1.e-3) eq = self.CreateEquation("applied_potential") eq.Residual = self.phi_applied() - ndD["segments"][0][0] #Middle segments for i in range(1, len(ndD["segments"]) - 1): time = time + ndD["segments"][i][1] self.ELSE_IF(dae.Time() < dae.Constant(time * s), 1.e-3) eq = self.CreateEquation("applied_potential") eq.Residual = self.phi_applied() - ndD["segments"][i][0] #Last segment self.ELSE() eq = self.CreateEquation("applied_potential") eq.Residual = self.phi_applied() - ndD["segments"][-1][0] self.END_IF() for eq in self.Equations: eq.CheckUnitsConsistency = False #Ending conditions for the simulation if self.profileType in ["CC", "CCsegments"]: #Vmax reached self.ON_CONDITION((self.phi_applied() <= ndD["phimin"]) & (self.endCondition() < 1), setVariableValues=[(self.endCondition, 1)]) #Vmin reached self.ON_CONDITION((self.phi_applied() >= ndD["phimax"]) & (self.endCondition() < 1), setVariableValues=[(self.endCondition, 2)])
def DeclareEquations(self): """ Iterates over *aliases*, *reduce analogue ports* and *regimes*, parses mathematical and logical expressions and generates daetools equation objects and state transition networks. :rtype: None :raises: RuntimeError """ # Add identifiers for __equation_parser__.dictIdentifiers = getEquationsExpressionParserIdentifiers( self) # 1) Create aliases (algebraic equations) aliases = list(self.ninemlComponent.aliases) if len(aliases) > 0: for i, alias in enumerate(aliases): eq = self.CreateEquation(alias.lhs, "") eq.Residual = self.nineml_aliases[i]( ) - __equation_parser__.parse_and_evaluate(alias.rhs) # 1a) Create equations for reduce ports (algebraic equations) for port in self.nineml_reduce_ports: port.generateEquation() # 2) Create regimes regimes = list(self.ninemlComponent.regimes) state_variables = list(self.ninemlComponent.state_variables) if len(regimes) > 0: # 2a) Create STN for model self.STN(nineml_daetools_bridge.ninemlSTNRegimesName) for regime in regimes: # 2b) Create State for each regime self.STATE(regime.name) # 2c) Create equations for all state variables/time derivatives # Sometime a time_derivative equation is not given and in that case the derivative is equal to zero # We have to discover which variables do not have a corresponding ODE and # we do that by creating a map {'state_var' : 'RHS'} which initially has # set rhs to '0'. RHS will be set later while iterating through ODEs map_statevars_timederivs = {} for state_var in state_variables: map_statevars_timederivs[state_var.name] = 0 time_derivatives = list(regime.time_derivatives) for time_deriv in time_derivatives: map_statevars_timederivs[ time_deriv.dependent_variable] = time_deriv.rhs #print map_statevars_timederivs for var_name, rhs in list(map_statevars_timederivs.items()): variable = self._findVariable(var_name) if variable == None: raise RuntimeError( 'Cannot find state variable {0}'.format(var_name)) # Create equation eq = self.CreateEquation(var_name, "") # If right-hand side expression is 0 do not parse it if rhs == 0: eq.Residual = variable.dt() else: eq.Residual = variable.dt( ) - __equation_parser__.parse_and_evaluate(rhs) # 2d) Create on_condition actions for on_condition in regime.on_conditions: condition = __equation_parser__.parse_and_evaluate( on_condition.trigger.rhs) switchTo = on_condition.target_regime.name triggerEvents = [] setVariableValues = [] for state_assignment in on_condition.state_assignments: variable = getObjectFromCanonicalName( self, state_assignment.lhs, look_for_variables=True) if variable == None: raise RuntimeError( 'Cannot find variable {0}'.format( state_assignment.lhs)) expression = __equation_parser__.parse_and_evaluate( state_assignment.rhs) setVariableValues.append((variable, expression)) for event_output in on_condition.event_outputs: event_port = getObjectFromCanonicalName( self, event_output.port_name, look_for_eventports=True) if event_port == None: raise RuntimeError( 'Cannot find event port {0}'.format( event_output.port_name)) triggerEvents.append((event_port, daet.Time())) # ACHTUNG!!! # Check the order of switchTo, triggerEvents and setVariableValues arguments in daetools 1.2.0+!!! self.ON_CONDITION(condition, switchTo=switchTo, setVariableValues=setVariableValues, triggerEvents=triggerEvents) # 2e) Create on_event actions for on_event in regime.on_events: source_event_port = getObjectFromCanonicalName( self, on_event.src_port_name, look_for_eventports=True) if source_event_port == None: raise RuntimeError('Cannot find event port {0}'.format( on_event.src_port_name)) switchToStates = [] triggerEvents = [] setVariableValues = [] for state_assignment in on_event.state_assignments: variable = getObjectFromCanonicalName( self, state_assignment.lhs, look_for_variables=True) if variable == None: raise RuntimeError( 'Cannot find variable {0}'.format( state_assignment.lhs)) expression = __equation_parser__.parse_and_evaluate( state_assignment.rhs) setVariableValues.append((variable, expression)) for event_output in on_event.event_outputs: event_port = getObjectFromCanonicalName( self, event_output.port_name, look_for_eventports=True) if event_port == None: raise RuntimeError( 'Cannot find event port {0}'.format( event_output.port_name)) triggerEvents.append((event_port, daet.Time())) # ACHTUNG!!! # Check the order of switchTo, triggerEvents and setVariableValues arguments in daetools 1.2.0+!!! self.ON_EVENT(source_event_port, switchToStates=switchToStates, setVariableValues=setVariableValues, triggerEvents=triggerEvents) self.END_STN() # 3) Create equations for outlet analog-ports: port.value() - variable() = 0 for analog_port in self.nineml_analog_ports: if analog_port.Type == daet.eOutletPort: eq = self.CreateEquation(analog_port.Name + '_portequation', "") var_to = findObjectInModel(self, analog_port.Name, look_for_variables=True) if var_to == None: raise RuntimeError('Cannot find variable/alias {0}'.format( analog_port.Name)) eq.Residual = analog_port.value() - var_to()