class Battery(): def __init__( self, cell_temperature_celcius, capacity_Ah, bruggeman_coefficient, charge_transfer_coefficient, current_collector_cross_section_area, electrolyte_diffusivity, transference_number): # Initialises physical properties of cell ########################################## Compute & Assign Constants ################################################## self.T = 273.0 + cell_temperature_celcius # Could move into electrode-level later self.Q = capacity_Ah * 3600.0 self.brug = bruggeman_coefficient # Could move into electrode-level later when electrodes may have unique brug. values self.alpha = charge_transfer_coefficient # Could move into electrode-level later when electrodes may have unique alpha values self.A = current_collector_cross_section_area self.De = electrolyte_diffusivity self.t_plus = transference_number def Battery_scale_quantities(self, overall_thickness, kappa_string): # self.L = overall_thickness self.kappa = lambdify( Ce, sympify(kappa_string), "numpy" ) # Function to return electrolyte electrical conductivity, given a species concentration # NOTE: Consider changing this function's name since it's only the kappa definition now ######################################## Battery-level Mesh Generation ################################################# def define_global_mesh(self, node_spacing, normalised_domain_thickness ): # Only Ce & phi_e are solved on this mesh self.dx = node_spacing # node_spacing vector passed can be either uniformly or non-uniformly spaced points self.L_normalised = normalised_domain_thickness self.axial_mesh = Grid1D(Lx=self.L_normalised, dx=self.dx) def Battery_cellvariables(self, neg_epsilon_e, sep_epsilon_e, pos_epsilon_e, neg_a_s, pos_a_s, nx_neg, nx_sep, nx_pos, j_battery_value, ce_initial, phi_e_initial, neg_L, sep_L, pos_L, neg_De_eff, sep_De_eff, pos_De_eff): self.Ce = CellVariable(mesh=self.axial_mesh, value=ce_initial, hasOld=True) self.phi_e = CellVariable(mesh=self.axial_mesh, value=phi_e_initial) self.epsilon_e_value = numerix.zeros(nx_neg + nx_sep + nx_pos) self.epsilon_e_value[0:nx_neg], self.epsilon_e_value[ nx_neg:nx_neg + nx_sep] = neg_epsilon_e, sep_epsilon_e self.epsilon_e_value[nx_neg + nx_sep:] = pos_epsilon_e self.epsilon_e = CellVariable(mesh=self.axial_mesh, value=self.epsilon_e_value) self.epsilon_e_eff_value = numerix.zeros(nx_neg + nx_sep + nx_pos) self.epsilon_e_eff_value[0:nx_neg] = neg_epsilon_e**self.brug self.epsilon_e_eff_value[nx_neg:nx_neg + nx_sep] = sep_epsilon_e**self.brug self.epsilon_e_eff_value[nx_neg + nx_sep:] = pos_epsilon_e**self.brug self.epsilon_e_eff = CellVariable(mesh=self.axial_mesh, value=self.epsilon_e_eff_value) self.a_s_value = numerix.zeros(nx_neg + nx_pos + nx_sep) self.a_s_value[0:nx_neg] = neg_a_s self.a_s_value[nx_neg:nx_neg + nx_sep] = 0.0 self.a_s_value[nx_neg + nx_sep:] = pos_a_s self.a_s = CellVariable(mesh=self.axial_mesh, value=self.a_s_value) self.L_value = numerix.zeros(nx_neg + nx_pos + nx_sep) self.L_value[0:nx_neg], self.L_value[nx_neg:nx_neg + nx_sep] = neg_L, sep_L self.L_value[nx_neg + nx_sep:] = pos_L self.L = CellVariable(mesh=self.axial_mesh, value=self.L_value) self.De_eff_value = numerix.zeros(nx_neg + nx_pos + nx_sep) self.De_eff_value[0:nx_neg], self.De_eff_value[ nx_neg:nx_neg + nx_sep], self.De_eff_value[ nx_neg + nx_sep:] = neg_De_eff, sep_De_eff, pos_De_eff self.De_eff = CellVariable(mesh=self.axial_mesh, value=self.De_eff_value) self.j_battery = CellVariable(mesh=self.axial_mesh, value=j_battery_value) ############################# Define Eff. Diff. Coefficients & Volume Fractions ######################################## def Battery_facevariables(self, neg_De_eff, sep_De_eff, pos_De_eff, neg_epsilon_e, sep_epsilon_e, pos_epsilon_e, normalised_neg_length, normalised_sep_length): self.De_eff = FaceVariable( mesh=self.axial_mesh ) # Define effective diffusion coefficient as a FiPy facevariable self.De_eff.setValue( neg_De_eff, where=(self.axial_mesh.faceCenters[0] <= normalised_neg_length) ) # Set its value to that for the neg. diff. coefficient in the negative electrode domain self.De_eff.setValue( sep_De_eff, where=(( normalised_neg_length # Set its value to the correct value in the separator < self.axial_mesh.faceCenters[0]) & (self.axial_mesh.faceCenters[0] < (normalised_neg_length + normalised_sep_length)))) self.De_eff.setValue( pos_De_eff, where=( self.axial_mesh.faceCenters[ 0] # Set its value to the correct value in the positive electrode domain >= (normalised_neg_length + normalised_sep_length))) self.epsilon_e_eff_facevariable = FaceVariable( mesh=self.axial_mesh ) # Define electrolyte phase volume fraction as a FiPy facevariable self.epsilon_e_eff_facevariable.setValue( neg_epsilon_e**self. brug, # Compute eff. value for neg. domain & set it in neg. electrode domain where=(self.axial_mesh.faceCenters[0] <= normalised_neg_length)) self.epsilon_e_eff_facevariable.setValue( sep_epsilon_e**self. brug, # Compute eff. value for sep. domain & set it in sep. electrode domain where=((normalised_neg_length < self.axial_mesh.faceCenters[0]) & (self.axial_mesh.faceCenters[0] < (normalised_neg_length + normalised_sep_length)))) self.epsilon_e_eff_facevariable.setValue( pos_epsilon_e**self. brug, # Compute eff. value for pos. domain & set it in pos. electrode domain where=(self.axial_mesh.faceCenters[0] >= (normalised_neg_length + normalised_sep_length)))
(X_faces**2 + Y_faces**2)**.5 > R_outer - cellSize / 10.) #convectionCoeff=200. #W/m^2/K Bi_desired = 10. #SETTING convectionCoeff = Bi_desired * k / R_inner logging.info('convection coefficient is %.2E' % convectionCoeff) Bi = convectionCoeff * R_inner / k #Biot number logging.info('Biot number is %.2E' % Bi) #ref: https://github.com/usnistgov/fipy/blob/develop/documentation/USAGE.rst#applying-robin-boundary-conditions #warning: avoid confusion between convectionCoeff in that fipy documentation, which refers to terms involving "a", and convectionCoeff here, which refers to a heat transfer convection coefficient at a boundary Gamma0 = D_thermal Gamma = FaceVariable(mesh=mesh, value=Gamma0) mask = surfaceFaces Gamma.setValue(0., where=mask) dPf = FaceVariable(mesh=mesh, value=mesh._faceToCellDistanceRatio * mesh.cellDistanceVectors) Af = FaceVariable(mesh=mesh, value=mesh._faceAreas) #RobinCoeff = (mask * Gamma0 * Af / (dPf.dot(a) + b)).divergence #a is zero in our case b = 1. RobinCoeff = (mask * Gamma0 * Af / b).divergence #a is zero in our case #eq = (TransientTerm() == DiffusionTerm(coeff=Gamma) + RobinCoeff * g - ImplicitSourceTerm(coeff=RobinCoeff * mesh.faceNormals.dot(a))) #a is zero in our case # g in this formulation is -convectionCoeff/k*var, where var=T-T_infinity eq = (TransientTerm() == DiffusionTerm(coeff=Gamma) + ImplicitSourceTerm(RobinCoeff * -convectionCoeff / k)) # embed() # sys.exit()
nx = 10 ny = 1 valueLeft = 0. fluxRight = 1. timeStepDuration = 1. L = 10. dx = L / nx dy = 1. mesh = Tri2D(dx, dy, nx, ny) var = CellVariable(name="solution variable", mesh=mesh, value=valueLeft) diffCoeff = FaceVariable(mesh=mesh, value=1.0) x = mesh.faceCenters[0] diffCoeff.setValue(0.1, where=(L / 4. <= x) & (x < 3. * L / 4.)) var.faceGrad.constrain([[1.], [0.]], mesh.facesRight) var.constrain(valueLeft, mesh.facesLeft) if __name__ == '__main__': import fipy.tests.doctestPlus exec(fipy.tests.doctestPlus._getScript()) input('finished')
class Circumbinary(object): def __init__(self, rmax=1.0e4, ncell=300, dt=1.0e-6, delta=1.0e-100, fudge=1.0e-3, q=1.0, gamma=100, mdisk=0.1, odir='output', bellLin=True, emptydt=0.001, **kargs): self.rmax = rmax self.ncell = ncell self.dt = dt self.delta = delta self.mDisk = mdisk Omega0 = (G*M/(gamma*a)**3)**0.5 nu0 = alpha*cs**2/Omega0 self.chi = 2*fudge*q**2*np.sqrt(G*M)/nu0/a*(gamma*a)**1.5 self.T0 = mu*Omega0/alpha/k*nu0 self.gamma = gamma self.fudge = fudge self.q = q self.nu0 = nu0 self.t = 0.0 self.odir = odir self.bellLin = bellLin self.emptydt = emptydt self._genGrid() self.r = self.mesh.cellCenters.value[0] self.rF = self.mesh.faceCenters.value[0] if self.q > 0.0: self.gap = np.where(self.rF < 1.7/gamma) else: self.gap = np.where(self.rF < 1.0/gamma) self._genSigma() self._genTorque() self._genT(bellLin=self.bellLin, **kargs) self._genVr() self._buildEq() def _genGrid(self, inB=1.0): """Generate a logarithmically spaced grid""" logFaces = np.linspace(-np.log(self.gamma/inB), np.log(self.rmax), num=self.ncell+1) logFacesLeft = logFaces[:-1] logFacesRight = logFaces[1:] dr = tuple(np.exp(logFacesRight) - np.exp(logFacesLeft)) self.mesh = CylindricalGrid1D(dr=dr, origin=(inB/self.gamma,)) def _genSigma(self, width=0.1): """Create dependent variable Sigma""" # Gaussian initial condition value = self.mDisk*M/np.sqrt(2*np.pi)/(self.gamma*a*width)*\ np.exp(-0.5*np.square(self.r-1.0)/width**2)/(2*np.pi*self.gamma*self.r*a) # Make it dimensionless value /= self.mDisk*M/(self.gamma*a)**2 idxs = np.where(self.r < 0.1) value[idxs] = 0.0 value = tuple(value) # Create the dependent variable and set the boundary conditions # to zero self.Sigma = CellVariable(name='Surface density', mesh=self.mesh, hasOld=True, value=value) #self.Sigma.constrain(0, self.mesh.facesLeft) #self.Sigma.constrain(0, self.mesh.facesRight) def _genTorque(self): """Generate Torque""" self.Lambda = FaceVariable(name='Torque at cell faces', mesh=self.mesh, rank=1) self.LambdaCell = CellVariable(name='Torque at cell centers', mesh=self.mesh) LambdaArr = np.zeros(self.rF.shape) LambdaArr[1:] = self.chi*np.power(1.0/(self.rF[1:]*self.gamma-1.0), 4) #LambdaArr[self.gap] = 0.0; LambdaArr[self.gap] = LambdaArr.max() self.Lambda.setValue(LambdaArr) self.LambdaCell.setValue(self.chi*np.power(1.0/(self.r*self.gamma-1.0), 4)) self.LambdaCell[np.where(self.LambdaCell > LambdaArr.max())] = LambdaArr.max() def _interpT(self): """ Get an initial guess for T using an interpolation of the solutions for T in the various thermodynamic limits. """ Lambda = self.Lambda/self.chi*self.fudge*self.q**2*G*M/a LambdaCell = self.LambdaCell/self.chi*self.fudge*self.q**2*G*M/a Sigma = self.Sigma*M/(self.gamma*a)**2 r = self.r*a*self.gamma #In physical units (cgs) self.Omega = np.sqrt(G*M/r**3) self.TvThin = np.power(9.0/4*alpha*k/sigma/mu/kappa0*self.Omega, 1.0/(3.0+beta)) self.TtiThin = np.power(1/sigma/kappa0*(OmegaIn-self.Omega)*LambdaCell, 1.0/(4.0+beta)) self.Ti = np.power(np.square(eta/7*L/4/np.pi/sigma)*k/mu/G/M*r**(-3), 1.0/7) self.TvThick = np.power(27.0/64*kappa0*alpha*k/sigma/mu*self.Omega*Sigma**2, 1.0/(3.0-beta)) self.TtiThick = np.power(3*kappa0/16/sigma*Sigma**2*(OmegaIn-self.Omega)*LambdaCell, 1.0/(4.0-beta)) #return np.power(self.TvThin**4 + self.TvThick**4 + self.TtiThin**4 + self.TtiThick**4 + self.Ti**4, 1.0/4)/self.T0 return np.power(self.TvThin**4 + self.TvThick**4 + self.Ti**4, 1.0/4)/self.T0 def _genT(self, bellLin=True, **kargs): """Create a cell variable for temperature""" if bellLin: @pickle_results(os.path.join(self.odir, "interpolator.pkl")) def buildInterpolator(r, gamma, q, fudge, mDisk, **kargs): # Keep in mind that buildTemopTable() returns the log10's of the values rGrid, SigmaGrid, temp = thermopy.buildTempTable(r*a*gamma, q=q, f=fudge, **kargs) # Go back to dimensionless units rGrid -= np.log10(a*gamma) SigmaGrid -= np.log10(mDisk*M/gamma**2/a**2) # Get the range of values for Sigma in the table rangeSigma = (np.power(10.0, SigmaGrid.min()), np.power(10.0, SigmaGrid.max())) # Interpolate in the log of dimensionless units return rangeSigma, RectBivariateSpline(rGrid, SigmaGrid, temp) # Pass the radial grid in phsyical units # Get back interpolator in logarithmic space rangeSigma, log10Interp = buildInterpolator(self.r, self.gamma, self.q, self.fudge, self.mDisk, **kargs) rGrid = np.log10(self.r) SigmaMin = np.ones(rGrid.shape)*rangeSigma[0] SigmaMax = np.ones(rGrid.shape)*rangeSigma[1] r = self.r*a*self.gamma #In physical units (cgs) self.Omega = np.sqrt(G*M/r**3) Ti = np.power(np.square(eta/7*L/4/np.pi/sigma)*k/mu/G/M*r**(-3), 1.0/7) T = np.zeros(Ti.shape) # Define wrapper function that uses the interpolator and stores the results # in an array given as a second argument. It can handle zero or negative # Sigma values. def func(Sigma): good = np.logical_and(Sigma > rangeSigma[0], Sigma < rangeSigma[1]) badMin = np.logical_and(True, Sigma < rangeSigma[0]) badMax = np.logical_and(True, Sigma > rangeSigma[1]) if np.sum(good) > 0: T[good] = np.power(10.0, log10Interp.ev(rGrid[good], np.log10(Sigma[good]))) if np.sum(badMin) > 0: T[badMin] = np.power(10.0, log10Interp.ev(rGrid[badMin], np.log10(SigmaMin[badMin]))) if np.sum(badMax) > 0: raise ValueError("Extrapolation to large values of Sigma is not allowed, build a table with a larger Sigmax") T[badMax] = np.power(10.0, log10Interp.ev(rGrid[badMax], np.log10(SigmaMax[badMax]))) return T # Store interpolator as an instance method self._bellLinT = func # Save the temperature as an operator variable self.T = self.Sigma._UnaryOperatorVariable(lambda x: self._bellLinT(x)) # Initialize T with the interpolation of the various thermodynamic limits else: self.T = self._interpT() def _genVr(self): """Generate the face variable that stores the velocity values""" r = self.r #In dimensionless units (cgs) # viscosity at cell centers in cgs self.nu = alpha*k/mu/self.Omega/self.nu0*self.T self.visc = r**0.5*self.nu*self.Sigma #self.visc.grad.constrain([self.visc/2/self.r[0]], self.mesh.facesLeft) #self.Sigma.constrain(self.visc.grad/self.nu*2*self.r**0.5, where=self.mesh.facesLeft) # I add the delta to avoid divisions by zero self.vrVisc = -3/self.rF**(0.5)/(self.Sigma.faceValue + self.delta)*self.visc.faceGrad if self.q > 0.0: self.vrTid = self.Lambda*np.sqrt(self.rF) def _buildEq(self): """ Build the equation to solve, we can change this method to impelement other schemes, e.g. Crank-Nicholson. """ # The current scheme is an implicit-upwind if self.q > 0.0: self.vr = self.vrVisc + self.vrTid self.eq = TransientTerm(var=self.Sigma) == - ExplicitUpwindConvectionTerm(coeff=self.vr, var=self.Sigma) else: self.vr = self.vrVisc mask_coeff = (self.mesh.facesLeft * self.mesh.faceNormals).getDivergence() self.eq = TransientTerm(var=self.Sigma) == - ExplicitUpwindConvectionTerm(coeff=self.vr, var=self.Sigma)\ - mask_coeff*3.0/2*self.nu/self.mesh.x*self.Sigma.old def dimensionalSigma(self): """ Return Sigma in dimensional form (cgs) """ return self.Sigma.value*self.mDisk*M/(self.gamma*a)**2 def dimensionalFJ(self): """ Return the viscous torque in dimensional units (cgs) """ return 3*np.pi*self.nu.value*self.nu0*self.dimensionalSigma()*np.sqrt(G*M*self.r*a*self.gamma) def dimensionalTime(self, t=None, mode='yr'): """ Return current time in dimensional units (years or seconds) """ if t == None: t = self.t if mode == 'yr' or mode == 'years' or mode == 'year': return t*(a*self.gamma)**2/self.nu0/(365*24*60*60) else: return t*(a*self.gamma)**2/self.nu0 def dimensionlessTime(self, t, mode='yr'): """ Returns the dimensionless value of the time given as an argument """ if mode == 'yr' or mode == 'years' or mode == 'year': return t/(a*self.gamma)**2*self.nu0*(365*24*60*60) else: return t/(a*self.gamma)**2*self.nu0 def singleTimestep(self, dtMax=0.001, dt=None, update=True, emptyDt=False): """ Evolve the system for a single timestep of size `dt` """ if dt: self.dt = dt if emptyDt: vr = self.vr.value[0] if self.q == 0.0: vr[0] = -3.0/2*self.nu.faceValue.value[0]/self.rF[0] #vr[np.where(self.Sigma.value)] = self.delta self.flux = self.rF[1:]*vr[1:]-self.rF[:-1]*vr[:-1] self.flux = np.maximum(self.flux, self.delta) self.dts = self.mesh.cellVolumes/(self.flux) self.dts[np.where(self.Sigma.value == 0.0)] = np.inf self.dts[self.gap] = np.inf self.dt = self.emptydt*np.amin(self.dts) self.dt = min(dtMax, self.dt) try: self.eq.sweep(dt=self.dt) if np.any(self.Sigma.value < 0.0): self.singleTimestep(dt=self.dt/2) if update: self.Sigma.updateOld() self.t += self.dt except FloatingPointError: import ipdb; ipdb.set_trace() def evolve(self, deltaTime, **kargs): """ Evolve the system using the singleTimestep method """ tEv = self.t + deltaTime while self.t < tEv: dtMax = tEv - self.t self.singleTimestep(dtMax=dtMax, **kargs) def revert(self): """ Revert evolve method if update=False was used, otherwise it has no effect. """ self.Sigma.setValue(self.Sigma.old.value) def writeToFile(self): fName = self.odir + '/t{0}.pkl'.format(self.t) with open(fName, 'wb') as f: pickle.dump((self.t, self.Sigma.getValue()), f) def readFromFile(self, fName): with open(fName, 'rb') as f: t, Sigma = pickle.load(f) self.t = t self.Sigma.setValue(Sigma) def loadTimesList(self): path = self.odir files = os.listdir(path) if '.DS_Store' in files: files.remove('.DS_Store') if 'interpolator.pkl' in files: files.remove('interpolator.pkl') if 'init.pkl' in files: files.remove('init.pkl') self.times = np.zeros((len(files),)) for i, f in enumerate(files): match = re.match(r"^t((\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?)\.pkl", f) if match == None: print "WARNING: File {0} has an unexepected name".format(f) files.remove(f) continue self.times[i] = float(match.group(1)) self.times.sort() self.files = files def loadTime(self, t): """ Load the file with the time closest to `t` """ idx = (np.abs(self.times-t)).argmin() fName = self.odir + '/t'+str(self.times[idx]) + '.pkl' self.readFromFile(fName)
""" from fipy import Variable, FaceVariable, CellVariable, Grid1D, TransientTerm, DiffusionTerm, Viewer from fipy.tools import numerix # Setting up a mesh nx = 50 dx = 0.02 L = nx * dx mesh = Grid1D(nx=nx, dx=dx) c = CellVariable(mesh=mesh, name=r"$c$", value=0.0) # Setting up the spatially varying diffusion coefficient # Due to the mechanics of the solver, the diffusion coefficient variable must be defined on the mesh face D = FaceVariable(mesh=mesh, value=2.0) x = mesh.faceCenters[0] D.setValue(1.0, where=(x > L / 2.0)) # Boundary conditions valueLeft = 1.0 valueRight = 0.0 c.constrain(valueLeft, mesh.facesLeft) c.constrain(valueRight, mesh.facesRight) #Equation eq = (TransientTerm() == DiffusionTerm(coeff=D)) dt = 0.001 t = Variable(0.0) time_stride = 20 timestep = 0 run_time = 1
# 1D convection diffusion equation (mobile domain) # version with \frac{\partial c_{im}}{\partial t} eqM = (TransientTerm(1.0,var=cm) + TransientTerm(betaT,var=cim) == DiffusionTerm(DR,var=cm) - ExponentialConvectionTerm(u/(Rm*phim),var=cm)) # immobile domain (lumped approach) eqIM = TransientTerm(Rim*phiim,var=cim) == beta/Rim*(cm - ImplicitSourceTerm(1.0,var=cim)) # couple equations eqn = eqM & eqIM viewer = Viewer(vars=(cm,cim), datamin=0.0, datamax=1.0) viewer.plot() time = 0.0 for step in range(steps): time += timeStep if time < 0.5: u.setValue((1.0,)) elif time < 1.0: u.setValue((0.0,)) else: u.setValue((-1.0,)) eqn.solve(dt=timeStep) viewer.plot()
valueLeft = 0. fluxRight = 1. timeStepDuration = 1. L = 10. dx = L / nx dy = 1. mesh = Tri2D(dx, dy, nx, ny) var = CellVariable( name = "solution variable", mesh = mesh, value = valueLeft) diffCoeff = FaceVariable(mesh = mesh, value = 1.0) x = mesh.faceCenters[0] diffCoeff.setValue(0.1, where=(L/4. <= x) & (x < 3. * L / 4.)) var.faceGrad.constrain([[1.], [0.]], mesh.facesRight) var.constrain(valueLeft, mesh.facesLeft) if __name__ == '__main__': import fipy.tests.doctestPlus exec(fipy.tests.doctestPlus._getScript()) input('finished')