def test_NativeSys__PartiallySolvedSystem__roots(idx): def f(x, y, p): return [-p[0] * y[0], p[0] * y[0] - p[1] * y[1], p[1] * y[1]] def roots(x, y): return ([y[0] - y[1]], [y[0] - y[2]], [y[1] - y[2]])[idx] odesys = SymbolicSys.from_callback(f, 3, 2, roots_cb=roots) _p, _q, tend = 7, 3, 0.7 dep0 = (1, 0, 0) ref = [0.11299628093544488, 0.20674119231833346, 0.3541828705348678] def check(odesys): res = odesys.integrate(tend, dep0, (_p, _q), integrator='cvode', return_on_root=True) assert abs(res.xout[-1] - ref[idx]) < 1e-7 check(odesys) native = NativeSys.from_other(odesys) check(native) psys = PartiallySolvedSystem( odesys, lambda t0, xyz, par0, be: {odesys.dep[0]: xyz[0] * be.exp(-par0[0] * (odesys.indep - t0))}) check(psys) pnative = NativeSys.from_other(psys) check(pnative)
def _test_chained_multi_native(NativeSys, integrator='cvode', rtol_close=0.02, atol=1e-10, rtol=1e-14, steps_fact=1, **kwargs): logc, logt, reduced = kwargs.pop('logc'), kwargs.pop('logt'), kwargs.pop( 'reduced') zero_time, zero_conc, nonnegative = kwargs.pop('zero_time'), kwargs.pop( 'zero_conc'), kwargs.pop('nonnegative') logexp = (sp.log, sp.exp) ny, nk = 3, 3 k = (.04, 1e4, 3e7) init_conc = (1, zero_conc, zero_conc) tend = 1e11 _yref_1e11 = (0.2083340149701255e-7, 0.8333360770334713e-13, 0.9999999791665050) lin_s = SymbolicSys.from_callback(get_ode_exprs(logc=False, logt=False)[0], ny, nk, lower_bounds=[0] * ny if nonnegative else None) logexp = (sp.log, sp.exp) if reduced: if logc or logt: PartSolvSys = PartiallySolvedSystem # we'll add NativeSys further down below else: class PartSolvSys(PartiallySolvedSystem, NativeSys): pass other1, other2 = [_ for _ in range(3) if _ != (reduced - 1)] def reduced_analytic(x0, y0, p0): return { lin_s.dep[reduced - 1]: y0[0] + y0[1] + y0[2] - lin_s.dep[other1] - lin_s.dep[other2] } our_sys = PartSolvSys(lin_s, reduced_analytic) else: our_sys = lin_s if logc or logt: class TransformedNativeSys(TransformedSys, NativeSys): pass SS = symmetricsys(logexp if logc else None, logexp if logt else None, SuperClass=TransformedNativeSys) our_sys = SS.from_other(our_sys) ori_sys = NativeSys.from_other(lin_s) for sys_iter, kw in [([our_sys, ori_sys], { 'nsteps': [100 * steps_fact, 1613 * 1.01 * steps_fact], 'return_on_error': [True, False] }), ([ori_sys], { 'nsteps': [1705 * 1.01 * steps_fact] })]: results = integrate_chained(sys_iter, kw, [(zero_time, tend)] * 3, [init_conc] * 3, [k] * 3, integrator=integrator, atol=atol, rtol=rtol, **kwargs) for res in results: x, y, nfo = res assert np.allclose(_yref_1e11, y[-1, :], atol=1e-16, rtol=rtol_close) assert nfo['success'] == True # noqa assert nfo['nfev'] > 100 assert nfo['njev'] > 10 assert nfo['nsys'] in (1, 2)
def get_odesys(rsys, include_params=True, substitutions=None, SymbolicSys=None, unit_registry=None, output_conc_unit=None, output_time_unit=None, cstr=False, constants=None, **kwargs): """ Creates a :class:`pyneqsys.SymbolicSys` from a :class:`ReactionSystem` The parameters passed to RateExpr will contain the key ``'time'`` corresponding to the independent variable of the IVP. Parameters ---------- rsys : ReactionSystem Each reaction of ``rsys`` will have their :meth:`Reaction.rate_expr()` invoked. Note that if :attr:`Reaction.param` is not a :class:`RateExpr` (or convertible to one through :meth:`as_RateExpr`) it will be used to construct a :class:`MassAction` instance. include_params : bool (default: True) Whether rate constants should be included into the rate expressions or left as free parameters in the :class:`pyneqsys.SymbolicSys` instance. substitutions : dict, optional Variable substitutions used by rate expressions (in respective Reaction.param). values are allowed to be values of instances of :class:`Expr`. SymbolicSys : class (optional) Default : :class:`pyneqsys.SymbolicSys`. unit_registry: dict (optional) See :func:`chempy.units.get_derived_units`. output_conc_unit : unit (Optional) output_time_unit : unit (Optional) cstr : bool Generate expressions for continuously stirred tank reactor. constants : module e.g. ``chempy.units.default_constants``, parameter keys not found in substitutions will be looked for as an attribute of ``constants`` when provided. \\*\\*kwargs : Keyword arguemnts passed on to `SymbolicSys`. Returns ------- pyodesys.symbolic.SymbolicSys extra : dict, with keys: - param_keys : list of str instances - unique : OrderedDict mapping str to value (possibly None) - p_units : list of units - max_euler_step_cb : callable or None - linear_dependencies : None or factory of solver callback - rate_exprs_cb : callable - cstr_fr_fc : None or (feed-ratio-key, subtance-key-to-feed-conc-key-map) Examples -------- >>> from chempy import Equilibrium, ReactionSystem >>> eq = Equilibrium({'Fe+3', 'SCN-'}, {'FeSCN+2'}, 10**2) >>> substances = 'Fe+3 SCN- FeSCN+2'.split() >>> rsys = ReactionSystem(eq.as_reactions(kf=3.0), substances) >>> odesys, extra = get_odesys(rsys) >>> init_conc = {'Fe+3': 1.0, 'SCN-': .3, 'FeSCN+2': 0} >>> tout, Cout, info = odesys.integrate(5, init_conc) >>> Cout[-1, :].round(4) array([0.7042, 0.0042, 0.2958]) """ if SymbolicSys is None: from pyodesys.symbolic import SymbolicSys r_exprs = [rxn.rate_expr() for rxn in rsys.rxns] _ori_pk = set.union(*(ratex.all_parameter_keys() for ratex in r_exprs)) _ori_uk = set.union(*(ratex.all_unique_keys() for ratex in r_exprs)) _subst_pk = set() _active_subst = OrderedDict() _passive_subst = OrderedDict() substitutions = substitutions or {} unique = OrderedDict() unique_units = {} cstr_fr_fc = ( 'feedratio', OrderedDict([(sk, 'fc_'+sk) for sk in rsys.substances]) ) if cstr is True else cstr if cstr_fr_fc: _ori_pk.add(cstr_fr_fc[0]) for k in cstr_fr_fc[1].values(): _ori_pk.add(k) def _reg_unique_unit(k, arg_dim, idx): if unit_registry is None: return unique_units[k] = reduce(mul, [1]+[unit_registry[dim]**v for dim, v in arg_dim[idx].items()]) def _get_arg_dim(expr, rxn): if unit_registry is None: return None else: return expr.args_dimensionality(reaction=rxn) def _reg_unique(expr, rxn=None): if not isinstance(expr, Expr): raise NotImplementedError("Currently only Expr sub classes are supported.") if expr.args is None: for idx, k in enumerate(expr.unique_keys): if k not in substitutions: unique[k] = None _reg_unique_unit(k, _get_arg_dim(expr, rxn), idx) else: for idx, arg in enumerate(expr.args): if isinstance(arg, Expr): _reg_unique(arg, rxn=rxn) elif expr.unique_keys is not None and idx < len(expr.unique_keys): uk = expr.unique_keys[idx] if uk not in substitutions: unique[uk] = arg _reg_unique_unit(uk, _get_arg_dim(expr, rxn), idx) for sk, sv in substitutions.items(): if sk not in _ori_pk and sk not in _ori_uk: raise ValueError("Substitution: '%s' does not appear in any rate expressions." % sk) if isinstance(sv, Expr): _subst_pk.update(sv.parameter_keys) _active_subst[sk] = sv if not include_params: _reg_unique(sv) else: # if unit_registry is None: if unit_registry is not None: sv = unitless_in_registry(sv, unit_registry) _passive_subst[sk] = sv all_pk = [] for pk in filter(lambda x: x not in substitutions and x != 'time', _ori_pk.union(_subst_pk)): if hasattr(constants, pk): const = getattr(constants, pk) if unit_registry is None: const = magnitude(const) else: const = unitless_in_registry(const, unit_registry) _passive_subst[pk] = const else: all_pk.append(pk) if not include_params: for rxn, ratex in zip(rsys.rxns, r_exprs): _reg_unique(ratex, rxn) all_pk_with_unique = list(chain(all_pk, filter(lambda k: k not in all_pk, unique.keys()))) if include_params: param_names_for_odesys = all_pk else: param_names_for_odesys = all_pk_with_unique if unit_registry is None: p_units = None else: # We need to make rsys_params unitless and create # a pre- & post-processor for SymbolicSys pk_units = [_get_derived_unit(unit_registry, k) for k in all_pk] p_units = pk_units if include_params else (pk_units + [unique_units[k] for k in unique]) new_r_exprs = [] for ratex in r_exprs: _pu, _new_ratex = ratex.dedimensionalisation(unit_registry) new_r_exprs.append(_new_ratex) r_exprs = new_r_exprs time_unit = get_derived_unit(unit_registry, 'time') conc_unit = get_derived_unit(unit_registry, 'concentration') def post_processor(x, y, p): time = x*time_unit if output_time_unit is not None: time = rescale(time, output_time_unit) conc = y*conc_unit if output_conc_unit is not None: conc = rescale(conc, output_conc_unit) return time, conc, np.array([elem*p_unit for elem, p_unit in zip(p.T, p_units)], dtype=object).T kwargs['to_arrays_callbacks'] = ( lambda x: to_unitless(x, time_unit), lambda y: to_unitless(y, conc_unit), lambda p: np.array([to_unitless(px, p_unit) for px, p_unit in zip( p.T if hasattr(p, 'T') else p, p_units)]).T ) kwargs['post_processors'] = kwargs.get('post_processors', []) + [post_processor] def dydt(t, y, p, backend=math): variables = dict(chain(y.items(), p.items())) if 'time' in variables: raise ValueError("Key 'time' is reserved.") variables['time'] = t for k, act in _active_subst.items(): if unit_registry is not None and act.args: _, act = act.dedimensionalisation(unit_registry) variables[k] = act(variables, backend=backend) variables.update(_passive_subst) return rsys.rates(variables, backend=backend, ratexs=r_exprs, cstr_fr_fc=cstr_fr_fc) def reaction_rates(t, y, p, backend=math): variables = dict(chain(y.items(), p.items())) if 'time' in variables: raise ValueError("Key 'time' is reserved.") variables['time'] = t for k, act in _active_subst.items(): if unit_registry is not None and act.args: _, act = act.dedimensionalisation(unit_registry) variables[k] = act(variables, backend=backend) variables.update(_passive_subst) return [ratex(variables, backend=backend, reaction=rxn) for rxn, ratex in zip(rsys.rxns, r_exprs)] names = [s.name for s in rsys.substances.values()] latex_names = [None if s.latex_name is None else ('\\mathrm{' + s.latex_name + '}') for s in rsys.substances.values()] compo_vecs, compo_names = rsys.composition_balance_vectors() odesys = SymbolicSys.from_callback( dydt, dep_by_name=True, par_by_name=True, names=names, latex_names=latex_names, param_names=param_names_for_odesys, linear_invariants=None if len(compo_vecs) == 0 else compo_vecs, linear_invariant_names=None if len(compo_names) == 0 else list(map(str, compo_names)), **kwargs) symbolic_ratexs = reaction_rates( odesys.indep, dict(zip(odesys.names, odesys.dep)), dict(zip(odesys.param_names, odesys.params)), backend=odesys.be) rate_exprs_cb = odesys._callback_factory(symbolic_ratexs) if rsys.check_balance(strict=True): # Composition available, we can provide callback for calculating # maximum allowed Euler forward step at start of integration. def max_euler_step_cb(x, y, p=()): _x, _y, _p = odesys.pre_process(*odesys.to_arrays(x, y, p)) upper_bounds = rsys.upper_conc_bounds(_y) fvec = odesys.f_cb(_x[0], _y, _p) h = [] for idx, fcomp in enumerate(fvec): if fcomp == 0: h.append(float('inf')) elif fcomp > 0: h.append((upper_bounds[idx] - _y[idx])/fcomp) else: # fcomp < 0 h.append(-_y[idx]/fcomp) min_h = min(h) return min(min_h, 1) def linear_dependencies(preferred=None): if preferred is not None: if len(preferred) == 0: raise ValueError("No preferred substance keys provided") if len(preferred) >= len(rsys.substances): raise ValueError("Cannot remove all concentrations from linear dependencies") for k in preferred: if k not in rsys.substances: raise ValueError("Unknown substance key: %s" % k) def analytic_solver(x0, y0, p0, be): if preferred is None: _preferred = None else: _preferred = list(preferred) A = be.Matrix(compo_vecs) rA, pivots = A.rref() analytic_exprs = OrderedDict() for ri, ci1st in enumerate(pivots): for idx in range(ci1st, odesys.ny): key = odesys.names[idx] if rA[ri, idx] == 0: continue if _preferred is None or key in _preferred: terms = [rA[ri, di]*(odesys.dep[di] - y0[odesys.dep[di]]) for di in range(ci1st, odesys.ny) if di != idx] analytic_exprs[odesys[key]] = y0[odesys.dep[idx]] - sum(terms)/rA[ri, idx] if _preferred is not None: _preferred.remove(key) break for k in reversed(list(analytic_exprs.keys())): analytic_exprs[k] = analytic_exprs[k].subs(analytic_exprs) if _preferred is not None and len(_preferred) > 0: raise ValueError("Failed to obtain analytic expression for: %s" % ', '.join(_preferred)) return analytic_exprs return analytic_solver else: max_euler_step_cb = None linear_dependencies = None return odesys, { 'param_keys': all_pk, 'unique': unique, 'p_units': p_units, 'max_euler_step_cb': max_euler_step_cb, 'linear_dependencies': linear_dependencies, 'rate_exprs_cb': rate_exprs_cb, 'cstr_fr_fc': cstr_fr_fc, 'unit_registry': unit_registry }
def get_odesys(rsys, include_params=True, substitutions=None, SymbolicSys=None, unit_registry=None, output_conc_unit=None, output_time_unit=None, **kwargs): """ Creates a :class:`pyneqsys.SymbolicSys` from a :class:`ReactionSystem` Parameters ---------- rsys : ReactionSystem note that if :attr:`param` if not RateExpr (or convertible to one through :meth:`_as_RateExpr`) it will be used to construct a :class:`MassAction` instance. include_params : bool (default: False) whether rate constants should be included into the rate expressions or left as free parameters in the :class:`pyneqsys.SymbolicSys` instance. substitutions : dict, optional variable substitutions used by rate expressions (in respective Reaction.param). values are allowed to be tuple like: (new_vars, callback) SymbolicSys : class (optional) default : :class:`pyneqsys.SymbolicSys` unit_registry: dict (optional) see :func:`chempy.units.get_derived_units` output_conc_unit : unit (Optional) output_time_unit : unit (Optional) \*\*kwargs : Keyword arguemnts pass on to `SymbolicSys` Returns ------- pyodesys.symbolic.SymbolicSys param_keys unique_keys p_units Examples -------- >>> from chempy import Equilibrium, ReactionSystem >>> eq = Equilibrium({'Fe+3', 'SCN-'}, {'FeSCN+2'}, 10**2) >>> substances = 'Fe+3 SCN- FeSCN+2'.split() >>> rsys = ReactionSystem(eq.as_reactions(kf=3.0), substances) >>> odesys = get_odesys(rsys)[0] >>> init_conc = {'Fe+3': 1.0, 'SCN-': .3, 'FeSCN+2': 0} >>> tout, Cout, info = odesys.integrate(5, init_conc) >>> Cout[-1, :].round(4) array([ 0.7042, 0.0042, 0.2958]) """ if SymbolicSys is None: from pyodesys.symbolic import SymbolicSys substance_keys = list(rsys.substances.keys()) if 'names' not in kwargs: kwargs['names'] = list(rsys.substances.values()) # pyodesys>=0.5.3 r_exprs = [rxn.rate_expr() for rxn in rsys.rxns] _original_param_keys = set.union(*(set(ratex.parameter_keys) for ratex in r_exprs)) _from_subst = set() _active_subst = {} _passive_subst = {} substitutions = substitutions or {} for key, v in substitutions.items(): if key not in _original_param_keys: raise ValueError( "Substitution: '%s' does not appear in any rate expressions.") if isinstance(v, Expr): _from_subst.update(v.parameter_keys) _active_subst[key] = v else: _passive_subst[key] = v param_keys = list( filter(lambda x: x not in substitutions, _original_param_keys.union(_from_subst))) unique_keys = [] p_defaults = [] if not include_params: for ratex in r_exprs: if ratex.unique_keys is not None: unique_keys.extend(ratex.unique_keys) p_defaults.extend(ratex.args) if unit_registry is None: def pre_processor(x, y, p): return (x, rsys.as_per_substance_array(y), [p[k] for k in param_keys] + [p[k] for k in unique_keys]) def post_processor(x, y, p): return ( x, y, # dict(zip(substance_keys, y)), dict(zip(param_keys + unique_keys, p))) p_units = [None] * (len(param_keys) + len(unique_keys)) else: # We need to make rsys_params unitless and create # a pre- & post-processor for SymbolicSys p_units = [get_derived_unit(unit_registry, k) for k in param_keys] new_r_exprs = [] for ratex in r_exprs: _pu, _new_rate = ratex._recursive_as_RateExpr( ).dedimensionalisation(unit_registry) p_units.extend(_pu) new_r_exprs.append(_new_rate) r_exprs = new_r_exprs time_unit = get_derived_unit(unit_registry, 'time') conc_unit = get_derived_unit(unit_registry, 'concentration') def pre_processor(x, y, p): return (to_unitless(x, time_unit), rsys.as_per_substance_array(to_unitless(y, conc_unit)), [ to_unitless(p[k], p_unit) for k, p_unit in zip( chain(param_keys, unique_keys), p_units) ]) def post_processor(x, y, p): time = x * time_unit if output_time_unit is not None: time = time.rescale(output_time_unit) conc = y * conc_unit if output_conc_unit is not None: conc = conc.rescale(output_conc_unit) return time, conc, [ elem * p_unit for elem, p_unit in zip(p, p_units) ] kwargs['pre_processors'] = [pre_processor] + kwargs.get( 'pre_processors', []) kwargs['post_processors'] = kwargs.get('post_processors', []) + [post_processor] def dydt(t, y, p, backend=math): variables = dict( chain(zip(substance_keys, y), zip(param_keys, p[:len(param_keys)]), zip(unique_keys, p[len(param_keys):]))) for k, act in _active_subst.items(): if unit_registry is not None: _, act = act.dedimensionalisation(unit_registry) variables[k] = act(variables, backend=backend) variables.update(_passive_subst) return dCdt(rsys, [rat(variables, backend=backend) for rat in r_exprs]) return SymbolicSys.from_callback( dydt, len(substance_keys), len(param_keys) + (0 if include_params else len(unique_keys)), **kwargs), param_keys, unique_keys, p_units
def get_odesys(rsys, include_params=True, substitutions=None, SymbolicSys=None, unit_registry=None, output_conc_unit=None, output_time_unit=None, cstr=False, constants=None, **kwargs): """ Creates a :class:`pyneqsys.SymbolicSys` from a :class:`ReactionSystem` The parameters passed to RateExpr will contain the key ``'time'`` corresponding to the independent variable of the IVP. Parameters ---------- rsys : ReactionSystem Each reaction of ``rsys`` will have their :meth:`Reaction.rate_expr()` invoked. Note that if :attr:`Reaction.param` is not a :class:`RateExpr` (or convertible to one through :meth:`as_RateExpr`) it will be used to construct a :class:`MassAction` instance. include_params : bool (default: True) Whether rate constants should be included into the rate expressions or left as free parameters in the :class:`pyneqsys.SymbolicSys` instance. substitutions : dict, optional Variable substitutions used by rate expressions (in respective Reaction.param). values are allowed to be values of instances of :class:`Expr`. SymbolicSys : class (optional) Default : :class:`pyneqsys.SymbolicSys`. unit_registry: dict (optional) See :func:`chempy.units.get_derived_units`. output_conc_unit : unit (Optional) output_time_unit : unit (Optional) cstr : bool Generate expressions for continuously stirred tank reactor. constants : module e.g. ``chempy.units.default_constants``, parameter keys not found in substitutions will be looked for as an attribute of ``constants`` when provided. \\*\\*kwargs : Keyword arguemnts passed on to `SymbolicSys`. Returns ------- pyodesys.symbolic.SymbolicSys extra : dict, with keys: - param_keys : list of str instances - unique : OrderedDict mapping str to value (possibly None) - p_units : list of units - max_euler_step_cb : callable or None - linear_dependencies : None or factory of solver callback - rate_exprs_cb : callable - cstr_fr_fc : None or (feed-ratio-key, subtance-key-to-feed-conc-key-map) Examples -------- >>> from chempy import Equilibrium, ReactionSystem >>> eq = Equilibrium({'Fe+3', 'SCN-'}, {'FeSCN+2'}, 10**2) >>> substances = 'Fe+3 SCN- FeSCN+2'.split() >>> rsys = ReactionSystem(eq.as_reactions(kf=3.0), substances) >>> odesys, extra = get_odesys(rsys) >>> init_conc = {'Fe+3': 1.0, 'SCN-': .3, 'FeSCN+2': 0} >>> tout, Cout, info = odesys.integrate(5, init_conc) >>> Cout[-1, :].round(4) array([0.7042, 0.0042, 0.2958]) """ if SymbolicSys is None: from pyodesys.symbolic import SymbolicSys r_exprs = [rxn.rate_expr() for rxn in rsys.rxns] _ori_pk = set.union(*(ratex.all_parameter_keys() for ratex in r_exprs)) _ori_uk = set.union(*(ratex.all_unique_keys() for ratex in r_exprs)) _subst_pk = set() _active_subst = OrderedDict() _passive_subst = OrderedDict() substitutions = substitutions or {} unique = OrderedDict() unique_units = {} cstr_fr_fc = ( 'feedratio', OrderedDict([(sk, 'fc_'+sk) for sk in rsys.substances]) ) if cstr is True else cstr if cstr_fr_fc: _ori_pk.add(cstr_fr_fc[0]) for k in cstr_fr_fc[1].values(): _ori_pk.add(k) def _reg_unique_unit(k, arg_dim, idx): if unit_registry is None: return unique_units[k] = reduce(mul, [1]+[unit_registry[dim]**v for dim, v in arg_dim[idx].items()]) def _get_arg_dim(expr, rxn): if unit_registry is None: return None else: return expr.args_dimensionality(reaction=rxn) def _reg_unique(expr, rxn=None): if not isinstance(expr, Expr): raise NotImplementedError("Currently only Expr sub classes are supported.") if expr.args is None: for idx, k in enumerate(expr.unique_keys): if k not in substitutions: unique[k] = None _reg_unique_unit(k, _get_arg_dim(expr, rxn), idx) else: for idx, arg in enumerate(expr.args): if isinstance(arg, Expr): _reg_unique(arg, rxn=rxn) elif expr.unique_keys is not None and idx < len(expr.unique_keys): uk = expr.unique_keys[idx] if uk not in substitutions: unique[uk] = arg _reg_unique_unit(uk, _get_arg_dim(expr, rxn), idx) for sk, sv in substitutions.items(): if sk not in _ori_pk and sk not in _ori_uk: raise ValueError("Substitution: '%s' does not appear in any rate expressions." % sk) if isinstance(sv, Expr): _subst_pk.update(sv.parameter_keys) _active_subst[sk] = sv if not include_params: _reg_unique(sv) else: # if unit_registry is None: if unit_registry is not None: sv = unitless_in_registry(sv, unit_registry) _passive_subst[sk] = sv all_pk = [] for pk in filter(lambda x: x not in substitutions and x != 'time', _ori_pk.union(_subst_pk)): if hasattr(constants, pk): const = getattr(constants, pk) if unit_registry is None: const = magnitude(const) else: const = unitless_in_registry(const, unit_registry) _passive_subst[pk] = const else: all_pk.append(pk) if not include_params: for rxn, ratex in zip(rsys.rxns, r_exprs): _reg_unique(ratex, rxn) all_pk_with_unique = list(chain(all_pk, filter(lambda k: k not in all_pk, unique.keys()))) if include_params: param_names_for_odesys = all_pk else: param_names_for_odesys = all_pk_with_unique if unit_registry is None: p_units = None else: # We need to make rsys_params unitless and create # a pre- & post-processor for SymbolicSys pk_units = [_get_derived_unit(unit_registry, k) for k in all_pk] p_units = pk_units if include_params else (pk_units + [unique_units[k] for k in unique]) new_r_exprs = [] for ratex in r_exprs: _pu, _new_ratex = ratex.dedimensionalisation(unit_registry) new_r_exprs.append(_new_ratex) r_exprs = new_r_exprs time_unit = get_derived_unit(unit_registry, 'time') conc_unit = get_derived_unit(unit_registry, 'concentration') def post_processor(x, y, p): time = x*time_unit if output_time_unit is not None: time = rescale(time, output_time_unit) conc = y*conc_unit if output_conc_unit is not None: conc = rescale(conc, output_conc_unit) return time, conc, np.array([elem*p_unit for elem, p_unit in zip(p.T, p_units)], dtype=object).T kwargs['to_arrays_callbacks'] = ( lambda x: to_unitless(x, time_unit), lambda y: to_unitless(y, conc_unit), lambda p: np.array([to_unitless(px, p_unit) for px, p_unit in zip( p.T if hasattr(p, 'T') else p, p_units)]).T ) kwargs['post_processors'] = kwargs.get('post_processors', []) + [post_processor] def dydt(t, y, p, backend=math): variables = dict(chain(y.items(), p.items())) if 'time' in variables: raise ValueError("Key 'time' is reserved.") variables['time'] = t for k, act in _active_subst.items(): if unit_registry is not None and act.args: _, act = act.dedimensionalisation(unit_registry) variables[k] = act(variables, backend=backend) variables.update(_passive_subst) return rsys.rates(variables, backend=backend, ratexs=r_exprs, cstr_fr_fc=cstr_fr_fc) def reaction_rates(t, y, p, backend=math): variables = dict(chain(y.items(), p.items())) if 'time' in variables: raise ValueError("Key 'time' is reserved.") variables['time'] = t for k, act in _active_subst.items(): if unit_registry is not None and act.args: _, act = act.dedimensionalisation(unit_registry) variables[k] = act(variables, backend=backend) variables.update(_passive_subst) return [ratex(variables, backend=backend, reaction=rxn) for rxn, ratex in zip(rsys.rxns, r_exprs)] names = [s.name for s in rsys.substances.values()] latex_names = [None if s.latex_name is None else ('\\mathrm{' + s.latex_name + '}') for s in rsys.substances.values()] compo_vecs, compo_names = rsys.composition_balance_vectors() odesys = SymbolicSys.from_callback( dydt, dep_by_name=True, par_by_name=True, names=names, latex_names=latex_names, param_names=param_names_for_odesys, linear_invariants=None if len(compo_vecs) == 0 else compo_vecs, linear_invariant_names=None if len(compo_names) == 0 else list(map(str, compo_names)), **kwargs) symbolic_ratexs = reaction_rates( odesys.indep, dict(zip(odesys.names, odesys.dep)), dict(zip(odesys.param_names, odesys.params)), backend=odesys.be) rate_exprs_cb = odesys._callback_factory(symbolic_ratexs) if rsys.check_balance(strict=True): # Composition available, we can provide callback for calculating # maximum allowed Euler forward step at start of integration. def max_euler_step_cb(x, y, p=()): _x, _y, _p = odesys.pre_process(*odesys.to_arrays(x, y, p)) upper_bounds = rsys.upper_conc_bounds(_y) fvec = odesys.f_cb(_x[0], _y, _p) h = [] for idx, fcomp in enumerate(fvec): if fcomp == 0: h.append(float('inf')) elif fcomp > 0: h.append((upper_bounds[idx] - _y[idx])/fcomp) else: # fcomp < 0 h.append(-_y[idx]/fcomp) min_h = min(h) return min(min_h, 1) def linear_dependencies(preferred=None): if preferred is not None: if len(preferred) == 0: raise ValueError("No preferred substance keys provided") if len(preferred) >= len(rsys.substances): raise ValueError("Cannot remove all concentrations from linear dependencies") for k in preferred: if k not in rsys.substances: raise ValueError("Unknown substance key: %s" % k) def analytic_solver(x0, y0, p0, be): if preferred is None: _preferred = None else: _preferred = list(preferred) A = be.Matrix(compo_vecs) rA, pivots = A.rref() analytic_exprs = OrderedDict() for ri, ci1st in enumerate(pivots): for idx in range(ci1st, odesys.ny): key = odesys.names[idx] if rA[ri, idx] == 0: continue if _preferred is None or key in _preferred: terms = [rA[ri, di]*(odesys.dep[di] - y0[odesys.dep[di]]) for di in range(ci1st, odesys.ny) if di != idx] analytic_exprs[odesys[key]] = y0[odesys.dep[idx]] - sum(terms)/rA[ri, idx] if _preferred is not None: _preferred.remove(key) break for k in reversed(list(analytic_exprs.keys())): analytic_exprs[k] = analytic_exprs[k].subs(analytic_exprs) if _preferred is not None and len(_preferred) > 0: raise ValueError("Failed to obtain analytic expression for: %s" % ', '.join(_preferred)) return analytic_exprs return analytic_solver else: max_euler_step_cb = None linear_dependencies = None return odesys, { 'param_keys': all_pk, 'unique': unique, 'p_units': p_units, 'max_euler_step_cb': max_euler_step_cb, 'linear_dependencies': linear_dependencies, 'rate_exprs_cb': rate_exprs_cb, 'cstr_fr_fc': cstr_fr_fc, 'unit_registry': unit_registry }
from pyodesys.symbolic import SymbolicSys def f(t, y, p): return [y[1], -y[0] + p[0] * y[1] * (1 - y[0]**2)] odesys = SymbolicSys.from_callback(f, 2, 1) xout, yout, info = odesys.integrate(10, [1, 0], [1], integrator='odeint', nsteps=1000) _ = odesys.plot_result() import matplotlib.pyplot as plt plt.show() # doctest: +SKIP
def get_odesys(rsys, include_params=True, substitutions=None, SymbolicSys=None, unit_registry=None, output_conc_unit=None, output_time_unit=None, **kwargs): """ Creates a :class:`pyneqsys.SymbolicSys` from a :class:`ReactionSystem` Parameters ---------- rsys : ReactionSystem note that if :attr:`param` if not RateExpr (or convertible to one through :meth:`_as_RateExpr`) it will be used to construct a :class:`MassAction` instance. include_params : bool (default: False) whether rate constants should be included into the rate expressions or left as free parameters in the :class:`pyneqsys.SymbolicSys` instance. substitutions : dict, optional variable substitutions used by rate expressions (in respective Reaction.param). values are allowed to be tuple like: (new_vars, callback) SymbolicSys : class (optional) default : :class:`pyneqsys.SymbolicSys` unit_registry: dict (optional) see :func:`chempy.units.get_derived_units` output_conc_unit : unit (Optional) output_time_unit : unit (Optional) \*\*kwargs : Keyword arguemnts pass on to `SymbolicSys` Returns ------- pyodesys.symbolic.SymbolicSys param_keys unique_keys p_units Examples -------- >>> from chempy import Equilibrium, ReactionSystem >>> eq = Equilibrium({'Fe+3', 'SCN-'}, {'FeSCN+2'}, 10**2) >>> substances = 'Fe+3 SCN- FeSCN+2'.split() >>> rsys = ReactionSystem(eq.as_reactions(kf=3.0), substances) >>> odesys = get_odesys(rsys)[0] >>> init_conc = {'Fe+3': 1.0, 'SCN-': .3, 'FeSCN+2': 0} >>> tout, Cout, info = odesys.integrate(5, init_conc) >>> Cout[-1, :].round(4) array([ 0.7042, 0.0042, 0.2958]) """ if SymbolicSys is None: from pyodesys.symbolic import SymbolicSys substance_keys = list(rsys.substances.keys()) if 'names' not in kwargs: kwargs['names'] = list(rsys.substances.values()) # pyodesys>=0.5.3 r_exprs = [rxn.rate_expr() for rxn in rsys.rxns] _original_param_keys = set.union(*(set(ratex.parameter_keys) for ratex in r_exprs)) _from_subst = set() _active_subst = {} _passive_subst = {} substitutions = substitutions or {} for key, v in substitutions.items(): if key not in _original_param_keys: raise ValueError("Substitution: '%s' does not appear in any rate expressions.") if isinstance(v, Expr): _from_subst.update(v.parameter_keys) _active_subst[key] = v else: _passive_subst[key] = v param_keys = list(filter(lambda x: x not in substitutions, _original_param_keys.union(_from_subst))) unique_keys = [] p_defaults = [] if not include_params: for ratex in r_exprs: if ratex.unique_keys is not None: unique_keys.extend(ratex.unique_keys) p_defaults.extend(ratex.args) if unit_registry is None: def pre_processor(x, y, p): return ( x, rsys.as_per_substance_array(y), [p[k] for k in param_keys] + [p[k] for k in unique_keys] ) def post_processor(x, y, p): return ( x, y, # dict(zip(substance_keys, y)), dict(zip(param_keys+unique_keys, p)) ) p_units = [None]*(len(param_keys) + len(unique_keys)) else: # We need to make rsys_params unitless and create # a pre- & post-processor for SymbolicSys p_units = [get_derived_unit(unit_registry, k) for k in param_keys] new_r_exprs = [] for ratex in r_exprs: _pu, _new_rate = ratex._recursive_as_RateExpr().dedimensionalisation(unit_registry) p_units.extend(_pu) new_r_exprs.append(_new_rate) r_exprs = new_r_exprs time_unit = get_derived_unit(unit_registry, 'time') conc_unit = get_derived_unit(unit_registry, 'concentration') def pre_processor(x, y, p): return ( to_unitless(x, time_unit), rsys.as_per_substance_array(to_unitless(y, conc_unit)), [to_unitless(p[k], p_unit) for k, p_unit in zip(chain(param_keys, unique_keys), p_units)] ) def post_processor(x, y, p): time = x*time_unit if output_time_unit is not None: time = time.rescale(output_time_unit) conc = y*conc_unit if output_conc_unit is not None: conc = conc.rescale(output_conc_unit) return time, conc, [elem*p_unit for elem, p_unit in zip(p, p_units)] kwargs['pre_processors'] = [pre_processor] + kwargs.get('pre_processors', []) kwargs['post_processors'] = kwargs.get('post_processors', []) + [post_processor] def dydt(t, y, p, backend=math): variables = dict(chain( zip(substance_keys, y), zip(param_keys, p[:len(param_keys)]), zip(unique_keys, p[len(param_keys):]) )) for k, act in _active_subst.items(): if unit_registry is not None: _, act = act.dedimensionalisation(unit_registry) variables[k] = act(variables, backend=backend) variables.update(_passive_subst) return dCdt(rsys, [rat(variables, backend=backend) for rat in r_exprs]) return SymbolicSys.from_callback( dydt, len(substance_keys), len(param_keys) + (0 if include_params else len(unique_keys)), **kwargs), param_keys, unique_keys, p_units