Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![Documentation](https://readthedocs.org/projects/encomp/badge/?version=latest)](https://encomp.readthedocs.io)
[![License](https://img.shields.io/pypi/l/encomp.svg)](https://github.com/wlaur/encomp/blob/main/LICENSE)

<img src="https://raw.githubusercontent.com/wlaur/encomp/v1.6.3/docs/img/logo.png" alt="encomp logo" width="150">
<img src="https://raw.githubusercontent.com/wlaur/encomp/v1.6.4/docs/img/logo.png" alt="encomp logo" width="150">

> General-purpose library for *en*gineering *comp*utations.

Expand Down Expand Up @@ -46,8 +46,9 @@ pip install encomp

## The `Quantity` class

`encomp.units.Quantity` (alias `Q`) extends `pint.Quantity`.
`encomp.units.Quantity` extends `pint.Quantity`.
A quantity has a *magnitude* and a *unit*; each unit has a *dimensionality* (a combination of the base dimensions), and each dimensionality has multiple associated units.
The examples below abbreviate it as `Q` via `from encomp.units import Quantity as Q`; the library does not export a name `Q`.

```python
from encomp.units import Quantity as Q
Expand Down Expand Up @@ -78,6 +79,8 @@ A newly created dimensionality gets a class name of the form `Dimensionality[...
The second type parameter is the magnitude container. It defaults to `Numpy1DArray`, so annotate scalar quantities explicitly as `Quantity[Pressure, float]` (or `Q[Pressure, float]`).

```python
from typing import Any

from encomp.misc import isinstance_types
from encomp.units import ExpectedDimensionalityError
from encomp.units import Quantity as Q
Expand All @@ -99,8 +102,10 @@ rho = m / V # Quantity[Density, float]
m_ = Q(25, "kg/week") # Quantity[UnknownDimensionality, float]

# at runtime, the dimensionality of m_ is evaluated to MassFlow;
# use isinstance_types for parameterized Quantity checks in type-checked code
assert isinstance_types(m_, Q[MassFlow])
# use isinstance_types for parameterized Quantity checks in type-checked code.
# always spell the magnitude parameter here: a bare Q[MassFlow] means
# Quantity[MassFlow, Numpy1DArray] to a type checker, which narrows m_ to Never
assert isinstance_types(m_, Q[MassFlow, Any])

# these operations (Mass**2 divided by Volume) are not explicitly defined as overloads
# at runtime, the type will be evaluated to
Expand Down Expand Up @@ -377,6 +382,8 @@ Settings are loaded when `encomp.settings` is imported; for runtime changes to q

Because the `.env` file is resolved relative to the current working directory, a stray `.env` that sets an invalid `ENCOMP_*` value (for example `ENCOMP_UNITS` pointing at a missing file) makes `import encomp` fail with a `pydantic.ValidationError`, even in an unrelated project. Remove or correct the offending value; unrelated keys in the `.env` are ignored.

`import encomp` also installs `encomp.units.UNIT_REGISTRY` as pint's process-wide *application registry*. This is deliberate: every quantity in the process must come from that registry, or the dimensionality subclasses, the custom `[currency]` / `[normal]` dimensions and `on_redefinition="raise"` would silently not apply. The consequence is that another pint-based library in the same process gets encomp's registry (and its unit definitions, including the `Nm³` reinterpretation) after `import encomp`. Registry options that encomp pins — `force_ndarray`, `force_ndarray_like`, `autoconvert_offset_to_baseunit` — cannot be reassigned; a write to one is discarded and logs a warning.

## Documentation

The usage guide, example notebooks, and API reference are at [encomp.readthedocs.io](https://encomp.readthedocs.io).
Expand Down
48 changes: 42 additions & 6 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ A {py:class}`encomp.units.Quantity` stores the *magnitude*, *unit* and *dimensio
Each dimensionality is a separate subclass, so static type checkers catch dimensionality errors before the code runs.

:::{note}
`Q` is an alias for `Quantity`: `from encomp.units import Quantity as Q`
Throughout this guide, `Q` is the alias created by `from encomp.units import Quantity as Q`.
The library does not export a name `Q` -- create the alias yourself in each module that wants it.
:::

Import the class, then create an instance representing an absolute pressure of 1 bar:
Expand All @@ -41,11 +42,17 @@ pressure_kpa = pressure.to("kPa")
```

For measured values with uncertainty, `Quantity.plus_minus()` keeps the unit and stores an `uncertainties` magnitude.
Uncertainty propagation is delegated to `pint` and `uncertainties`: the result is a `pint.Measurement`, not an `encomp` dimensionality subclass, so it carries no `Quantity[DT, MT]` typing and leaves the dimensionality-typed system.

The unit definition file (`encomp/defs/units.txt`) lists the accepted unit names.
It is based on the `default_en.txt` file from `pint`, with minor modifications.
The `Nm3`/`Nm³`/`nm3` spellings mean *normal cubic meter*, not nanometer cubed; use `nanometer**3` for nanoscale volumes.

The magnitude and the unit are always separate arguments, and each has a closed set of accepted types.
A magnitude is a real scalar, a 1-dimensional sequence, a NumPy array, a Polars `Series`/`Expr`, or a SymPy atom -- a string (`Q("24 kg")`) and a `bool` (`Q(True)`) are rejected, statically where possible and at runtime always.
A unit is a string, a `Unit`, a `pint.UnitsContainer`, or `None`; passing a `Quantity` as the unit is an error, because its magnitude would be silently dropped.
Use `qty.u` to reuse another quantity's unit, or {py:meth}`encomp.units.Quantity.to`, which does accept a `Quantity` and converts to its unit.

Quantities can also be constructed from unit registry attributes:

```python
Expand All @@ -64,6 +71,17 @@ v = d / ureg.s
mf = Q(25, ureg.kg / ureg.h)
```

:::{warning}
`import encomp` installs {py:data}`encomp.units.UNIT_REGISTRY` as `pint`'s process-wide
*application registry*. Every quantity in the process must come from it, or the
dimensionality subclasses, the custom `[currency]` / `[normal]` dimensions and
`on_redefinition="raise"` would silently not apply. Another `pint`-based library in the
same process therefore gets `encomp`'s registry (and its unit definitions) after the
import. The registry options `force_ndarray`, `force_ndarray_like` and
`autoconvert_offset_to_baseunit` are pinned: assigning to them is discarded and logs a
warning.
:::

### Quantity types

Each dimensionality is a unique subclass of {py:class}`encomp.units.Quantity`.
Expand Down Expand Up @@ -146,6 +164,8 @@ Check the physical dimensionality of a quantity with {py:meth}`encomp.units.Quan
For semantic dimensionality checks and parameterized types like `list[Quantity[Pressure]]`, use {py:func}`encomp.misc.isinstance_types`.

```python
from typing import Any

from encomp.misc import isinstance_types
from encomp.units import Quantity as Q
from encomp.utypes import Length, Pressure, Temperature, TemperatureDifference
Expand All @@ -165,15 +185,29 @@ Q(1, "delta_degC").check(Temperature) # True

# isinstance_types works with simple Quantity types and nested containers

isinstance_types(pressure, Q[Pressure]) # True
isinstance_types(pressure, Q[Length]) # False
isinstance_types([pressure, pressure], list[Q[Pressure]]) # True
isinstance_types({1: Q(2, "m"), 2: Q(25, "cm")}, dict[int, Q[Length]]) # True
isinstance_types(pressure, Q[Pressure, Any]) # True
isinstance_types(pressure, Q[Length, Any]) # False
isinstance_types([pressure, pressure], list[Q[Pressure, Any]]) # True
isinstance_types({1: Q(2, "m"), 2: Q(25, "cm")}, dict[int, Q[Length, Any]]) # True

# all Quantity[...] objects are subclasses of Quantity
isinstance_types(pressure, Q) # True
```

:::{warning}
Spell the magnitude parameter when `isinstance_types` narrows a variable you go on to use:
write `Q[Pressure, Any]` (or the exact magnitude type, `Q[Pressure, float]`), not a bare
`Q[Pressure]`.

At *runtime* `Q[Dim]` is magnitude-agnostic, so a bare `Q[Pressure]` matches a scalar
quantity. *Statically*, `Q[Dim]` means `Quantity[Dim, Numpy1DArray]` (the magnitude
parameter defaults to `Numpy1DArray`), and `isinstance_types` is a
{py:obj}`typing.TypeIs` predicate: a type checker intersects the declared type with the
narrowed one, so `Quantity[Pressure, float]` narrows to `Never` and every later use of
the variable is an error. `Q[Pressure, Any]` behaves identically at runtime and narrows
correctly.
:::

For functions and methods, use the `typeguard.typechecked` decorator instead of explicit checks in the function body:

```python
Expand All @@ -190,7 +224,7 @@ def func(_p1: Q[Pressure, float]) -> tuple[Q[Length, float], Q[Power, float]]:

`typeguard.TypeCheckError` is raised if the arguments or the return value have incorrect dimensionalities.

Scalar `Quantity` equality is tolerant (`rtol=1e-9`, `atol=1e-12`) and compares after unit conversion, so values that differ only by tiny floating-point noise may compare equal. Hashing is supported for float magnitudes and uses root units; vector magnitudes are unhashable.
Scalar `Quantity` equality is tolerant (`rtol=1e-9`, `atol=1e-12`) and compares after unit conversion, so values that differ only by tiny floating-point noise may compare equal. The same tolerance folds into the non-strict ordering comparisons, so `Q(1 + 1e-12, "m") <= Q(1, "m")` is `True`; `<` and `>` are strict. Hashing is supported for float magnitudes and uses root units; vector magnitudes are unhashable.

Pickling preserves the dimensionality class for module-global dimensionalities. Dynamically generated dimensionalities round-trip by deriving the dimensionality from the stored unit.

Expand Down Expand Up @@ -443,6 +477,8 @@ Pass the CoolProp fluid name and the fixed points (for example *P, T*) to the co
Not every combination of input values can fix the state: with an invalid state, derived properties evaluate to `nan` and `encomp.fluids` logs a warning, but no exception is raised.
Set `ENCOMP_IGNORE_COOLPROP_WARNINGS=true` (the default) to suppress these calculation warnings, or `false` to emit them through Python logging.
An invalid property *name* or output-only state input, on the other hand, raises `ValueError`.
So does an unknown *fluid* name: it is resolved against CoolProp in the constructor (and the answer is cached per name, so repeated construction of the same fluid costs nothing).
The *dimensionality* of each state-input quantity is still validated lazily, at the first property evaluation.

```python
from typing import Any
Expand Down
18 changes: 11 additions & 7 deletions encomp/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from dataclasses import dataclass, field
from typing import Any

from .units import Quantity as Q
from .units import Quantity
from .utypes import Pressure, Temperature

__all__ = ["CONSTANTS", "Constants"]
Expand All @@ -13,22 +13,26 @@
class Constants:
"""Namespace of constants exposed through the :data:`CONSTANTS` singleton."""

R: Q[Any, float] = field(default_factory=lambda: Q(8.31446261815324, "kg*m²/K/mol/s²"))
R: Quantity[Any, float] = field(default_factory=lambda: Quantity(8.31446261815324, "kg*m²/K/mol/s²"))
"""Molar gas constant, exact by the 2019 SI definition."""

SIGMA: Q[Any, float] = field(default_factory=lambda: Q(5.670374419e-8, "W/m**2/K**4"))
SIGMA: Quantity[Any, float] = field(default_factory=lambda: Quantity(5.670374419e-8, "W/m**2/K**4"))
"""Stefan-Boltzmann constant."""

normal_conditions_pressure: Q[Pressure, float] = field(default_factory=lambda: Q(1, "atm"))
normal_conditions_pressure: Quantity[Pressure, float] = field(default_factory=lambda: Quantity(1, "atm"))
"""Normal-condition pressure, 1 atm."""

normal_conditions_temperature: Q[Temperature, float] = field(default_factory=lambda: Q(0, "degC").to("K"))
normal_conditions_temperature: Quantity[Temperature, float] = field(
default_factory=lambda: Quantity(0, "degC").to("K")
)
"""Normal-condition temperature, 0 degC."""

standard_conditions_pressure: Q[Pressure, float] = field(default_factory=lambda: Q(1, "atm"))
standard_conditions_pressure: Quantity[Pressure, float] = field(default_factory=lambda: Quantity(1, "atm"))
"""Standard-condition pressure, 1 atm."""

standard_conditions_temperature: Q[Temperature, float] = field(default_factory=lambda: Q(15, "degC").to("K"))
standard_conditions_temperature: Quantity[Temperature, float] = field(
default_factory=lambda: Quantity(15, "degC").to("K")
)
"""Standard-condition temperature, 15 degC."""


Expand Down
14 changes: 12 additions & 2 deletions encomp/context.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
"""Context managers for process-local filesystem and formatting changes."""
"""Context managers for process-local filesystem and formatting changes.

Two unrelated groups share this module:

* *process utilities* -- :func:`working_dir`, :func:`temp_dir` and :func:`silence_stdout`
temporarily change the working directory or redirect ``stdout``; they have nothing to do
with units.
* *quantity formatting* -- :func:`quantity_format` scopes the process-wide default format
used when rendering quantities and units.
"""

import os
import sys
Expand Down Expand Up @@ -84,7 +93,8 @@ def quantity_format(fmt: str = "compact") -> Generator[None]:
----------
fmt : str
Unit format string: one of ``'~P', '~L', '~H', '~Lx'``.
Also accepts aliases: ``'compact': '~P'`` and ``'siunitx': '~Lx'``.
Also accepts the aliases ``'compact': '~P'``, ``'normal': '~P'``
and ``'siunitx': '~Lx'``.
"""

default = UNIT_REGISTRY.formatter.default_format or "~P"
Expand Down
4 changes: 4 additions & 0 deletions encomp/coolprop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,10 @@ def self_check() -> bool:
"""True if the plugin loads and evaluates one known value correctly (cached)."""
ok = _self_check_cached()
if not ok:
# DELIBERATELY warnings.warn, not the module logger the rest of encomp uses: this
# is an install-time diagnostic (a broken/mismatched native plugin), aimed at the
# developer running the check rather than at an application's log stream, and it
# is what the cibuildwheel test-command surfaces. Settled -- do not "unify".
warnings.warn(
"encomp.coolprop plugin self_check failed; the cached failed result will be reused in this process",
RuntimeWarning,
Expand Down
Loading
Loading