Skip to main content

engparams

Engineering parameters with units, for studies, simulations and reports.

  • Keep a study's inputs in a readable YAML file, each value in the units it was specified in.
  • Load them into nested Parameters, convert them to SI and hand plain numbers to your calculation.
  • Do unit-safe arithmetic: Parameter(10, "m") / Parameter(2, "s") is 5 m/s, and adding metres to seconds is an error.
  • Print parameter tables for reports as text, Markdown, HTML, CSV or LaTeX, from Python or the command line.

Units are handled by pint, so every unit pint knows works, and parameters interoperate with pint quantities and numpy.

Installation

pip install engparams

# with pydantic support, for validated parameter schemas
pip install "engparams[pydantic]"

# the development version
pip install "engparams @ git+https://github.com/davidson-engineering/engparams.git"

Requires Python 3.11 or newer.

Quick start

Write the parameters in YAML, in whatever units are natural:

# robot.yaml
arm:
  length: [1.2, m]
  mass: 18 kg
  joint_angles: [[0, 120, 240], deg]
motor:
  max_speed: 3000 rpm
  torque:
    value: 12
    units: N.m
    description: Continuous torque
payload: [5000, g]
gear_ratio: 50
controller: RC-100

Load them, look them up, and convert them:

>>> from engparams import Parameter, Parameters
>>> params = Parameters.from_yaml("robot.yaml")
>>> params["arm.length"]
Parameter(1.2, 'm')
>>> params.motor.max_speed
Parameter(3000, 'rpm')
>>> params.motor.torque * params.gear_ratio
Parameter(600, 'N.m')
>>> (params.motor.max_speed / params.gear_ratio).to("deg/s")
Parameter(360.0, 'deg/s')
>>> print(params.to_si())
+------------------+----------------------+-------+-------------------+
| Parameter        |                Value | Units | Description       |
+------------------+----------------------+-------+-------------------+
| arm.length       |                  1.2 | m     |                   |
| arm.mass         |                   18 | kg    |                   |
| arm.joint_angles | [0, 2.0944, 4.18879] | rad   |                   |
| motor.max_speed  |              314.159 | rad/s |                   |
| motor.torque     |                   12 | N.m   | Continuous torque |
| payload          |                    5 | kg    |                   |
| gear_ratio       |                   50 | -     |                   |
| controller       |               RC-100 | -     |                   |
+------------------+----------------------+-------+-------------------+

Hand plain SI numbers to a calculation:

>>> si = params.to_si().magnitudes()
>>> si["payload"], si["motor"]["torque"]
(5.0, 12)

Parameter files

Each leaf of a parameter file (or of a dict passed to Parameters) can be written as:

Form Example Result
[value, units] [150, mm] Parameter(150, 'mm')
array and units [[0, 120, 240], deg] Parameter([0, 120, 240], 'deg')
number with units 150 mm Parameter(150, 'mm')
mapping {value: 150, units: mm, description: Stroke} Parameter(150, 'mm', description='Stroke')
number or list of numbers 50 Parameter(50, '-') (dimensionless)
anything else RC-100, "0042", 2024-01-01, true a non-numeric parameter, kept as is

Nested mappings become groups, and a mapping with a value key is a single parameter, so value cannot be used as a name. YAML is read with YAML 1.2 rules: 1e-3 is a number, and on, yes and dates are text.

Units are written as pint understands them, with two conventions of their own: . multiplies (N.m, kg.m^2) and - means dimensionless. A product after / needs parentheses, as in W/(m.K), because W/m.K would mean W.K/m. Mistakes are reported with their location, so a typo in a large file is easy to find:

>>> Parameters({"motor": {"conductivity": [0.6, "W/m.K"]}})
Traceback (most recent call last):
...
engparams.errors.UnitError: motor.conductivity: invalid units 'W/m.K': ambiguous, put the units after '/' in parentheses, as in 'W/(m.K)'
>>> Parameters({"motor": {"torque": [12, "N.mm."]}})
Traceback (most recent call last):
...
engparams.errors.UnitError: motor.torque: invalid units 'N.mm.': unexpected '.'

Parameter

A Parameter is an immutable value with units. It can be a number, a numpy array or, for bookkeeping, a non-numeric value such as a name or flag.

>>> Parameter(10, "m") / Parameter(2, "s")
Parameter(5.0, 'm/s')
>>> Parameter(1, "ft") + Parameter(6, "inch")
Parameter(1.5, 'ft')
>>> Parameter(2, "m") ** 2
Parameter(4, 'm^2')
>>> Parameter(1, "m") + 1
Traceback (most recent call last):
...
pint.errors.DimensionalityError: Cannot convert from 'meter' to 'dimensionless'

Comparisons convert units first, and == allows for floating point error (see Parameter.isclose for control over the tolerance):

>>> Parameter(1, "m") == Parameter(1000, "mm")
True
>>> Parameter(300, "mm") < Parameter(1, "ft")
True
>>> Parameter(0.1, "m") + Parameter(0.2, "m") == Parameter(0.3, "m")
True

Convert to any compatible units with to(), or to SI with to_si(). SI conversion keeps named SI units, so a torque in kN.mm becomes N.m rather than kg.m^2/s^2:

>>> Parameter(1, "mile").to("km")
Parameter(1.609344, 'km')
>>> Parameter(3, "kN.mm").to_si()
Parameter(3.0, 'N.m')
>>> Parameter(0.1, "kg/mm^3").to_si()
Parameter(100000000.0, 'kg/m^3')
>>> Parameter(20, "degC").to_si()
Parameter(293.15, 'K')

Parameters work with numpy functions, and with pint quantities through .quantity:

>>> import numpy as np
>>> np.hypot(Parameter(3, "m"), Parameter(400, "cm"))
Parameter(5.0, 'm')
>>> Parameter([1, 2, 3], "m").to("mm")
Parameter([1000.0, 2000.0, 3000.0], 'mm')
>>> f"{Parameter(3.14159, 'm'):.2f}"
'3.14 m'

Parameters

Parameters is a nested mapping of groups and parameters. Items can be read with a dotted path (params["arm.length"]) or as attributes (params.arm.length), and set from any of the forms a parameter file accepts:

>>> params["arm.reach"] = "0.9 m"
>>> params.arm.reach
Parameter(0.9, 'm')
>>> "arm.reach" in params
True

merge() layers overrides onto a copy, which suits studies built from a baseline:

>>> heavy = params.merge({"arm": {"mass": [25, "kg"]}, "payload": "8 kg"})
>>> heavy.arm.mass, heavy.arm.length, params.arm.mass
(Parameter(25, 'kg'), Parameter(1.2, 'm'), Parameter(18, 'kg'))

stack() turns a group into a vector, converting to the units of its first member:

>>> cog = Parameters({"x": [50, "mm"], "y": [-0.001, "m"], "z": [0, "mm"]})
>>> cog.stack()
Parameter([50.0, -1.0, 0.0], 'mm')

Other methods:

  • flatten() returns a flat dict of parameters keyed by path.
  • to_dict() and to_yaml(path) write data that Parameters(...) and from_yaml() read back.
  • copy() copies the group structure.

Tables

print(params) shows a text table. render() produces other formats, with floats shown to precision significant figures:

>>> print(params.arm.render("markdown", precision=3))
| Parameter    |         Value | Units |
| :----------- | ------------: | :---- |
| length       |           1.2 | m     |
| mass         |            18 | kg    |
| joint_angles | [0, 120, 240] | deg   |
| reach        |           0.9 | m     |

Table formats are text, markdown, csv, html and latex. The yaml and json formats write the parameters losslessly instead. For full control, params.table() returns a PrettyTable. In Jupyter, a Parameters displays as an HTML table.

Validation with pydantic

With the pydantic extra, Parameter and Parameters can be pydantic fields. They accept every parameter file form, and Dimension checks units:

>>> from typing import Annotated
>>> from pydantic import BaseModel, ValidationError
>>> from engparams import Dimension
>>> class Arm(BaseModel):
...     length: Annotated[Parameter, Dimension("[length]")]
...     mass: Annotated[Parameter, Dimension("kg")]
>>> Arm(length=[1.2, "m"], mass="18 kg").length
Parameter(1.2, 'm')
>>> try:
...     Arm(length="1.2 s", mass="18 kg")
... except ValidationError as error:
...     print(error.errors()[0]["msg"])
Value error, expected units compatible with '[length]', got 's'

Dimension also works without pydantic: Dimension("[length]").validate(parameter).

Command line

The engparams command shows a parameter file as a table, optionally in SI units, or converts it to another format:

$ engparams robot.yaml motor --si
+-----------+---------+-------+-------------------+
| Parameter |   Value | Units | Description       |
+-----------+---------+-------+-------------------+
| max_speed | 314.159 | rad/s |                   |
| torque    |      12 | N.m   | Continuous torque |
+-----------+---------+-------+-------------------+

$ engparams robot.yaml --si --format markdown > parameters.md

Formats are text, markdown, csv, html, latex, yaml and json.

Extending

  • Units: engparams.define("smoot = 1.7018 * m") adds a unit using pint's syntax. Parameters use pint's application registry (engparams.ureg), shared with any other pint code in the program.
  • SI conversion: non-SI units convert to the first unit in engparams.units.SI_DERIVED_UNITS with the same dimensionality (psi to Pa, kWh to J), otherwise to SI base units. Add entries to prefer others.
  • Unit symbols: engparams.units.SYMBOLS overrides how units are written in results.
  • Calculations: Parameter.quantity gives a pint quantity, and Parameter(quantity) wraps one back up.
  • Schemas: pydantic models, as above.

Migrating from 0.1

Version 0.2 renames the package from parameter to engparams (on PyPI, parameter is an unrelated project) and rebuilds it on pint. The 0.1 unit handling had errors that silently gave wrong results: comparisons were almost always true, compound units such as kg/mm^3 converted by the wrong factor, and products and quotients kept the units of their first operand.

0.1 0.2
from parameter.parameter import ... from engparams import ...
param.si_units param.to_si()
read_parameters_from_yaml(path) Parameters.from_yaml(path)
dict_to_parameters(d) Parameters(d)
params.table_pretty params.table()
params.values_only params.to_si().magnitudes()
flat keys such as end_affector_cog__x nested groups: params["end_affector_cog.x"]
group_by_prefix(), get_common_value() params.end_affector_cog.stack()
dataclasses inheriting from Parameters pydantic models, or Parameters(dataclasses.asdict(obj)) for tables
param.value = ... parameters are immutable, so assign a new one

Behaviour changes to be aware of:

  • Adding or comparing parameters with incompatible units raises DimensionalityError, as does adding a plain number to a parameter with units.
  • float(parameter) needs a dimensionless parameter. Use parameter.to("m").value for a number in chosen units.
  • str(Parameter(1, "m")) is "1 m", with a space.

Development

uv sync
uv run pytest
uv run ruff check && uv run ruff format --check && uv run mypy

The examples in this README are run as part of the test suite.

To release, set __version__ in src/engparams/__init__.py, merge to main, and publish a GitHub release tagged with that version (for example v0.2.0). The Release workflow tests, builds and publishes it to PyPI using trusted publishing.

Metadata

Release files for engparams 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for engparams 0.2.1
File Size Uploaded
engparams-0.2.1.tar.gz 39.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for engparams 0.2.1
File Interpreter ABI Platform
engparams-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 64.7 kB

Release files / engparams-0.2.1.tar.gz

Download URL engparams-0.2.1.tar.gz
Size 39.7 kB
Tags Source
SHA-256 checksum
How to use checksums
1eaf9ceaf345a27cd3a7331c9c7b243ddb06919f76cef9b81a7b218ebbfd3899
BLAKE2b-256 checksum
How to use checksums
9fa0d564cc2a457bf1a656059a65a02cbbaf5d18b4ce214be197c21529523d6d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release files / engparams-0.2.1-py3-none-any.whl

Download URL engparams-0.2.1-py3-none-any.whl
Size 25.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
403d426846802d5735d7b3c15fdd535dcda949cb369f3a2ce8244a6691c3985f
BLAKE2b-256 checksum
How to use checksums
8072ea21fe596e141703baa745c94955934f8996053e8c149f54a50c1ce23138
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page