Exemplo n.º 1
0
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
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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
Exemplo n.º 5
0
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
Exemplo n.º 6
0
    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)])