Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
4988390
chore: pin ruff and choose the lint rules explicitly
Legend101Zz Oct 7, 2026
aa8220d
build: require Python 3.12+ and NumPy 2.2+, same as Brian2
Legend101Zz Oct 7, 2026
d956a85
docs: describe QuantSI in the README
Legend101Zz Oct 7, 2026
e47ceb1
test: make the pytest settings stricter
Legend101Zz Oct 7, 2026
0d38c9a
test: snapshot the public API so names can't go missing
Legend101Zz Oct 7, 2026
75858a3
test: make sure pickles from the current version keep loading
Legend101Zz Oct 7, 2026
8b2b85a
test: compare all display output against a golden file
Legend101Zz Oct 7, 2026
4c83ee9
test: add benchmarks for the common operations
Legend101Zz Oct 7, 2026
1f538f3
test: fail CI if QuantSI gets much slower relative to NumPy
Legend101Zz Oct 7, 2026
d7010e8
refactor: move Dimension into its own module
Legend101Zz Oct 7, 2026
2cf6ede
refactor: move DimensionMismatchError and the dimension checks
Legend101Zz Oct 7, 2026
f5124ae
refactor: move the unit registries into their own module
Legend101Zz Oct 7, 2026
4809c66
refactor: move Quantity into its own module
Legend101Zz Oct 7, 2026
7c0142f
refactor: move Unit into its own module
Legend101Zz Oct 7, 2026
da99e43
refactor: move check_units into its own module
Legend101Zz Oct 7, 2026
e071a99
docs: explain how the modules are organised
Legend101Zz Oct 7, 2026
23cb98f
test: check the import order between the private modules
Legend101Zz Oct 7, 2026
d7106c0
perf: stop importing SymPy
Legend101Zz Oct 7, 2026
0980427
perf: make Dimension equality much faster
Legend101Zz Oct 7, 2026
8c3bbe6
perf: cache the results of Dimension arithmetic
Legend101Zz Oct 7, 2026
735a6b8
perf: skip re-validating ufunc results
Legend101Zz Oct 7, 2026
ac01a21
perf: work out check_units' checks once per function
Legend101Zz Oct 7, 2026
c7bd411
refactor: look up ufunc rules by ufunc object
Legend101Zz Oct 7, 2026
6f3220f
perf: handle each ufunc rule in its own small function
Legend101Zz Oct 7, 2026
dfcf1b9
perf: don't dispatch twice for a unit times a number
Legend101Zz Oct 7, 2026
f6716cf
fix: keep the dimensions of out= arrays correct
Legend101Zz Oct 7, 2026
0c3badd
fix: give product reductions the right dimensions
Legend101Zz Oct 7, 2026
45ec511
feat: support ufunc methods like accumulate, outer and at
Legend101Zz Oct 7, 2026
d0e9408
feat: give every NumPy ufunc a rule
Legend101Zz Oct 7, 2026
6bec5a4
feat: add __array_function__ to decide how NumPy functions treat quan…
Legend101Zz Oct 7, 2026
24d05e5
fix: return plain indices from argmax, argmin, argsort and argpartition
Legend101Zz Oct 7, 2026
3285b96
feat: join, stack and reshape quantities with checked dimensions
Legend101Zz Oct 7, 2026
76973d5
feat: check dimensions when selecting, writing and comparing sets of …
Legend101Zz Oct 7, 2026
fddce9a
fix: compare and summarise quantities with the right dimensions
Legend101Zz Oct 7, 2026
2dd02e0
feat: give products and linear algebra the right dimensions
Legend101Zz Oct 7, 2026
a5ffea1
fix: require dimensionless input where NumPy's functions need it
Legend101Zz Oct 7, 2026
f770651
feat: warn about NumPy functions QuantSI hasn't reviewed yet
Legend101Zz Oct 7, 2026
1b28cf0
refactor: write quantities as text in one place
Legend101Zz Oct 7, 2026
fb59777
refactor: write scalar quantities without replacing characters
Legend101Zz Oct 7, 2026
3992163
feat: support format specs like f"{q:.2f}"
Legend101Zz Oct 7, 2026
f9b9adb
refactor: move the LaTeX output into the formatting module
Legend101Zz Oct 7, 2026
c54b630
perf: choose the display unit without rebuilding arrays each time
Legend101Zz Oct 7, 2026
16afe65
feat: add parse_quantity to read quantities from text without eval
Legend101Zz Oct 7, 2026
684c871
feat: suggest similar unit names, and add parse_dimensions
Legend101Zz Oct 7, 2026
4cce82e
test: keep tests from changing each other's display units
Legend101Zz Oct 7, 2026
362ba05
feat: read back what repr writes
Legend101Zz Oct 7, 2026
e58db66
test: don't register display units while collecting tests
Legend101Zz Oct 7, 2026
6b3be5b
docs: explain how quantities are written and read as text
Legend101Zz Oct 7, 2026
5277804
build: move the unit generator into QuantSI
Legend101Zz Oct 7, 2026
6a07e7c
feat: generate the short unit names from one rule
Legend101Zz Oct 7, 2026
962467c
feat: make every unit importable from the package root, by one rule
Legend101Zz Oct 7, 2026
f050f3f
feat: add unitsafefunctions from Brian2
Legend101Zz Oct 7, 2026
26ee4de
test: check that QuantSI has every name that brian2.units has
Legend101Zz Oct 7, 2026
f448f94
ci: run Brian2's test suite against QuantSI
Legend101Zz Oct 7, 2026
914d066
feat: add type annotations and ship py.typed
Legend101Zz Oct 7, 2026
4923a41
ci: test the oldest and newest supported Python and NumPy
Legend101Zz Oct 7, 2026
dd77baf
docs: update the README and add a performance page
Legend101Zz Oct 7, 2026
ee1e637
ci: fetch Brian2's full history in the Brian2 workflow
Legend101Zz Oct 7, 2026
eb950a3
test: move the tests out of the package
Legend101Zz Oct 8, 2026
3736c1f
feat: type arithmetic on quantities as Quantity
Legend101Zz Oct 8, 2026
4a043e0
refactor: put the private modules' contents in reading order
Legend101Zz Oct 8, 2026
f3bee16
docs: add a short "in this file" guide to each private module
Legend101Zz Oct 8, 2026
3c72aa3
fix: make != on units the opposite of ==
Legend101Zz Oct 8, 2026
dd8a0dc
refactor: move the private modules into QuantSI/_core
Legend101Zz Oct 8, 2026
cc39b22
test: cover unit-safe helper edge cases
Legend101Zz Oct 23, 2026
438edb3
fix: handle array exponents without recursing
Legend101Zz Oct 23, 2026
2683c8c
test: exercise quantity and unit compatibility paths
Legend101Zz Oct 23, 2026
30ce8f2
test: check parsing and display error paths
Legend101Zz Oct 23, 2026
a3147a9
test: cover NumPy dispatch and reduction edge cases
Legend101Zz Oct 23, 2026
f77e5da
ci: require full line coverage
Legend101Zz Oct 23, 2026
da9e6c4
fix: preserve base-SI numeric formatting
Legend101Zz Oct 23, 2026
93deb86
build: follow SPEC-0 minimum versions
Legend101Zz Oct 23, 2026
00a6828
fix: check dimensions beyond leading zero arrays
Legend101Zz Oct 23, 2026
e3b7ab7
fix: validate output units before joining arrays
Legend101Zz Oct 23, 2026
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
37 changes: 37 additions & 0 deletions .github/workflows/benchmarks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Benchmarks

on: [push, pull_request]

jobs:
benchmarks:
name: Run benchmarks
runs-on: ubuntu-24.04

steps:
- name: Check out the repository
uses: actions/checkout@v7

- name: Set up PDM
uses: pdm-project/setup-pdm@v4
with:
python-version: 3.13

- name: Install dependencies
run: |
pdm install -G test

# Timings on shared CI runners are noisy, so we only save them as an artifact.
# To look into a slowdown, compare the files of two runs.
- name: Run benchmarks
run: |
pdm run pytest tests/benchmarks --benchmark-enable --benchmark-only --benchmark-json=benchmarks.json

- name: Check overhead against plain NumPy
run: |
pdm run pytest -m perf_guard -v

- name: Upload results
uses: actions/upload-artifact@v4
with:
name: benchmarks
path: benchmarks.json
44 changes: 44 additions & 0 deletions .github/workflows/brian2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Brian2

on: [push, pull_request]

jobs:
brian2:
name: Brian2's test suite with this QuantSI
runs-on: ubuntu-24.04

steps:
- name: Check out QuantSI
uses: actions/checkout@v7
with:
path: QuantSI
fetch-depth: 0 # the version number comes from the git history

- name: Check out Brian2
uses: actions/checkout@v7
with:
repository: brian-team/brian2
path: brian2
fetch-depth: 0 # Brian2 also gets its version number from its git tags

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install Brian2 and this QuantSI
run: |
python -m pip install --upgrade pip
python -m pip install ./QuantSI -e "./brian2[test]"

# What the companion change in Brian2 does: brian2.units re-exports QuantSI,
# plus the few changes in Brian2 that QuantSI's fixes need (see the patch).
- name: Make brian2.units use QuantSI
run: |
python QuantSI/tools/brian2/make_facade.py brian2
git -C brian2 apply ../QuantSI/tools/brian2/brian2-companion.patch

- name: Run Brian2's tests (without code generation, and with the numpy target)
working-directory: brian2
run: |
python -c "import brian2, sys; sys.exit(0 if brian2.test('numpy', test_codegen_independent=True) else 1)"
26 changes: 22 additions & 4 deletions .github/workflows/pytest-ubuntu.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@ on: [push, pull_request]

jobs:
test:
name: Run tests
name: Python ${{ matrix.python-version }} with ${{ matrix.numpy }}
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
# The oldest versions QuantSI supports (see pyproject.toml).
- python-version: "3.13"
numpy: "numpy==2.3.*"
# Also test current NumPy on both Python versions.
- python-version: "3.13"
numpy: "numpy"
- python-version: "3.14"
numpy: "numpy"

steps:
- name: Check out the repository
Expand All @@ -14,21 +26,27 @@ jobs:
- name: Set up PDM
uses: pdm-project/setup-pdm@v4
with:
python-version: 3.13
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
pdm install -G test
pdm run python -m ensurepip --upgrade
pdm run python -m pip install "${{ matrix.numpy }}"

# Update output format to enable automatic inline annotations.
- name: Run Ruff
run: pdm run ruff check --output-format=github .

- name: Check types
run: pdm run mypy

- name: Run tests
run: |
pdm run -v pytest --cov=QuantSI --cov-report lcov --doctest-modules
pdm run -v pytest --cov=QuantSI --cov-report=term-missing --cov-report=lcov --cov-fail-under=100

- name: Publish coverage
if: matrix.python-version == '3.13' && matrix.numpy == 'numpy'
uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # v2.3.7
with:
format: lcov
format: lcov
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.11.6
rev: v0.16.10
hooks:
# Run the linter.
- id: ruff
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ version: 2
build:
os: 'ubuntu-22.04'
tools:
python: "3.12"
python: "3.13"

# Build documentation in the "docs/" directory with Sphinx
sphinx:
Expand Down
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,49 @@
[![Tests](https://github.com/brian-team/QuantSI/actions/workflows/pytest-ubuntu.yaml/badge.svg)](https://github.com/brian-team/QuantSI/actions/workflows/pytest-ubuntu.yaml)
[![Coverage Status](https://coveralls.io/repos/github/brian-team/QuantSI/badge.svg?branch=main)](https://coveralls.io/github/brian-team/QuantSI?branch=main)

Work in progress: unit system as used by the Brian2 simulator
**Pragmatic dimensional checking for NumPy-based SI calculations.**

QuantSI attaches a physical dimension to NumPy arrays and checks, operation by
operation, that the physics makes sense. It is the unit system of the
[Brian2](https://briansimulator.org) simulator, packaged so that it can also be
used on its own.

QuantSI requires Python 3.13 or newer and NumPy 2.3 or newer. Minimum supported
versions follow [SPEC-0](https://scientific-python.org/specs/spec-0000/).

```python
>>> import numpy as np
>>> from QuantSI import Mohm, mV, nA, parse_quantity
>>> v = np.array([-70.0, -55.0]) * mV
>>> v
array([-70., -55.]) * mvolt
>>> 2 * nA * 50 * Mohm
100. * mvolt
>>> f"{v.max() / mV:.1f} mV"
'-55.0 mV'
>>> v + 1 * nA
Traceback (most recent call last):
...
DimensionMismatchError: Cannot calculate [-70. -55.] mV add 1. nA, the units do not match (units are V and A).
>>> parse_quantity("-70 * mV")
-70. * mvolt

```

## What QuantSI does

- Stores every value in base SI units, with one shared `Dimension` object per
kind of quantity (so checking "same kind?" is a single identity check).
- Checks arithmetic, comparisons and the supported NumPy functions, and raises
`DimensionMismatchError` when dimensions do not match.
- Displays values in a sensible unit (`3. mV`), parses simple unit expressions
(`"3.5 * mV"`), and provides ~2000 named units generated from one rule.

## What QuantSI deliberately does not do

- No offset units (degrees Celsius/Fahrenheit) and no user-defined base dimensions.
- No unit-conversion contexts, definition files or a general units language.
- Values never remember the unit they were entered in: `3 * mV` *is* `0.003 V`.

If you need any of these, have a look at [pint](https://pint.readthedocs.io) or
[astropy.units](https://docs.astropy.org/en/stable/units/).
77 changes: 77 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import contextlib
import importlib

import pytest


def pytest_collection_modifyitems(config, items):
"""Skip the doctests of objects marked ``_do_not_run_doctests``.

Such objects (e.g. Quantity.fill, the wrappers in unitsafefunctions) reuse a
NumPy docstring, whose examples are about NumPy arrays. Brian2 uses the same
marker.
"""

# (Deselected rather than skipped: their docstrings have no line in our files,
# which pytest needs to report a skip.)
def marked(item):
doctest = getattr(item, "dtest", None)
return doctest is not None and getattr(
_resolve(doctest.name), "_do_not_run_doctests", False
)

deselected = [item for item in items if marked(item)]
if deselected:
items[:] = [item for item in items if not marked(item)]
config.hook.pytest_deselected(items=deselected)


def _resolve(dotted_name):
"""The object called ``dotted_name`` (module path, then attributes), or None."""
parts = dotted_name.split(".")
for split in range(len(parts), 0, -1):
try:
obj = importlib.import_module(".".join(parts[:split]))
except ImportError:
continue
for attribute in parts[split:]:
obj = getattr(obj, attribute, None)
return obj
return None


@contextlib.contextmanager
def preserved_unit_registries():
"""Undo, on exit, every unit registered inside the ``with`` block.

Units are registered globally, either explicitly with register_new_unit or
implicitly whenever units are combined (``siemens / cm**2``), and that changes
how quantities are displayed from then on.
"""
from QuantSI.fundamentalunits import (
additional_unit_register,
standard_unit_register,
user_unit_register,
)

registries = (standard_unit_register, user_unit_register, additional_unit_register)
saved = [
(dict(r.units), {dim: dict(units) for dim, units in r.units_for_dimensions.items()})
for r in registries
]
try:
yield
finally:
for registry, (units, by_dimension) in zip(registries, saved, strict=True):
registry.units.clear()
registry.units.update(units)
registry.units_for_dimensions.clear()
registry.units_for_dimensions.update(by_dimension)
registry._tables.clear()


@pytest.fixture(autouse=True)
def _isolated_unit_registries():
"""No test (or doctest) can change how quantities display in another one."""
with preserved_unit_registries():
yield
8 changes: 0 additions & 8 deletions docs_sphinx/QuantSI.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
QuantSI package
===============

Subpackages
-----------

.. toctree::
:maxdepth: 2

QuantSI.tests

Submodules
----------

Expand Down
18 changes: 0 additions & 18 deletions docs_sphinx/QuantSI.tests.rst

This file was deleted.

7 changes: 0 additions & 7 deletions docs_sphinx/QuantSI.tests.test_units.rst

This file was deleted.

Loading