Skip to content
Open
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
22 changes: 22 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,25 @@ jobs:
- name: Check type information
run: |
mypy quantities

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

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

- name: Install Ruff
run: |
python -m pip install -U pip
python -m pip install -U ruff

- name: Run Ruff
# Advisory: report style issues but do not fail the build while the
# existing codebase is being brought into full Ruff conformance.
# Drop --exit-zero once the codebase is clean.
run: |
ruff check --exit-zero quantities
27 changes: 27 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# GitLab CI configuration for the EBRAINS GitLab mirror.
#
# The canonical CI is GitHub Actions (see .github/workflows/). This pipeline
# runs on the EBRAINS GitLab mirror only, and exists primarily to compute and
# publish a code-coverage badge. The badge URL is derived from this project's
# slug on gitlab.ebrains.eu:
#
# https://gitlab.ebrains.eu/<group>/<project>/badges/master/coverage.svg

stages:
- test

coverage:
stage: test
image: python:3.13
before_script:
- python -m pip install --upgrade pip
- pip install -e ".[test]"
script:
- pytest --cov=quantities --cov-report=term --cov-report=xml
coverage: '/^TOTAL.*\s+(\d+(?:\.\d+)?)%/'
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
42 changes: 42 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Contributing to quantities

Contributions to *quantities*, whether bug reports, bug fixes, new units or constants, documentation improvements, or new features, are welcome.

## Reporting bugs and requesting features

Please use the [GitHub issue
tracker](https://github.com/python-quantities/python-quantities/issues).

For bug reports, please include:

- the version of *quantities* and of NumPy you are using,
- your Python version and operating system,
- a minimal example that reproduces the problem,
- the actual output and the output you expected.

## Submitting changes

Development happens on GitHub via pull requests against the `master` branch.
A short walkthrough — from cloning the repository to opening your first PR —
is in the [Onboarding guide][onboarding] in the developer documentation,
and the [Development workflow][workflow] page covers branching, review,
versioning and the deprecation policy.

Before opening a pull request:

1. Make sure the test suite passes locally (`pytest`).
2. Add a test for any bug fix or new feature.
3. Add an entry to `CHANGES.txt` describing the change.

[onboarding]: https://python-quantities.readthedocs.io/en/latest/devel/onboarding.html
[workflow]: https://python-quantities.readthedocs.io/en/latest/devel/workflow.html

## Licensing of contributions

*quantities* is distributed under the BSD 3-Clause License (see
[`doc/user/license.rst`](doc/user/license.rst)).
By submitting a contribution (for example by opening
a pull request) you agree that your contribution is licensed under the same
license and that you have the right to make it under those terms.

No separate Contributor License Agreement (CLA) is required.
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
see doc/user/license.rst
11 changes: 10 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ supported. Quantities is actively developed, and while the current features
and API are stable, test coverage is incomplete so the package is not
suggested for mission-critical applications.

|pypi version|_ |Build status|_
|pypi version|_ |Build status|_ |Coverage Status|_

.. |pypi version| image:: https://img.shields.io/pypi/v/quantities.png
.. _`pypi version`: https://pypi.python.org/pypi/quantities
.. |Build status| image:: https://github.com/python-quantities/python-quantities/actions/workflows/test.yml/badge.svg?branch=master
.. _`Build status`: https://github.com/python-quantities/python-quantities/actions/workflows/test.yml
.. |Coverage Status| image:: https://gitlab.ebrains.eu/NeuralEnsemble/python-quantities/badges/master/coverage.svg
.. _`Coverage Status`: https://gitlab.ebrains.eu/NeuralEnsemble/python-quantities/-/pipelines
.. _tutorial: http://python-quantities.readthedocs.io/en/latest/user/tutorial.html


Expand Down Expand Up @@ -77,6 +79,13 @@ And run::
in the current directory. The master branch is automatically tested by
GitHub Actions.

Contributing
------------
Bug reports, fixes, and pull requests are welcome. Please see
`CONTRIBUTING.md <CONTRIBUTING.md>`_ for a quick overview, and the
`developer documentation <https://python-quantities.readthedocs.io/en/latest/devel/index.html>`_
for the onboarding guide, architecture overview, and development workflow.

Author
------
quantities was originally written by Darren Dale, and has received contributions from `many people`_.
Expand Down
140 changes: 140 additions & 0 deletions doc/devel/architecture.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
.. _architecture-overview:

*********************
Architecture Overview
*********************

This page gives a high-level view of how *quantities* is put together
internally, intended for new contributors who want to understand the design
before making non-trivial changes.

For the user-facing API, see the :doc:`user guide </user/index>`.


Design rationale
================

*quantities* is a NumPy extension library. Rather than implement its own
array machinery, it subclasses :class:`numpy.ndarray` so that
:class:`Quantity` instances behave like regular arrays — they participate
in vectorised arithmetic, broadcasting, ufuncs and indexing — while also
carrying dimensional (unit) information that is validated and propagated
through every operation.

The design has three pillars:

1. **Dimensionality** is data, not a string. Units multiply, divide and
raise to integer powers; *quantities* stores those exponents in a
mapping and uses it both for arithmetic and for display.
2. **Conversions** are pure scalar multiplications. The unit registry
produces conversion factors that are simply numbers; the dimensional
bookkeeping lives in the :class:`Dimensionality` mapping.
3. **Interoperability with NumPy is paramount.** *quantities* hooks into
the ufunc machinery so that ``np.sin``, ``np.sqrt``, ``np.add`` and so
on work as expected on :class:`Quantity` arrays.


Class hierarchy
===============

The core type hierarchy is::

numpy.ndarray
Quantity # quantity.py
UnitQuantity # unitquantity.py
UnitLength # quantities/units/length.py
UnitMass # quantities/units/mass.py
UnitTime # quantities/units/time.py
... # one subclass per SI base dimension
CompoundUnit # preserves compound expressions
UncertainQuantity # uncertainquantity.py

:class:`Quantity`
Array values plus a ``_dimensionality`` attribute. This is what users
create with expressions such as ``42 * pq.metre``.

:class:`UnitQuantity`
A scalar :class:`Quantity` with magnitude 1 that represents a named
unit (``pq.metre``, ``pq.second``, …). Every :class:`UnitQuantity`
self-registers in the global ``unit_registry`` on construction.

:class:`UnitLength`, :class:`UnitMass`, …
SI-base-dimension subclasses of :class:`UnitQuantity`. They behave the
same as :class:`UnitQuantity` but their type marks the base dimension
they represent.

:class:`CompoundUnit`
A :class:`UnitQuantity` that preserves the compound expression
(e.g. ``m**2/m**3``) instead of simplifying it. Useful for display
when the user wants the original notation kept.

:class:`UncertainQuantity`
A :class:`Quantity` subclass that carries a ``_uncertainty`` attribute
(itself a :class:`Quantity`) and propagates uncertainty through
arithmetic.


Dimensionality
==============

:class:`Dimensionality` (``dimensionality.py``) is a :class:`dict`
subclass that maps :class:`UnitQuantity` objects to integer exponents.
For example, the dimensionality of a velocity is::

{metre: 1, second: -1}

The class supports several text representations — ``.string``, ``.unicode``,
``.latex`` and ``.html`` — produced by ``markup.py``. Dimension checking
and conversion-factor computation happen in ``quantity.py``
(``get_conversion_factor``, ``validate_dimensionality``).


The unit registry
=================

:class:`UnitRegistry` (``registry.py``) is a singleton that maps string
names and symbols to :class:`UnitQuantity` objects. When the user writes::

q.units = "kg*m/s**2"

the string is parsed by the registry. To avoid arbitrary code execution,
the parser uses Python's :mod:`ast` module and only allows a restricted
set of node types (names, numeric literals, binary arithmetic, power).
This is safer than ``eval`` and is verified by the test suite.

:class:`UnitQuantity` subclasses register themselves in this registry on
construction via their ``__init__``, so importing a module from
``quantities/units/`` is enough to make all the units it defines
available.


Math support
============

``umath.py`` wraps NumPy ufuncs and reductions (trigonometric functions,
``cumsum``, ``gradient``, ``trapz``, …) so they handle dimensional
analysis correctly. For example, ``pq.sin`` checks that the input has
units of angle, whereas plain ``np.sin`` silently extracts the magnitude.

The ``known issues`` section of the user guide documents which NumPy
operations are not yet dimension-aware.


The ``PREFERRED`` list
======================

``quantity.py`` defines a module-level list called ``PREFERRED`` which
downstream packages can mutate to express their preferred units for
display and simplification. This is the main extension point used by
domain-specific consumers of *quantities*.


Units and constants data
========================

* ``quantities/units/`` — one module per physical dimension. Each module
defines :class:`UnitQuantity` instances which self-register.
* ``quantities/constants/`` — physical constants as :class:`UnitConstant`
objects (a subclass of :class:`UncertainQuantity`). The values come
from ``NIST_codata.txt``; ``_codata.py`` is auto-generated from that
file by ``python setup.py data``.
9 changes: 7 additions & 2 deletions doc/devel/devnotes.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
.. _development:

Development
===========

Quantities development uses the principles of test-driven development. New
features or bug fixes need to be accompanied by unit tests based on Python's
unittest package. Unit tests can be run with the following::
features or bug fixes need to be accompanied by unit tests, which can be run
with::

pytest

For a full walkthrough of setting up an environment, the project layout, and
submitting your first contribution, see :ref:`onboarding`.
8 changes: 6 additions & 2 deletions doc/devel/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Developer's Guide
.. toctree::
:maxdepth: 2

documenting.rst
devnotes.rst
onboarding
architecture
workflow
documenting
devnotes
release
maintenance
79 changes: 79 additions & 0 deletions doc/devel/maintenance.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
.. _maintenance:

***********
Maintenance
***********

This page collects the routine maintenance tasks that come up between
releases. For the release procedure itself see :ref:`release-process`,
and for the general workflow see :ref:`development-workflow`.


Updating dependencies
=====================

*quantities* has a single runtime dependency (NumPy) and a handful of
optional ones (``pytest`` for tests, ``sphinx`` for docs, ``ruff`` for
linting, ``scipy`` as an optional backend for numerical integration).

When NumPy releases a new minor version:

1. Add the new version to the ``numpy-version`` matrix in
``.github/workflows/test.yml`` (both the ``test`` and ``type-check``
jobs). Update the ``exclude`` list to reflect supported
Python × NumPy combinations as needed.
2. Run the test suite locally against the new NumPy::

pip install "numpy==X.Y.*"
pytest

3. Fix any regressions and record the new supported version in
``CHANGES.txt``.

When dropping support for an old Python or NumPy version, also bump
``requires-python`` and the ``numpy`` lower bound in ``pyproject.toml``
and update the ``target-version`` of the ``[tool.ruff]`` config.


Rebuilding the documentation locally
====================================

The published documentation is rebuilt automatically by Read the Docs on
every push to ``master``. To rebuild locally during development::

pip install -e ".[doc]"
cd doc
make html

The HTML is written to ``doc/_build/html/``. Open ``index.html`` in your
browser to preview.

The Read the Docs build is configured by ``.readthedocs.yaml`` (Python
version, build OS, requirements file) and ``doc/conf.py`` (Sphinx
extensions, theme, project metadata).


Coordinating with downstream packagers
======================================

*quantities* is packaged in several downstream channels:

* `PyPI <https://pypi.org/project/quantities/>`__ — the primary
distribution, updated by the maintainer at release time (see
:ref:`release-process`).
* `conda-forge <https://github.com/conda-forge/quantities-feedstock>`__ —
the conda-forge bot opens a PR on the feedstock automatically when a
new PyPI release appears.
* `Spack <https://packages.spack.io/package.html?name=py-quantities>`__
— the recipe lives in the
`spack-packages <https://github.com/spack/spack-packages>`__ repository
at ``repos/spack_repo/builtin/packages/py_quantities/package.py``.
Updates are made by opening a PR against ``spack/spack-packages`` after
a release.

For other downstream packagers we rely on the packagers' own
update workflows. `Repology
<https://repology.org/project/python:quantities/versions>`__ shows a
useful cross-distribution view of which version each packager is
currently shipping; please open an issue if you find a downstream
package that has fallen significantly behind.
Loading
Loading