Getting started

A quick tour of the two core classes: Quantity (typed physical quantities on top of pint) and Fluid (thermodynamic properties via CoolProp).

Quantities

A Quantity combines a magnitude with a unit. The dimensionality is tracked as a type: quantities with incompatible dimensionalities cannot be combined or compared.

[1]:
from encomp.units import Quantity as Q

mf = Q(25, "t/h")
mf
[1]:
25.0 t/h
[2]:
# convert to another unit with .to()
mf.to("kg/s")
[2]:
6.944444444444445 kg/s
[3]:
# arithmetic keeps track of the resulting dimensionality
energy_content = Q(4.5, "MWh/t")

power = (mf * energy_content).to("MW")
power, type(power)
[3]:
(<Quantity(112.5, 'megawatt')>, encomp.units.Quantity[Power, float])
[4]:
from encomp.utypes import MassFlow, Power

# check dimensionalities at runtime
power.check(Power), power.check(MassFlow)
[4]:
(True, False)

Incompatible operations raise a DimensionalityError (from pint.errors, so a single except clause catches everything unit-related):

[5]:
from pint.errors import DimensionalityError

try:
    mf + power
except DimensionalityError as e:
    print(e)
Quantities with different dimensionalities are not compatible: <class 'encomp.units.Quantity[MassFlow, float]'> and <class 'encomp.units.Quantity[Power, float]'>.

Vector magnitudes

The magnitude can also be a 1-D numpy array (or a polars Series/expression) instead of a single float:

[6]:
import numpy as np

T = Q(np.linspace(25, 95, 8), "degC")
T.to("K")
[6]:
Magnitude
[298.15 308.15 318.15 328.15 338.15 348.15 358.15 368.15]
UnitsK

Temperature vs. temperature difference

Absolute temperatures and temperature differences share the [temperature] dimension but are distinct dimensionality types: subtracting two temperatures yields a TemperatureDifference, and the two cannot be mixed up silently.

[7]:
dT = Q(95, "degC") - Q(25, "degC")
dT, type(dT)
[7]:
(<Quantity(70.0, 'delta_degree_Celsius')>,
 encomp.units.Quantity[TemperatureDifference, float])
[8]:
# a ΔT can be expressed in any multiplicative temperature unit (K, delta_degF, ...)
cp = Q(4.186, "kJ/(kg K)")

duty = (Q(2.0, "kg/s") * cp * dT).to("kW")
duty
[8]:
586.04 kW

Fluid properties

Fluid (and the Water / HumidAir convenience classes) evaluate thermodynamic properties through CoolProp. A fluid is fixed by two state points (three for humid air), passed as quantities in any compatible unit:

[9]:
from encomp.fluids import Fluid, HumidAir, Water

water = Water(P=Q(4, "bar"), T=Q(140, "degC"))
water
[9]:
<Water (Liquid), P=400 kPa, T=140.0 °C, D=926.2 kg/m³, V=0.2 cP>
[10]:
# properties are Quantity objects with the correct dimensionality type
water.D, water.H, water.C
[10]:
(<Quantity(926.1529497875844, 'kilogram / meter ** 3')>,
 <Quantity(589.2252002247815, 'kilojoule / kilogram')>,
 <Quantity(4.2859264113875, 'kilojoule / kilogram / kelvin')>)
[11]:
# saturated steam: fix the state with pressure and vapor quality
steam = Water(P=Q(4, "bar"), Q=Q(1, ""))
steam.T, steam.H
[11]:
(<Quantity(143.61253299838273, 'degree_Celsius')>,
 <Quantity(2738.0566229312813, 'kilojoule / kilogram')>)
[12]:
# any CoolProp fluid name works, with vector inputs too
co2 = Fluid("CO2", P=Q(np.linspace(50, 150, 4), "bar"), T=Q(25, "degC"))
co2.D
[12]:
Magnitude
[131.27476733866456 785.0819246938266 841.3564483986074 876.4729275452014]
Unitskg/m3
[13]:
humid_air = HumidAir(P=Q(1, "atm"), T=Q(25, "degC"), R=Q(0.6, ""))

# W: humidity ratio, Vha: mixture volume per humid air
humid_air.W, humid_air.Vha
[13]:
(<Quantity(0.01194902651563823, 'dimensionless')>,
 <Quantity(0.8503644116822783, 'meter ** 3 / kilogram')>)

Polars integration

Quantities can hold polars expressions, and fluid properties evaluate lazily inside a polars query through a GIL-free plugin – independent properties run in parallel. See the Parallel CoolProp evaluation with Polars section of the docs for details.

[14]:
import polars as pl

df = pl.DataFrame(
    {
        "P_bar": [1.0, 5.0, 10.0, 50.0],
        "T_C": [25.0, 80.0, 160.0, 250.0],
    }
)

# wrap columns as quantities to convert them to the units CoolProp expects
P = Q(pl.col("P_bar"), "bar").to("Pa")
T = Q(pl.col("T_C"), "degC").to("K")

water = Water(P=P, T=T)

df.with_columns(
    rho=water.D.m,
    h=water.H.to("kJ/kg").m,
)
[14]:
shape: (4, 4)
P_barT_Crhoh
f64f64f64f64
1.025.0997.047435104.928068
5.080.0971.981068335.308884
10.0160.0907.678747675.797392
50.0250.0800.0814291085.661995