diff --git a/CHANGES.rst b/CHANGES.rst index d9fb7886..e580d340 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v0.4.1 +====== +- Fixes for enhanced robustness: + - ``.kinetics.ode.get_odesys`` + - ``.chemistry.as_per_substance_array`` +- Minor changes. + v0.4.0 ====== - Multiple fixes throughout diff --git a/chempy/chemistry.py b/chempy/chemistry.py index 30e1942a..43718e78 100644 --- a/chempy/chemistry.py +++ b/chempy/chemistry.py @@ -1301,7 +1301,7 @@ def as_per_substance_array(self, cont, dtype='float64', unit=None, raise_on_unk= raise KeyError("Unkown substance key: %s" % k) cont = [cont[k] for k in substance_keys] - cont = np.asarray(cont, dtype=dtype) + cont = np.atleast_1d(np.asarray(cont, dtype=dtype).squeeze()) if cont.shape[-1] != self.ns: raise ValueError("Incorrect size") return cont*(unit if unit is not None else 1) diff --git a/chempy/kinetics/_rates.py b/chempy/kinetics/_rates.py index ca79b559..b8b10be5 100644 --- a/chempy/kinetics/_rates.py +++ b/chempy/kinetics/_rates.py @@ -16,7 +16,7 @@ class TPolyMassAction(TPoly, MassAction): argument_names = ('temperature_offset', Ellipsis) parameter_keys = TPoly.parameter_keys - def rate_coeff(self, variables, backend): + def rate_coeff(self, variables, backend=math): return self.eval_poly(variables, backend) @@ -27,7 +27,7 @@ def rate_coeff(self, variables, backend): class PiecewiseTPolyMassAction(TPiecewisePoly, MassAction): parameter_keys = TPoly.parameter_keys - def rate_coeff(self, variables, backend): + def rate_coeff(self, variables, backend=math): return self.eval_poly(variables, backend) @@ -39,19 +39,27 @@ def g_value(self, variables, backend): return self.eval_poly(variables, backend) +class RTPolyRadiolytic(RTPoly, Radiolytic): + nargs = None + parameter_keys = Radiolytic.parameter_keys + RTPoly.parameter_keys + + def g_value(self, variables, backend): + return self.eval_poly(variables, backend) + + class RTPolyMassAction(RTPoly, MassAction): """ Arguments: temperature_offset, c0, c1, ... """ parameter_keys = RTPoly.parameter_keys nargs = None - def rate_coeff(self, variables, backend): + def rate_coeff(self, variables, backend=math): return self.eval_poly(variables, backend) class _Log10XPolyMassAction(MassAction): skip_poly = 1 # kunit - def rate_coeff(self, variables, backend): + def rate_coeff(self, variables, backend=math): k_unit = self.arg(variables, 0) return 10**self.eval_poly(variables, backend)*k_unit diff --git a/chempy/kinetics/ode.py b/chempy/kinetics/ode.py index d1a8d218..894c222f 100644 --- a/chempy/kinetics/ode.py +++ b/chempy/kinetics/ode.py @@ -162,7 +162,6 @@ def get_odesys(rsys, include_params=True, substitutions=None, 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 ( @@ -184,9 +183,9 @@ def post_processor(x, y, p): p_units = [get_derived_unit(unit_registry, k) for k in param_keys] new_r_exprs = [] for ratex in r_exprs: - _pu, _new_rates = ratex.dedimensionalisation(unit_registry) + _pu, _new_rate = ratex._recursive_as_RateExpr().dedimensionalisation(unit_registry) p_units.extend(_pu) - new_r_exprs.append(_new_rates) + new_r_exprs.append(_new_rate) r_exprs = new_r_exprs time_unit = get_derived_unit(unit_registry, 'time') diff --git a/chempy/kinetics/rates.py b/chempy/kinetics/rates.py index bd8d56f3..21e6bcda 100644 --- a/chempy/kinetics/rates.py +++ b/chempy/kinetics/rates.py @@ -31,6 +31,22 @@ def rxn(self, value): if isinstance(arg, RateExpr): arg.rxn = value + def _recursive_as_RateExpr(self): + new_args = [] + for arg in self.args: + if isinstance(arg, Expr): + new_args.append(arg) + else: + if hasattr(arg, '_as_RateExpr'): + new_args.append(arg._as_RateExpr(self.rxn)) + else: + new_args.append(arg) + if self.kw is None: + kw = {} + else: + kw = {k: getattr(self, k) for k in self.kw} + return self.__class__(new_args, self.unique_keys, **kw) + @classmethod @deprecated(use_instead=Expr.from_callback) def subclass_from_callback(cls, cb, cls_attrs=None): @@ -91,7 +107,7 @@ class _Radiolytic(RadiolyticBase): parameter_keys = (doserate_name, 'density') print_name = 'Radiolytic' if doserate_name == 'doserate' else ('Radiolytic{'+doserate_name+'}') - def g_value(self, variables, backend): # for subclasses + def g_value(self, variables, backend=math): # for subclasses return self.arg(variables, 0, backend=backend) def __call__(self, variables, backend=math): @@ -168,6 +184,22 @@ def as_mass_action(self, variables, backend=math): class ArrheniusMassAction(MassAction): + """ Rate expression for a Arrhenius-type of rate + + Examples + -------- + >>> from math import exp + >>> from chempy import Reaction + >>> from chempy.units import allclose, default_units as u + >>> A = 1e11 / u.second + >>> Ea_over_R = 42e3/8.3145 * u.K**-1 + >>> ratex = ArrheniusMassAction([A, Ea_over_R]) + >>> rxn = Reaction({'R'}, {'P'}, ratex) + >>> dRdt = rxn.rate({'R': 3*u.M, 'temperature': 298.15*u.K})['R'] + >>> allclose(dRdt, -3*1e11*exp(-42e3/8.3145/298.15)*u.M/u.s) + True + + """ argument_names = ('A', 'Ea_over_R') parameter_keys = ('temperature',) diff --git a/chempy/tests/test_chemistry.py b/chempy/tests/test_chemistry.py index cb7d5366..95ef28c5 100644 --- a/chempy/tests/test_chemistry.py +++ b/chempy/tests/test_chemistry.py @@ -187,8 +187,7 @@ def test_ReactionSystem__as_per_substance_array_dict(): m = default_units.metre M = default_units.molar rs = ReactionSystem([], [Substance('H2O')]) - c = rs.as_per_substance_array({'H2O': 1*M}, - unit=M) + c = rs.as_per_substance_array({'H2O': 1*M}, unit=M) assert c.dimensionality == M.dimensionality assert abs(c[0]/(1000*mol/m**3) - 1) < 1e-16 diff --git a/chempy/tests/test_units.py b/chempy/tests/test_units.py index 41bb4699..74efd242 100644 --- a/chempy/tests/test_units.py +++ b/chempy/tests/test_units.py @@ -11,9 +11,9 @@ from ..util.testing import requires from ..units import ( - allclose, get_derived_unit, is_unitless, linspace, + allclose, get_derived_unit, is_unitless, linspace, logspace_from_lin, SI_base_registry, unitless_in_registry, format_string, get_physical_quantity, - to_unitless, magnitude, default_unit_in_registry, Backend, + to_unitless, magnitude, default_unit_in_registry, Backend, latex_of_unit, unit_of, unit_registry_to_human_readable, units_library, unit_registry_from_human_readable, _sum, UncertainQuantity, default_units as u @@ -104,6 +104,7 @@ def test_to_unitless(): assert abs(to_unitless(3/(u.second*u.molar), u.metre**3/u.mole/u.second) - 3e-3) < 1e-12 assert abs(to_unitless(2*u.dm3, u.cm3) - 2000) < 1e-12 + assert abs(to_unitless(2*u.m3, u.dm3) - 2000) < 1e-12 assert (float(to_unitless(UncertainQuantity(2, u.dm3, .3), u.cm3)) - 2000) < 1e-12 g1 = UncertainQuantity(4.46, u.per100eV, 0) @@ -137,6 +138,13 @@ def test_linspace(): assert abs(to_unitless(ls[0], u.hour) - 2/3600.) < 1e-15 +@requires(units_library) +def test_logspace_from_lin(): + ls = logspace_from_lin(2*u.second, 3*u.second) + assert abs(to_unitless(ls[0], u.hour) - 2/3600.) < 1e-15 + assert abs(to_unitless(ls[-1], u.hour) - 3/3600.) < 1e-15 + + @requires(units_library) def test_get_derived_unit(): registry = SI_base_registry.copy() @@ -323,3 +331,8 @@ def test_joule_html(): joule_htm = 'kg⋅m2/s2' joule = u.J.dimensionality.simplified assert joule.html == joule_htm + + +@requires(units_library) +def test_latex_of_unit(): + assert latex_of_unit(u.gram/u.metre**2) == r'\mathrm{\frac{g}{m^{2}}}' diff --git a/chempy/units.py b/chempy/units.py index d47888e3..1dde2d2a 100644 --- a/chempy/units.py +++ b/chempy/units.py @@ -5,19 +5,21 @@ - ``chempy.units.default_constants`` - ``chempy.units.SI_base_registry`` +together with some functions. + Currently `quantities `_ is used as the underlying package to handle units. If it is possible you should try to -only use the `chempy.units` module (in case ``ChemPy`` changes this backend). +only use the `chempy.units` module (in case ``ChemPy`` changes this backend), +and avoid relying on any attributes of the Quantity instances (and rather use +functions in `chempy.units`). """ from __future__ import (absolute_import, division, print_function) -from operator import mul from functools import reduce +from operator import mul from ._util import NameSpace -# Currently we use quantities for units. This may change, therefore use this -# file for all units. A requirement is first-class numpy support. units_library = 'quantities' # info used for selective testing. @@ -40,6 +42,7 @@ default_units = NameSpace(pq) default_units.decimetre = pq.UnitQuantity( 'decimetre', default_units.m / 10.0, u_symbol='dm') + default_units.m3 = default_units.metre**3 default_units.dm3 = default_units.decimetre**3 default_units.cm3 = default_units.centimetre**3 if not hasattr(default_units, 'molar'): @@ -147,6 +150,18 @@ def unit_registry_to_human_readable(unit_registry): return new_registry +def latex_of_unit(quant): + """ Returns LaTeX reperesentation of the unit of a quantity + + Examples + -------- + >>> print(latex_of_unit(1/default_units.kelvin)) + \\mathrm{\\frac{1}{K}} + + """ + return quant.dimensionality.latex.strip('$') + + def unit_registry_from_human_readable(unit_registry): """ Deserialization of unit_registry. """ if unit_registry is None: @@ -308,7 +323,15 @@ def allclose(a, b, rtol=1e-8, atol=None): def linspace(start, stop, num=50): - """ Analogous to ``numpy.linspace``. """ + """ Analogous to ``numpy.linspace``. + + Examples + -------- + >>> abs(linspace(2, 8, num=3)[1] - 5) < 1e-15 + True + + """ + # work around for quantities v0.10.1 and NumPy import numpy as np unit = unit_of(start) @@ -317,6 +340,22 @@ def linspace(start, stop, num=50): return np.linspace(start_, stop_, num)*unit +def logspace_from_lin(start, stop, num=50): + """ Logarithmically spaced data points + + Examples + -------- + >>> abs(logspace_from_lin(2, 8, num=3)[1] - 4) < 1e-15 + True + + """ + import numpy as np + unit = unit_of(start) + start_ = np.log2(to_unitless(start, unit)) + stop_ = np.log2(to_unitless(stop, unit)) + return np.exp2(np.linspace(start_, stop_, num))*unit + + def _sum(iterable): try: result = next(iterable) diff --git a/chempy/util/_expr.py b/chempy/util/_expr.py index d8bebebf..647df07b 100644 --- a/chempy/util/_expr.py +++ b/chempy/util/_expr.py @@ -12,6 +12,7 @@ from functools import reduce from itertools import chain from operator import add, mul, truediv, sub +from .pyutil import defaultkeydict class Expr(object): @@ -55,8 +56,8 @@ class Expr(object): >>> cv = {s.name: EinsteinSolid([einT(s), s.mass]) for s in (Al, Be)} >>> print('%.4f' % cv['Al']({'temperature': 273.15, 'molar_gas_constant': 8.3145})) # J/(g*K) 0.8108 - >>> import sympy - >>> print(cv['Be']({'temperature': sympy.Symbol('T'), 'molar_gas_constant': sympy.Symbol('R')}, backend=sympy)) + >>> import sympy; from sympy import Symbol as Symb + >>> print(cv['Be']({'temperature': Symb('T'), 'molar_gas_constant': Symb('R')}, backend=sympy)) 112105.346283965*R/(T**2*sinh(580.32/T)**2) Attributes @@ -104,6 +105,44 @@ def __init__(self, args, unique_keys=None, **kwargs): if kwargs: raise ValueError("Unexpected keyword arguments %s" % kwargs) + @classmethod + def from_callback(cls, callback, **kwargs): + """ Factory of subclasses + + Parameters + ---------- + callback : callable + signature: *args, backend=None + argument_names : tuple of str, optional + parameter_keys : tuple of str, optional, + kw : dict, optional + nargs : int, optional + + Examples + -------- + >>> from operator import add; from functools import reduce + >>> def poly(args, x, backend=None): + ... x0 = args[0] + ... return reduce(add, [c*(x-x0)**i for i, c in enumerate(args[1:])]) + ... + >>> Poly = Expr.from_callback(poly, parameter_keys=('x',), argument_names=('x0', Ellipsis)) + >>> p = Poly([1, 3, 2, 5]) + >>> p({'x': 7}) == 3 + 2*(7-1) + 5*(7-1)**2 + True + >>> q = Poly([1, 3, 2, 5], unique_keys=('x0_q',)) + >>> q({'x': 7, 'x0_q': 0}) == 3 + 2*7 + 5*7**2 + True + + """ + class Wrapper(cls): + def __call__(self, variables, backend=math): + args = self.all_args(variables, backend=backend) + params = self.all_params(variables, backend=backend) + return callback(args, *params, backend=backend) + for k, v in kwargs.items(): + setattr(Wrapper, k, v) + return Wrapper + def __call__(self, variables, backend=None): raise NotImplementedError("Subclass and implement __call__") @@ -134,20 +173,20 @@ def __repr__(self): def string(self, arg_fmt=str, **kwargs): return self._str(arg_fmt, **kwargs) - def arg(self, variables, index, backend=None): + def arg(self, variables, index, backend=None, evaluate=True): if isinstance(index, str): index = self.argument_names.index(index) if self.unique_keys is None or len(self.unique_keys) <= index: res = self.args[index] else: res = variables.get(self.unique_keys[index], self.args[index]) - if isinstance(res, Expr): + if isinstance(res, Expr) and evaluate: return res(variables, backend=backend) else: return res - def all_args(self, variables, backend=None): - return [self.arg(variables, i, backend) for i in range(len(self.args))] + def all_args(self, variables, backend=None, evaluate=True): + return [self.arg(variables, i, backend, evaluate) for i in range(len(self.args))] def all_params(self, variables, backend=None): return [v(variables, backend=backend) if isinstance(v, Expr) else v for v @@ -189,9 +228,9 @@ def dedimensionalisation(self, unit_registry, variables={}, backend=math): """ from ..units import default_unit_in_registry, to_unitless units = [None if isinstance(arg, Expr) else default_unit_in_registry(arg, unit_registry) for arg - in self.all_args(variables, backend=backend)] + in self.all_args(variables, backend=backend, evaluate=False)] new_units, unitless_args = [], [] - for arg, unit in zip(self.all_args(variables, backend=backend), units): + for arg, unit in zip(self.all_args(variables, backend=backend, evaluate=False), units): if isinstance(arg, Expr): if unit is not None: raise ValueError() @@ -206,12 +245,14 @@ def dedimensionalisation(self, unit_registry, variables={}, backend=math): kw = {k: getattr(self, k) for k in self.kw} return new_units, self.__class__(unitless_args, self.unique_keys, **kw) - def _sympy_format(self, method, variables, backend): + def _sympy_format(self, method, variables, backend, default): variables = variables or {} if backend is None: import sympy as backend - variables = {k: v if isinstance(v, Expr) else backend.Symbol(v) for k, v in variables.items()} - expr = self(variables, backend=backend) + variables = defaultkeydict( + None if default is None else (lambda k: backend.Symbol(default(k))), + {k: v if isinstance(v, Expr) else backend.Symbol(v) for k, v in variables.items()}) + expr = self(variables, backend=backend).simplify() if method == 'latex': return backend.latex(expr) elif method == 'unicode': @@ -222,12 +263,14 @@ def _sympy_format(self, method, variables, backend): else: raise NotImplementedError("Unknown method: %s" % method) - def latex(self, variables=None, backend=None): + def latex(self, variables=None, backend=None, default=None): r""" Parameters ---------- variables : dict backend : module + default : callable + Format string based on missing key, signature: str -> str. Examples -------- @@ -243,45 +286,7 @@ def latex(self, variables=None, backend=None): Requires SymPy """ - return self._sympy_format('latex', variables, backend) - - @classmethod - def from_callback(cls, callback, **kwargs): - """ Factory of subclasses - - Parameters - ---------- - callback : callable - signature: *args, backend=None - argument_names : tuple of str, optional - parameter_keys : tuple of str, optional, - kw : dict, optional - nargs : int, optional - - Examples - -------- - >>> from operator import add; from functools import reduce - >>> def poly(args, x, backend=None): - ... x0 = args[0] - ... return reduce(add, [c*(x-x0)**i for i, c in enumerate(args[1:])]) - ... - >>> Poly = Expr.from_callback(poly, parameter_keys=('x',), argument_names=('x0', Ellipsis)) - >>> p = Poly([1, 3, 2, 5]) - >>> p({'x': 7}) == 3 + 2*(7-1) + 5*(7-1)**2 - True - >>> q = Poly([1, 3, 2, 5], unique_keys=('x0_q',)) - >>> q({'x': 7, 'x0_q': 0}) == 3 + 2*7 + 5*7**2 - True - - """ - class Wrapper(cls): - def __call__(self, variables, backend=math): - args = self.all_args(variables, backend=backend) - params = self.all_params(variables, backend=backend) - return callback(args, *params, backend=backend) - for k, v in kwargs.items(): - setattr(Wrapper, k, v) - return Wrapper + return self._sympy_format('latex', variables, backend=backend, default=default) def __eq__(self, other): if self.__class__ != other.__class__: diff --git a/chempy/util/bkh.py b/chempy/util/bkh.py index 4365f496..767c9296 100644 --- a/chempy/util/bkh.py +++ b/chempy/util/bkh.py @@ -8,13 +8,13 @@ from itertools import chain from chempy.kinetics.ode import get_odesys -from chempy.units import to_unitless, linspace +from chempy.units import to_unitless, linspace, logspace_from_lin def integration_with_sliders( rsys, tend, c0, parameters, fig_kwargs=None, unit_registry=None, output_conc_unit=None, output_time_unit=None, slider_kwargs=None, x_axis_type="linear", y_axis_type="linear", - integrate_kwargs=None): + integrate_kwargs=None, substitutions=None): import numpy as np from bokeh.plotting import Figure @@ -24,7 +24,7 @@ def integration_with_sliders( if slider_kwargs is None: slider_kwargs = {} odesys, state_keys, rarg_keys, p_units = get_odesys( - rsys, unit_registry=unit_registry, + rsys, unit_registry=unit_registry, substitutions=substitutions, output_conc_unit=output_conc_unit, output_time_unit=output_time_unit)[:4] if output_conc_unit is None: @@ -33,7 +33,13 @@ def integration_with_sliders( output_conc_unit = 1 param_keys = list(chain(state_keys, rarg_keys)) - tout = linspace(tend*0, tend) + if x_axis_type == 'linear': + tout = linspace(tend*0, tend) + elif x_axis_type == 'log': + tout = logspace_from_lin(tend*1e-9, tend) + else: + raise NotImplementedError("Unknown x_axis_type: %s" % x_axis_type) + tout, Cout, info = odesys.integrate(tout, c0, parameters, **(integrate_kwargs or {})) sources = [ColumnDataSource(data={ 'tout': to_unitless(tout, output_time_unit), @@ -73,7 +79,7 @@ def _dict_to_unitless(d, u): slider_kwargs.get(k, dict(start=v/10, end=v*10, step=v/10)), u))) for k, v, u in zip(param_keys, p_ul, p_units)]) - all_widgets = list(chain(c0_widgets.values(), param_widgets.values())) + all_widgets = list(chain(param_widgets.values(), c0_widgets.values())) def update_data(attrname, old, new): _c0 = defaultdict(lambda: 0*output_conc_unit) diff --git a/chempy/util/pyutil.py b/chempy/util/pyutil.py index 5d65ab10..7adf6052 100644 --- a/chempy/util/pyutil.py +++ b/chempy/util/pyutil.py @@ -4,7 +4,7 @@ """ from __future__ import (absolute_import, division, print_function) -from collections import namedtuple, Mapping +from collections import defaultdict, namedtuple, Mapping from functools import wraps import os import warnings @@ -27,6 +27,29 @@ def wrapper(*args): return decorator +class defaultkeydict(defaultdict): + """ defaultdict where default_factory should have the signature key -> value + + Examples + -------- + >>> d = defaultkeydict(lambda k: '[%s]' % k, {'a': '[a]', 'b': '[B]'}) + >>> d['a'] + '[a]' + >>> d['b'] + '[B]' + >>> d['c'] + '[c]' + + """ + + def __missing__(self, key): + if self.default_factory is None: + raise KeyError("Missing key: %s" % key) + else: + self[key] = self.default_factory(key) + return self[key] + + def defaultnamedtuple(typename, field_names, defaults=()): """ Generates a new subclass of tuple with default values. diff --git a/chempy/util/tests/test_expr.py b/chempy/util/tests/test_expr.py index 1aeec8e6..5f320b6e 100644 --- a/chempy/util/tests/test_expr.py +++ b/chempy/util/tests/test_expr.py @@ -270,18 +270,25 @@ def test_Expr__latex(): p = Poly([1, 2, 3, 4]) import sympy t = sympy.Symbol('t') - ref = sympy.latex(2 + 3*(t-1) + 4*(t-1)**2) + ref = sympy.latex((2 + 3*(t-1) + 4*(t-1)**2).simplify()) assert p.latex({'x': 't'}) == ref TE = Poly([3, 7, 5]) cv_Al = _get_cv()['Al'] T, E, R, m = sympy.symbols('T E R m') _TE = 7 + 5*(E-3) - ref = sympy.latex((3*R*(_TE/(2*T))**2 * sympy.sinh(_TE/(2*T))**-2)/m) + ref = sympy.latex(((3*R*(_TE/(2*T))**2 * sympy.sinh(_TE/(2*T))**-2)/m).simplify()) cv_Al.unique_keys = ('TE_Al', 'm_Al') res = cv_Al.latex({'TE_Al': TE, 'temperature': 'T', 'x': 'E', 'molar_gas_constant': 'R', 'm_Al': 'm'}) assert res == ref + X = sympy.symbols('X') + _TE2 = 7 + 5*(X-3) + ref2 = sympy.latex(((3*R*(_TE2/(2*T))**2 * sympy.sinh(_TE2/(2*T))**-2)/m).simplify()) + res2 = cv_Al.latex({'TE_Al': TE, 'temperature': 'T', 'molar_gas_constant': 'R', 'm_Al': 'm'}, + default=lambda k: k.upper()) + assert res2 == ref2 + def test_Expr__single_arg(): class Pressure(Expr): diff --git a/chempy/util/tests/test_pyutil.py b/chempy/util/tests/test_pyutil.py index ebfae87f..2dfe8a21 100644 --- a/chempy/util/tests/test_pyutil.py +++ b/chempy/util/tests/test_pyutil.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import (absolute_import, division, print_function) -from ..pyutil import defaultnamedtuple +from ..pyutil import defaultkeydict, defaultnamedtuple def test_defaultnamedtuple(): @@ -37,3 +37,8 @@ def __new__(cls, x2, y2=_default_y): p3 = MySubclass(9) assert p3.x == 3 assert p3.y == 10**.5 + + +def test_defaultkeydict(): + d = defaultkeydict(lambda k: k*2) + assert d['as'] == 'asas' diff --git a/examples/bokeh_interactive_arrhenius_units.py b/examples/bokeh_interactive_arrhenius_units.py index ebe865c8..94d8e002 100644 --- a/examples/bokeh_interactive_arrhenius_units.py +++ b/examples/bokeh_interactive_arrhenius_units.py @@ -18,7 +18,7 @@ Af, Ab, Ea, Er = 1e16/u.molar/u.s, 1.5e15/u.s, 72e3*u.J/u.mol, -12e3*u.J/u.mol curdoc().add_root(integration_with_sliders( get_rsys(Af, Ab, Ea, Er), tend=3*u.s, - c0=defaultdict(float, {'Fe+3': 3e-3*u.molar, 'SCN-': 1.5e-3*u.molar}), + c0=defaultdict(lambda: 0*u.molar, {'Fe+3': 3e-3*u.molar, 'SCN-': 1.5e-3*u.molar}), parameters={'temperature': 298.15*u.K}, slider_kwargs={'temperature': dict(start=273.15*u.K, end=313.15*u.K, step=.05*u.K)}, unit_registry=SI_base_registry,