How to use the unit system

Important: The unit system is a feature under development. The following section might not work properly yet.

Here, we show some examples using the unit system in ecell4. This feature requires Python library, pint. Install pint before running this example as follows: pip install pint. See also the website https://pint.readthedocs.io/en/latest/.


In [1]:
%matplotlib inline
from ecell4 import *

With no units

First, imagine a very simple system only with binding and unbinding reactions like:


In [2]:
with species_attributes():
    A | B | C | {'D': 1, 'radius': 0.005}

with reaction_rules():
    A + B == C | (0.01, 0.3)

m = get_model()

The species_attributes section defines a diffusion constant and radius of Species, A, B and C. For example, the diffusion rate of A is 1, and its dimensionality is expected to be [length**2/time]. However, what is the scale? Is it meter? Or mile?

Once the base units are determined, e.g. micrometer as [length] and second as [time], all the units must be consistent within the model. The second order rate consant must have dimensionality [1/(substance/length**3)/time], which is micrometer**3/item/second. Thus, when the parameter is given as 1/molar/min in some literature, you have to translate it by yourself.


In [3]:
show(m)


A|{'D': <ecell4_base.core.Quantity object at 0x14c42fd74da0>, 'radius': <ecell4_base.core.Quantity object at 0x14c42fd74e10>}
B|{'radius': <ecell4_base.core.Quantity object at 0x14c42fd74dd8>, 'D': <ecell4_base.core.Quantity object at 0x14c42fd74da0>}
C|{'D': <ecell4_base.core.Quantity object at 0x14c42fd74da0>, 'radius': <ecell4_base.core.Quantity object at 0x14c42fd74e10>}
A+B>C|0.01
C>A+B|0.3

Introducing units

ecell4 provides the way to handle units in the modeling environment. Here is an example.


In [4]:
from ecell4.extra.unit import getUnitRegistry

In [5]:
ureg = getUnitRegistry()
Q_ = ureg.Quantity

with species_attributes():
    A | B | C | {'D': Q_(1, 'um**2/s'), 'radius': Q_(0.005, 'um')}

with reaction_rules():
    A + B == C | (Q_(0.01, '1/(item/um**3)/s'), Q_(0.3, '1/s'))

m = get_model()

First, create your own unit system,ureg, by using ecell4.extra.unit.getUnitRegistry. With this UnitRegistry, you can make a quantity with its unit as ureg.Quantity(value, unit). (Please be careful about the type of Quantity. It looks same with Quantity given by pint, but is slightly changed in ecell4 though all the original functionality in pint is availble even in ecell4. Please not use ureg = pint.UnitRegistry().)


In [6]:
help(ureg.Quantity)


Help on class Quantity in module pint.quantity:

class Quantity(_Quantity)
 |  Implements a class to describe a physical quantity:
 |  the product of a numerical value and a unit of measurement.
 |  
 |  :param value: value of the physical quantity to be created.
 |  :type value: str, Quantity or any numeric type.
 |  :param units: units of the physical quantity to be created.
 |  :type units: UnitsContainer, str or Quantity.
 |  
 |  Method resolution order:
 |      Quantity
 |      _Quantity
 |      pint.util.PrettyIPython
 |      pint.util.SharedRegistryObject
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __add__ = wrapped(self, other)
 |  
 |  __div__ = wrapped(self, other)
 |  
 |  __mul__ = wrapped(self, other)
 |  
 |  __pow__ = wrapped(self, other)
 |  
 |  __truediv__ = wrapped(self, other)
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  force_ndarray = False
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from _Quantity:
 |  
 |  __abs__(self)
 |  
 |  __array_prepare__(self, obj, context=None)
 |  
 |  __array_ufunc__(self, ufunc, method, *inputs, **kwargs)
 |  
 |  __array_wrap__(self, obj, context=None)
 |  
 |  __bool__(self)
 |  
 |  __bytes__(self)
 |  
 |  __complex__(self)
 |  
 |  __copy__(self)
 |  
 |  __deepcopy__(self, memo)
 |  
 |  __divmod__ = wrapped(self, *args, **kwargs)
 |  
 |  __eq__ = wrapped(self, *args, **kwargs)
 |  
 |  __float__(self)
 |  
 |  __floordiv__ = wrapped(self, *args, **kwargs)
 |  
 |  __format__(self, spec)
 |      default object formatter
 |  
 |  __ge__ lambda self, other
 |  
 |  __getattr__(self, item)
 |  
 |  __getitem__(self, key)
 |  
 |  __gt__ lambda self, other
 |  
 |  __hash__(self)
 |      Return hash(self).
 |  
 |  __iadd__(self, other)
 |  
 |  __idiv__ = __itruediv__(self, other)
 |  
 |  __ifloordiv__(self, other)
 |  
 |  __imod__(self, other)
 |  
 |  __imul__(self, other)
 |  
 |  __int__(self)
 |      # Mathematical operations
 |  
 |  __ipow__(self, other)
 |  
 |  __isub__(self, other)
 |  
 |  __itruediv__(self, other)
 |  
 |  __le__ lambda self, other
 |  
 |  __len__(self)
 |  
 |  __long__(self)
 |  
 |  __lt__ lambda self, other
 |  
 |  __mod__ = wrapped(self, *args, **kwargs)
 |  
 |  __ne__(self, other)
 |      Return self!=value.
 |  
 |  __neg__(self)
 |  
 |  __nonzero__ = __bool__(self)
 |  
 |  __pos__(self)
 |  
 |  __radd__ = __add__(self, other)
 |  
 |  __rdiv__ = __rtruediv__(self, other)
 |  
 |  __rdivmod__ = wrapped(self, *args, **kwargs)
 |  
 |  __reduce__(self)
 |      helper for pickle
 |  
 |  __repr__(self)
 |      Return repr(self).
 |  
 |  __rfloordiv__ = wrapped(self, *args, **kwargs)
 |  
 |  __rmod__ = wrapped(self, *args, **kwargs)
 |  
 |  __rmul__ = __mul__(self, other)
 |  
 |  __round__(self, ndigits=0)
 |  
 |  __rpow__ = wrapped(self, *args, **kwargs)
 |  
 |  __rsub__(self, other)
 |  
 |  __rtruediv__(self, other)
 |  
 |  __setitem__(self, key, value)
 |  
 |  __str__(self)
 |      Return str(self).
 |  
 |  __sub__(self, other)
 |  
 |  __unicode__ = __str__(self)
 |      Return str(self).
 |  
 |  check(self, dimension)
 |      Return true if the quantity's dimension matches passed dimension.
 |  
 |  clip(self, first=None, second=None, out=None, **kwargs)
 |  
 |  compare = wrapped(self, *args, **kwargs)
 |  
 |  compatible_units(self, *contexts)
 |  
 |  fill(self, value)
 |  
 |  format_babel(self, spec='', **kwspec)
 |  
 |  ito(self, other=None, *contexts, **ctx_kwargs)
 |      Inplace rescale to different units.
 |      
 |      :param other: destination units.
 |      :type other: Quantity, str or dict
 |  
 |  ito_base_units(self)
 |      Return Quantity rescaled to base units
 |  
 |  ito_reduced_units(self)
 |      Return Quantity scaled in place to reduced units, i.e. one unit per
 |      dimension. This will not reduce compound units (intentionally), nor
 |      can it make use of contexts at this time.
 |  
 |  ito_root_units(self)
 |      Return Quantity rescaled to base units
 |  
 |  m_as(self, units)
 |      Quantity's magnitude expressed in particular units.
 |      
 |      :param units: destination units
 |      :type units: Quantity, str or dict
 |  
 |  plus_minus(self, error, relative=False)
 |      # Measurement support
 |  
 |  put(self, indices, values, mode='raise')
 |  
 |  searchsorted(self, v, side='left', sorter=None)
 |  
 |  to(self, other=None, *contexts, **ctx_kwargs)
 |      Return Quantity rescaled to different units.
 |      
 |      :param other: destination units.
 |      :type other: Quantity, str or dict
 |  
 |  to_base_units(self)
 |      Return Quantity rescaled to base units
 |  
 |  to_compact(self, unit=None)
 |      Return Quantity rescaled to compact, human-readable units.
 |      
 |      To get output in terms of a different unit, use the unit parameter.
 |      
 |      >>> import pint
 |      >>> ureg = pint.UnitRegistry()
 |      >>> (200e-9*ureg.s).to_compact()
 |      <Quantity(200.0, 'nanosecond')>
 |      >>> (1e-2*ureg('kg m/s^2')).to_compact('N')
 |      <Quantity(10.0, 'millinewton')>
 |  
 |  to_reduced_units(self)
 |      Return Quantity scaled in place to reduced units, i.e. one unit per
 |      dimension. This will not reduce compound units (intentionally), nor
 |      can it make use of contexts at this time.
 |  
 |  to_root_units(self)
 |      Return Quantity rescaled to base units
 |  
 |  to_timedelta(self)
 |  
 |  to_tuple(self)
 |  
 |  tolist(self)
 |  
 |  ----------------------------------------------------------------------
 |  Class methods inherited from _Quantity:
 |  
 |  from_tuple(tup) from builtins.type
 |  
 |  ----------------------------------------------------------------------
 |  Static methods inherited from _Quantity:
 |  
 |  __new__(cls, value, units=None)
 |      Create and return a new object.  See help(type) for accurate signature.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from _Quantity:
 |  
 |  T
 |  
 |  debug_used
 |  
 |  dimensionality
 |      Quantity's dimensionality (e.g. {length: 1, time: -1})
 |  
 |  dimensionless
 |      Return true if the quantity is dimensionless.
 |  
 |  flat
 |  
 |  imag
 |  
 |  m
 |      Quantity's magnitude. Short form for `magnitude`
 |  
 |  magnitude
 |      Quantity's magnitude. Long form for `m`
 |  
 |  real
 |  
 |  shape
 |  
 |  u
 |      Quantity's units. Short form for `units`
 |      
 |      :rtype: UnitContainer
 |  
 |  unitless
 |      Return true if the quantity does not have units.
 |  
 |  units
 |      Quantity's units. Long form for `u`
 |      
 |      :rtype: UnitContainer
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes inherited from _Quantity:
 |  
 |  __array_priority__ = 17
 |  
 |  default_format = ''
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from pint.util.PrettyIPython:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

The default base units are meter for [length], second for [time], and item (which means the number of molecules) for [substance]. When you change the default base unit, do like ureg = getUnitRegistry(length='micrometer').


In [7]:
show(m)


A|{'D': <ecell4_base.core.Quantity object at 0x14c42c4860b8>, 'radius': <ecell4_base.core.Quantity object at 0x14c42c486278>}
B|{'radius': <ecell4_base.core.Quantity object at 0x14c42c4860f0>, 'D': <ecell4_base.core.Quantity object at 0x14c42c4860b8>}
C|{'D': <ecell4_base.core.Quantity object at 0x14c42c4860b8>, 'radius': <ecell4_base.core.Quantity object at 0x14c42c486278>}
A+B>C|1e-20
C>A+B|0.3

Now you can provide quantities in any unit regardless of the base units.


In [8]:
ureg = getUnitRegistry()
Q_ = ureg.Quantity

with species_attributes():
    A | B | C | {'D': Q_(1e-8, 'cm**2/s'), 'radius': Q_(5, 'nm')}

with reaction_rules():
    A + B == C | (Q_(6.02214129, '1/uM/s'), Q_(18, '1/min'))

m = get_model()
show(m)


A|{'D': <ecell4_base.core.Quantity object at 0x14c42c674518>, 'radius': <ecell4_base.core.Quantity object at 0x14c42c6b7080>}
B|{'radius': <ecell4_base.core.Quantity object at 0x14c42c6b7080>, 'D': <ecell4_base.core.Quantity object at 0x14c42c6b7048>}
C|{'D': <ecell4_base.core.Quantity object at 0x14c42c674518>, 'radius': <ecell4_base.core.Quantity object at 0x14c42c6b7080>}
A+B>C|1e-20
C>A+B|0.3

You can operate quantities, and make a new quantity. See https://pint.readthedocs.io/en/latest/ for more details.


In [9]:
volume = Q_(1, 'fL')
conc = Q_(100, 'nM')
print((volume * conc).to('item'))


60.221412900000004 item

In addition to the model creation, run_simulation (and ensemble_simulations) also supports the unit system.


In [10]:
run_simulation(Q_(0.1, 'min'), y0={'C': Q_(60, 'item')}, volume=Q_(1, 'fL'), model=m, solver='ode')


Even if you change the base units, the behavior of simulations is kept consistent. In the following example, base units are rescaled to micrometer and minute with no change in the modeling section.


In [11]:
ureg = getUnitRegistry(length='micrometer', time='minute')
Q_ = ureg.Quantity

with species_attributes():
    A | B | C | {'D': Q_(1e-8, 'cm**2/s'), 'radius': Q_(5, 'nm')}

with reaction_rules():
    A + B == C | (Q_(6.02214129, '1/uM/s'), Q_(18, '1/min'))

m = get_model()
show(m)
run_simulation(Q_(0.1, 'min'), y0={'C': Q_(60, 'item')}, volume=Q_(1, 'fL'), model=m, solver='ode')


A|{'D': <ecell4_base.core.Quantity object at 0x14c42c519d68>, 'radius': <ecell4_base.core.Quantity object at 0x14c42c519e48>}
B|{'radius': <ecell4_base.core.Quantity object at 0x14c42c519d30>, 'D': <ecell4_base.core.Quantity object at 0x14c42c519d68>}
C|{'D': <ecell4_base.core.Quantity object at 0x14c42c519d68>, 'radius': <ecell4_base.core.Quantity object at 0x14c42c519e48>}
A+B>C|0.6
C>A+B|18

Checking dimensionality

As default, the dimensionality of units is checked. When you turn off unit checking, call ecell4.extra.unit.use_strict(False) before using those features.

For example, the dimensionality of a diffusion constant must be [length**2/time]. When you give a unit with a wrong dimensionality, an exception would be thrown like:


In [12]:
try:
    with species_attributes():
        A | {'D': Q_(1.0, 'um/s')}
except ValueError as e:
    print('{}: {}'.format(e.__class__.__name__, e))

Similarly, a kinetic rate constant of reactions is verified based on the order of the reaction. The first order reaction rate should have [1/time], and the second order should have [l/(substance/length**3)/time] in volume.

try: with reactionrules(): A + B > C | Q(0.3, '1/s') except ValueError as e: print('{}: {}'.format(e.class.name, e))

Additionally, rate law representations accept quantities with a unit too. See the example below:


In [13]:
ureg = getUnitRegistry()
Q_ = ureg.Quantity

with reaction_rules():
    S > P | Q_(1.0, 'uM/s') * S / (Q_(100, 'nM') + S)

m = get_model()
show(m)


A|{'D': <ecell4_base.core.Quantity object at 0x14c42a30ec88>}
1*S>1*P|0

Here, the reaction above has two quantities, Vmax = Q_(1.0, 'uM/s') and Km = Q_(100, 'nM'). First, Km must have the same dimensionality with S, which is [concentration].


In [14]:
try:
    with reaction_rules():
        S > P | Q_(1.0, 'uM/s') * S / (Q_(100, 'nM/s') + S)
except ValueError as e:
    print('{}: {}'.format(e.__class__.__name__, e))

Secondly, the dimensionality of a rate equation must be [concentration/time]. Therefore, the dimensionality of Vmax should be [concentration/time] too.


In [15]:
try:
    with reaction_rules():
        S > P | Q_(1.0, '1/s') * S / (Q_(100, 'nM') + S)
except RuntimeError as e:
    print('{}: {}'.format(e.__class__.__name__, e))

When you give a value with no unit, it is regarded as dimensionless.


In [16]:
with reaction_rules():
    S > P | 10.0 * Q_(0.1, 'uM/s') * S**2 / (Q_(100, 'nM')**2 + S**2)
m = get_model()
show(m)


1*S>1*P|0
1*S>1*P|0
1*S>1*P|0