def test_get_odesys_3(): M = u.molar s = u.second mol = u.mol m = u.metre substances = list(map(Substance, 'H2O H+ OH-'.split())) dissociation = Reaction({'H2O': 1}, {'H+': 1, 'OH-': 1}, 2.47e-5/s) recombination = Reaction({'H+': 1, 'OH-': 1}, {'H2O': 1}, 1.37e11/M/s) rsys = ReactionSystem([dissociation, recombination], substances) odesys = get_odesys( rsys, include_params=True, unit_registry=SI_base_registry, output_conc_unit=M)[0] c0 = {'H2O': 55.4*M, 'H+': 1e-7*M, 'OH-': 1e-4*mol/m**3} x, y, p = odesys.to_arrays(-42*u.second, rsys.as_per_substance_array(c0, unit=M), ()) fout = odesys.f_cb(x, y, p) time_unit = get_derived_unit(SI_base_registry, 'time') conc_unit = get_derived_unit(SI_base_registry, 'concentration') r1 = to_unitless(55.4*2.47e-5*M/s, conc_unit/time_unit) r2 = to_unitless(1e-14*1.37e11*M/s, conc_unit/time_unit) assert np.all(abs(fout[:, 0] - r2 + r1)) < 1e-10 assert np.all(abs(fout[:, 1] - r1 + r2)) < 1e-10 assert np.all(abs(fout[:, 2] - r1 + r2)) < 1e-10
def test_get_odesys__Eyring(): R = 8.314472 T_K = 300 dH = 80e3 dS = 10 rsys1 = ReactionSystem.from_string(""" NOBr -> NO + Br; EyringParam(dH={dH}*J/mol, dS={dS}*J/K/mol) """.format(dH=dH, dS=dS), substances='NOBr NO Br'.split()) kref = 20836643994.118652*T_K*np.exp(-(dH - T_K*dS)/(R*T_K)) NOBr0_M = 0.7 init_cond = dict( NOBr=NOBr0_M*u.M, NO=0*u.M, Br=0*u.M ) t = 5*u.second params = dict( temperature=T_K*u.K ) def check(rsys): odesys, extra = get_odesys(rsys, unit_registry=SI_base_registry, constants=const) res = odesys.integrate(t, init_cond, params, integrator='cvode') NOBr_ref = NOBr0_M*np.exp(-kref*to_unitless(res.xout, u.second)) ref = np.array([NOBr_ref] + [NOBr0_M - NOBr_ref]*2).T cmp = to_unitless(res.yout, u.M) assert np.allclose(cmp, ref) check(rsys1) rsys2 = ReactionSystem.from_string(""" NOBr -> NO + Br; MassAction(EyringHS([{dH}*J/mol, {dS}*J/K/mol])) """.format(dH=dH, dS=dS), substances='NOBr NO Br'.split()) check(rsys2)
def test_get_odesys__with_units(): a = Substance('A') b = Substance('B') molar = u.molar second = u.second r = Reaction({'A': 2}, {'B': 1}, param=1e-3/molar/second) rsys = ReactionSystem([r], [a, b]) odesys = get_odesys(rsys, include_params=True, unit_registry=SI_base_registry)[0] c0 = { 'A': 13*u.mol / u.metre**3, 'B': .2 * u.molar } conc_unit = get_derived_unit(SI_base_registry, 'concentration') t = np.linspace(0, 10)*u.hour xout, yout, info = odesys.integrate( t, rsys.as_per_substance_array(c0, unit=conc_unit), atol=1e-10, rtol=1e-12) t_unitless = to_unitless(xout, u.second) Aref = dimerization_irrev(t_unitless, 1e-6, 13.0) # Aref = 1/(1/13 + 2*1e-6*t_unitless) yref = np.zeros((xout.size, 2)) yref[:, 0] = Aref yref[:, 1] = 200 + (13-Aref)/2 assert allclose(yout, yref*conc_unit)
def decompose_yields(yields, rxns, atol=1e-10): """ Decomposes yields into mass-action reactions This function offers a way to express a reaction with non-integer stoichiometric coefficients as a linear combination of production reactions with integer coefficients. Ak = y A is (n_species x n_reactions) matrix, k is "rate coefficient", y is yields Parameters ---------- yields : OrderedDict Specie names as keys and yields as values. rxns : iterable :class:`Reaction` instances Dict keys must match those of ``yields`` each pair of dictionaries gives stoichiometry (1st is reactant, 2nd is products). atol : float Absolute tolerance for residuals. Examples -------- >>> from chempy import Reaction >>> h2a = Reaction({'H2O': 1}, {'H2': 1, 'O': 1}) >>> h2b = Reaction({'H2O': 1}, {'H2': 1, 'H2O2': 1}, inact_reac={'H2O': 1}) >>> decompose_yields({'H2': 3, 'O': 2, 'H2O2': 1}, [h2a, h2b]) array([2., 1.]) Raises ------ ValueError When atol is exceeded numpy.LinAlgError When numpy.linalg.lstsq fails to converge Returns ------- 1-dimensional array of effective rate coefficients. """ from chempy import ReactionSystem # Sanity check: rxn_keys = set.union(*(rxn.keys() for rxn in rxns)) for key in yields.keys(): if key not in rxn_keys: raise ValueError("Substance key: %s not in reactions" % key) rsys = ReactionSystem(rxns, rxn_keys) A = rsys.net_stoichs(yields.keys()) b = list(yields.values()) unit = unit_of(b[0]) x, residuals, rank, s = np.linalg.lstsq( np.asarray(A.T, dtype=np.float64), to_unitless(b, unit), rcond=2e-16*max(A.shape)) if len(residuals) > 0: if np.any(residuals > atol): raise ValueError("atol not satisfied") return x*unit
def decompose_yields(yields, rxns, atol=1e-10): """ Decomposes yields into mass-action reactions This function offers a way to express a reaction with non-integer stoichiometric coefficients as a linear combination of production reactions with integer coefficients. Ak = y A is (n_species x n_reactions) matrix, k is "rate coefficient", y is yields Parameters ---------- yields: OrderedDict specie names as keys and yields as values rxns: iterable :class:`Reaction` instances dict keys must match those of ``yields`` each pair of dictionaries gives stoichiometry (1st is reactant, 2nd is products) atol: float absolute tolerance for residuals Examples -------- >>> from chempy import Reaction >>> h2a = Reaction({'H2O': 1}, {'H2': 1, 'O': 1}) >>> h2b = Reaction({'H2O': 1}, {'H2': 1, 'H2O2': 1}, inact_reac={'H2O': 1}) >>> decompose_yields({'H2': 3, 'O': 2, 'H2O2': 1}, [h2a, h2b]) array([ 2., 1.]) Raises ------ ValueError When atol is exceeded numpy.LinAlgError When numpy.linalg.lstsq fails to converge Returns ------- 1-dimensional array of effective rate coefficients. """ from chempy import ReactionSystem # Sanity check: rxn_keys = set.union(*(rxn.keys() for rxn in rxns)) for key in yields.keys(): if key not in rxn_keys: raise ValueError("Substance key: %s not in reactions" % key) rsys = ReactionSystem(rxns, rxn_keys) A = rsys.net_stoichs(yields.keys()) b = list(yields.values()) unit = unit_of(b[0]) x, residuals, rank, s = np.linalg.lstsq(A.T, to_unitless(b, unit)) if len(residuals) > 0: if np.any(residuals > atol): raise ValueError("atol not satisfied") return x*unit
def test_get_odesys__Eyring_2nd_order_reversible(): R = 8.314472 T_K = 273.15 + 20 # 20 degree celsius kB = 1.3806504e-23 h = 6.62606896e-34 dHf = 74e3 dSf = R * np.log(h / kB / T_K * 1e16) dHb = 79e3 dSb = dSf - 23 rsys1 = ReactionSystem.from_string(""" Fe+3 + SCN- -> FeSCN+2; EyringParam(dH={dHf}*J/mol, dS={dSf}*J/K/mol) FeSCN+2 -> Fe+3 + SCN-; EyringParam(dH={dHb}*J/mol, dS={dSb}*J/K/mol) """.format(dHf=dHf, dSf=dSf, dHb=dHb, dSb=dSb)) kf_ref = 20836643994.118652 * T_K * np.exp(-(dHf - T_K * dSf) / (R * T_K)) kb_ref = 20836643994.118652 * T_K * np.exp(-(dHb - T_K * dSb) / (R * T_K)) Fe0 = 6e-3 SCN0 = 2e-3 init_cond = {'Fe+3': Fe0 * u.M, 'SCN-': SCN0 * u.M, 'FeSCN+2': 0 * u.M} t = 3 * u.second def check(rsys, params): odes, extra = get_odesys(rsys, include_params=False, unit_registry=SI_base_registry, constants=const) for odesys in [odes, odes.as_autonomous()]: res = odesys.integrate(t, init_cond, params, integrator='cvode') t_sec = to_unitless(res.xout, u.second) FeSCN_ref = binary_rev(t_sec, kf_ref, kb_ref, 0, Fe0, SCN0) cmp = to_unitless(res.yout, u.M) ref = np.empty_like(cmp) ref[:, odesys.names.index('FeSCN+2')] = FeSCN_ref ref[:, odesys.names.index('Fe+3')] = Fe0 - FeSCN_ref ref[:, odesys.names.index('SCN-')] = SCN0 - FeSCN_ref assert np.allclose(cmp, ref) check(rsys1, {'temperature': T_K * u.K}) rsys2 = ReactionSystem.from_string(""" Fe+3 + SCN- -> FeSCN+2; MassAction(EyringHS([{dHf}*J/mol, {dSf}*J/K/mol])) FeSCN+2 -> Fe+3 + SCN-; MassAction(EyringHS([{dHb}*J/mol, {dSb}*J/K/mol])) """.format(dHf=dHf, dSf=dSf, dHb=dHb, dSb=dSb)) check(rsys2, {'temperature': T_K * u.K}) rsys3 = ReactionSystem.from_string(""" Fe+3 + SCN- -> FeSCN+2; MassAction(EyringHS.fk('dHf', 'dSf')) FeSCN+2 -> Fe+3 + SCN-; MassAction(EyringHS.fk('dHb', 'dSb')) """) check( rsys3, dict(temperature=T_K * u.K, dHf=dHf * u.J / u.mol, dSf=dSf * u.J / u.mol / u.K, dHb=dHb * u.J / u.mol, dSb=dSb * u.J / u.mol / u.K))
def test_get_odesys__Eyring_2nd_order_reversible(): R = 8.314472 T_K = 273.15 + 20 # 20 degree celsius kB = 1.3806504e-23 h = 6.62606896e-34 dHf = 74e3 dSf = R*np.log(h/kB/T_K*1e16) dHb = 79e3 dSb = dSf - 23 rsys1 = ReactionSystem.from_string(""" Fe+3 + SCN- -> FeSCN+2; EyringParam(dH={dHf}*J/mol, dS={dSf}*J/K/mol) FeSCN+2 -> Fe+3 + SCN-; EyringParam(dH={dHb}*J/mol, dS={dSb}*J/K/mol) """.format(dHf=dHf, dSf=dSf, dHb=dHb, dSb=dSb)) kf_ref = 20836643994.118652*T_K*np.exp(-(dHf - T_K*dSf)/(R*T_K)) kb_ref = 20836643994.118652*T_K*np.exp(-(dHb - T_K*dSb)/(R*T_K)) Fe0 = 6e-3 SCN0 = 2e-3 init_cond = { 'Fe+3': Fe0*u.M, 'SCN-': SCN0*u.M, 'FeSCN+2': 0*u.M } t = 3*u.second def check(rsys, params): odes, extra = get_odesys(rsys, include_params=False, unit_registry=SI_base_registry, constants=const) for odesys in [odes, odes.as_autonomous()]: res = odesys.integrate(t, init_cond, params, integrator='cvode') t_sec = to_unitless(res.xout, u.second) FeSCN_ref = binary_rev(t_sec, kf_ref, kb_ref, 0, Fe0, SCN0) cmp = to_unitless(res.yout, u.M) ref = np.empty_like(cmp) ref[:, odesys.names.index('FeSCN+2')] = FeSCN_ref ref[:, odesys.names.index('Fe+3')] = Fe0 - FeSCN_ref ref[:, odesys.names.index('SCN-')] = SCN0 - FeSCN_ref assert np.allclose(cmp, ref) check(rsys1, {'temperature': T_K*u.K}) rsys2 = ReactionSystem.from_string(""" Fe+3 + SCN- -> FeSCN+2; MassAction(EyringHS([{dHf}*J/mol, {dSf}*J/K/mol])) FeSCN+2 -> Fe+3 + SCN-; MassAction(EyringHS([{dHb}*J/mol, {dSb}*J/K/mol])) """.format(dHf=dHf, dSf=dSf, dHb=dHb, dSb=dSb)) check(rsys2, {'temperature': T_K*u.K}) rsys3 = ReactionSystem.from_string(""" Fe+3 + SCN- -> FeSCN+2; MassAction(EyringHS.fk('dHf', 'dSf')) FeSCN+2 -> Fe+3 + SCN-; MassAction(EyringHS.fk('dHb', 'dSb')) """) check(rsys3, dict(temperature=T_K*u.K, dHf=dHf*u.J/u.mol, dSf=dSf*u.J/u.mol/u.K, dHb=dHb*u.J/u.mol, dSb=dSb*u.J/u.mol/u.K))
def test_get_odesys__Eyring_2nd_order_linearly_ramped_temperature(): from scipy.special import expi def analytic_unit0(t, k, m, dH, dS): R = 8.314472 kB = 1.3806504e-23 h = 6.62606896e-34 A = kB / h * np.exp(dS / R) B = dH / R return k * np.exp(B * (k * t + 2 * m) / (m * (k * t + m))) / ( A * (-B**2 * np.exp(B / (k * t + m)) * expi(-B / (k * t + m)) - B * k * t - B * m + k**2 * t**2 + 2 * k * m * t + m**2) * np.exp(B / m) + (A * B**2 * np.exp(B / m) * expi(-B / m) - A * m * (-B + m) + k * np.exp(B / m)) * np.exp(B / (k * t + m))) T_K = 290 dTdt_Ks = 3 dH = 80e3 dS = 10 rsys1 = ReactionSystem.from_string(""" 2 NO2 -> N2O4; EyringParam(dH={dH}*J/mol, dS={dS}*J/K/mol) """.format(dH=dH, dS=dS)) NO2_M = 1.0 init_cond = dict(NO2=NO2_M * u.M, N2O4=0 * u.M) t = 20 * u.second def check(rsys): odes, extra = get_odesys(rsys, unit_registry=SI_base_registry, constants=const, substitutions={ 'temperature': RampedTemp( [T_K * u.K, dTdt_Ks * u.K / u.s]) }) for odesys in [odes, odes.as_autonomous()]: res = odesys.integrate(t, init_cond, integrator='cvode') t_sec = to_unitless(res.xout, u.second) NO2_ref = analytic_unit0(t_sec, dTdt_Ks, T_K, dH, dS) cmp = to_unitless(res.yout, u.M) ref = np.empty_like(cmp) ref[:, odesys.names.index('NO2')] = NO2_ref ref[:, odesys.names.index('N2O4')] = (NO2_M - NO2_ref) / 2 assert np.allclose(cmp, ref) check(rsys1) rsys2 = ReactionSystem.from_string(""" 2 NO2 -> N2O4; MassAction(EyringHS([{dH}*J/mol, {dS}*J/K/mol])) """.format(dH=dH, dS=dS)) check(rsys2)
def test_get_odesys__Eyring_1st_order_linearly_ramped_temperature(): from scipy.special import expi def analytic_unit0(t, T0, dH, dS): R = 8.314472 kB = 1.3806504e-23 h = 6.62606896e-34 A = kB / h * np.exp(dS / R) B = dH / R return np.exp(A * ((-B**2 * np.exp(B / T0) * expi(-B / T0) - T0 * (B - T0)) * np.exp(-B / T0) + (B**2 * np.exp(B / (t + T0)) * expi(-B / (t + T0)) - (t + T0) * (-B + t + T0)) * np.exp(-B / (t + T0))) / 2) T_K = 290 dH = 80e3 dS = 10 rsys1 = ReactionSystem.from_string(""" NOBr -> NO + Br; EyringParam(dH={dH}*J/mol, dS={dS}*J/K/mol) """.format(dH=dH, dS=dS)) NOBr0_M = 0.7 init_cond = dict(NOBr=NOBr0_M * u.M, NO=0 * u.M, Br=0 * u.M) t = 20 * u.second def check(rsys): odes, extra = get_odesys(rsys, unit_registry=SI_base_registry, constants=const, substitutions={ 'temperature': RampedTemp([T_K * u.K, 1 * u.K / u.s]) }) for odesys in [odes, odes.as_autonomous()]: res = odesys.integrate(t, init_cond, integrator='cvode') t_sec = to_unitless(res.xout, u.second) NOBr_ref = NOBr0_M * analytic_unit0(t_sec, T_K, dH, dS) cmp = to_unitless(res.yout, u.M) ref = np.empty_like(cmp) ref[:, odesys.names.index('NOBr')] = NOBr_ref ref[:, odesys.names.index('Br')] = NOBr0_M - NOBr_ref ref[:, odesys.names.index('NO')] = NOBr0_M - NOBr_ref assert np.allclose(cmp, ref) check(rsys1) rsys2 = ReactionSystem.from_string(""" NOBr -> NO + Br; MassAction(EyringHS([{dH}*J/mol, {dS}*J/K/mol])) """.format(dH=dH, dS=dS)) check(rsys2)
def test_EyringMassAction(): args = kB_h_times_exp_dS_R, dH_over_R, c0 = 1.2e11 / 273.15, 40e3 / 8, 1 ama = MassAction(Eyring(args, ("Sfreq", "Hact"))) rxn1 = Reaction({"A": 2, "B": 1}, {"C": 1}, ama, {"B": 1}) T_ = "temperature" def ref(v): return (v.get("Sfreq", 1.2e11 / 273.15) * v[T_] * math.exp(-v.get("Hact", 40e3 / 8) / v[T_]) * v["B"] * v["A"]**2) for params in [(11.0, 13.0, 17.0, 311.2), (12, 8, 5, 270)]: var = dict(zip(["A", "B", "C", T_], params)) ref_val = ref(var) assert abs((ama(var, reaction=rxn1) - ref_val) / ref_val) < 1e-14 with pytest.raises(ValueError): MassAction(Eyring([1, 1, 1, 1, 1])) # assert ama.as_mass_action({T_: 273.15}).args[0] == 1.2e11*math.exp(-40e3/8/273.15) ama2 = MassAction( Eyring4([1.2e11 / 273, 40e3 / 8, 1.2, 1e3], ("Sfreq", "Hact", "Sref", "Href"))) rxn2 = Reaction({"C": 1}, {"A": 2, "B": 2}, ama2) var2 = {"C": 29, "temperature": 273} def ref2(var): return (var["C"] * var.get("temperature", 273) * var.get("Sfreq", 1.2e11 / 273) / var.get("Sref", 1.2) * math.exp((var.get("Href", 1e3) - var.get("Hact", 5e3)) / var.get("temperature", 273))) r2 = ref2(var2) assert abs((ama2(var2, reaction=rxn2) - r2) / r2) < 1e-14 rsys = ReactionSystem([rxn1, rxn2]) var3 = { "A": 11, "B": 13, "C": 17, "temperature": 298, "Sfreq": 1.2e11 / 298 } rates = rsys.rates(var3) rf3 = ref(var3) rb3 = ref2(var3) ref_rates = {"A": 2 * (rb3 - rf3), "B": 2 * (rb3 - rf3), "C": rf3 - rb3} for k, v in ref_rates.items(): assert abs((rates[k] - v) / v) < 1e-14
def test_EyringMassAction(): args = kB_h_times_exp_dS_R, dH_over_R, c0 = 1.2e11 / 273.15, 40e3 / 8, 1 ama = MassAction(Eyring(args, ('Sfreq', 'Hact'))) rxn1 = Reaction({'A': 2, 'B': 1}, {'C': 1}, ama, {'B': 1}) T_ = 'temperature' def ref(v): return v.get('Sfreq', 1.2e11 / 273.15) * v[T_] * math.exp( -v.get('Hact', 40e3 / 8) / v[T_]) * v['B'] * v['A']**2 for params in [(11., 13., 17., 311.2), (12, 8, 5, 270)]: var = dict(zip(['A', 'B', 'C', T_], params)) ref_val = ref(var) assert abs((ama(var, reaction=rxn1) - ref_val) / ref_val) < 1e-14 with pytest.raises(ValueError): MassAction(Eyring([1, 1, 1, 1, 1])) # assert ama.as_mass_action({T_: 273.15}).args[0] == 1.2e11*math.exp(-40e3/8/273.15) ama2 = MassAction( Eyring4([1.2e11 / 273, 40e3 / 8, 1.2, 1e3], ('Sfreq', 'Hact', 'Sref', 'Href'))) rxn2 = Reaction({'C': 1}, {'A': 2, 'B': 2}, ama2) var2 = {'C': 29, 'temperature': 273} def ref2(var): return var['C'] * var.get('temperature', 273) * var.get( 'Sfreq', 1.2e11 / 273) / var.get('Sref', 1.2) * math.exp( (var.get('Href', 1e3) - var.get('Hact', 5e3)) / var.get('temperature', 273)) r2 = ref2(var2) assert abs((ama2(var2, reaction=rxn2) - r2) / r2) < 1e-14 rsys = ReactionSystem([rxn1, rxn2]) var3 = { 'A': 11, 'B': 13, 'C': 17, 'temperature': 298, 'Sfreq': 1.2e11 / 298 } rates = rsys.rates(var3) rf3 = ref(var3) rb3 = ref2(var3) ref_rates = {'A': 2 * (rb3 - rf3), 'B': 2 * (rb3 - rf3), 'C': rf3 - rb3} for k, v in ref_rates.items(): assert abs((rates[k] - v) / v) < 1e-14
def test_get_odesys__Eyring_1st_order_linearly_ramped_temperature(): from scipy.special import expi def analytic_unit0(t, T0, dH, dS): R = 8.314472 kB = 1.3806504e-23 h = 6.62606896e-34 A = kB/h*np.exp(dS/R) B = dH/R return np.exp(A*( (-B**2*np.exp(B/T0)*expi(-B/T0) - T0*(B - T0))*np.exp(-B/T0) + (B**2*np.exp(B/(t + T0))*expi(-B/(t + T0)) - (t + T0)*(-B + t + T0))*np.exp(-B/(t + T0)) )/2) T_K = 290 dH = 80e3 dS = 10 rsys1 = ReactionSystem.from_string(""" NOBr -> NO + Br; EyringParam(dH={dH}*J/mol, dS={dS}*J/K/mol) """.format(dH=dH, dS=dS)) NOBr0_M = 0.7 init_cond = dict( NOBr=NOBr0_M*u.M, NO=0*u.M, Br=0*u.M ) t = 20*u.second def check(rsys): odes, extra = get_odesys(rsys, unit_registry=SI_base_registry, constants=const, substitutions={ 'temperature': RampedTemp([T_K*u.K, 1*u.K/u.s])}) for odesys in [odes, odes.as_autonomous()]: res = odesys.integrate(t, init_cond, integrator='cvode') t_sec = to_unitless(res.xout, u.second) NOBr_ref = NOBr0_M*analytic_unit0(t_sec, T_K, dH, dS) cmp = to_unitless(res.yout, u.M) ref = np.empty_like(cmp) ref[:, odesys.names.index('NOBr')] = NOBr_ref ref[:, odesys.names.index('Br')] = NOBr0_M - NOBr_ref ref[:, odesys.names.index('NO')] = NOBr0_M - NOBr_ref assert np.allclose(cmp, ref) check(rsys1) rsys2 = ReactionSystem.from_string(""" NOBr -> NO + Br; MassAction(EyringHS([{dH}*J/mol, {dS}*J/K/mol])) """.format(dH=dH, dS=dS)) check(rsys2)
def test_get_odesys__Eyring_2nd_order(): R = 8.314472 T_K = 300 dH = 80e3 dS = 10 rsys1b = ReactionSystem.from_string( """ NO + Br -> NOBr; EyringParam(dH={dH}*J/mol, dS={dS}*J/K/mol) """.format( dH=dH, dS=dS ) ) c0 = 1 # mol/dm3 === 1000 mol/m3 kbref = 20836643994.118652 * T_K * np.exp(-(dH - T_K * dS) / (R * T_K)) / c0 NO0_M = 1.5 Br0_M = 0.7 init_cond = dict(NOBr=0 * u.M, NO=NO0_M * u.M, Br=Br0_M * u.M) t = 5 * u.second params = dict(temperature=T_K * u.K) def analytic_b(t): U, V = NO0_M, Br0_M d = U - V return (U * (1 - np.exp(-kbref * t * d))) / (U / V - np.exp(-kbref * t * d)) def check(rsys): odesys, extra = get_odesys( rsys, unit_registry=SI_base_registry, constants=const ) res = odesys.integrate(t, init_cond, params, integrator="cvode") t_sec = to_unitless(res.xout, u.second) NOBr_ref = analytic_b(t_sec) cmp = to_unitless(res.yout, u.M) ref = np.empty_like(cmp) ref[:, odesys.names.index("NOBr")] = NOBr_ref ref[:, odesys.names.index("Br")] = Br0_M - NOBr_ref ref[:, odesys.names.index("NO")] = NO0_M - NOBr_ref assert np.allclose(cmp, ref) check(rsys1b) rsys2b = ReactionSystem.from_string( """ NO + Br -> NOBr; MassAction(EyringHS([{dH}*J/mol, {dS}*J/K/mol])) """.format( dH=dH, dS=dS ) ) check(rsys2b)
def test_create_odesys__ShiftedTPoly(): rxn = Reaction({'A': 1, 'B': 1}, {'C': 3, 'D': 2}, 'k_bi', {'A': 3}) rsys = ReactionSystem([rxn], 'A B C D') _k0, _k1, T0C = 10, 2, 273.15 rate = MassAction(ShiftedTPoly([T0C*u.K, _k0/u.molar/u.s, _k1/u.molar/u.s/u.K])) T_C = 25 T = (T0C+T_C)*u.kelvin p1 = rate.rate_coeff({'temperature': T}) assert allclose(p1, (_k0 + _k1*T_C)/u.molar/u.s) odesys, odesys_extra = create_odesys(rsys) ics = {'A': 3*u.molar, 'B': 5*u.molar, 'C': 11*u.molar, 'D': 13*u.molar} pars = dict(k_bi=p1) validation = odesys_extra['validate'](dict(ics, **pars)) assert set(map(str, validation['not_seen'])) == {'C', 'D'} dedim_ctx = _mk_dedim(SI_base_registry) (t, c, _p), dedim_extra = dedim_ctx['dedim_tcp'](-37*u.s, [ics[k] for k in odesys.names], pars) fout = odesys.f_cb(t, c, [_p[pk] for pk in odesys.param_names]) r = 3*5*(_k0 + _k1*25)*1000 # mol/m3/s ref = [-4*r, -r, 3*r, 2*r] assert np.all(abs((fout - ref)/ref) < 1e-14) odesys.integrate(t, c, _p)
def test_get_native__first_step(solve): integrator = 'cvode' forgive = 20 def k(num): return "MassAction(unique_keys=('k%d',))" % num lines = [ # fictitious isomerization "CNO -> ONC; %s" % k(1), "ONC -> NCO; %s" % k(2), "NCO -> CON; %s" % k(3) ] rsys = ReactionSystem.from_string('\n'.join(lines), 'CNO ONC NCO CON') odesys, extra = get_odesys(rsys, include_params=False) if len(solve) > 0: from pyodesys.symbolic import PartiallySolvedSystem odesys = PartiallySolvedSystem(odesys, extra['linear_dependencies'](solve)) c0 = defaultdict(float, {'CNO': .7}) rate_coeffs = (1e78, 2, 3.) args = (5, c0, dict(zip('k1 k2 k3'.split(), rate_coeffs))) kwargs = dict(integrator=integrator, atol=1e-9, rtol=1e-9, nsteps=1000) native = get_native(rsys, odesys, integrator) h0 = extra['max_euler_step_cb'](0, *args[1:]) xout1, yout1, info1 = odesys.integrate(*args, first_step=h0, **kwargs) xout2, yout2, info2 = native.integrate(*args, **kwargs) ref1 = decay_get_Cref(rate_coeffs, [c0[key] for key in native.names], xout1) ref2 = decay_get_Cref(rate_coeffs, [c0[key] for key in native.names], xout2) allclose_kw = dict(atol=kwargs['atol']*forgive, rtol=kwargs['rtol']*forgive) assert np.allclose(yout1[:, :3], ref1, **allclose_kw) assert info2['success'] assert info2['nfev'] > 10 and info2['nfev'] > 1 and info2['time_cpu'] < 10 and info2['time_wall'] < 10 assert np.allclose(yout2[:, :3], ref2, **allclose_kw)
def test_get_ode__Radiolytic__substitutions__units(): rad = Radiolytic([2.4e-7 * u.mol / u.joule]) rxn = Reaction({'A': 4, 'B': 1}, {'C': 3, 'D': 2}, rad) rsys = ReactionSystem([rxn], 'A B C D') g_dm3 = u.gram / u.decimetre**3 kg_dm3 = u.kg / u.decimetre**3 substance_rho = Density( [1 * kg_dm3, -1 * g_dm3 / u.kelvin, 273.15 * u.kelvin]) odesys = get_odesys(rsys, include_params=True, unit_registry=SI_base_registry, substitutions={'density': substance_rho})[0] conc = { 'A': 3 * u.molar, 'B': 5 * u.molar, 'C': 11 * u.molar, 'D': 13 * u.molar } x, y, p = odesys.to_arrays(-37 * u.second, conc, { 'doserate': 0.4 * u.gray / u.second, 'temperature': 298.15 * u.kelvin }) fout = odesys.f_cb(x, y, p) r = 2.4e-7 * 0.4 * 0.975 * 1e3 # mol/m3/s ref = [-4 * r, -r, 3 * r, 2 * r] assert np.all(abs((fout - ref) / ref) < 1e-14)
def test_get_odesys__late_binding(): def _gibbs(args, T, R, backend, **kwargs): H, S = args return backend.exp(-(H - T*S)/(R*T)) def _eyring(args, T, R, k_B, h, backend, **kwargs): H, S = args return k_B/h*T*backend.exp(-(H - T*S)/(R*T)) gibbs_pk = ('temperature', 'molar_gas_constant') eyring_pk = gibbs_pk + ('Boltzmann_constant', 'Planck_constant') GibbsEC = MassActionEq.from_callback(_gibbs, argument_names=('H', 'S'), parameter_keys=gibbs_pk) EyringMA = MassAction.from_callback(_eyring, argument_names=('H', 'S'), parameter_keys=eyring_pk) uk_equil = ('He_assoc', 'Se_assoc') beta = GibbsEC(unique_keys=uk_equil) # equilibrium parameters uk_kinet = ('Ha_assoc', 'Sa_assoc') bimol_barrier = EyringMA(unique_keys=uk_kinet) # activation parameters eq = Equilibrium({'Fe+3', 'SCN-'}, {'FeSCN+2'}, beta) rsys = ReactionSystem(eq.as_reactions(kf=bimol_barrier)) odesys, extra = get_odesys(rsys, include_params=False) pk, unique, p_units = map(extra.get, 'param_keys unique p_units'.split()) assert sorted(unique) == sorted(uk_equil + uk_kinet) assert sorted(pk) == sorted(eyring_pk)
def test_get_odesys__linear_dependencies__PartiallySolvedSystem(preferred): import sympy from pyodesys.symbolic import PartiallySolvedSystem rsys = ReactionSystem.from_string('\n'.join( ['H2O -> H+ + OH-; 1e-4', 'OH- + H+ -> H2O; 1e10'])) odesys, extra = get_odesys(rsys) c0 = {'H2O': 0, 'H+': 2e-7, 'OH-': 3e-7} h0max = extra['max_euler_step_cb'](0, c0) analytic_factory = extra['linear_dependencies']() y0 = {k: sympy.Symbol(k + '0') for k in rsys.substances} analytic_factory(None, {odesys[k]: v for k, v in y0.items()}, None, sympy) psys = PartiallySolvedSystem(odesys, analytic_factory) xout, yout, info = psys.integrate(1, c0, atol=1e-12, rtol=1e-10, first_step=h0max * 1e-12, integrator='cvode') c_reac = c0['H+'], c0['OH-'] H2O_ref = binary_rev(xout, 1e10, 1e-4, c0['H2O'], max(c_reac), min(c_reac)) assert np.allclose(yout[:, psys.names.index('H2O')], H2O_ref) assert np.allclose(yout[:, psys.names.index('H+')], c0['H+'] + c0['H2O'] - H2O_ref) assert np.allclose(yout[:, psys.names.index('OH-')], c0['OH-'] + c0['H2O'] - H2O_ref)
def test_get_odesys__linear_dependencies__preferred(substances): rsys = ReactionSystem.from_string( "\n".join(["H2O -> H+ + OH-; 1e-4", "OH- + H+ -> H2O; 1e10"]), substances ) assert isinstance(rsys.substances, OrderedDict) odesys, extra = get_odesys(rsys) af_H2O_H = extra["linear_dependencies"](["H+", "H2O"]) import sympy y0 = {k: sympy.Symbol(k + "0") for k in rsys.substances} af_H2O_H( None, {odesys[k]: v for k, v in y0.items()}, None, sympy ) # ensure idempotent exprs_H2O_H = af_H2O_H(None, {odesys[k]: v for k, v in y0.items()}, None, sympy) ref_H2O_H = { "H2O": y0["H2O"] + y0["OH-"] - odesys["OH-"], # oxygen "H+": 2 * y0["H2O"] + y0["H+"] + y0["OH-"] - odesys["OH-"] - 2 * (y0["H2O"] + y0["OH-"] - odesys["OH-"]), # hydrogen } for k, v in ref_H2O_H.items(): assert (exprs_H2O_H[odesys[k]] - v) == 0
def test_create_odesys(): rsys = ReactionSystem.from_string(""" A -> B; 'k1' B + C -> P; 'k2' """, substance_factory=Substance) odesys, odesys_extra = create_odesys(rsys, unit_registry=SI_base_registry) tend = 10 c0 = {'A': 1e-6, 'B': 0, 'C': 1, 'P': 0} p = dict(k1=3, k2=4) tend_units = tend*u.s p_units = {'k1': p['k1']/u.s, 'k2': p['k2']/u.M/u.s} c0_units = {k: v*u.molar for k, v in c0.items()} validation = odesys_extra['validate'](dict(c0_units, **p_units)) P, = validation['not_seen'] assert P.name == 'P' ref_rates = {'A': -p_units['k1']*c0_units['A'], 'P': p_units['k2']*c0_units['B']*c0_units['C']} ref_rates['B'] = -ref_rates['A'] - ref_rates['P'] ref_rates['C'] = -ref_rates['P'] assert validation['rates'] == ref_rates result1, result1_extra = odesys_extra['unit_aware_solve']( tend_units, c0_units, p_units, integrator='cvode') assert result1.info['success'] result2 = odesys.integrate(tend, c0, p, integrator='cvode') assert np.allclose(result2.yout[-1, :], to_unitless(result1.yout[-1, :], u.molar))
def test_get_odesys__Expr_as_param__unique_as_param(): def _eyring_pe_coupled(args, T, S, backend=math, **kwargs): freq, = args return freq*T/S EyringPreExpCoupled = Expr.from_callback(_eyring_pe_coupled, argument_names=('freq',), parameter_keys=('temperature', 'S_u')) def _k(args, T, backend=math, **kwargs): A, H, S = args return A*backend.exp(-(H - T*S)/(8.314511*T)) EyringMA = MassAction.from_callback(_k, parameter_keys=('temperature',), argument_names=('Aa', 'Ha', 'Sa')) kb_h = 2.08e10 rxn = Reaction({'A'}, {'B'}, EyringMA(unique_keys=('A_u', 'H_u', 'S_u'))) rsys = ReactionSystem([rxn], ['A', 'B']) odesys2, extra2 = get_odesys(rsys, include_params=False, substitutions={'A_u': EyringPreExpCoupled(kb_h)}) y0 = defaultdict(float, {'A': 7.0}) rt = 293.15 xout2, yout2, info2 = odesys2.integrate(5, y0, {'H_u': 107e3, 'S_u': 150, 'temperature': rt}, integrator='cvode', atol=1e-12, rtol=1e-10, nsteps=1000) kref2 = kb_h*rt*np.exp(-(107e3 - rt*150)/(8.314511*rt))/150 ref2 = y0['A']*np.exp(-kref2*xout2) assert np.allclose(yout2[:, 0], ref2) assert np.allclose(yout2[:, 1], y0['A'] - ref2)
def test_get_native__conc_roots(): M, s = u.M, u.s rsys = ReactionSystem.from_string("2 O3 -> 3 O2; 'k2'") u_reg = SI_base_registry.copy() odesys, extra = get_odesys(rsys, include_params=False, unit_registry=u_reg) c0 = {"O3": 4.2e-3 * M, "O2": 0 * M} cr = ["O2"] native = get_native(rsys, odesys, "cvode", conc_roots=cr) tend = 1e5 * u.s params = {"k2": logspace_from_lin(1e-3 / M / s, 1e3 / M / s, 14)} tgt_O2 = 1e-3 * M results = native.integrate( tend, c0, params, integrator="native", return_on_root=True, special_settings=[unitless_in_registry(tgt_O2, u_reg)], ) assert len(results) == params["k2"].size # dydt = -p*y**2 # 1/y0 - 1/y = -2*pt # t = 1/2/p*(1/y - 1/y0) tgt_O3 = c0["O3"] - 2 / 3 * tgt_O2 for r in results: ref = rescale( 1 / 2 / r.named_param("k2") * (1 / tgt_O3 - 1 / c0["O3"]), u.s) assert allclose(r.xout[-1], ref, rtol=1e-6)
def test_get_odesys__Eyring_2nd_order_linearly_ramped_temperature(): from scipy.special import expi def analytic_unit0(t, k, m, dH, dS): R = 8.314472 kB = 1.3806504e-23 h = 6.62606896e-34 A = kB/h*np.exp(dS/R) B = dH/R return k*np.exp(B*(k*t + 2*m)/(m*(k*t + m)))/( A*(-B**2*np.exp(B/(k*t + m))*expi(-B/(k*t + m)) - B*k*t - B*m + k**2*t**2 + 2*k*m*t + m**2)*np.exp(B/m) + (A*B**2*np.exp(B/m)*expi(-B/m) - A*m*(-B + m) + k*np.exp(B/m))*np.exp(B/(k*t + m)) ) T_K = 290 dTdt_Ks = 3 dH = 80e3 dS = 10 rsys1 = ReactionSystem.from_string(""" 2 NO2 -> N2O4; EyringParam(dH={dH}*J/mol, dS={dS}*J/K/mol) """.format(dH=dH, dS=dS)) NO2_M = 1.0 init_cond = dict( NO2=NO2_M*u.M, N2O4=0*u.M ) t = 20*u.second def check(rsys): odes, extra = get_odesys(rsys, unit_registry=SI_base_registry, constants=const, substitutions={ 'temperature': RampedTemp([T_K*u.K, dTdt_Ks*u.K/u.s])}) for odesys in [odes, odes.as_autonomous()]: res = odesys.integrate(t, init_cond, integrator='cvode') t_sec = to_unitless(res.xout, u.second) NO2_ref = analytic_unit0(t_sec, dTdt_Ks, T_K, dH, dS) cmp = to_unitless(res.yout, u.M) ref = np.empty_like(cmp) ref[:, odesys.names.index('NO2')] = NO2_ref ref[:, odesys.names.index('N2O4')] = (NO2_M - NO2_ref)/2 assert np.allclose(cmp, ref) check(rsys1) rsys2 = ReactionSystem.from_string(""" 2 NO2 -> N2O4; MassAction(EyringHS([{dH}*J/mol, {dS}*J/K/mol])) """.format(dH=dH, dS=dS)) check(rsys2)
def test_get_odesys__max_euler_step_cb(): rsys = ReactionSystem.from_string('\n'.join(['H2O -> H+ + OH-; 1e-4', 'OH- + H+ -> H2O; 1e10'])) odesys, extra = get_odesys(rsys) r1 = 1.01e-4 r2 = 6e-4 dH2Odt = r2 - r1 euler_ref = 2e-7/dH2Odt assert abs(extra['max_euler_step_cb'](0, {'H2O': 1.01, 'H+': 2e-7, 'OH-': 3e-7}) - euler_ref)/euler_ref < 1e-8
def test_get_native__Radiolytic__named_parameter__units(dep_scaling): rsys = ReactionSystem.from_string(""" -> H; Radiolytic(2*per100eV) H + H -> H2; 'k2' """, checks=('substance_keys', 'duplicate', 'duplicate_names')) gval = 2 * u.per100eV from pyodesys.symbolic import ScaledSys kwargs = {} if dep_scaling == 1 else dict(SymbolicSys=ScaledSys, dep_scaling=dep_scaling) odesys, extra = get_odesys(rsys, include_params=False, unit_registry=SI_base_registry, **kwargs) c0 = {'H': 42e-6 * u.molar, 'H2': 17e3 * u.nanomolar} native = get_native(rsys, odesys, 'cvode') tend = 7 * 60 * u.minute params = { 'doserate': 314 * u.Gy / u.hour, 'k2': 53 / u.molar / u.minute, 'density': 998 * u.g / u.dm3 } result = native.integrate(tend, c0, params, atol=1e-15, rtol=1e-15, integrator='cvode', nsteps=8000) assert result.info['success'] def analytic_H(t, p, k, H0): # dH/dt = p - k2*H**2 x0 = np.sqrt(2) * np.sqrt(p) x1 = x0 x2 = np.sqrt(k) x3 = t * x1 * x2 x4 = H0 * x2 x5 = np.sqrt(x0 + 2 * x4) x6 = np.sqrt(-1 / (2 * H0 * x2 - x0)) x7 = x5 * x6 * np.exp(x3) x8 = np.exp(-x3) / (x5 * x6) return x1 * (x7 - x8) / (2 * x2 * (x7 + x8)) t_ul = to_unitless(result.xout, u.s) p_ul = to_unitless(params['doserate'] * params['density'] * gval, u.micromolar / u.s) ref_H_uM = analytic_H(t_ul, p_ul, to_unitless(params['k2'], 1 / u.micromolar / u.s), to_unitless(c0['H'], u.micromolar)) ref_H2_uM = to_unitless(c0['H2'], u.micromolar) + to_unitless( c0['H'], u.micromolar) / 2 + t_ul * p_ul / 2 - ref_H_uM / 2 assert np.allclose(to_unitless(result.named_dep('H'), u.micromolar), ref_H_uM) assert np.allclose(to_unitless(result.named_dep('H2'), u.micromolar), ref_H2_uM)
def test_get_native__named_parameter__units(dep_scaling): rsys = ReactionSystem.from_string( """ -> H; 'p' H + H -> H2; 'k2' """, checks=("substance_keys", "duplicate", "duplicate_names"), ) from pyodesys.symbolic import ScaledSys kwargs = ({} if dep_scaling == 1 else dict(SymbolicSys=ScaledSys, dep_scaling=dep_scaling)) odesys, extra = get_odesys(rsys, include_params=False, unit_registry=SI_base_registry, **kwargs) c0 = {"H": 42e-6 * u.molar, "H2": 17 * u.micromolar} native = get_native(rsys, odesys, "cvode") tend = 7 * 60 * u.minute g_rho_Ddot = g, rho, Ddot = 2 * u.per100eV, 998 * u.g / u.dm3, 314 * u.Gy / u.hour params = {"p": reduce(mul, g_rho_Ddot), "k2": 53 / u.molar / u.minute} result = native.integrate(tend, c0, params, atol=1e-15, rtol=1e-15, integrator="cvode", nsteps=16000) assert result.info["success"] def analytic_H(t, p, k, H0): # dH/dt = p - k2*H**2 x0 = np.sqrt(2) * np.sqrt(p) x1 = x0 x2 = np.sqrt(k) x3 = t * x1 * x2 x4 = H0 * x2 x5 = np.sqrt(x0 + 2 * x4) x6 = np.sqrt(-1 / (2 * H0 * x2 - x0)) x7 = x5 * x6 * np.exp(x3) x8 = np.exp(-x3) / (x5 * x6) return x1 * (x7 - x8) / (2 * x2 * (x7 + x8)) t_ul = to_unitless(result.xout, u.s) p_ul = to_unitless(params["p"], u.micromolar / u.s) ref_H_uM = analytic_H( t_ul, p_ul, to_unitless(params["k2"], 1 / u.micromolar / u.s), to_unitless(c0["H"], u.micromolar), ) ref_H2_uM = (to_unitless(c0["H2"], u.micromolar) + to_unitless(c0["H"], u.micromolar) / 2 + t_ul * p_ul / 2 - ref_H_uM / 2) assert np.allclose(to_unitless(result.named_dep("H"), u.micromolar), ref_H_uM) assert np.allclose(to_unitless(result.named_dep("H2"), u.micromolar), ref_H2_uM)
def test_chained_parameter_variation_from_ReactionSystem(): g_E_mol_J = 2.1e-7 rsys = ReactionSystem.from_string( """ (H2O) -> e-(aq) + H+ + OH; Radiolytic(%.2e*mol/J) 2 OH -> H2O2; 3.6e9/M/s H+ + OH- -> H2O; 1.4e11/M/s H2O -> H+ + OH-; 1.4e-3/s N2O + e-(aq) -> N2 + O-; 9.6e9/M/s O- + H+ -> OH; 1e11/M/s """ % g_E_mol_J # neglecting a large body of reactions (just a test-case after all) ) ureg = SI_base_registry field_u = get_derived_unit(ureg, 'doserate') * get_derived_unit(ureg, 'density') rd = ReactionDiffusion.from_ReactionSystem(rsys, fields=[[0*field_u]], unit_registry=ureg, param_names=['doserate']) dens_kg_dm3 = 0.998 odesys = rd._as_odesys( variables_from_params=dict( density=lambda self, params: dens_kg_dm3*1e3*u.kg/u.m**3 ) ) npoints = 5 durations = [59*u.second, 42*u.minute, 2*u.hour] doserates = [135*u.Gy/u.s, 11*u.Gy/u.s, 180*u.Gy/u.minute] M = u.molar ic = defaultdict(lambda: 0*M, {'H2O': 55.4*M, 'H+': 1e-7*M, 'OH-': 1e-7*M, 'N2O': 20e-3*M}) result = odesys.chained_parameter_variation(durations, ic, {'doserate': doserates}, npoints=npoints) ref_xout_s = [0] for dur in map(lambda dur: to_unitless(dur, u.s), durations): ref_xout_s += list(np.linspace(ref_xout_s[-1], ref_xout_s[-1] + dur, npoints+1)[1:]) assert allclose(result.xout, ref_xout_s*u.s) N2_M = to_unitless(result.named_dep('N2'), u.M) H2O2_M = to_unitless(result.named_dep('H2O2'), u.M) e_accum_molar = 0 for i, (dur, dr) in enumerate(zip(durations, doserates)): dur_s = to_unitless(dur, u.s) dr_Gy_s = to_unitless(dr, u.Gy/u.s) local_ts = np.linspace(0, dur_s, npoints+1) # local_ic = {k: result.named_dep(k)[i*npoints] for k in odesys.names} for j, (lt, ld) in enumerate(zip(local_ts[1:], np.diff(local_ts))): e_accum_molar += ld*g_E_mol_J*dr_Gy_s*dens_kg_dm3 assert abs(N2_M[i*npoints + j + 1] - e_accum_molar)/e_accum_molar < 1e-3 assert abs(H2O2_M[i*npoints + j + 1] - e_accum_molar)/e_accum_molar < 1e-3 res2 = odesys.integrate(durations[0], ic, {'doserate': doserates[0]}, integrator='cvode') dr2 = res2.params[res2.odesys.param_names.index('doserate')] assert np.asarray(res2.params).shape[-1] == len(odesys.param_names) assert allclose(dr2, doserates[0]) assert allclose(res2.xout[-1], durations[0]) assert allclose(res2.named_dep('N2')[-1], durations[0]*doserates[0]*g_E_mol_J*u.mol/u.J*dens_kg_dm3*u.kg/u.dm3) to_unitless(res2.xout, u.s) to_unitless(res2.yout, u.molar) to_unitless(dr2, u.Gy/u.s)
def test_get_odesys__Eyring_2nd_order(): R = 8.314472 T_K = 300 dH = 80e3 dS = 10 rsys1b = ReactionSystem.from_string(""" NO + Br -> NOBr; EyringParam(dH={dH}*J/mol, dS={dS}*J/K/mol) """.format(dH=dH, dS=dS)) c0 = 1 # mol/dm3 === 1000 mol/m3 kbref = 20836643994.118652*T_K*np.exp(-(dH - T_K*dS)/(R*T_K))/c0 NO0_M = 1.5 Br0_M = 0.7 init_cond = dict( NOBr=0*u.M, NO=NO0_M*u.M, Br=Br0_M*u.M ) t = 5*u.second params = dict( temperature=T_K*u.K ) def analytic_b(t): U, V = NO0_M, Br0_M d = U - V return (U*(1 - np.exp(-kbref*t*d)))/(U/V - np.exp(-kbref*t*d)) def check(rsys): odesys, extra = get_odesys(rsys, unit_registry=SI_base_registry, constants=const) res = odesys.integrate(t, init_cond, params, integrator='cvode') t_sec = to_unitless(res.xout, u.second) NOBr_ref = analytic_b(t_sec) cmp = to_unitless(res.yout, u.M) ref = np.empty_like(cmp) ref[:, odesys.names.index('NOBr')] = NOBr_ref ref[:, odesys.names.index('Br')] = Br0_M - NOBr_ref ref[:, odesys.names.index('NO')] = NO0_M - NOBr_ref assert np.allclose(cmp, ref) check(rsys1b) rsys2b = ReactionSystem.from_string(""" NO + Br -> NOBr; MassAction(EyringHS([{dH}*J/mol, {dS}*J/K/mol])) """.format(dH=dH, dS=dS)) check(rsys2b)
def test_get_odesys_2(): g = Radiolytic([3.14]) a = Substance('A') b = Substance('B') r = Reaction({'A': 1}, {'B': 1}, param=g) rsys = ReactionSystem([r], [a, b]) odesys = get_odesys(rsys, include_params=True)[0] c0 = { 'A': 1.0, 'B': 3.0, } t = np.linspace(0.0, .1) xout, yout, info = odesys.integrate(t, rsys.as_per_substance_array(c0), {'doserate': 2.72, 'density': .998}) yref = np.zeros((t.size, 2)) k = 3.14*2.72*.998 yref[:, 0] = 1 - k*t yref[:, 1] = 3 + k*t assert np.allclose(yout, yref)
def test_from_ReactionSystem__g_values(): from chempy import ReactionSystem as RS rs = RS.from_string('-> H + OH; Radiolytic(2.1e-7)', checks=()) rd = ReactionDiffusion.from_ReactionSystem(rs, variables={'density': 998, 'doserate': 0.15}) gv = rd.g_values assert len(gv) == 1 assert np.allclose(gv[0], rs.as_per_substance_array({'H': 2.1e-7, 'OH': 2.1e-7})) assert len(rd.fields) == 1 assert len(rd.fields[0]) == 1 assert np.allclose(rd.fields[0][0], 998*0.15)
def test_get_odesys__Equilibrium_as_reactions(): 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, integrator='cvode', atol=1e-11, rtol=1e-12) cmplx_ref = binary_rev(tout, 3, 3.0/100, init_conc['FeSCN+2'], init_conc['Fe+3'], init_conc['SCN-']) assert np.allclose(Cout[:, 2], cmplx_ref)
def get_rsys(): r1 = Reaction({'A'}, {'B'}, MassAction([4. / 100], unique_keys=['k1']), name='R1: A cons.') r2 = Reaction({'B', 'C'}, {'A', 'C'}, MassAction([1e4], unique_keys=['k2']), name='R2: A reform.') r3 = Reaction({'B': 2}, {'B', 'C'}, MassAction([3e7], unique_keys=['k3']), name='R3: C form.') return ReactionSystem([r1, r2, r3])
def test_get_ode__Radiolytic(): rad = Radiolytic([2.4e-7]) rxn = Reaction({"A": 4, "B": 1}, {"C": 3, "D": 2}, rad) rsys = ReactionSystem([rxn], "A B C D") odesys = get_odesys(rsys, include_params=True)[0] c = {"A": 3, "B": 5, "C": 11, "D": 13} x, y, p = odesys.to_arrays(-37, c, {"doserate": 0.4, "density": 0.998}) fout = odesys.f_cb(x, y, p) r = 2.4e-7 * 0.4 * 0.998 ref = [-4 * r, -r, 3 * r, 2 * r] assert np.all(abs((fout - ref) / ref) < 1e-14)
def test_get_odesys_2(): g = Radiolytic([3.14]) a = Substance("A") b = Substance("B") r = Reaction({"A": 1}, {"B": 1}, param=g) rsys = ReactionSystem([r], [a, b]) odesys = get_odesys(rsys, include_params=True)[0] c0 = { "A": 1.0, "B": 3.0, } t = np.linspace(0.0, 0.1) xout, yout, info = odesys.integrate( t, rsys.as_per_substance_array(c0), {"doserate": 2.72, "density": 0.998} ) yref = np.zeros((t.size, 2)) k = 3.14 * 2.72 * 0.998 yref[:, 0] = 1 - k * t yref[:, 1] = 3 + k * t assert np.allclose(yout, yref)
def test_get_ode__Radiolytic(): rad = Radiolytic([2.4e-7]) rxn = Reaction({'A': 4, 'B': 1}, {'C': 3, 'D': 2}, rad) rsys = ReactionSystem([rxn], 'A B C D') odesys = get_odesys(rsys, include_params=True)[0] c = {'A': 3, 'B': 5, 'C': 11, 'D': 13} x, y, p = odesys.to_arrays(-37, c, {'doserate': 0.4, 'density': 0.998}) fout = odesys.f_cb(x, y, p) r = 2.4e-7*0.4*0.998 ref = [-4*r, -r, 3*r, 2*r] assert np.all(abs((fout - ref)/ref) < 1e-14)
def test_get_ode__TPoly(): rate = MassAction(ShiftedTPoly([273.15*u.K, 10/u.molar/u.s, 2/u.molar/u.s/u.K])) rxn = Reaction({'A': 1, 'B': 1}, {'C': 3, 'D': 2}, rate, {'A': 3}) rsys = ReactionSystem([rxn], 'A B C D') odesys = get_odesys(rsys, unit_registry=SI_base_registry)[0] conc = {'A': 3*u.molar, 'B': 5*u.molar, 'C': 11*u.molar, 'D': 13*u.molar} x, y, p = odesys.to_arrays(-37*u.second, conc, {'temperature': 298.15*u.kelvin}) fout = odesys.f_cb(x, y, p) r = 3*5*(10+2*25)*1000 # mol/m3/s ref = [-4*r, -r, 3*r, 2*r] assert np.all(abs((fout - ref)/ref) < 1e-14)
def test_get_ode__ArrheniusParam(): rxn = Reaction({'A': 1}, {'B': 1}, None) rxn.param = ArrheniusParam(1e10, 40e3) rsys = ReactionSystem([rxn], 'A B') odesys = get_odesys(rsys, include_params=True)[0] conc = {'A': 3, 'B': 5} x, y, p = odesys.to_arrays(-37, conc, {'temperature': 200}) fout = odesys.f_cb(x, y, p) ref = 3*1e10*np.exp(-40e3/8.314472/200) assert np.all(abs((fout[:, 0] + ref)/ref) < 1e-14) assert np.all(abs((fout[:, 1] - ref)/ref) < 1e-14)
def test_EyringMassAction(): args = kB_h_times_exp_dS_R, dH_over_R, c0 = 1.2e11/273.15, 40e3/8, 1 ama = MassAction(Eyring(args, ('Sfreq', 'Hact'))) rxn1 = Reaction({'A': 2, 'B': 1}, {'C': 1}, ama, {'B': 1}) T_ = 'temperature' def ref(v): return v.get('Sfreq', 1.2e11/273.15)*v[T_]*math.exp(-v.get('Hact', 40e3/8)/v[T_])*v['B']*v['A']**2 for params in [(11., 13., 17., 311.2), (12, 8, 5, 270)]: var = dict(zip(['A', 'B', 'C', T_], params)) ref_val = ref(var) assert abs((ama(var, reaction=rxn1) - ref_val)/ref_val) < 1e-14 with pytest.raises(ValueError): MassAction(Eyring([1, 1, 1, 1, 1])) # assert ama.as_mass_action({T_: 273.15}).args[0] == 1.2e11*math.exp(-40e3/8/273.15) ama2 = MassAction(Eyring4([1.2e11/273, 40e3/8, 1.2, 1e3], ('Sfreq', 'Hact', 'Sref', 'Href'))) rxn2 = Reaction({'C': 1}, {'A': 2, 'B': 2}, ama2) var2 = {'C': 29, 'temperature': 273} def ref2(var): return var['C']*var.get('temperature', 273)*var.get('Sfreq', 1.2e11/273)/var.get('Sref', 1.2)*math.exp( (var.get('Href', 1e3) - var.get('Hact', 5e3))/var.get('temperature', 273)) r2 = ref2(var2) assert abs((ama2(var2, reaction=rxn2) - r2)/r2) < 1e-14 rsys = ReactionSystem([rxn1, rxn2]) var3 = {'A': 11, 'B': 13, 'C': 17, 'temperature': 298, 'Sfreq': 1.2e11/298} rates = rsys.rates(var3) rf3 = ref(var3) rb3 = ref2(var3) ref_rates = {'A': 2*(rb3 - rf3), 'B': 2*(rb3 - rf3), 'C': rf3 - rb3} for k, v in ref_rates.items(): assert abs((rates[k] - v)/v) < 1e-14
def test_from_ReactionSystem__g_values__units(): from chempy import ReactionSystem as RS from chempy.units import SI_base_registry, default_units as u rs = RS.from_string('-> H + OH; Radiolytic(2.1*per100eV)', checks=()) variables = {'density': .998 * u.kg/u.dm3, 'doserate': 0.15*u.Gy/u.s} rd = ReactionDiffusion.from_ReactionSystem(rs, variables=variables, unit_registry=SI_base_registry) gv = rd.g_values per100eV_as_mol_per_joule = 1.0364268556366418e-07 ref = 2.1 * per100eV_as_mol_per_joule assert len(gv) == 1 assert np.allclose(gv[0], rs.as_per_substance_array({'H': ref, 'OH': ref})) assert len(rd.fields) == 1 assert len(rd.fields[0]) == 1 assert np.allclose(rd.fields[0][0], 998*0.15)
def test_get_native__a_substance_no_composition(solve): rsys = ReactionSystem.from_string('\n'.join(['H2O -> H2O+ + e-(aq); 1e-8', 'e-(aq) + H2O+ -> H2O; 1e10'])) odesys, extra = get_odesys(rsys) c0 = {'H2O': 0, 'H2O+': 2e-9, 'e-(aq)': 3e-9} if len(solve) > 0: from pyodesys.symbolic import PartiallySolvedSystem odesys = PartiallySolvedSystem(odesys, extra['linear_dependencies'](solve)) odesys = get_native(rsys, odesys, 'gsl') xout, yout, info = odesys.integrate(1, c0, atol=1e-15, rtol=1e-15, integrator='gsl') c_reac = c0['H2O+'], c0['e-(aq)'] H2O_ref = binary_rev(xout, 1e10, 1e-4, c0['H2O'], max(c_reac), min(c_reac)) assert np.allclose(yout[:, odesys.names.index('H2O')], H2O_ref) assert np.allclose(yout[:, odesys.names.index('H2O+')], c0['H2O+'] + c0['H2O'] - H2O_ref) assert np.allclose(yout[:, odesys.names.index('e-(aq)')], c0['e-(aq)'] + c0['H2O'] - H2O_ref)
def test_create_odesys__Radiolytic(): rsys1 = ReactionSystem.from_string(""" -> e-(aq); Radiolytic.fk('g_emaq') """, checks=()) ic1 = {'e-(aq)': 0.0} t1 = 5 p1 = dict( g_emaq=42.0, doserate=17.0, density=5.0 ) odesys1, odesys_extra = create_odesys(rsys1) result1 = odesys1.integrate(t1, ic1, p1) yref1 = result1.xout*p1['g_emaq']*p1['doserate']*p1['density'] assert np.allclose(yref1, result1.yout.squeeze())
def test_get_native__Radiolytic__named_parameter__units(scaling_density): scaling, density = scaling_density rsys = ReactionSystem.from_string(""" -> H; Radiolytic(2*per100eV) H + H -> H2; 'k2' """, checks=('substance_keys', 'duplicate', 'duplicate_names')) gval = 2*u.per100eV from pyodesys.symbolic import ScaledSys kwargs = {} if scaling == 1 else dict(SymbolicSys=ScaledSys, dep_scaling=scaling) dens = {'density': 998*u.g/u.dm3} odesys, extra = get_odesys(rsys, include_params=False, substitutions=dens if density else {}, unit_registry=SI_base_registry, **kwargs) c0 = {'H': 42e-6*u.molar, 'H2': 17e3*u.nanomolar} native = get_native(rsys, odesys, 'cvode') tend = 7*60*u.minute params = {'doserate': 314*u.Gy/u.hour, 'k2': 53/u.molar/u.minute} if not density: params.update(dens) result = native.integrate(tend, c0, params, atol=1e-15, rtol=1e-15, integrator='cvode', nsteps=8000) assert result.info['success'] def analytic_H(t, p, k, H0): # dH/dt = p - k2*H**2 x0 = np.sqrt(2)*np.sqrt(p) x1 = x0 x2 = np.sqrt(k) x3 = t*x1*x2 x4 = H0*x2 x5 = np.sqrt(x0 + 2*x4) x6 = np.sqrt(-1/(2*H0*x2 - x0)) x7 = x5*x6*np.exp(x3) x8 = np.exp(-x3)/(x5*x6) return x1*(x7 - x8)/(2*x2*(x7 + x8)) t_ul = to_unitless(result.xout, u.s) p_ul = to_unitless(params['doserate']*dens['density']*gval, u.micromolar/u.s) ref_H_uM = analytic_H( t_ul, p_ul, to_unitless(params['k2'], 1/u.micromolar/u.s), to_unitless(c0['H'], u.micromolar) ) ref_H2_uM = to_unitless(c0['H2'], u.micromolar) + to_unitless(c0['H'], u.micromolar)/2 + t_ul*p_ul/2 - ref_H_uM/2 assert np.allclose(to_unitless(result.named_dep('H'), u.micromolar), ref_H_uM) assert np.allclose(to_unitless(result.named_dep('H2'), u.micromolar), ref_H2_uM)
def test_get_odesys__linear_dependencies__preferred(substances): rsys = ReactionSystem.from_string('\n'.join(['H2O -> H+ + OH-; 1e-4', 'OH- + H+ -> H2O; 1e10']), substances) assert isinstance(rsys.substances, OrderedDict) odesys, extra = get_odesys(rsys) af_H2O_H = extra['linear_dependencies'](['H+', 'H2O']) import sympy y0 = {k: sympy.Symbol(k+'0') for k in rsys.substances} af_H2O_H(None, {odesys[k]: v for k, v in y0.items()}, None, sympy) # ensure idempotent exprs_H2O_H = af_H2O_H(None, {odesys[k]: v for k, v in y0.items()}, None, sympy) ref_H2O_H = { 'H2O': y0['H2O'] + y0['OH-'] - odesys['OH-'], # oxygen 'H+': 2*y0['H2O'] + y0['H+'] + y0['OH-'] - odesys['OH-'] - 2*( y0['H2O'] + y0['OH-'] - odesys['OH-']) # hydrogen } for k, v in ref_H2O_H.items(): assert (exprs_H2O_H[odesys[k]] - v) == 0
def test_get_odesys_rsys_with_units__named_params(): rsys = ReactionSystem.from_string(""" A -> B; 'k1' B + C -> P; 'k2' """, substance_factory=Substance) odesys, extra = get_odesys(rsys, include_params=False, unit_registry=SI_base_registry) tend = 10 tend_units = tend*u.s c0 = {'A': 1e-6, 'B': 0, 'C': 1, 'P': 0} p = {'k1': 3, 'k2': 4} p_units = {'k1': 3/u.s, 'k2': 4/u.M/u.s} c0_units = {k: v*u.molar for k, v in c0.items()} result1 = odesys.integrate(tend_units, c0_units, p_units, integrator='odeint') assert result1.info['success'] with pytest.raises(Exception): odesys.integrate(tend, c0, p, integrator='odeint')