Quantity related functionality

This module defines classes and functionality around pint quantities. These quantities can be symbolic (hosting casadi.SX as magnitudes), or numeric.

Objects

Quantity

class simu.Quantity

Proper quantity base-class for sub-classing.

Being a subclass of pint.Quantity, this class only really adds the __json__ method to return its json representation.

The constructor is used as for pint.Quantity.

static __new__(cls, *args, **kwargs)

SymbolQuantity

class simu.SymbolQuantity

A quantity class specialised to host casadi symbols (SX) of in particular, but not necessarily, independent variables.

static __new__(cls, *args, **kwargs)

Really generate an object of type Quantity. This is just a hacky way to specialise the constructor, due to the way the base-class is implemented. The arguments are:

  • name (str): The name of the casadi.SX symbol

  • units (str): The unit of measurement conform to pint units

  • sub_keys (Iterator[str]): If not None, defines a vectorial quantity with given sub-keys.

    >>> s = SymbolQuantity("speed", "m/s")
    >>> print(f"{s:~}")
    speed m / s
    
    >>> v = SymbolQuantity("vel", "m/s", "xyz")
    >>> print(f"{v:~}")
    [vel.x, vel.y, vel.z] m / s
    

QFunction

class simu.QFunction

Wrapper around casadi.Function to consider units of measurements. This derived function object is defined by a dictionary of arguments and results, both of which are Quantity instances with casadi symbols as magnitudes. These symbols are independent symbols for args and derived symbols for results.

The function object is then called as well with a dictionary of Quantity values. The units of measurement must be consistent, and a conversion is done to the initially defined units for the individual arguments. The result is given back as a dictionary of Quantity values in the same units as initially defined.

__init__(
args: NestedMap[Quantity],
results: NestedMap[Quantity],
func_name: str = 'f',
simplify_units: bool = True,
)
property result_structure: NestedMap[str]

Return the result structure as a nested dictionary, only including the units of measurements as values of end nodes

property arg_structure: NestedMap[str]

Return the argument structure as a nested dictionary, only including the units of measurements as values of end nodes

Symbolic Functions

The following functions redefine mathematical functions on the symbolic quantities.

jacobian

simu.jacobian(
dependent: Quantity,
independent: Quantity,
) Quantity

Calculate the casadi Jacobian and reattach the units of measurements.

>>> x = SymbolQuantity("x", "m")
>>> y = (x * x) / 2
>>> dy_dx = jacobian(y, x)
>>> print(f"{dy_dx:~}")
x m

qsum

simu.qsum(
quantity: Quantity,
) Quantity

Sum a symbol vector quantity same was a casadi.sum1, considering units of measurements. This function only applies to quantity objects with casadi.SX objects as magnitudes.

Note

This function sums the elements of the single, but vectorial argument, and is in that different from the builtin sum function that sums over an iterator of objects.

log

simu.log(quantity: _V) _V

Determine natural logarithms, considering units of measurements. The main intent is to use this version for symbolic quantities and QuantityDict objects, but it also works on floats.

>>> x = Quantity(10.0, "cm/m")
>>> log(x)
<Quantity(-2.30258509, 'dimensionless')>
>>> a = {"A": SymbolQuantity("A", "dimless"),
...      "B": SymbolQuantity("B", "dimless")}
>>> y = log(a)
>>> for key, value in y.items(): print(f"{key}: {value:~}")
A: log(A)
B: log(B)
>>> log(10)
<Quantity(2.30258509, 'dimensionless')>

The other unary functions are defined in the same manner.

sqrt

simu.sqrt(quantity: _V) _V

The square root function is a special case of a unary function in that the argument is not required to be dimensionless.

>>> a = QuantityDict({
...         "B": Quantity("1 m**2"),
...         "C": Quantity("2500 cm**2")})
>>> print(sqrt(a))
{'B': <Quantity(1.0, 'meter')>, 'C': <Quantity(50.0, 'centimeter')>}

qpow

simu.qpow(
base: Quantity,
exponent: Quantity,
) Quantity

Determine power of base to exponent, considering units of measurements. Both arguments must be dimensionless. For the special case that exponent is a constant scalar (not a symbol), use the ** operator

conditional

simu.conditional(
condition: SX,
negative: Quantity,
positive: Quantity,
) Quantity

Element-wise branching into the positive and negative branch depending on the condition and considering the units of measurements.

>>> x = SymbolQuantity("x", "m")
>>> y = conditional(x > 0, -x, x)  # abs function
>>> print(y)
@1=0, @2=((@1<x)==@1), ((@2?(-x):0)+((!@2)?x:0)) meter

As non-recommended as it is to use this function exessively, the resulting casadi expression is indeed overcomplicated and could simplify to

(x>0)?(x):(-x) meter

Note

You cannot just code x if x > 0 else -x, as this would not allow casadi branching dynamically dependent on the value of x later-on.

Utility functions

qvertcat

simu.qvertcat(
*quantities: Quantity,
) Quantity

Concatenate a bunch of scalar symbolic quantities with compatible units to a vector quantity

base_unit

simu.base_unit(unit: str) str

Create the base unit of given unit.

>>> print(base_unit("light_year"))
m
>>> print(base_unit("week"))
s

base_magnitude

simu.base_magnitude(
quantity: Quantity,
) float | SX

Return the magnitude of the quantity in base units. This works for symbolic and numeric quantities, and for scalars and vectors

>>> base_magnitude(Quantity("1 km"))
1000.0
>>> base_magnitude(Quantity("20 degC"))
293.15
>>> int(base_magnitude(Quantity("1 barg")))
201325
>>> base_magnitude(Quantity("speed_of_light"))
299792458.0

The base units are likely the SI unit system, but code shall not rely on this fact - only that it is a cosistent (and offset-free) unit system.

Note

Use SI or I will BTU you with my feet!

I saw this sentence once on the T-shirt of a nerd. It turns out it was a mirror.

flatten_dictionary

simu.flatten_dictionary(
structure: NestedMap[_V],
prefix: str = '',
) Mapping[str, _V]

Convert the given structure into a flat list of key value pairs, where the keys are SEPARATOR-separated concatonations of the paths, and values are the values of the leafs. Non-string keys are converted to strings. Occurances of SEPARATOR are escaped by \.

>>> d: NestedMap[int] = {"a": {"b": 1, "c": 2}, "d": {"e/f": 3}}
>>> flatten_dictionary(d)
{'a/b': 1, 'a/c': 2, 'd/e\\/f': 3}

unflatten_dictionary

simu.unflatten_dictionary(
flat_structure: Mapping[str, _V],
) NestedMap[_V]

This is the reverse of flatten_dictionary(), inflating the given one-depth dictionary into a nested structure.

>>> d = {"a/b": 1, "a/c": 2, r"d/e\/f": 3}
>>> unflatten_dictionary(d)
{'a': {'b': 1, 'c': 2}, 'd': {'e/f': 3}}

extract_units_dictionary

simu.extract_units_dictionary(
structure: NestedMap[Quantity] | Quantity,
) NestedMap[str] | str

Based on a nested dictionary of Quantities, create a new nested dictionaries with only the (base) units of measurement

>>> d = {"a": {"b": Quantity("1 m"), "c": Quantity("1 min")}}
>>> extract_units_dictionary(d)
{'a': {'b': 'm', 'c': 's'}}

simplify_quantity

simu.simplify_quantity(
quantity: Quantity,
) Quantity

Try to convert the unit into a more compact notation, allowing derived units, such as Watt, Joule and Pascal to be used.

Examples:

>>> q = Quantity(1.0, "kg * m**2 / mol / s**2")
>>> print(f"{simplify_quantity(q):~}")
1.0 J / mol
>>> q = Quantity(1.0, "kg / K / s**3")
>>> print(f"{simplify_quantity(q):~}")
1.0 W / K / m ** 2
>>> q = Quantity(1.0, "kg / m**2 / s**2")
>>> print(f"{simplify_quantity(q):~}")
1.0 Pa / m
>>> q = Quantity(1.0, "kg * m**2 / s**3 / A**2")
>>> print(f"{simplify_quantity(q):~}")
1.0 Ω
>>> q = Quantity(1.0, "m**3 / s")
>>> print(f"{simplify_quantity(q):~}")
1.0 m ** 3 / s

parse_quantities_in_struct

simu.parse_quantities_in_struct(
struct: NestedMap[str] | str,
) Quantity | NestedMap[Quantity]

Return a new struct that contains parsed quantities at the leaf values of the given input structure.

The structure can be a nested dictionary, given the keys as strings and the leaf values as strings that can be parsed as pint quantities. For example:

>>> from pprint import pprint
>>> y = parse_quantities_in_struct({
...    'speed': {
...        'car': '100 km/hr',
...        'snail': '1 cm/min',
...        'fingernail': '1.2 mm/day'},
...    'weight': {
...        'car': '1.5 t',
...        'snail': '10 g',
...        'fingernail': '300 mg'}
... })
>>> pprint(y)
{'speed': {'car': <Quantity(100.0, 'kilometer / hour')>,
           'fingernail': <Quantity(1.2, 'millimeter / day')>,
           'snail': <Quantity(1.0, 'centimeter / minute')>},
 'weight': {'car': <Quantity(1.5, 'metric_ton')>,
            'fingernail': <Quantity(300, 'milligram')>,
            'snail': <Quantity(10, 'gram')>}}

quantity_dict_to_strings

simu.quantity_dict_to_strings(
struct: Quantity | NestedMap[Quantity],
significant_digits: int = 17,
) str | NestedMap[str]

Return a new structure with the quantity instances replaced by a string representation that is parsable by the simu.Quantity constructor.

Example:

>>> from pprint import pprint
>>> from simu import Quantity
>>> struct = {'speed': {'car': Quantity(400 / 3, 'kilometer / hour'),
...                     'fingernail': Quantity(1.2, 'millimeter / day'),
...                     'snail': Quantity(1.0, 'centimeter / minute')},
...           'weight': {'car': Quantity(1.5, 'metric_ton'),
...                      'fingernail': Quantity(300, 'milligram'),
...                      'snail': Quantity(10, 'gram')}}
>>> pprint(quantity_dict_to_strings(struct))
{'speed': {'car': '133.33333333333334 km / h',
           'fingernail': '1.2 mm / d',
           'snail': '1 cm / min'},
 'weight': {'car': '1.5 t', 'fingernail': '300 mg', 'snail': '10 g'}}

extract_sub_structure

simu.extract_sub_structure(
source: NestedMap[Quantity],
structure: NestedMap[str],
) NestedMap[Quantity]

Given a nested structure map structure that defines the units of measurement of leaf value quantities, extract those quantities from a source structure source. A KeyError is raised if the source structure does not contain the requested data, and a DimensionalityError is raised if the queried quantity is of incompatible dimensions.

Example:

>>> from simu import Quantity
>>> src = {"a": {"b": Quantity(1, "km")},
...        "c": Quantity(3, "degC"),
...        "d": {"e": Quantity(2, "s"), "f": Quantity(3, "kJ")}}
>>> struct = {"a": {"b": "m"}, "d": {"e": "s"}}
>>> print(extract_sub_structure(src, struct))
{'a': {'b': <Quantity(1, 'kilometer')>}, 'd': {'e': <Quantity(2, 'second')>}}