Source code for openpnm.algorithms._reactive_transport
importloggingimportsysimportnumpyasnpfromnumpy.linalgimportnormtry:# For scipy < 1.14fromscipy.optimize.nonlinimportTerminationConditionexceptImportError:# For newer Scipyfromscipy.optimize._nonlinimportTerminationConditionfromtqdm.autoimporttqdmfromopenpnm.algorithmsimportTransportfromopenpnm.utilsimportDocorator,TypedList__all__=["ReactiveTransport"]docstr=Docorator()logger=logging.getLogger(__name__)@docstr.get_sections(base="ReactiveTransportSettings",sections=["Parameters"])@docstr.dedentclassReactiveTransportSettings:r""" Parameters ---------- %(TransportSettings.parameters)s sources : list List of source terms that have been added relaxation_factor : float (default = 1.0) A relaxation factor to control under-relaxation for the quantity solving for. Factor approaching 0 leads to improved stability but slower simulation. Factor approaching 1 gives fast simulation but may be unstable. newton_maxiter : int Maximum number of iterations allowed for the nonlinear solver to converge. f_rtol : float Relative tolerance for the solution residual x_rtol : float Relative tolerance for the solution vector """relaxation_factor=1.0newton_maxiter=5000f_rtol=1e-6x_rtol=1e-6
[docs]@docstr.get_sections(base="ReactiveTransport",sections=["Parameters"])@docstr.dedentclassReactiveTransport(Transport):r""" A subclass for steady-state simulations with (optional) source terms. Parameters ---------- %(Transport.parameters)s Notes ----- This subclass performs steady simulations of transport phenomena with reactions when source terms are added. """def__init__(self,name="react_trans_?",**kwargs):super().__init__(name=name,**kwargs)self.settings._update(ReactiveTransportSettings())
[docs]defset_source(self,pores,propname,mode="add"):r""" Applies a given source term to the specified pores Parameters ---------- pores : array_like The pore indices where the source term should be applied. propname : str The property name of the source term model to be applied. mode : str Controls how the sources are applied. Options are: =========== ===================================================== mode meaning =========== ===================================================== 'add' (default) Adds supplied source term to already existing ones. 'remove' Deletes given source term from the specified locations. 'clear' Deletes given source term from all locations (ignores the ``pores`` argument). =========== ===================================================== Notes ----- Source terms cannot be applied in pores where boundary conditions have already been set. Attempting to do so will result in an error being raised. """# If a list of modes was sent, do them each in orderifisinstance(mode,list):foriteminmode:self.set_source(pores=pores,propname=propname,mode=item)returnpropname=self._parse_prop(propname,"pore")# Check if any BC is already set in the same locationslocs_BC=np.zeros(self.Np,dtype=bool)foriteminself["pore.bc"].keys():locs_BC=np.isfinite(self[f"pore.bc.{item}"])ifnp.any(locs_BC[pores]):raiseException("BCs present in given pores, can't assign source term")prop="pore.source."+propname.split(".",1)[1]ifmode=="add":ifpropnotinself.keys():self[prop]=Falseself[prop][pores]=Trueelifmode=="remove":self[prop][pores]=Falseelifmode=="clear":self[prop]=Falseelse:raiseException(f"Unsupported mode {mode}")
def_apply_sources(self):""" Updates ``A`` and ``b``, applying source terms to specified pores. Notes ----- Phase and physics objects are also updated before applying source terms to ensure that source terms values are associated with the current value of 'quantity'. """try:phase=self.project[self.settings.phase]foriteminself["pore.source"].keys():# Fetch linearized values of the source termPs=self["pore.source."+item]S1,S2=[phase[f"pore.{item}.{Si}"]forSiin["S1","S2"]]# Modify A and b: diag(A) += -S1, b += S2diag=self.A.diagonal()diag[Ps]+=-S1[Ps]self.A.setdiag(diag)self.b[Ps]+=S2[Ps]exceptKeyError:passdef_run_special(self,solver,x0,verbose=None):r""" Repeatedly updates ``A``, ``b``, and the solution guess within according to the applied source term then calls ``_solve`` to solve the resulting system of linear equations. Stops when the max-norm of the residual drops by at least ``f_rtol``: ``norm(R_n) < norm(R_0) * f_rtol`` AND ``norm(dx) < norm(x) * x_rtol`` where R_i is the residual at ith iteration, x is the solution at current iteration, and dx is the change in the solution between two consecutive iterations. ``f_rtol`` and ``x_rtol`` are defined in the algorithm's settings under: ``alg.settings['f_rtol']``, and ``alg.settings['x_rtol']``, respectively. Parameters ---------- x0 : ndarray Initial guess of the unknown variable """w=self.settings["relaxation_factor"]maxiter=self.settings["newton_maxiter"]f_rtol=self.settings["f_rtol"]x_rtol=self.settings["x_rtol"]xold=self.xdx=self.x-xoldcondition=TerminationCondition(f_tol=np.inf,f_rtol=f_rtol,x_rtol=x_rtol,norm=norm)tqdm_settings={"total":100,"desc":f"{self.name} : Newton iterations","disable":notverbose,"file":sys.stdout,"leave":False,}withtqdm(**tqdm_settings)aspbar:foriinrange(maxiter):res=self._get_residual()progress=self._get_progress(res)pbar.update(progress-pbar.n)is_converged=bool(condition.check(f=res,x=xold,dx=dx))ifis_converged:pbar.update(100-pbar.n)self.soln.is_converged=is_convergedlogger.info(f"Solution converged, residual norm: {norm(res):.4e}")returnsuper()._run_special(solver=solver,x0=xold,w=w)dx=self.x-xoldxold=self.xlogger.info(f"Iteration #{i:<4d} | Residual norm: {norm(res):.4e}")self.soln.num_iter=i+1self.soln.is_converged=Falselogger.warning(f"{self.name} didn't converge after {maxiter} iterations")def_get_progress(self,res):""" Returns an approximate value for completion percent of Newton iterations. """ifnothasattr(self,"_f0_norm"):self._f0_norm=norm(res)f_rtol=self.settings.f_rtolnorm_reduction=norm(res)/self._f0_norm/f_rtolprogress=(1-max(np.log10(norm_reduction),0)/np.log10(1/f_rtol))*100returnmax(0,progress)def_update_A_and_b(self):r""" Builds/updates A, b based on the recent solution on algorithm object. """self._update_iterative_props()super()._update_A_and_b()self._apply_sources()def_get_residual(self,x=None):r""" Calculates solution residual based on the given ``x`` based on the following formula: ``R = A * x - b`` """ifxisNone:x=self.xreturnself.A*x-self.b
[docs]defset_BC(self,pores=None,bctype=[],bcvalues=[],mode="add"):msg="Source term already present in given pores, can't assign BCs"# Ensure that given pores do not have source terms already settry:foriteminself["pore.source"].keys():ifnp.any(self["pore.source."+item][pores]):raiseException(msg)exceptKeyError:pass# Assign BCs if above check passessuper().set_BC(pores=pores,bctype=bctype,bcvalues=bcvalues,mode=mode)