Examples
Many examples can be also found in the root of this page.
Unit Conversion¤
When exact=True
is used, the exact conversion factor is returned.
import isqx
import isqx.usc as usc
tsfc_converter = isqx.convert(
usc.LB / (isqx.HOUR * usc.LBF), isqx.KG / (isqx.S * isqx.N), exact=True
)
print(f"1 lb/(h·lbf) = {tsfc_converter.scale} g/(s·kN)")
1 lb/(h·lbf) = 50/1765197 g/(s·kN)
Disambiguation¤
isqx
defines many quantity kinds that are generic over
different unit systems (MKS, imperial...). Calling the quantity kind with a
particular unit will produce a tagged expression:
print(isqx.SPECIFIC_HEAT_CAPACITY(usc.BTU_IT * usc.LB**-1 * usc.R**-1))
(btu_it · pound⁻¹ · rankine⁻¹)['specific_heat_capacity']
- btu_it = 1055.05585262 · joule
- joule = newton · meter
- newton = kilogram · meter · second⁻²
- pound = 0.45359237 · kilogram
- rankine = 5/9 · kelvin
Both quantity kinds and tagged expressions can be indexed.
B = A["some_tag"]
can roughly be interpreted as B
is a subclass of A
.
However, because tags are designed to avoid strict inheritance hierarchies.
Tagged expressions are incompatible with another and will raise an error:
import isqx.aerospace as aero
try:
isqx.convert(
aero.PRESSURE_ALTITUDE(isqx.M), aero.GEOMETRIC_ALTITUDE(isqx.M)
)
except isqx.DimensionMismatchError as e:
print(e)
cannot convert from `meter['altitude', relative to `'mean_sea_level'`, 'pressure']` to `meter['altitude', relative to `'mean_sea_level'`, 'geometric']`.
= help: expected compatible dimensions, but found:
dimension of origin: `L['altitude', relative to `'mean_sea_level'`, 'pressure']`
dimension of target: `L['altitude', relative to `'mean_sea_level'`, 'geometric']`
Introspection¤
Annotations can be inspected at runtime. One example is to dynamically generate docstrings from the annotations:
import typing as t
from dataclasses import dataclass
from typing_extensions import ParamSpec
T = t.TypeVar("T")
P = ParamSpec("P")
@t.overload
def sphinx_doc(
*, formatter: t.Union[isqx.Formatter, str]
) -> t.Callable[[T], T]: ...
@t.overload
def sphinx_doc(obj: T, /) -> T: ...
def sphinx_doc(
obj: t.Optional[T] = None,
/,
*,
formatter: t.Union[isqx.Formatter, str] = isqx.BasicFormatter(
verbose=False
),
):
def wrapper(obj_: T) -> T:
doc = obj_.__doc__ or ""
if doc.startswith(obj_.__qualname__ + "("):
doc = ""
hints = t.get_type_hints(obj_, include_extras=True)
params_doc = []
for name, hint in hints.items():
if t.get_origin(hint) is not t.Annotated:
continue
# assuming the first metadata item is the isqx unit expression
unit_expr = t.get_args(hint)[1]
unit_str = isqx.fmt(unit_expr, formatter=formatter)
directive = ":returns:" if name == "return" else f":param {name}:"
params_doc.append(f"{directive} `{unit_str}`")
if doc and params_doc:
doc += "\n\n" + "\n".join(params_doc)
elif params_doc:
doc = "\n".join(params_doc)
obj_.__doc__ = doc.strip()
return obj_
if obj is None:
return wrapper
return wrapper(obj)
We can apply this decorator to an annotated function to automatically document the units of its parameters:
_T = t.TypeVar("_T")
MPerSCruiseTas = t.Annotated[_T, aero.TAS["cruise"](isqx.M_PERS)]
LOverD = t.Annotated[_T, isqx.ratio(isqx.LIFT(isqx.N), isqx.DRAG(isqx.N))]
SIsp = t.Annotated[_T, isqx.TIME["specific_impulse"](isqx.S)]
WiOverWf = t.Annotated[
_T, isqx.ratio(aero.TAKEOFF_MASS(isqx.KG), aero.LANDING_MASS(isqx.KG))
]
Range = t.Annotated[_T, isqx.DISTANCE(isqx.M)]
@sphinx_doc
def breguet_range(
v: MPerSCruiseTas,
l_over_d: LOverD,
isp: SIsp,
wi_over_wf: WiOverWf,
*,
xp, # library that implements the array api
) -> Range:
"""Calculates the Breguet range of an aircraft."""
return v * l_over_d * isp * xp.log(wi_over_wf)
print(breguet_range.__doc__)
Calculates the Breguet range of an aircraft.
:param v: `(meter · second⁻¹)['airspeed', 'true', 'cruise']`
:param l_over_d: `ratio[`newton[_Tensor(rank=1), 'lift']` to `newton[_Tensor(rank=1), 'drag']`]`
:param isp: `second['specific_impulse']`
:param wi_over_wf: `ratio[`kilogram['aircraft', 'takeoff']` to `kilogram['aircraft', 'landing']`]`
:returns: `meter['distance']`
@sphinx_doc
@dataclass
class AircraftParams:
"""Aircraft performance parameters."""
l_over_d: LOverD
specific_impulse: SIsp
print(AircraftParams.__doc__)
Aircraft performance parameters.
:param l_over_d: `ratio[`newton[_Tensor(rank=1), 'lift']` to `newton[_Tensor(rank=1), 'drag']`]`
:param specific_impulse: `second['specific_impulse']`