def from_callback(cls, cb, transf_cbs, nx, nparams=0, pre_adj=None, **kwargs): """ Generate a TransformedSys instance from a callback Parameters ---------- cb : callable transf_cbs : pair or iterable of pairs of callables nx : int nparams : int pre_adj : callable \\*\\*kwargs : passed onto TransformedSys """ be = Backend(kwargs.pop('backend', None)) x = be.real_symarray('x', nx) p = be.real_symarray('p', nparams) try: transf = [(transf_cbs[idx][0](xi), transf_cbs[idx][1](xi)) for idx, xi in enumerate(x)] except TypeError: transf = zip(_map2(transf_cbs[0], x), _map2(transf_cbs[1], x)) try: exprs = cb(x, p, be) except TypeError: exprs = _ensure_3args(cb)(x, p, be) return cls(x, _map2l(pre_adj, exprs), transf, p, backend=be, **kwargs)
def from_callback(cls, cb, ny, nparams=0, **kwargs): """ Create an instance from a callback. Parameters ---------- cb : callbable Signature ``rhs(x, y[:], p[:]) -> f[:]`` ny : int length of y nparams : int (default: 0) length of p \*\*kwargs : keyword arguments passed onto :class:`SymbolicSys` Returns ------- An instance of :class:`SymbolicSys`. """ be = Backend(kwargs.pop('backend', None)) x, = be.real_symarray('x', 1) y = be.real_symarray('y', ny) p = be.real_symarray('p', nparams) try: exprs = cb(x, y, p, be) except TypeError: exprs = _ensure_4args(cb)(x, y, p, be) return cls(zip(y, exprs), x, p, backend=be, **kwargs)
def from_callback(cls, cb, nx=None, nparams=None, **kwargs): """ Generate a SymbolicSys instance from a callback""" if kwargs.get('x_by_name', False): if 'names' not in kwargs: raise ValueError("Need ``names`` in kwargs.") if nx is None: nx = len(kwargs['names']) elif nx != len(kwargs['names']): raise ValueError("Inconsistency between nx and length of ``names``.") if kwargs.get('par_by_name', False): if 'param_names' not in kwargs: raise ValueError("Need ``param_names`` in kwargs.") if nparams is None: nparams = len(kwargs['param_names']) elif nparams != len(kwargs['param_names']): raise ValueError("Inconsistency between ``nparam`` and length of ``param_names``.") if nparams is None: nparams = 0 if nx is None: raise ValueError("Need ``nx`` of ``names`` together with ``x_by_name==True``.") be = Backend(kwargs.pop('backend', None)) x = be.real_symarray('x', nx) p = be.real_symarray('p', nparams) _x = dict(zip(kwargs['names'], x)) if kwargs.get('x_by_name', False) else x _p = dict(zip(kwargs['param_names'], p)) if kwargs.get('par_by_name', False) else p try: exprs = cb(_x, _p, be) except TypeError: exprs = _ensure_3args(cb)(_x, _p, be) return cls(x, exprs, p, backend=be, **kwargs)
def from_callback(cls, cb, transf_cbs, nx, nparams=0, pre_adj=None, **kwargs): """ Generate a TransformedSys instance from a callback Parameters ---------- cb: callable transf_cbs: pair or iterable of pairs of callables nx: int nparams: int pre_adj: callable \*\*kwargs: passed onto TransformedSys """ be = Backend(kwargs.pop('backend', None)) x = be.real_symarray('x', nx) p = be.real_symarray('p', nparams) try: transf = [(transf_cbs[idx][0](xi), transf_cbs[idx][1](xi)) for idx, xi in enumerate(x)] except TypeError: transf = zip(_map2(transf_cbs[0], x), _map2(transf_cbs[1], x)) try: exprs = cb(x, p, be) except TypeError: exprs = _ensure_3args(cb)(x, p, be) return cls(x, _map2l(pre_adj, exprs), transf, p, backend=be, **kwargs)
def __init__(self, x, exprs, params=(), jac=True, backend=None, **kwargs): self.x = x self.exprs = exprs self.params = params self._jac = jac self.be = Backend(backend) self.nf, self.nx = len(exprs), len(x) # needed by get_*_cb self.band = kwargs.get('band', None) # needed by get_*_cb self.module = kwargs.pop('module', 'numpy') super(SymbolicSys, self).__init__(self.nf, self.nx, self._get_f_cb(), self._get_j_cb(), **kwargs)
def main(): for key in 'sympy pysym symengine'.split(): print(key) print(' Differentiation:') be = Backend(key) x, y = map(be.Symbol, 'x y'.split()) expr = (x - be.acos(y))*be.exp(x + y) print(expr) Dexpr = expr.diff(y) print(Dexpr) print("")
def _validate(conditions, rsys, symbols, odesys, backend=None, transform=None, ignore=('time',)): """ For use with create_odesys Parameters ---------- conditions : OrderedDict Parameters, values with units from ``chempy.units``. rsys : ReactionSystem symbols : dict Mapping variable name to symbols. backend : module Module for symbolic mathematics. (defaults to SymPy) transform : callable for rewriting expressions Raises ------ ``KeyError`` if a key in conditions is not in odesys.names or odesys.param_names """ if backend is None: from sym import Backend backend = Backend(backend) if transform is None: if backend.__name__ != 'sympy': warnings.warn("Backend != SymPy, provide your own transform function.") def transform(arg): expr = backend.logcombine(arg, force=True) v, w = map(backend.Wild, 'v w'.split()) expr = expr.replace(backend.log(w**v), v*backend.log(w)) return expr args = [symbols[key] for key in conditions] seen = [False]*len(args) rates = {} for k, v in rsys.rates(symbols).items(): expr = transform(v) if expr == 0: rate = 0 * u.molar/u.second else: rate = backend.lambdify(args, expr)(*conditions.values()) to_unitless(rate, u.molar/u.second) rates[k] = rate seen = [b or a in expr.free_symbols for b, a in zip(seen, args)] not_seen = [a for s, a in zip(seen, args) if not s] for k in conditions: if k not in odesys.param_names and k not in odesys.names and k not in ignore: raise KeyError("Unknown param: %s" % k) return {'not_seen': not_seen, 'rates': rates}
def from_callback(cls, cb, ny, nparams=0, dep_transf_cbs=None, indep_transf_cbs=None, **kwargs): """ Create an instance from a callback. Analogous to :func:`SymbolicSys.from_callback`. Parameters ---------- cb : callable Signature ``rhs(x, y[:], p[:]) -> f[:]`` ny : int length of y nparams : int length of p dep_transf_cbs : iterable of pairs callables callables should have the signature f(yi) -> expression in yi indep_transf_cbs : pair of callbacks callables should have the signature f(x) -> expression in x \*\*kwargs : keyword arguments passed onto :class:`TransformedSys` """ be = Backend(kwargs.pop('backend', None)) x, = be.real_symarray('x', 1) y = be.real_symarray('y', ny) p = be.real_symarray('p', nparams) exprs = _ensure_4args(cb)(x, y, p, be) if dep_transf_cbs is not None: dep_transf = [(fw(yi), bw(yi)) for (fw, bw), yi in zip(dep_transf_cbs, y)] else: dep_transf = None if indep_transf_cbs is not None: indep_transf = indep_transf_cbs[0](x), indep_transf_cbs[1](x) else: indep_transf = None return cls(list(zip(y, exprs)), x, dep_transf, indep_transf, p, backend=be, **kwargs)
def from_callback(cls, cb, nx=None, nparams=None, **kwargs): """ Generate a SymbolicSys instance from a callback""" if kwargs.get('x_by_name', False): if 'names' not in kwargs: raise ValueError("Need ``names`` in kwargs.") if nx is None: nx = len(kwargs['names']) elif nx != len(kwargs['names']): raise ValueError( "Inconsistency between nx and length of ``names``.") if kwargs.get('par_by_name', False): if 'param_names' not in kwargs: raise ValueError("Need ``param_names`` in kwargs.") if nparams is None: nparams = len(kwargs['param_names']) elif nparams != len(kwargs['param_names']): raise ValueError( "Inconsistency between ``nparam`` and length of ``param_names``." ) if nparams is None: nparams = 0 if nx is None: raise ValueError( "Need ``nx`` of ``names`` together with ``x_by_name==True``.") be = Backend(kwargs.pop('backend', None)) x = be.real_symarray('x', nx) p = be.real_symarray('p', nparams) _x = dict(zip(kwargs['names'], x)) if kwargs.get('x_by_name', False) else x _p = dict(zip(kwargs['param_names'], p)) if kwargs.get( 'par_by_name', False) else p try: exprs = cb(_x, _p, be) except TypeError: exprs = _ensure_3args(cb)(_x, _p, be) return cls(x, exprs, p, backend=be, **kwargs)
def __init__(self, dep_exprs, indep=None, params=(), jac=True, dfdx=True, roots=None, backend=None, **kwargs): self.dep, self.exprs = zip(*dep_exprs) self.indep = indep self.params = params self._jac = jac self._dfdx = dfdx self.roots = roots self.be = Backend(backend) if kwargs.get('names', None) is True: kwargs['names'] = [y.name for y in self.dep] # we need self.band before super().__init__ self.band = kwargs.get('band', None) super(SymbolicSys, self).__init__( self.get_f_ty_callback(), self.get_j_ty_callback(), self.get_dfdx_callback(), self.get_roots_callback(), nroots=None if roots is None else len(roots), **kwargs)
def _create_odesys(rsys, substance_symbols=None, parameter_symbols=None, pretty_replace=lambda x: x, backend=None, SymbolicSys=None, time_symbol=None, unit_registry=None): """ This will be a simpler version of get_odesys without the unit handling code. The motivation is to reduce complexity (the code of get_odesys is long with multiple closures). This will also rely on SymPy explicitly and the user will be expected to deal with SymPy expressions. Only when this function has the same capabilities as get_odesys will it become and public API (along with a deprecation of get_odesys). Parameters ---------- rsys : ReactionSystem instance substance_symbols : OrderedDict If ``None``: ``rsys.substances`` will be used. parameter_symbols : OrderedDict backend : str or module Symbolic backend (e.g. sympy). The package ``sym`` is used as a wrapper. Returns ------- SymbolicSys (subclass of ``pyodesys.ODESys``) dict : - ``'symbols'``: dict mapping str to symbol. - ``'validate'``: callable acppeting a dictionary mapping str to quantities """ if backend is None: from sym import Backend backend = Backend(backend) if SymbolicSys is None: from pyodesys.symbolic import SymbolicSys if substance_symbols is None: substance_symbols = OrderedDict([(key, backend.Symbol(key)) for key in rsys.substances]) if isinstance(substance_symbols, OrderedDict): if list(substance_symbols) != list(rsys.substances): raise ValueError("substance_symbols needs to have same (oredered) keys as rsys.substances") if parameter_symbols is None: keys = [] for rxn in rsys.rxns: uk, = rxn.param.unique_keys keys.append(uk) for pk in rxn.param.parameter_keys: if pk not in keys: keys.append(pk) parameter_symbols = OrderedDict([(key, backend.Symbol(key)) for key in keys]) if not isinstance(parameter_symbols, OrderedDict): raise ValueError("parameter_symbols needs to be an OrderedDict") symbols = OrderedDict(chain(substance_symbols.items(), parameter_symbols.items())) symbols['time'] = time_symbol or backend.Symbol('t') if any(symbols['time'] == v for k, v in symbols.items() if k != 'time'): raise ValueError("time_symbol already in use (name clash?)") rates = rsys.rates(symbols) compo_vecs, compo_names = rsys.composition_balance_vectors() odesys = SymbolicSys( zip([substance_symbols[key] for key in rsys.substances], [rates[key] for key in rsys.substances]), symbols['time'], parameter_symbols.values(), names=list(rsys.substances.keys()), latex_names=[s.latex_name for s in rsys.substances.values()], param_names=parameter_symbols.keys(), latex_param_names=[pretty_replace(n) for n in parameter_symbols.keys()], linear_invariants=compo_vecs, linear_invariant_names=list(map(str, compo_names)), backend=backend, dep_by_name=True, par_by_name=True ) validate = partial(_validate, rsys=rsys, symbols=symbols, odesys=odesys, backend=backend) return odesys, { 'symbols': symbols, 'validate': validate, 'unit_aware_solve': _mk_unit_aware_solve(odesys, unit_registry, validate=validate) if unit_registry else None }
class SymbolicSys(NeqSys): """ Parameters ---------- x : iterable of Symbols exprs : iterable of expressions for f params : iterable of Symbols (optional) list of symbols appearing in exprs which are parameters jac : ImmutableMatrix or bool If True: calculate jacobian from exprs If False: do not compute jacobian (numeric approximation) If ImmutableMatrix: user provided expressions for the jacobian backend : str or sym.Backend See documentation of `sym.Backend \ <https://pythonhosted.org/sym/sym.html#sym.backend.Backend>`_. module : str ``module`` keyword argument passed to ``backend.Lambdify``. \*\*kwargs: See :py:class:`pyneqsys.core.NeqSys` Examples -------- >>> import sympy as sp >>> e = sp.exp >>> x = x0, x1 = sp.symbols('x:2') >>> params = a, b = sp.symbols('a b') >>> neqsys = SymbolicSys(x, [a*(1 - x0), b*(x1 - x0**2)], params) >>> xout, sol = neqsys.solve('scipy', [-10, -5], [1, 10]) >>> print(xout) [ 1. 1.] >>> print(neqsys.get_jac()[0, 0]) -a Notes ----- When using SymPy as backend a limited number of unknowns is supported. The reason is that (currently) ``sympy.lambdify`` has an upper limit on number of arguments. """ def __init__(self, x, exprs, params=(), jac=True, backend=None, **kwargs): self.x = x self.exprs = exprs self.params = params self._jac = jac self.be = Backend(backend) self.nf, self.nx = len(exprs), len(x) # needed by get_*_cb self.band = kwargs.get('band', None) # needed by get_*_cb self.module = kwargs.pop('module', 'numpy') super(SymbolicSys, self).__init__(self.nf, self.nx, self._get_f_cb(), self._get_j_cb(), **kwargs) @classmethod def from_callback(cls, cb, nx=None, nparams=None, **kwargs): """ Generate a SymbolicSys instance from a callback""" if kwargs.get('x_by_name', False): if 'names' not in kwargs: raise ValueError("Need ``names`` in kwargs.") if nx is None: nx = len(kwargs['names']) elif nx != len(kwargs['names']): raise ValueError( "Inconsistency between nx and length of ``names``.") if kwargs.get('par_by_name', False): if 'param_names' not in kwargs: raise ValueError("Need ``param_names`` in kwargs.") if nparams is None: nparams = len(kwargs['param_names']) elif nparams != len(kwargs['param_names']): raise ValueError( "Inconsistency between ``nparam`` and length of ``param_names``." ) if nparams is None: nparams = 0 if nx is None: raise ValueError( "Need ``nx`` of ``names`` together with ``x_by_name==True``.") be = Backend(kwargs.pop('backend', None)) x = be.real_symarray('x', nx) p = be.real_symarray('p', nparams) _x = dict(zip(kwargs['names'], x)) if kwargs.get('x_by_name', False) else x _p = dict(zip(kwargs['param_names'], p)) if kwargs.get( 'par_by_name', False) else p try: exprs = cb(_x, _p, be) except TypeError: exprs = _ensure_3args(cb)(_x, _p, be) return cls(x, exprs, p, backend=be, **kwargs) def get_jac(self): """ Return the jacobian of the expressions """ if self._jac is True: if self.band is None: f = self.be.Matrix(self.nf, 1, self.exprs) _x = self.be.Matrix(self.nx, 1, self.x) return f.jacobian(_x) else: # Banded return self.be.Matrix( banded_jacobian(self.exprs, self.x, *self.band)) elif self._jac is False: return False else: return self._jac def _get_f_cb(self): args = list(chain(self.x, self.params)) kw = dict(module=self.module, dtype=object if self.module == 'mpmath' else None) try: cb = self.be.Lambdify(args, self.exprs, **kw) except TypeError: cb = self.be.Lambdify(args, self.exprs) def f(x, params): return cb(np.concatenate((x, params), axis=-1)) return f def _get_j_cb(self): args = list(chain(self.x, self.params)) kw = dict(module=self.module, dtype=object if self.module == 'mpmath' else None) try: cb = self.be.Lambdify(args, self.get_jac(), **kw) except TypeError: cb = self.be.Lambdify(args, self.get_jac()) def j(x, params): return cb(np.concatenate((x, params), axis=-1)) return j _use_symbol_latex_names = True def _repr_latex_(self): # pretty printing in Jupyter notebook from ._sympy import NeqSysTexPrinter if self.latex_names and (self.latex_param_names if len(self.params) else True): pretty = { s: n for s, n in chain( zip(self.x, self.latex_names) if self. _use_symbol_latex_names else [], zip(self.params, self.latex_param_names)) } else: pretty = {} return '$%s$' % NeqSysTexPrinter(dict(symbol_names=pretty)).doprint( self.exprs)
class SymbolicSys(OdeSys): """ ODE System from symbolic expressions Creates a :class:`OdeSys` instance from symbolic expressions. Jacboian and second derivatives are derived when needed. Parameters ---------- dep_exprs : iterable of (symbol, expression)-pairs indep : Symbol Independent variable (default: None => autonomous system). params : iterable of symbols Problem parameters. jac : ImmutableMatrix or bool (default: True) if True: calculate jacobian from exprs if False: do not compute jacobian (use explicit steppers) if instance of ImmutableMatrix: user provided expressions for the jacobian roots : iterable of expressions Equations to look for root's for during integration (currently available through cvode). backend : str or sym.Backend See documentation of `sym.Backend \ <https://pythonhosted.org/sym/sym.html#sym.backend.Backend>`_. \*\*kwargs: See :py:class:`OdeSys` Attributes ---------- dep : tuple of symbols Dependent variables. exprs : tuple of expressions Expressions for the derivatives of the dependent variables (:attr:`dep`) with respect to the independent variable (:attr:`indep`). indep : Symbol or None Independent variable (``None`` indicates autonomous system). params : iterable of symbols Problem parameters. roots : iterable of expressions or None Roots to report for during integration. ny : int ``len(self.dep)`` be : module Symbolic backend. """ def __init__(self, dep_exprs, indep=None, params=(), jac=True, dfdx=True, roots=None, backend=None, **kwargs): self.dep, self.exprs = zip(*dep_exprs) self.indep = indep self.params = params self._jac = jac self._dfdx = dfdx self.roots = roots self.be = Backend(backend) if kwargs.get('names', None) is True: kwargs['names'] = [y.name for y in self.dep] # we need self.band before super().__init__ self.band = kwargs.get('band', None) super(SymbolicSys, self).__init__( self.get_f_ty_callback(), self.get_j_ty_callback(), self.get_dfdx_callback(), self.get_roots_callback(), nroots=None if roots is None else len(roots), **kwargs) @classmethod def from_callback(cls, cb, ny, nparams=0, **kwargs): """ Create an instance from a callback. Parameters ---------- cb : callbable Signature ``rhs(x, y[:], p[:]) -> f[:]`` ny : int length of y nparams : int (default: 0) length of p \*\*kwargs : keyword arguments passed onto :class:`SymbolicSys` Returns ------- An instance of :class:`SymbolicSys`. """ be = Backend(kwargs.pop('backend', None)) x, = be.real_symarray('x', 1) y = be.real_symarray('y', ny) p = be.real_symarray('p', nparams) try: exprs = cb(x, y, p, be) except TypeError: exprs = _ensure_4args(cb)(x, y, p, be) return cls(zip(y, exprs), x, p, backend=be, **kwargs) @classmethod def from_other(cls, ori, **kwargs): # provisional new_kw = kwargs.copy() if ori.roots is not None: raise NotImplementedError('roots currently unsupported') if 'params' not in new_kw: new_kw['params'] = ori.params if len(ori.pre_processors) > 0: if 'pre_processors' not in new_kw: new_kw['pre_processors'] = [] new_kw['pre_processors'] = ori.pre_processors + new_kw['pre_processors'] if len(ori.post_processors) > 0: if 'post_processors' not in new_kw: new_kw['post_processors'] = [] new_kw['post_processors'] = ori.post_processors + new_kw['post_processors'] return cls(zip(ori.dep, ori.exprs), ori.indep, **new_kw) @property def ny(self): """ Number of dependent variables in the system. """ return len(self.exprs) @property def autonomous(self): if self.indep is None: return True for expr in self.exprs: if expr.has(self.indep): return False return True def get_jac(self): """ Derives the jacobian from ``self.exprs`` and ``self.dep``. """ if self._jac is True: if self.band is None: f = self.be.Matrix(1, self.ny, self.exprs) self._jac = f.jacobian(self.be.Matrix(1, self.ny, self.dep)) else: # Banded self._jac = self.be.banded_jacobian(self.exprs, self.dep, *self.band) elif self._jac is False: return False return self._jac def get_dfdx(self): """ Calculates 2nd derivatives of ``self.exprs`` """ if self._dfdx is True: if self.indep is None: zero = 0*self.be.Dummy()**0 self._dfdx = self.be.Matrix(1, self.ny, [zero]*self.ny) else: self._dfdx = self.be.Matrix(1, self.ny, [expr.diff(self.indep) for expr in self.exprs]) elif self._dfdx is False: return False return self._dfdx def _callback_factory(self, exprs): args = [self.indep] + list(self.dep) + list(self.params) return _Wrapper(self.be.Lambdify(args, exprs), self.ny) def get_f_ty_callback(self): """ Generates a callback for evaluating ``self.exprs``. """ return self._callback_factory(self.exprs) def get_j_ty_callback(self): """ Generates a callback for evaluating the jacobian. """ j_exprs = self.get_jac() if j_exprs is False: return None return self._callback_factory(j_exprs) def get_dfdx_callback(self): """ Generate a callback for evaluating derivative of ``self.exprs`` """ dfdx_exprs = self.get_dfdx() if dfdx_exprs is False: return None return self._callback_factory(dfdx_exprs) def get_roots_callback(self): """ Generate a callback for evaluating ``self.roots`` """ if self.roots is None: return None return self._callback_factory(self.roots) # Not working yet: def _integrate_mpmath(self, xout, y0, params=()): """ Not working at the moment, need to fix (low priority - taylor series is a poor method)""" raise NotImplementedError xout, y0, self.internal_params = self.pre_process(xout, y0, params) from mpmath import odefun def rhs(x, y): rhs.ncall += 1 return [ e.subs( ([(self.indep, x)] if self.indep is not None else []) + list(zip(self.dep, y)) ) for e in self.exprs ] rhs.ncall = 0 cb = odefun(rhs, xout[0], y0) yout = [] for x in xout: yout.append(cb(x)) info = {'nrhs': rhs.ncall} return self.post_process(xout, yout, self.internal_params)[:2] + (info,) def _get_analytic_stiffness_cb(self): J = self.get_jac() eig_vals = list(J.eigenvals().keys()) return self._callback_factory(eig_vals) def analytic_stiffness(self, xyp=None): """ Running stiffness ratio from last integration. Calculate sittness ratio, i.e. the ratio between the largest and smallest absolute eigenvalue of the (analytic) jacobian matrix. See :meth:`OdeSys.stiffness` for more info. """ return self.stiffness(xyp, self._get_analytic_stiffness_cb())
def _validate( conditions, rsys, symbols, odesys, backend=None, transform=None, ignore=("time", ), check_conditions_no_extra=False, ): """For use with create_odesys Parameters ---------- conditions : OrderedDict Parameters, values with units from ``chempy.units``. rsys : ReactionSystem symbols : dict Mapping variable name to symbols. backend : module Module for symbolic mathematics. (defaults to SymPy) transform : callable for rewriting expressions check_conditions_no_extra : bool When True, conditions may not contain keys not referenced in any expression. Raises ------ ``KeyError`` if a key in conditions is not in odesys.names or odesys.param_names """ if backend is None: from sym import Backend backend = Backend(backend) if transform is None: if backend.__name__ != "sympy": warnings.warn( "Backend != SymPy, provide your own transform function.") def transform(arg): expr = backend.logcombine(arg, force=True) v, w = map(backend.Wild, "v w".split()) return expr.replace(backend.log(w**v), v * backend.log(w)) rates = {} seen = set() for k, v in rsys.rates(symbols).items(): expr = transform(v) if expr == 0: rate = 0 * u.molar / u.second else: rate = None for term in (expr.args if hasattr(expr, "is_Add") and expr.is_Add else (expr, )): args = sorted(expr.free_symbols, key=lambda e: e.name) values = [conditions[s.name] for s in args] result = backend.lambdify(args, term)(*map(_exact, values)) to_unitless(result, u.molar / u.second) # raises an exception upon unit error if rate is None: rate = result else: rate += result rates[k] = rate seen |= set([s.name for s in expr.free_symbols]) if check_conditions_no_extra: for k in conditions: if (k not in odesys.param_names and k not in odesys.names and k not in ignore): raise KeyError("Unknown param: %s" % k) return { "not_seen": (set(rsys.substances) | set(conditions)) - seen, "rates": rates }
def _create_odesys( rsys, substance_symbols=None, parameter_symbols=None, pretty_replace=lambda x: x, backend=None, SymbolicSys=None, time_symbol=None, unit_registry=None, rates_kw=None, parameter_expressions=None, symbolic_kw=None, ): """This will be a simpler version of get_odesys without the unit handling code. The motivation is to reduce complexity (the code of get_odesys is long with multiple closures). This will also rely on SymPy explicitly and the user will be expected to deal with SymPy expressions. Only when this function has the same capabilities as get_odesys will it become and public API (along with a deprecation of get_odesys). Parameters ---------- rsys : ReactionSystem instance substance_symbols : OrderedDict If ``None``: ``rsys.substances`` will be used. parameter_symbols : OrderedDict backend : str or module Symbolic backend (e.g. sympy). The package ``sym`` is used as a wrapper. SymbolicSys: class See ``pyodesys`` for API. time_symbol : Symbol unit_registry : object e.g. ``chempy.units.SI_base_registry`` rates_kw : dict Keyword arguments passed to the ``rates`` method of rsys. parameter_expressions : dict Optional overrides. symbolic_kw : dict Keyword arguments passed on to SymbolicSys. Returns ------- SymbolicSys (subclass of ``pyodesys.ODESys``) dict : - ``'symbols'``: dict mapping str to symbol. - ``'validate'``: callable acppeting a dictionary mapping str to quantities """ if backend is None: from sym import Backend backend = Backend(backend) if SymbolicSys is None: from pyodesys.symbolic import SymbolicSys if substance_symbols is None: substance_symbols = OrderedDict([(key, backend.Symbol(key)) for key in rsys.substances]) if isinstance(substance_symbols, OrderedDict): if list(substance_symbols) != list(rsys.substances): raise ValueError( "substance_symbols needs to have same (oredered) keys as rsys.substances" ) if parameter_symbols is None: keys = [] for rxnpar in map(attrgetter("param"), rsys.rxns): if isinstance(rxnpar, str): if rxnpar in (parameter_expressions or {}): for pk in parameter_expressions[rxnpar].all_parameter_keys( ): keys.append(pk) else: keys.append(rxnpar) elif isinstance(rxnpar, Expr): keys.extend(rxnpar.all_unique_keys()) for pk in rxnpar.all_parameter_keys(): if pk not in keys: keys.append(pk) else: raise NotImplementedError("Unknown") if rates_kw and "cstr_fr_fc" in rates_kw: flowrate_volume, feed_conc = rates_kw["cstr_fr_fc"] keys.append(flowrate_volume) keys.extend(feed_conc.values()) assert all(sk in rsys.substances for sk in feed_conc) if len(keys) != len(set(keys)): raise ValueError("Duplicates in keys") parameter_symbols = OrderedDict([(key, backend.Symbol(key)) for key in keys]) if not isinstance(parameter_symbols, OrderedDict): raise ValueError("parameter_symbols needs to be an OrderedDict") symbols = OrderedDict( chain(substance_symbols.items(), parameter_symbols.items())) symbols["time"] = time_symbol or backend.Symbol("t") if any(symbols["time"] == v for k, v in symbols.items() if k != "time"): raise ValueError("time_symbol already in use (name clash?)") varbls = dict(symbols, **parameter_symbols) varbls.update(parameter_expressions or {}) rates = rsys.rates(varbls, **(rates_kw or {})) compo_vecs, compo_names = rsys.composition_balance_vectors() odesys = SymbolicSys( zip( [substance_symbols[key] for key in rsys.substances], [rates[key] for key in rsys.substances], ), symbols["time"], parameter_symbols.values(), names=list(rsys.substances.keys()), latex_names=[s.latex_name for s in rsys.substances.values()], param_names=parameter_symbols.keys(), latex_param_names=[ pretty_replace(n) for n in parameter_symbols.keys() ], linear_invariants=compo_vecs, linear_invariant_names=list(map(str, compo_names)), backend=backend, dep_by_name=True, par_by_name=True, **(symbolic_kw or {})) validate = partial(_validate, rsys=rsys, symbols=symbols, odesys=odesys, backend=backend) return odesys, { "symbols": symbols, "validate": validate, "unit_aware_solve": _mk_unit_aware_solve(odesys, unit_registry, validate=validate) if unit_registry else None, }