Package API: kodo.quantities

Types for representing physical quantities, which have a magnitude and a unit

Defining Units

Unit types for quantities are created by subclassing the QuantityUnit enum base:

>>> class Time(QuantityUnit):
...     MILLISECONDS = 1
...     SECONDS = 1000
...     MINUTES = 60 * SECONDS
>>> class Distance(QuantityUnit):
...     MILLIMETERS = 1
...     CENTIMETERS = 10 * MILLIMETERS
...     METERS = 100 * CENTIMETERS
...     KILOMETERS = 1000 * METERS
...     INCH = 24 * MILLIMETERS
...     HALF_INCH = INCH // 2  # Hey! Hands off
...     QUARTER_INCH = INCH // 4

The enum members form units of relative size to each other. They MUST be integers so typically the smallest (highest precision) unit is 1 and the others are some multiple of it. Note that the designated ‘unit’ can be changed without breaking dependant code (as long as the code is using the quantities right). In this case the unitary value is 0.5mm:

>>> class Distance(QuantityUnit):
...     MILLIMETERS = 2  # Scaled to allow SIXTEENTH_INCH to be an integer
...     # [...]
...     SIXTEENTH_INCH = 3  # 1/16″ is 1.5mm
...     INCH = 16 * SIXTEENTH_INCH

Although it would probably be easier to make the unitary value 0.1mm:

>>> class Distance(QuantityUnit):
...     MILLIMETERS = 10
...     # [...]
...     SIXTEENTH_INCH = 15
...     INCH = 16 * SIXTEENTH_INCH

In full:

>>> class Distance(QuantityUnit):
...     MILLIMETERS = 10
...     CENTIMETERS = 10 * MILLIMETERS
...     METERS = 100 * CENTIMETERS
...     KILOMETERS = 1000 * METERS
...     SIXTEENTH_INCH = 15
...     INCH = 16 * SIXTEENTH_INCH
...     HALF_INCH = 8 * SIXTEENTH_INCH
...     QUARTER_INCH = 4 * SIXTEENTH_INCH

Creating Physical Quantities

A physical quantity can be created using the matrix multiplication operator “@” with a quantity unit, e.g. 2 seconds:

>>> quantity: Quantity[Time] = 2 @ Time.SECONDS

Quantities of the same type relate to one another as you would expect (parentheses for clarity):

>>> assert (2 @ Time.SECONDS) == (2000 @ Time.MILLISECONDS)

Note that quantities are really just integers which, at runtime, have no additional information attached to them. This means that Python will happily accept any Quantity wherever it would accept an integer; however static type checkers such as MyPy will complain about it, which is good as it is almost certainly a mistake to attempt to, for example, sum time and distance quantities, or sum a quantity with an arbitrary value:

>>> meaningless_value = (2 @ Time.SECONDS) + (10 @ Distance.MILLIMETERS)
>>> # Depending on the declaration of Distance, 100 here could be interpreted by the
>>> # runtime as 100m, 100cm, 100/24″, or anything else...
>>> unreliable_value = (2 @ Distance.METERS) + 100

Multiplying quantities with other quantities, even of the same type, would produce a different unit, which is not supported. (However, it is conceivable that it could be supported in the future.) The following will also fail static type checks:

>>> area = (2 @ Distance.METERS) * (2 @ Distance.METERS)  # 4.0m²
>>> speed = (100 @ Distance.METERS) / (1 @ Time.SECONDS)  # 100m/s

Using Physical Quantities

At some point quantities will need to be passed through an interface of some sort where the unit information will be lost. Such interfaces will define a single unit they accept; for instance sleep() requires an argument in seconds. Upon reaching such an interface, quantities can be stripped of their scalar types and converted to the required unit with the right bit-shift operator “>>” or floor division operator “//”:

>>> delay = 2 @ Time.MINUTES
>>> # [...]
>>> import time
>>> time.sleep(delay >> Time.SECONDS)

With the “>>” operator the type of the resulting value is always a float and will only be precise up to the highest precision unit for a defined QuantityUnit type (the unit with a magnitude of 1, which need not be explicitly defined).

With the “//” operator the resulting type will be int, with whatever loss of precision that implies.

>>> delay = 3600 @ Time.MILLISECONDS
>>> # [...]
>>> time.sleep(delay // Time.SECONDS)  # Will sleep for 3 seconds

Operations on Quantities

At runtime all quantities are a subclass of integers, so all operations that work on integers will work[^*] however type checkers only allow a subset of operations with certain types.

[^*]: One small difference is multiplication by floats and division by float or int, which would normally return floats, returns a new integer quantity. However division by a quantity returns a float. Don’t worry too much about this.

Quantities may be added to or subtracted from other quantities with the same unit, returning a new quantity of that unit:

>>> assert (2 @ Time.SECONDS) + (500 @ Time.MILLISECONDS) == (2500 @ Time.MILLISECONDS)

They may be scaled by multiplying (*) and dividing (/) by unitless numeric values _only_, resulting in a new quantity of the same unit. Note however that when scaling down there will probably be some rounding loss depending on the precision of the unit.

>>> assert (2 @ Time.SECONDS) * 2 == (4 @ Time.SECONDS)
>>> assert (2 @ Time.SECONDS) / 2 == (1 @ Time.SECONDS)
>>> assert (2 @ Time.SECONDS) / 3 == (666 @ Time.MILLISECONDS)  # Rounded down to whole milliseconds

In addition you may use _floor_ division (//) on quantities with another quantity of the same unit to calculate how many times it can be divided into that size. Note when using single units this is equivalent to converting to an untyped value of those units, so this is the same as using the floor division operator with a unit value.

>>> assert (10 @ Time.SECONDS) // (2 @ Time.SECONDS) == 5
>>> assert (10 @ Time.SECONDS) // (1 @ Time.SECONDS) == 10
>>> assert (10 @ Time.SECONDS) // Time.SECONDS == 10  # Unit may be used as a convenience

To find the remainder after floor division, the modulus operator (%) returns a new quantity:

>>> assert (10 @ Time.SECONDS) % (3 @ Time.SECONDS) == (1 @ Time.SECONDS)
>>> assert (3.6 @ Time.SECONDS) % Time.SECONDS == (600 @ Time.MILLISECONDS)

This pairs well with the shift and floor division operators to get the modulus values as floats or ints of a particular unit:

>>> assert (3.6 @ Time.SECONDS) % Time.SECONDS >> Time.SECONDS == 0.6
>>> assert (3.6 @ Time.SECONDS) % Time.SECONDS // Time.MILLISECONDS == 600

Choice of Operators

The operators for constructing (“@”) and deconstructing (“>>”) quantities may seem a bit odd, given that what they actually do is multiply and divide the values. They were chosen to be visually distinct from other multiplication and division operations on quantities and scalar units.

The matrix multiplication operator therefore replaces the scalar multiplication operator, while the shift operator, which looks arrow-like, is used to convert to the indicated unit. i.e.:

>>> # quantity  (converted to)  units
>>> delay             >>        Time.SECONDS
3.6
class Quantity[source]

Bases: int, Generic[kodo.quantities.U]

A physical quantity of a given unit type ‘U’

class QuantityUnit(new_class_name, /, names, *, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Bases: enum.Enum

Enum base class for units