Data structures

MCounter

class simu.MCounter

This is a slight extention of the Collections.Counter class to also allow multiplication with scalar numbers:

>>> a = MCounter({"a": 1})
>>> b = MCounter({"b": 1})
>>> a + 2.5 * b
MCounter({'b': 2.5, 'a': 1})

Note that we stretch the use of MCounter to allow floats. Therefore the elements method is removed as a compromise in design.

elements()

Iterator over elements repeating each as many times as its count.

>>> c = Counter('ABCABC')
>>> sorted(c.elements())
['A', 'A', 'B', 'B', 'C', 'C']

Knuth’s example for prime factors of 1836: 2**2 * 3**3 * 17**1

>>> import math
>>> prime_factors = Counter({2: 2, 3: 3, 17: 1})
>>> math.prod(prime_factors.elements())
1836

Note, if an element’s count has been set to zero or is a negative number, elements() will ignore it.

QuantityDict

class simu.QuantityDict

Many properties on process modelling level are vectorial. This includes any species-specific properties, such as for instance mole fractions, chemical potentials or partial enthalpy. By keeping such data in instances of this class, they can always be accessed as a dictionary, using the bracket-operator (__get_item__).

Additionally, this class supports most arithmetic operations, such as +, -, *, /, ** - all of them interpreted element-wise. As two instances can have deviating keys (mostly species), there are some rules:

  • missing elements are assumed as zero, and structural zeros are omitted, i.e.

    >>> a = QuantityDict({
    ...         "A": Quantity("1 m"),
    ...         "B": Quantity("50 cm")})
    >>> b = QuantityDict({
    ...         "B": Quantity("1 m"),
    ...         "C": Quantity("50 cm")})
    >>> y = a + b
    >>> for key, value in y.items(): print(f"{key}: {value:~}")
    A: 1 m
    B: 150.0 cm
    C: 50 cm
    
    >>> y = a * b
    >>> for key, value in y.items(): print(f"{key}: {value:~}")
    B: 50 cm * m
    
  • A missing denominator element in division directly raises ZeroDivisionError

    >>> y = a / b
    Traceback (most recent call last):
    ...
    ZeroDivisionError: Missing denominator element in QuantityDict division
    
  • Operations can be mixed with scalar Quantities

    >>> y = a["A"] * b
    >>> for key, value in y.items(): print(f"{key}: {value:~}")
    B: 1 m ** 2
    C: 50 cm * m
    
  • floats as second operands in binary operators act as dimensionless quantities

    >>> y = 3 * a
    >>> for key, value in y.items(): print(f"{key}: {value:~}")
    A: 3 m
    B: 150 cm
    
    >>> y = 3 + a
    Traceback (most recent call last):
    ...
    pint.errors.DimensionalityError: ...
    
classmethod from_vector_quantity(
quantity: Quantity,
keys: list[str],
) Self

As the magnitude of a Quantity can be a container itself, this convenience factory method combines such a vector quantity with a set of given keys into a QuantityDict object

>>> raw = Quantity([1, 2, 3], "m")
>>> dic = QuantityDict.from_vector_quantity(raw, ["A", "B", "C"])
>>> for name, value in dic.items():
...     print(f"{name}: {value:~}")
A: 1 m
B: 2 m
C: 3 m
sum() Quantity

Sum all elements of the QuantityDict object. Naturally, all elements must have equal physical dimensions

>>> a = QuantityDict({
...         "B": Quantity("1 m"),
...         "C": Quantity("50 cm")})
>>> print(f"{a.sum():~}")
1.5 m

ParameterDictionary

class simu.ParameterDictionary

This class is a nested dictionary of SymbolQuantities to represent parameters with functionality to be populated using the register_* methods.

class SparseArray

This helper class represents a nexted dictionary that contains an arbitrary level of nested keys to address a value that is represented by a quantity.

__init__(order)
register_scalar(key: str, unit: str)

Create a scalar quantity and add the structure to the dictionary. The given unit is converted to base units before being applied. Calling the method returns the created quantity

>>> pdict = ParameterDictionary()
>>> print(pdict.register_scalar("speed", "cm/h"))
speed meter / second

In this output, speed is the name of the casadi.SX node representing the magnitude of returned Quantity. The dictionary then contains the following entry:

>>> print(pdict)
{'speed': <Quantity(speed, 'meter / second')>}
register_vector(
key: str,
sub_keys: Iterable[str],
unit: str,
) Quantity

Create a quantity vector with symbols and add the structure to the dictionary. The given unit is converted to base units before being applied. Calling the method returns the created quantity

>>> pdict = ParameterDictionary()
>>> print(pdict.register_vector("velocity", "xyz", "knot"))
[velocity.x, velocity.y, velocity.z] meter / second

The dictionary then contains the following entries:

>>> from pprint import pprint
>>> pprint(pdict)
{'velocity': {'x': <Quantity(velocity.x, 'meter / second')>,
              'y': <Quantity(velocity.y, 'meter / second')>,
              'z': <Quantity(velocity.z, 'meter / second')>}}
register_sparse_matrix(
key: str,
pairs: Iterable[tuple[str, str]],
unit: str,
) NestedMap[Quantity]

Create a sparse matrix quantity and add the structure to the dictionary. The given unit is converted to base units before being applied.

>>> pdict = ParameterDictionary()
>>> binaries = [("H2O", "CO2"), ("H2O", "CH4")]
>>> from pprint import pprint
>>> pprint(pdict.register_sparse_matrix("K_ij", binaries, "K"))
{'H2O': {'CH4': <Quantity(K_ij.H2O.CH4, 'kelvin')>,
         'CO2': <Quantity(K_ij.H2O.CO2, 'kelvin')>}}

After above call, the dictionary contains the following entries:

>>> from pprint import pprint
>>> pprint(pdict)
{'K_ij': {'H2O': {'CH4': <Quantity(K_ij.H2O.CH4, 'kelvin')>,
                  'CO2': <Quantity(K_ij.H2O.CO2, 'kelvin')>}}}
register_sparse_3d(
key: str,
pairs: Iterable[tuple[str, str, str]],
unit: str,
) NestedMap[Quantity]

Create a sparse 3d matrix quantity and add the structure to the dictionary. The given unit is converted to base units before being applied.

>>> pdict = ParameterDictionary()
>>> ternaries = [("A", "B", "C"), ("A", "C", "D")]
>>> from pprint import pprint
>>> pprint(pdict.register_sparse_3d("C", ternaries, "K"))
{'A': {'B': {'C': <Quantity(C.A.B.C, 'kelvin')>},
       'C': {'D': <Quantity(C.A.C.D, 'kelvin')>}}}

After above call, the dictionary contains the following entries:

>>> from pprint import pprint
>>> pprint(pdict)
{'C': {'A': {'B': {'C': <Quantity(C.A.B.C, 'kelvin')>},
                'C': {'D': <Quantity(C.A.C.D, 'kelvin')>}}}}
get_quantity(*keys)

Extract a quantity from the given sequence of key. Being a nested dictionary, each key from the argument list is used to navigate into the structure. The value of the most inner addressed key is returned. For normal usage, this should be of type Quantity.

get_vector_quantity(*keys)

Extract a vector quantity from the given sequence of keys. The method extracts the values of the structure below the sequence of argument keys, and concatenates them as a single vector property.

Formula parser

class simu.core.utilities.molecules.FormulaParser

This class implements the functionality to analyse chemical sum formulae. The atomic composition and molecular weight can be obtained.

__init__()
property atomic_weights: Mapping[str, Quantity]

Return a dictionary with all elements mapped to their molecular weight

parse(formula: str) MCounter

Parse a formula and return a Counter object with the atomic symbols as keys and the number of occurances as values:

Plain formulae:
>>> parser = FormulaParser()
>>> parser.parse("H3PO4")
MCounter({'O': 4, 'H': 3, 'P': 1})
>>> parser.parse("KMnO4")
MCounter({'O': 4, 'K': 1, 'Mn': 1})
>>> parser.parse("FISH")
MCounter({'F': 1, 'I': 1, 'S': 1, 'H': 1})
With parantheses:
>>> parser.parse("(NH4)2HPO4")
MCounter({'H': 9, 'O': 4, 'N': 2, 'P': 1})
With structure:
>>> parser.parse("CH3-(CH2)3-CH=O>")
MCounter({'H': 10, 'C': 5, 'O': 1})
>>> parser.parse("|N≡N|")
MCounter({'N': 2})
>>> parser.parse("<O=O>")
MCounter({'O': 2})
With charge:
>>> parser.parse("SO4:2-")
MCounter({'O': 4, 'S': 1})
With a complex:
>>> parser.parse("Na(UO2)3[Zn(H2O)6](CH3CO2)9")
MCounter({'H': 39, 'O': 30, 'C': 18, 'U': 3, 'Na': 1, 'Zn': 1})
With crystal water:
>>> parser.parse("CuSO4·5H2O")
MCounter({'H': 10, 'O': 9, 'Cu': 1, 'S': 1})
With crystal water and another solvent:
>>> parser.parse("CuSO4·3H2O·2(CH3)-COOH")
MCounter({'H': 14, 'O': 11, 'C': 4, 'Cu': 1, 'S': 1})
molecular_weight(formula: str) Quantity

Return the molecular weight for a given formula. The atomic weights are originally obtained from [CS16].

>>> parser = FormulaParser()
>>> mw = parser.molecular_weight("CH3-(CH2)24-CH3")
>>> print(f"{mw:~.2f}")
366.72 g / mol
charge(formula: str) Quantity

Return the charge associated to the given formula.

>>> parser = FormulaParser()
>>> for n in "H2SO4 SO4:2- Al:3+ S6:12-".split():
...     print(f"{n}: {parser.charge(n):~}")
H2SO4: 0 e / mol
SO4:2-: -2 e / mol
Al:3+: 3 e / mol
S6:12-: -12 e / mol