diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc7f136..9d512f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,143 +22,3 @@ jobs: - uses: actions-rs/cargo@v1 with: command: publish - - linux: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.11 - - name: Build Wheels - uses: PyO3/maturin-action@v1 - with: - manylinux: auto - command: build - args: --release --out dist -m si-units/Cargo.toml - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheel-linux-x86_64 - path: dist - - macos-x86_64: - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.11 - architecture: x64 - - name: Build wheels - x86_64 - uses: PyO3/maturin-action@v1 - with: - target: x86_64 - args: --release --out dist -m si-units/Cargo.toml - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheel-macos-x86_64 - path: dist - - macos-aarch64: - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.11 - architecture: arm64 - - name: Build wheels - aarch64 - uses: PyO3/maturin-action@v1 - with: - target: aarch64 - args: --release --out dist -m si-units/Cargo.toml - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheel-aarch64-apple-darwin - path: dist - - windows: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.11 - architecture: x64 - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: x64 - args: --release --out dist -m si-units/Cargo.toml - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheel-windows-x64 - path: dist - - deploy-pypi: - name: Publish wheels to PyPI and TestPyPI - runs-on: ubuntu-latest - needs: [linux, windows, macos-x86_64, macos-aarch64] - steps: - - uses: actions/download-artifact@v4 - with: - pattern: wheel-* - path: wheels - merge-multiple: true - - uses: actions/setup-python@v5 - with: - python-version: 3.11 - - name: Publish to PyPi - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - pip install --upgrade twine - twine upload --skip-existing wheels/* - - build-documentation: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup python - uses: actions/setup-python@v5 - with: - python-version: 3.11 - - name: Install python dependencies - run: | - pip install sphinx sphinx-rtd-theme numpydoc - - name: Build Wheels - uses: PyO3/maturin-action@v1 - with: - manylinux: auto - command: build - args: --release --out dist -m si-units/Cargo.toml - - name: Install module - run: | - pip install si-units --no-index --find-links dist --force-reinstall - - name: Build documentation - run: sphinx-build si-units/docs/ public/ -b html - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: documentation - path: public - - release-documentation: - needs: [build-documentation] - runs-on: ubuntu-latest - steps: - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - name: documentation - path: public - - name: Deploy documentation to gh-pages - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./public diff --git a/.github/workflows/release_docs.yml b/.github/workflows/release_docs.yml new file mode 100644 index 0000000..c575207 --- /dev/null +++ b/.github/workflows/release_docs.yml @@ -0,0 +1,49 @@ +name: Release Documentation + +on: + push: + branches: [master] + +jobs: + build-documentation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: 3.11 + - name: Install python dependencies + run: | + pip install mkdocs-material mkdocstrings-python + - name: Build Wheels + uses: PyO3/maturin-action@v1 + with: + manylinux: auto + command: build + args: --release --out dist -m si-units/Cargo.toml + - name: Install module + run: | + pip install si-units --no-index --find-links dist --force-reinstall + - name: Build documentation + run: mkdocs build -f si-units/mkdocs.yml -d ../public + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: documentation + path: public + + release-documentation: + needs: [build-documentation] + runs-on: ubuntu-latest + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: documentation + path: public + - name: Deploy documentation to gh-pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./public diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml new file mode 100644 index 0000000..1262363 --- /dev/null +++ b/.github/workflows/release_pypi.yml @@ -0,0 +1,90 @@ +name: Release PyPI + +on: + push: + tags: ["s-units-v*"] + +jobs: + linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.11 + - name: Build Wheels + uses: PyO3/maturin-action@v1 + with: + manylinux: auto + command: build + args: --release --out dist -m si-units/Cargo.toml + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheel-linux-x86_64 + path: dist + + macos: + strategy: + matrix: + target: [ {arch: x64, target: x86_64}, {arch: arm64, target: aarch64} ] + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.11 + architecture: ${{ matrix.target.arch }} + - name: Build wheels - ${{ matrix.target.target }} + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target.target }} + args: --release --out dist -m si-units/Cargo.toml + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheel-macos-${{ matrix.target.target }} + path: dist + + windows: + runs-on: windows-latest + strategy: + matrix: + target: [x64, x86] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.11 + architecture: ${{ matrix.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist -m si-units/Cargo.toml + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheel-windows-${{ matrix.target }} + path: dist + + deploy-pypi: + name: Publish wheels to PyPI and TestPyPI + runs-on: ubuntu-latest + needs: [linux, windows, macos] + steps: + - uses: actions/download-artifact@v4 + with: + pattern: wheel-* + path: wheels + merge-multiple: true + - uses: actions/setup-python@v5 + with: + python-version: 3.11 + - name: Publish to PyPi + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + pip install --upgrade twine + twine upload --skip-existing wheels/* diff --git a/.github/workflows/test_documentation.yml b/.github/workflows/test_documentation.yml deleted file mode 100644 index 7c63b76..0000000 --- a/.github/workflows/test_documentation.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Test Documentation - -on: - push: - branches: [master] - pull_request: - branches: [master] - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup python - uses: actions/setup-python@v5 - with: - python-version: 3.11 - - name: Install python dependencies - run: | - pip install sphinx sphinx-rtd-theme numpydoc numpy torch - - name: Build wheels - uses: messense/maturin-action@v1 - with: - manylinux: auto - command: build - args: --release --out dist -m si-units/Cargo.toml - - name: Install module - run: | - pip install si-units --no-index --find-links dist --force-reinstall - - name: Run doctests - run: sphinx-build si-units/docs/ public/ -b doctest diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index 89a94aa..18be9ca 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 @@ -30,6 +30,7 @@ jobs: maturin build --release --out dist pip install extend_quantity --no-index --find-links dist --force-reinstall - name: Build wheels for si-units + working-directory: ./si-units run: | maturin build --release --out dist pip install si-units --no-index --find-links dist --force-reinstall diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2971cf5..136fe6a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -23,57 +23,45 @@ jobs: with: name: wheel-linux-x86_64 path: dist - macos-x86_64: + macos: + strategy: + matrix: + target: [ {arch: x64, target: x86_64}, {arch: arm64, target: aarch64} ] runs-on: macos-14 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.11 - architecture: x64 - - name: Build wheels - x86_64 + architecture: ${{ matrix.target.arch }} + - name: Build wheels - ${{ matrix.target.target }} uses: PyO3/maturin-action@v1 with: - target: x86_64 + target: ${{ matrix.target.target }} args: --release --out dist -m si-units/Cargo.toml - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: wheel-macos-x86_64 - path: dist - macos-aarch64: - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.11 - architecture: arm64 - - name: Build wheels - aarch64 - uses: PyO3/maturin-action@v1 - with: - target: aarch64 - args: --release --out dist -m si-units/Cargo.toml - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheel-aarch64-apple-darwin + name: wheel-macos-${{ matrix.target.target }} path: dist windows: runs-on: windows-latest + strategy: + matrix: + target: [x64, x86] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.11 - architecture: x64 + architecture: ${{ matrix.target }} - name: Build wheels uses: PyO3/maturin-action@v1 with: - target: x64 + target: ${{ matrix.target }} args: --release --out dist -m si-units/Cargo.toml - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: wheel-windows-x64 + name: wheel-windows-${{ matrix.target }} path: dist diff --git a/.gitignore b/.gitignore index 21e096c..18a4648 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,22 @@ **/target Cargo.lock *.so -si-units/docs/_build -si-units/docs/generated -/venv + +# jupyter notebook .ipynb_checkpoints *.ipynb + __pycache__ -test_readme.py \ No newline at end of file +test_readme.py +*.lock +*.py[oc] +*.egg-info + +# venv +.venv + +# generated +build/ +dist/ +wheels/ +site/ diff --git a/example/extend_quantity/Cargo.toml b/example/extend_quantity/Cargo.toml index 1afd22d..886503e 100644 --- a/example/extend_quantity/Cargo.toml +++ b/example/extend_quantity/Cargo.toml @@ -8,6 +8,6 @@ name = "extend_quantity" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.23.0", features = ["extension-module", "abi3-py39"] } +pyo3 = { version = "0.23", features = ["extension-module", "abi3-py39"] } quantity = { version = "*", path = "../../", features = ["python_numpy"] } ndarray = "0.16" diff --git a/example/extend_quantity/src/lib.rs b/example/extend_quantity/src/lib.rs index 1ab8033..e8e9384 100644 --- a/example/extend_quantity/src/lib.rs +++ b/example/extend_quantity/src/lib.rs @@ -1,42 +1,37 @@ -use ndarray::Array1; -use pyo3::prelude::*; -use quantity::*; +use pyo3::pymodule; -#[pyfunction] -fn bar() -> Pressure { - BAR -} +#[pymodule] +mod extend_quantity { + use ndarray::Array1; + use pyo3::pyfunction; + use quantity::*; -#[pyfunction] -fn ideal_gas(temperature: Temperature, volume: Volume, moles: Moles) -> Pressure { - moles * RGAS * temperature / volume -} + #[pyfunction] + fn bar() -> Pressure { + BAR + } -#[pyfunction] -fn ideal_gas_array( - temperature: Temperature>, - volume: Volume>, - moles: Moles>, -) -> Pressure> { - moles * RGAS * temperature / volume -} + #[pyfunction] + fn ideal_gas(temperature: Temperature, volume: Volume, moles: Moles) -> Pressure { + moles * RGAS * temperature / volume + } -#[pyfunction] -fn law_of_cosines1(a: Length, b: Length, gamma: Angle) -> Length { - (a * a + b * b - 2.0 * a * b * gamma.cos()).sqrt() -} + #[pyfunction] + fn ideal_gas_array( + temperature: Temperature>, + volume: Volume>, + moles: Moles>, + ) -> Pressure> { + moles * RGAS * temperature / volume + } -#[pyfunction] -fn law_of_cosines2(a: Length, b: Length, c: Length) -> Angle { - Angle::acos((a * a + b * b - c * c).convert_into(2.0 * a * b)) -} + #[pyfunction] + fn law_of_cosines1(a: Length, b: Length, gamma: Angle) -> Length { + (a * a + b * b - 2.0 * a * b * gamma.cos()).sqrt() + } -#[pymodule] -fn extend_quantity(m: &Bound) -> PyResult<()> { - m.add_function(wrap_pyfunction!(bar, m)?)?; - m.add_function(wrap_pyfunction!(ideal_gas, m)?)?; - m.add_function(wrap_pyfunction!(ideal_gas_array, m)?)?; - m.add_function(wrap_pyfunction!(law_of_cosines1, m)?)?; - m.add_function(wrap_pyfunction!(law_of_cosines2, m)?)?; - Ok(()) + #[pyfunction] + fn law_of_cosines2(a: Length, b: Length, c: Length) -> Angle { + Angle::acos((a * a + b * b - c * c).convert_into(2.0 * a * b)) + } } diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 69fe497..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,7 +0,0 @@ -[build-system] -requires = ["maturin>=1.0,<2.0"] -build-backend = "maturin" - -[tool.maturin] -bindings = "pyo3" -manifest-path = "si-units/Cargo.toml" diff --git a/si-units/.python-version b/si-units/.python-version new file mode 100644 index 0000000..c8cfe39 --- /dev/null +++ b/si-units/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/si-units/Cargo.toml b/si-units/Cargo.toml index cd8224d..148d3b8 100644 --- a/si-units/Cargo.toml +++ b/si-units/Cargo.toml @@ -1,31 +1,28 @@ -[package] -name = "si-units" -version = "0.10.0" -authors = [ - "Philipp Rehner ", - "Gernot Bauer ", -] -rust-version = "1.81" -edition = "2021" -license = "MIT OR Apache-2.0" -description = "Representation of SI unit valued scalars and arrays." -homepage = "https://github.com/itt-ustutt/quantity/tree/master/si-units" -readme = "README.md" -repository = "https://github.com/itt-ustutt/quantity" -keywords = ["physics", "units", "SI"] -categories = ["data-structures", "science"] -exclude = ["*.ipynb", "/docs"] - -[lib] -name = "si_units" -crate-type = ["cdylib"] - -[dependencies] -ndarray = "0.16" -numpy = "0.23" -thiserror = "2.0" -regex = "1.10" - -[dependencies.pyo3] -version = "0.23" -features = ["extension-module", "abi3", "abi3-py37"] +[package] +name = "si-units" +version = "0.10.0" +authors = [ + "Philipp Rehner ", + "Gernot Bauer ", +] +rust-version = "1.81" +edition = "2021" +license = "MIT OR Apache-2.0" +description = "Representation of SI unit valued scalars and arrays." +homepage = "https://github.com/itt-ustutt/quantity/tree/master/si-units" +readme = "README.md" +repository = "https://github.com/itt-ustutt/quantity" +keywords = ["physics", "units", "SI"] +categories = ["data-structures", "science"] +exclude = ["*.ipynb", "/docs"] + +[lib] +name = "_core" +crate-type = ["cdylib"] + +[dependencies] +ndarray = "0.16" +numpy = "0.23" +pyo3 = { version = "0.23", features = ["extension-module", "abi3-py39"] } +regex = "1.11" +thiserror = "2.0" diff --git a/si-units/README.md b/si-units/README.md index bab3b41..a70bacc 100644 --- a/si-units/README.md +++ b/si-units/README.md @@ -3,7 +3,7 @@ [![documentation](https://img.shields.io/badge/docs-github--pages-blue)](https://itt-ustutt.github.io/quantity/index.html) [![PyPI version](https://badge.fury.io/py/si_units.svg)](https://badge.fury.io/py/si_units) -Representation of quantities with SI units. +Representation of quantities with SI units. The package is written with flexibility in mind and is able to represent arbitrarily complex units. In addition to simple scalar quantities, it can be used to decorate any complex data type (numpy arrays, PyTorch tensors) to provide unit checks. @@ -47,4 +47,4 @@ print(sqms) # [4, 9, 16] m² ## Documentation -For the documentation, see [here](https://itt-ustutt.github.io/quantity/index.html). +For the documentation, see [here](https://itt-ustutt.github.io/quantity/index.html). \ No newline at end of file diff --git a/si-units/docs/Makefile b/si-units/docs/Makefile deleted file mode 100644 index d4bb2cb..0000000 --- a/si-units/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/si-units/docs/api.md b/si-units/docs/api.md new file mode 100644 index 0000000..7a64d7a --- /dev/null +++ b/si-units/docs/api.md @@ -0,0 +1,21 @@ +::: si_units._core.SIObject + options: + show_symbol_type_heading: true + members: + - __init__ + - cbrt + - sqrt + - has_unit + summary: + attributes: false + functions: true + +::: si_units._core.array + options: + show_symbol_type_heading: true +::: si_units._core.linspace + options: + show_symbol_type_heading: true +::: si_units._core.logspace + options: + show_symbol_type_heading: true diff --git a/si-units/docs/api.rst b/si-units/docs/api.rst deleted file mode 100644 index 3238b6c..0000000 --- a/si-units/docs/api.rst +++ /dev/null @@ -1,109 +0,0 @@ -API ---- - -.. currentmodule:: si_units - -.. contents:: Table of Contents - :depth: 3 - -SI Base Units and Associated Constants -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. csv-table:: Seven SI base units and their associated, exact-valued constants. - :header: "Unit", "Unit symbol", "Quantity", "Associated constant", "Associated constant name", "Associated constant value" - :widths: auto - - .. autoclass:: SECOND, :math:`\text{s}`, "time", .. autoclass:: DVCS, "Hyperfine transition frequency of Cs", :math:`\Delta\nu_\text{Cs}=9192631770~\text{Hz}` - .. autoclass:: METER, :math:`\text{m}`, length, .. autoclass:: CLIGHT, Speed of light, :math:`c=299792458~\frac{\text{m}}{\text{s}}` - .. autoclass:: KILOGRAM, :math:`\text{kg}`, mass, .. autoclass:: PLANCK, Planck constant, :math:`h=6.62607015\times 10^{-34}~\text{J}\cdot\text{s}` - .. autoclass:: AMPERE, :math:`\text{A}`, electric current, .. autoclass:: QE, Elementary charge, :math:`e=1.602176634\times 10^{-19}~\text{C}` - .. autoclass:: KELVIN, :math:`\text{K}`, thermodynamic temperature, .. autoclass:: KB, Boltzmann constant, :math:`k_\text{B}=1.380649\times 10^{-23}~\frac{\text{J}}{\text{K}}` - .. autoclass:: MOL, :math:`\text{mol}`, amount of substance, .. autoclass:: NAV, Avogadro constant, :math:`N_\text{A}=6.02214076\times 10^{23}~\text{mol}^{-1}` - .. autoclass:: CANDELA, :math:`\text{cd}`, luminous intensity, .. autoclass:: KCD, Luminous efficacy of :math:`540~\text{THz}` radiation, :math:`K_\text{cd}=683~\frac{\text{lm}}{\text{W}}` - -Derived Units -~~~~~~~~~~~~~ - -.. csv-table:: - :header: "Unit", "Unit symbol", "Quantity", "Definition" - :widths: auto - - .. autoclass:: HERTZ, :math:`\text{Hz}`, frequency, :math:`\text{s}^{-1}` - .. autoclass:: NEWTON, :math:`\text{N}`, force; weight, :math:`\text{kg}\frac{\text{m}}{\text{s}^2}` - .. autoclass:: PASCAL, :math:`\text{Pa}`, pressure; stress, :math:`\frac{\text{N}}{\text{m}^2}` - .. autoclass:: JOULE, :math:`\text{J}`, energy; work; heat, :math:`\text{N}\text{m}` - .. autoclass:: WATT, :math:`\text{W}`, power; radiant flux, :math:`\frac{\text{J}}{\text{s}}` - .. autoclass:: COULOMB, :math:`\text{C}`, electric charge, :math:`\text{A}\text{s}` - .. autoclass:: VOLT, :math:`\text{V}`, electrical potential difference, :math:`\frac{\text{W}}{\text{A}}` - .. autoclass:: FARAD, :math:`\text{F}`, capacitance, :math:`\frac{\text{C}}{\text{V}}` - .. autoclass:: OHM, :math:`\text{Ω}`, resistance; impedance; reactance, :math:`\frac{\text{V}}{\text{A}}` - .. autoclass:: SIEMENS, :math:`\text{S}`, electrical conductance, :math:`\text{Ω}^{-1}` - .. autoclass:: WEBER, :math:`\text{Wb}`, magnetic flux, :math:`\text{V}\text{s}` - .. autoclass:: TESLA, :math:`\text{T}`, magnetic flux density, :math:`\frac{\text{Wb}}{\text{m}^2}` - .. autoclass:: HENRY, :math:`\text{H}`, inductance, :math:`\frac{\text{Wb}}{\text{A}}` - -Additional Units -~~~~~~~~~~~~~~~~ - -For convenience, a number of commonly used units that are not directly combinations of SI base units is also included. -These constants simplify the specification of properties, that are not given in SI units. However, as the representation -of quantities is unique, they do not appear in formatted outputs. - -.. csv-table:: - :header: "Unit", "Unit symbol", "Quantity", "Definition" - :widths: auto - - .. autoclass:: ANGSTROM, :math:`\text{Å}`, length, :math:`10^{-10}~\text{m}` - .. autoclass:: AMU, :math:`\text{u}`, mass, :math:`1.6605390671738466\times 10^{-27}~\text{kg}` - .. autoclass:: AU, :math:`\text{au}`, length, :math:`149597870700~\text{m}` - .. autoclass:: BAR, :math:`\text{bar}`, pressure, :math:`10^5~\text{Pa}` - .. autoclass:: CALORIE, :math:`\text{cal}`, energy, :math:`4.184~\text{J}` - .. autoclass:: DAY, :math:`\text{d}`, time, :math:`86400~\text{s}` - .. autoclass:: DEBYE, :math:`\text{De}`, dipole moment, :math:`\sqrt{10^{-19}~\text{JÅ}^3}` - .. autoclass:: DEGREES, :math:`^\circ`, angle, :math:`\frac{\pi}{180}~\text{rad}` - .. autoclass:: GRAM, :math:`\text{g}`, mass, :math:`10^{-3}~\text{kg}` - .. autoclass:: HOUR, :math:`\text{h}`, time, :math:`3600~\text{s}` - .. autoclass:: LITER, :math:`\text{l}`, volume, :math:`10^{-3}~\text{m}^3` - .. autoclass:: MINUTE, :math:`\text{min}`, time, :math:`60~\text{s}` - .. autoclass:: RADIANS, :math:`\text{rad}`, angle, - -Additional Constants -~~~~~~~~~~~~~~~~~~~~ - -.. csv-table:: - :header: "Constant", "Name", "Symbol", "Value" - :widths: auto - - .. autoclass:: G, Gravitational constant, :math:`G`, :math:`6.6743\times 10^{-11}~\frac{\text{m}^3}{\text{kg}\cdot\text{s}^2}` - .. autoclass:: RGAS, Ideal gas constant, :math:`R=N_\text{Av}k_\text{B}`, :math:`8.31446261815324~\frac{\text{J}}{\text{mol}\cdot\text{K}}` - -Prefixes -~~~~~~~~ - -All units can be combined with the following prefixes: - -.. csv-table:: - :header: "Prefix", "Prefix symbol", "Value", "Prefix", "Prefix symbol", "Value" - :widths: auto - - .. autoclass:: DECI, :math:`\text{d}`, :math:`10^{-1}`, .. autoclass:: DECA, :math:`\text{da}`, :math:`10^{1}` - .. autoclass:: CENTI, :math:`\text{c}`, :math:`10^{-2}`, .. autoclass:: HECTO, :math:`\text{h}`, :math:`10^{2}` - .. autoclass:: MILLI, :math:`\text{m}`, :math:`10^{-3}`, .. autoclass:: KILO, :math:`\text{k}`, :math:`10^{3}` - .. autoclass:: MICRO, :math:`\text{µ}`, :math:`10^{-6}`, .. autoclass:: MEGA, :math:`\text{M}`, :math:`10^{6}` - .. autoclass:: NANO, :math:`\text{n}`, :math:`10^{-9}`, .. autoclass:: GIGA, :math:`\text{G}`, :math:`10^{9}` - .. autoclass:: PICO, :math:`\text{p}`, :math:`10^{-12}`, .. autoclass:: TERA, :math:`\text{T}`, :math:`10^{12}` - .. autoclass:: FEMTO, :math:`\text{f}`, :math:`10^{-15}`, .. autoclass:: PETA, :math:`\text{P}`, :math:`10^{15}` - .. autoclass:: ATTO, :math:`\text{a}`, :math:`10^{-18}`, .. autoclass:: EXA, :math:`\text{E}`, :math:`10^{18}` - .. autoclass:: ZEPTO, :math:`\text{z}`, :math:`10^{-21}`, .. autoclass:: ZETTA, :math:`\text{Z}`, :math:`10^{21}` - .. autoclass:: YOCTO, :math:`\text{y}`, :math:`10^{-24}`, .. autoclass:: YOTTA, :math:`\text{Y}`, :math:`10^{24}` - .. autoclass:: RONTO, :math:`\text{r}`, :math:`10^{-27}`, .. autoclass:: RONNA, :math:`\text{R}`, :math:`10^{27}` - .. autoclass:: QUECTO, :math:`\text{q}`, :math:`10^{-30}`, .. autoclass:: QUETTA, :math:`\text{Q}`, :math:`10^{30}` - -Datatypes -~~~~~~~~~ - -.. autosummary:: - :toctree: generated/ - - PySIObject - SIArray1 diff --git a/si-units/docs/base.md b/si-units/docs/base.md new file mode 100644 index 0000000..006616c --- /dev/null +++ b/si-units/docs/base.md @@ -0,0 +1,26 @@ +# SI Base Units and Associated Constants (AC) + +All capitalized entries in the `Unit` and `AC` (associated constants) columns from the table below can be imported: + + +| Unit | Symbol | Quantity | AC | AC Name | AC value | +| -------- | ------------ | ------------------------- | ------ | ----------------------------------------------- | -------------------------------------------------------------- | +| SECOND | $\text{s}$ | time | DVCS | Hyperfine transition frequency of Cs | $\Delta\nu_\text{Cs}=9192631770~\text{Hz}$ | +| METER | $\text{m}$ | length | CLIGHT | Speed of light | $c=299792458~\frac{\text{m}}{\text{s}}$ | +| KILOGRAM | $\text{kg}$ | mass | PLANCK | Planck constant | $h=6.62607015\times 10^{-34}~\text{J}\cdot\text{s}$ | +| AMPERE | $\text{A}$ | electric current | QE | Elementary charge | $e=1.602176634\times 10^{-19}~\text{C}$ | +| KELVIN | $\text{K}$ | thermodynamic temperature | KB | Boltzmann constant | $k_\text{B}=1.380649\times 10^{-23}~\frac{\text{J}}{\text{K}}$ | +| MOL | $\text{mol}$ | amount of substance | NAV | Avogadro constant | $N_\text{A}=6.02214076\times 10^{23}~\text{mol}^{-1}$ | +| CANDELA | $\text{cd}$ | luminous intensity | KCD | Luminous efficacy of $540~\text{THz}$ radiation | $K_\text{cd}=683~\frac{\text{lm}}{\text{W}}$ | + +## Example + +```py linenums="1" +from si_units import SECOND, DVCS +print(SECOND) +print(DVCS) +``` +``` +1 s +9.19263177 GHz +``` \ No newline at end of file diff --git a/si-units/docs/conf.py b/si-units/docs/conf.py deleted file mode 100644 index 97678d0..0000000 --- a/si-units/docs/conf.py +++ /dev/null @@ -1,65 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import sys -import si_units - -sys.path.append(os.path.abspath(os.path.join(__file__, "../.."))) -sys.path.insert(0, os.path.abspath('.')) - - -# -- Project information ----------------------------------------------------- - -project = 'si_units' -copyright = '2021, Philipp Rehner, Gernot Bauer' -author = 'Philipp Rehner, Gernot Bauer' - -# The full version, including alpha/beta/rc tags -release = si_units.__version__ - - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.doctest', - 'sphinx.ext.mathjax', - 'numpydoc', -] - -autosummary_generate = True -numpydoc_show_class_members = False - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'sphinx_rtd_theme' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] diff --git a/si-units/docs/derived.md b/si-units/docs/derived.md new file mode 100644 index 0000000..94cdf0a --- /dev/null +++ b/si-units/docs/derived.md @@ -0,0 +1,61 @@ +# Derived Units + +| Unit | Unit symbol | Quantity | Definition | +| ------- | ----------- | -------------------------------- | -------------------------------------- | +| HERTZ | $\text{Hz}$ | frequency | $\text{s}^{-1}$ | +| NEWTON | $\text{N}$ | force; weight | $\text{kg}\frac{\text{m}}{\text{s}^2}$ | +| PASCAL | $\text{Pa}$ | pressure; stress | $\frac{\text{N}}{\text{m}^2}$ | +| JOULE | $\text{J}$ | energy; work; heat | $\text{N}\text{m}$ | +| WATT | $\text{W}$ | power; radiant flux | $\frac{\text{J}}{\text{s}}$ | +| COULOMB | $\text{C}$ | electric charge | $\text{A}\text{s}$ | +| VOLT | $\text{V}$ | electrical potential difference | $\frac{\text{W}}{\text{A}}$ | +| FARAD | $\text{F}$ | capacitance | $\frac{\text{C}}{\text{V}}$ | +| OHM | $\text{Ω}$ | resistance; impedance; reactance | $\frac{\text{V}}{\text{A}}$ | +| SIEMENS | $\text{S}$ | electrical conductance | $\text{Ω}^{-1}$ | +| WEBER | $\text{Wb}$ | magnetic flux | $\text{V}\text{s}$ | +| TESLA | $\text{T}$ | magnetic flux density | $\frac{\text{Wb}}{\text{m}^2}$ | +| HENRY | $\text{H}$ | inductance | $\frac{\text{Wb}}{\text{A}}$ | + + +## Additional Units + +For convenience, a number of commonly used units that are not directly combinations of SI base units is also included. +These constants simplify the specification of properties, that are not given in SI units. However, as the representation +of quantities is unique, they do not appear in formatted outputs. + +| Unit | Unit symbol | Quantity | Definition | +| -------- | ------------ | ------------- | --------------------------------------------- | +| ANGSTROM | $\text{Å}$ | length | $10^{-10}~\text{m}$ | +| AMU | $\text{u}$ | mass | $1.6605390671738466\times 10^{-27}~\text{kg}$ | +| AU | $\text{au}$ | length | $149597870700~\text{m}$ | +| BAR | $\text{bar}$ | pressure | $10^5~\text{Pa}$ | +| CALORIE | $\text{cal}$ | energy | $4.184~\text{J}$ | +| DAY | $\text{d}$ | time | $86400~\text{s}$ | +| DEBYE | $\text{De}$ | dipole moment | $\sqrt{10^{-19}~\text{JÅ}^3}$ | +| DEGREES | $^\circ$ | angle | $\frac{\pi}{180}~\text{rad}$ | +| GRAM | $\text{g}$ | mass | $10^{-3}~\text{kg}$ | +| HOUR | $\text{h}$ | time | $3600~\text{s}$ | +| LITER | $\text{l}$ | volume | $10^{-3}~\text{m}^3$ | +| MINUTE | $\text{min}$ | time | $60~\text{s}$ | +| RADIANS | $\text{rad}$ | angle | | + +## Additional Constants + +| Constant | Name | Symbol | Value | +| -------- | ---------------------- | ------------------------- | ------------------------------------------------------------------- | +| G | Gravitational constant | $G$ | $6.6743\times 10^{-11}~\frac{\text{m}^3}{\text{kg}\cdot\text{s}^2}$ | +| RGAS | Ideal gas constant | $R=N_\text{Av}k_\text{B}$ | $8.31446261815324~\frac{\text{J}}{\text{mol}\cdot\text{K}}$ | + +### Example + +```py linenums="1" +from si_units import RGAS, CALORIE +from si_units import KILO # see prefixes +print(RGAS) +KCAL = KILO * CALORIE +print(KCAL) +``` +``` +8.31446261815324 J/mol/K +4.184 kJ +``` diff --git a/si-units/docs/examples.md b/si-units/docs/examples.md new file mode 100644 index 0000000..d224e59 --- /dev/null +++ b/si-units/docs/examples.md @@ -0,0 +1,226 @@ +## How it works: values and units + +A quantity consists of two parts: a *value* and a *unit*. +Internally, a unit is represented by a list of seven integers, one for each SI base unit: + +```py +unit: list[int] # [meter, kilogram, second, ampere, mol, kelvin, candela] +``` + +Each value in this list corresponds to the *exponent* of the respective unit. +While it is possible to construct a `SIObject` by providing a value and a unit, +it is not recommendend because it is not very readable and prone to error: +```py linenums="1" +import si_units as si +mass = si.SIObject(125.0, [0, 1, 0, 0, 0, 0, 0]) +print(mass) +``` +``` +125 kg +``` + +Instead, we can use one of the "units" (a `SIObject` with value one) +defined in the package and multiply it by the value. +Here we use `KILOGRAM` to yield the same result as above: + +```py linenums="1" +import si_units as si +mass = 125.0 * si.KILOGRAM +print(mass) +``` +``` +125 kg +``` + +For convenience and readability, `si_units` also defines *prefixes*. +The following again yields the same result using the `KILO` +prefix together with `GRAM`: + +```py linenums="1" +import si_units as si +mass = 125.0 * si.KILO * si.GRAM +print(mass) +``` +``` +125 kg +``` + +Prefixes and units can be used to define new types that you may want +to reuse across your code: +```py linenums="1" +import si_units as si +MPA = si.MEGA * si.PASCAL +KG_M3 = si.KILOGRAM / si.METER**3 +FS = si.FEMTO * si.SECOND +... +``` + + +## Unit conversion + +Consider the pressure of an ideal gas: + +```py linenums="1" +from si_units import * +temperature = 298.15 * KELVIN +volume = 1.5 * METER**3 +moles = 75.0 * MOL +pressure = moles * RGAS * temperature / volume +print(pressure) +``` + +``` +123.94785148011941 kPa +``` + +Internally, all `SIObject`s are represented in base SI units. +Dividing by a unit (including prefixes) yields the value which can be a +scalar or an array or tensor. This can be used to "convert" a quantity +into a value of the desired unit: + +```py linenums="1" hl_lines="6-8" +import si_units as si +temperature = 298.15 * si.KELVIN +volume = 1.5 * si.METER**3 +moles = 75.0 * si.MOL +pressure = moles * si.RGAS * temperature / volume +print('pressure / bar: ', pressure / si.BAR) +print('pressure / mN/A^2:', pressure / (si.MILLI * si.NEWTON / si.ANGSTROM**2)) +print('volume / l: ', volume / si.LITER) +``` + +``` +pressure / bar: 1.2394785148011942 +pressure / mN/A^2: 1.239478514801194e-12 +volume / l: 1500.0 +``` + +## One-dimensional array constructors + +There are several ways to construct a `SIObject` where the value +is a one-dimensional array. + +```py linenums="1" +import si_units as si +# from a list of SIObjects +temperatures = si.array([298.15 * si.KELVIN, 313.15 * si.KELVIN]) +# linearly spaced +heights = si.linspace(15 * si.CENTI * si.METER, 15 * si.METER, 1000) +# log-spaced +pressures = si.logspace(100 * si.KILO * si.PASCAL, 50 * si.BAR, 100) +``` + +For multi-dimensional arrays, first construct a `numpy.ndarray` of the +desired shape and multiply it by an unit. + +## Gravitational pull of the moon on the earth + +```py linenums="1" +import si_units as si +mass_earth = 5.9724e24 * si.KILOGRAM +mass_moon = 7.346e22 * si.KILOGRAM +distance = 383.398 * si.KILO * si.METER +force = si.G * mass_earth * mass_moon / distance**2 +print(force) +``` + +``` +1.992075748302325e26 N +``` + +## Pressure distribution in the atmosphere + +Using the barometric formula. +This example demonstrates how dimensioned arrays can be constructed +using `numpy.ndarray`s. + +```py linenums="1" +import si_units as si +import numpy as np + +z = np.linspace(1.0, 70.0e3, 10) * si.METER +g = 9.81 * si.METER / si.SECOND**2 +m = 28.949 * si.GRAM / si.MOL +t = 283.15 * si.KELVIN +p0 = si.BAR +pressure = p0 * np.exp((-z * m * g) / (si.RGAS * t)) + +# dividing with the unit of an SIObject returns a numpy.ndarray +# iteration is currently not implemented. +for zi, pi in zip(z / si.METER, pressure / (si.KILO * si.PASCAL)): + print(f'z = {zi:16.10f} p = {pi:16.10f}') +``` + +```title="Output" +z = 1.0000000000 p = 99.9879378249 +z = 7778.6666666667 p = 39.1279560236 +z = 15556.3333333333 p = 15.3118163640 +z = 23334.0000000000 p = 5.9919235296 +z = 31111.6666666667 p = 2.3448000375 +z = 38889.3333333333 p = 0.9175830080 +z = 46667.0000000000 p = 0.3590747881 +z = 54444.6666666667 p = 0.1405155744 +z = 62222.3333333333 p = 0.0549875048 +z = 70000.0000000000 p = 0.0215180823 +``` + +Alternatively, we could have used `si_units.linspace` instead of numpy: +```py linenums="1" +import si_units as si +# instead of this +z = np.linspace(1.0, 70.0e3, 10) * si.METER +# we can also use this +z = si.linspace(1.0 * si.METER, 70.0e3 * si.METER, 10) +``` + +## Using `numpy` or `torch` functions + +Some functions work with methods or the equivalent numpy functions. + +```py linenums="1" +import si_units as si +import numpy as np + +sqm = si.METER**2 +print(np.sqrt(sqm)) +print(sqm.sqrt()) # this is equivalent +``` + +``` +1 m +1 m +``` + +Some behaviour is not as you would expect. For example, when we +change the above to an array, numpy will throw an exception: +```py linenums="1" +import si_units as si +import numpy as np +sqm = np.array([1.0, 2.0]) * si.METER**2 +print(np.sqrt(sqm)) +print(sqm.sqrt()) # both calls raise an exception +``` +``` +AttributeError: 'numpy.ndarray' object has no attribute 'sqrt' +``` + +In such a case, we can divide by the unit to return the inner data type, +perform the operation to the value and the unit separately, and finally +multiply by the unit to get back a `SIObject`. + +For `torch.tensor`'s this is not an issue and the following works just +fine: + +```py linenums="1" +import si_units as si +import torch +ms = torch.tensor([2.0, 3.0, 4.0]) * si.METER +sqms = ms**2 +print(sqms) +print(sqms.sqrt()) +``` + +``` +tensor([ 4., 9., 16.]) m² +tensor([2., 3., 4.]) m +``` diff --git a/si-units/docs/examples.rst b/si-units/docs/examples.rst deleted file mode 100644 index 14e1f68..0000000 --- a/si-units/docs/examples.rst +++ /dev/null @@ -1,96 +0,0 @@ -Examples --------- - -Pressure of an ideal gas -~~~~~~~~~~~~~~~~~~~~~~~~ - - >>> from si_units import * - >>> temperature = 298.15 * KELVIN - >>> volume = 1.5 * METER**3 - >>> moles = 75.0 * MOL - >>> pressure = moles * RGAS * temperature / volume - >>> print(pressure) - 123.94785148011941 kPa - -You can use division to perform unit conversions. - - >>> from si_units import * - >>> temperature = 298.15 * KELVIN - >>> volume = 1.5 * METER**3 - >>> moles = 75.0 * MOL - >>> pressure = moles * RGAS * temperature / volume - >>> print(pressure / BAR) - 1.2394785148011942 - >>> print(pressure / (MILLI * NEWTON / ANGSTROM**2)) - 1.239478514801194e-12 - >>> print(volume / LITER) - 1500.0 - -Gravitational pull of the moon on the earth -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - >>> from si_units import * - >>> mass_earth = 5.9724e24 * KILOGRAM - >>> mass_moon = 7.346e22 * KILOGRAM - >>> distance = 383.398 * KILO * METER - >>> force = G * mass_earth * mass_moon / distance**2 - >>> print(force) - 1.992075748302325e26 N - -Pressure distribution in the atmosphere -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Using the barometric formula. -This example demonstrates how dimensioned arrays can be constructed using numpy.ndarray's. - -.. code-block:: python - - >>> from si_units import * - >>> import numpy as np - - >>> z = np.linspace(1.0, 70.0e3, 10) * METER - >>> g = 9.81 * METER / SECOND**2 - >>> m = 28.949 * GRAM / MOL - >>> t = 283.15 * KELVIN - >>> p0 = BAR - >>> pressure = p0 * np.exp((-z * m * g) / (RGAS * t)) - >>> - >>> # dividing with the unit of an SIArray returns a numpy.ndarray - >>> # iteration is currently not implemented. - >>> for zi, pi in zip(z / METER, pressure / (KILO * PASCAL)): - >>> print(f'z = {zi:16.10f} p = {pi:16.10f}') - - z = 1.0000000000 p = 99.9879378249 - z = 7778.6666666667 p = 39.1279560236 - z = 15556.3333333333 p = 15.3118163640 - z = 23334.0000000000 p = 5.9919235296 - z = 31111.6666666667 p = 2.3448000375 - z = 38889.3333333333 p = 0.9175830080 - z = 46667.0000000000 p = 0.3590747881 - z = 54444.6666666667 p = 0.1405155744 - z = 62222.3333333333 p = 0.0549875048 - z = 70000.0000000000 p = 0.0215180823 - -Using `numpy` or `torch` Functions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Functions such as `exp`, `sqrt` and `cbrt` work with methods or the equivalent numpy functions. - - >>> from si_units import * - >>> import numpy as np - >>> sqm = METER**2 - >>> np.sqrt(sqm) - 1 m - >>> sqm.sqrt() # this is equivalent - 1 m - -This also works with torch.Tensor's. - - >>> from si_units import * - >>> import torch - >>> ms = torch.tensor([2.0, 3.0, 4.0]) * METER - >>> sqms = ms**2 - >>> sqms - tensor([ 4., 9., 16.]) m² - >>> sqms.sqrt() - tensor([2., 3., 4.]) m diff --git a/si-units/docs/index.md b/si-units/docs/index.md new file mode 100644 index 0000000..f6b9072 --- /dev/null +++ b/si-units/docs/index.md @@ -0,0 +1,95 @@ +# Welcome to `si-units` + +This package provides representations of quantities with SI units. +It is written with flexibility in mind and is able to represent arbitrarily complex units. +In addition to simple scalar quantities, it can be used to decorate any complex data type (numpy arrays, PyTorch tensors) to provide unit checks. + +## Installation + +You can install `si-units` from pypi using `pip`: + +``` +pip install si-units +``` + +## Build from source + +To build the code from source, you need the [rust compiler](https://www.rust-lang.org/tools/install). + +### maturin + +With [maturin](https://github.com/PyO3/maturin) installed: + +``` +git clone https://github.com/itt-ustutt/quantity +cd quantity/si-units +maturin build --release +``` + +You can then install the generated wheel (placed in `quantity/target/wheels`) into your Python environment. + +### uv + +You can use [uv](https://github.com/astral-sh/uv) to build the wheel: + +``` +git clone https://github.com/itt-ustutt/quantity +cd quantity/si-units +uv build +``` + +You can then install the generated wheel (placed in `dist/`) into your Python environment. + +## Usage + +```py title="Ideal gas pressure" linenums="1" +from si_units import * +temperature = 25.0 * CELSIUS +volume = 1.5 * METER**3 +moles = 75.0 * MOL +pressure = moles * RGAS * temperature / volume +print(pressure) +``` + +``` +123.94785148011941 kPa +``` + +This also works with `numpy.ndarray`: + +```py title="Using numpy" linenums="1" +from si_units import * +import numpy as np +ms = np.linspace(2.0, 4.0, 3) * METER +sqms = ms**2 +print(sqms) +``` + +``` +[4, 9, 16] m² +``` + +When you divide a quantity by its unit, the value +(i.e. `float` for scalars or `numpy.ndarray[float]` for arrays) is returned. +You can use this to convert units: + +```py title="Unit conversion" linenums="1" +from si_units import MOL, METER, ANGSTROM, NAV + +molar_density = 3000 * MOL / METER**3 +particle_density = molar_density * NAV +nparticles = 16_000 +volume = nparticles / particle_density + +# make sure we actually have a volume +# this checks the quantity (here length**3), not the actual unit +# i.e. we could have used volume.has_unit(METER**3) for the same effect. +assert volume.has_unit(METER**3), "Something went wrong." +print(f'V = {volume / ANGSTROM**3:.2g} A^3') +``` + +``` +V = 8.9e+06 A^3 +``` + +See [Examples](examples.md) section for more use cases. \ No newline at end of file diff --git a/si-units/docs/index.rst b/si-units/docs/index.rst deleted file mode 100644 index c4272e4..0000000 --- a/si-units/docs/index.rst +++ /dev/null @@ -1,30 +0,0 @@ -Welcome to si-units -=================== - -This package enables calculation with dimensioned values. - -Installation ------------- - -You can install `si-units` from pypi using `pip`: - -``` -pip install si-units -``` - -Build from source -~~~~~~~~~~~~~~~~~ - -To build the code from source, you need the `rust compiler `_ and `maturin `_. -You can then install the latest master directly from github: - -``` -pip install git+https://github.com/itt-ustutt/quantity -``` - -.. toctree:: - - examples - api - -* :ref:`search` diff --git a/si-units/docs/javascript/mathjax.js b/si-units/docs/javascript/mathjax.js new file mode 100644 index 0000000..3d0d925 --- /dev/null +++ b/si-units/docs/javascript/mathjax.js @@ -0,0 +1,19 @@ +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex" + } + }; + + document$.subscribe(() => { + MathJax.startup.output.clearCache() + MathJax.typesetClear() + MathJax.texReset() + MathJax.typesetPromise() + }) \ No newline at end of file diff --git a/si-units/docs/prefixes.md b/si-units/docs/prefixes.md new file mode 100644 index 0000000..e63d28e --- /dev/null +++ b/si-units/docs/prefixes.md @@ -0,0 +1,34 @@ +# Prefixes + +All units can be combined with the following prefixes (capitalized): + +| Prefix | Prefix symbol | Value | Prefix | Prefix symbol | Value | +| ------ | ------------- | ---------- | ------ | ------------- | --------- | +| DECI | $\text{d}$ | $10^{-1}$ | DECA | $\text{da}$ | $10^{1}$ | +| CENTI | $\text{c}$ | $10^{-2}$ | HECTO | $\text{h}$ | $10^{2}$ | +| MILLI | $\text{m}$ | $10^{-3}$ | KILO | $\text{k}$ | $10^{3}$ | +| MICRO | $\text{µ}$ | $10^{-6}$ | MEGA | $\text{M}$ | $10^{6}$ | +| NANO | $\text{n}$ | $10^{-9}$ | GIGA | $\text{G}$ | $10^{9}$ | +| PICO | $\text{p}$ | $10^{-12}$ | TERA | $\text{T}$ | $10^{12}$ | +| FEMTO | $\text{f}$ | $10^{-15}$ | PETA | $\text{P}$ | $10^{15}$ | +| ATTO | $\text{a}$ | $10^{-18}$ | EXA | $\text{E}$ | $10^{18}$ | +| ZEPTO | $\text{z}$ | $10^{-21}$ | ZETTA | $\text{Z}$ | $10^{21}$ | +| YOCTO | $\text{y}$ | $10^{-24}$ | YOTTA | $\text{Y}$ | $10^{24}$ | +| RONTO | $\text{r}$ | $10^{-27}$ | RONNA | $\text{R}$ | $10^{27}$ | +| QUECTO | $\text{q}$ | $10^{-30}$ | QUETTA | $\text{Q}$ | $10^{30}$ | + +## Example + +```py linenums="1" +from si_units import MICRO, SECOND, MEGA, PASCAL +dt = 15 * MICRO * SECOND +print(dt) + +MPA = MEGA * PASCAL +pressure = 20 * MPA +print(pressure) +``` +``` +15 µs +20 MPa +``` diff --git a/si-units/license-apache b/si-units/license-apache index fffb378..67e3ce6 120000 --- a/si-units/license-apache +++ b/si-units/license-apache @@ -1 +1 @@ -license-apache \ No newline at end of file +../license-apache \ No newline at end of file diff --git a/si-units/license-mit b/si-units/license-mit index a949aa4..137b4b1 120000 --- a/si-units/license-mit +++ b/si-units/license-mit @@ -1 +1 @@ -license-mit \ No newline at end of file +../license-mit \ No newline at end of file diff --git a/si-units/mkdocs.yml b/si-units/mkdocs.yml new file mode 100644 index 0000000..f6600c2 --- /dev/null +++ b/si-units/mkdocs.yml @@ -0,0 +1,84 @@ +theme: + name: material + font: + text: Fira Sans + code: Fira Sans Mono + features: + - navigation.sections + - toc.integrate + - header.autohide + - content.code.copy + palette: + # Palette toggle for automatic mode + - media: '(prefers-color-scheme)' + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: '(prefers-color-scheme: light)' + scheme: default + primary: black + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: black + toggle: + icon: material/brightness-4 + name: Switch to system preference + icon: + repo: fontawesome/brands/github + +site_name: si-units +site_url: https://itt-ustutt.github.io/quantity + +repo_url: https://github.com/itt-ustutt/quantity/tree/master/si-units + +markdown_extensions: + - pymdownx.arithmatex: # Render LaTeX via MathJax + generic: true + - pymdownx.superfences # Seems to enable syntax highlighting when used with the Material theme. + - pymdownx.details # Allowing hidden expandable regions denoted by ??? + - pymdownx.snippets: # Include one Markdown file into another + base_path: docs + - admonition + - tables + +extra_javascript: + - javascript/mathjax.js + - https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js + +plugins: + - search # default search plugin; needs manually re-enabling when using any other plugins + - mkdocstrings: + handlers: + python: + paths: [src] + docstring_style: google + show_signature_annotations: true + options: + find_stubs_package: true + heading_level: 3 + show_root_heading: true + show_root_full_path: false + docstring_section_style: spacy + merge_init_into_class: true + show_docstring_description: true + annotations_path: brief + docstring_options: + ignore_init_summary: false + trim_doctest_flags: true + +nav: + - Introduction: 'index.md' + - Examples: 'examples.md' + - API: + - Classes & Functions: 'api.md' + - Units, Constants, Prefixes: + - Base Units: 'base.md' + - Derived Units & Constants: 'derived.md' + - Prefixes: 'prefixes.md' diff --git a/si-units/pyproject.toml b/si-units/pyproject.toml new file mode 100644 index 0000000..3a8299e --- /dev/null +++ b/si-units/pyproject.toml @@ -0,0 +1,44 @@ +[project] +name = "si-units" +version = "0.10.0" +description = "Representation of quantites, i.e. of unit valued scalars and arrays." +readme = "README.md" +authors = [ + { name = "Philipp Rehner", email = "prehner@ethz.ch" }, + { name = "Gernot Bauer", email = "bauer@itt.uni-stuttgart.de" }, +] +requires-python = ">=3.9" + +[tool.maturin] +module-name = "si_units._core" +python-packages = ["si_units"] +python-source = "src" + +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[dependency-groups] +dev = [ + "ipykernel>=6.29.5", + "maturin>=1.7.8", + "mkdocs-material>=9.5.48", + "mkdocstrings>=0.27.0", + "mkdocstrings-python>=1.12.2", + "numpy>=2.0.2", + "ruff>=0.8.3", +] + +[tool.ruff] +line-length = 80 + +[tool.ruff.lint] +extend-select = [ + "D", # pydocstyle +] + +[tool.ruff.lint.pydocstyle] +convention = "google" # https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings + +[tool.ruff.lint.pycodestyle] +max-doc-length = 80 diff --git a/si-units/src/lib.rs b/si-units/src/lib.rs index b2ac81c..109ee35 100644 --- a/si-units/src/lib.rs +++ b/si-units/src/lib.rs @@ -26,7 +26,7 @@ pub enum QuantityError { DebyePower, } -#[pyclass(name = "SIObject", module = "si_units", frozen)] +#[pyclass(name = "SIObject", module = "si_units._core", frozen)] pub struct PySIObject { value: PyObject, unit: SIUnit, @@ -104,15 +104,6 @@ impl PySIObject { }) } - /// Try to calculate the square root of self. - /// - /// Examples - /// -------- - /// - /// >>> import si - /// >>> m2 = METER**2 - /// >>> m2.sqrt() - /// 1 m pub fn sqrt(&self, py: Python) -> PyResult { let value = if let Ok(v) = self.value.extract::(py) { PyFloat::new(py, v.sqrt()).into_any().unbind() @@ -122,15 +113,6 @@ impl PySIObject { Ok(Self::new(value, self.unit.sqrt()?)) } - /// Try to calculate the cubic root of self. - /// - /// Examples - /// -------- - /// - /// >>> import si - /// >>> m3 = METER**3 - /// >>> m3.cbrt() - /// 1 m pub fn cbrt(&self, py: Python) -> PyResult { let value = if let Ok(v) = self.value.extract::(py) { PyFloat::new(py, v.cbrt()).into_any().unbind() @@ -140,16 +122,6 @@ impl PySIObject { Ok(Self::new(value, self.unit.cbrt()?)) } - /// Test if the quantity has the same unit as the argument. - /// - /// Parameters - /// ---------- - /// other : SINumber - /// The unit that is compared. - /// - /// Returns - /// ------- - /// bool pub fn has_unit(&self, other: PyRef<'_, Self>) -> bool { self.unit.eq(&other.unit) } @@ -321,104 +293,69 @@ impl<'py> FromPyObject<'py> for SINumber { } } -#[pyclass] -struct SIArray1; - -#[pymethods] -impl SIArray1 { - fn __call__<'py>(&self, value: Bound<'py, PyAny>) -> PyResult> { - let py = value.py(); - if let Ok(v) = value.extract::() { - let value = arr1(&[1.0]) * v.value; - let value = value.into_pyarray(py).into_any().unbind(); - Bound::new(py, PySIObject::new(value, v.unit)) - } else if let Ok(v) = value.extract::>() { - let mut unit = SIUnit::DIMENSIONLESS; - let (value, units): (Vec<_>, Vec<_>) = v.into_iter().map(|v| (v.value, v.unit)).unzip(); - for u in units { - if unit != SIUnit::DIMENSIONLESS && unit != u { - Err(QuantityError::InconsistentUnits { - unit1: unit, - unit2: u, - })?; - } else { - unit = u - } +#[pyfunction] +fn array(value: Bound<'_, PyAny>) -> PyResult> { + let py = value.py(); + if let Ok(v) = value.extract::() { + let value = arr1(&[1.0]) * v.value; + let value = value.into_pyarray(py).into_any().unbind(); + Bound::new(py, PySIObject::new(value, v.unit)) + } else if let Ok(v) = value.extract::>() { + let mut unit = SIUnit::DIMENSIONLESS; + let (value, units): (Vec<_>, Vec<_>) = v.into_iter().map(|v| (v.value, v.unit)).unzip(); + for u in units { + if unit != SIUnit::DIMENSIONLESS && unit != u { + Err(QuantityError::InconsistentUnits { + unit1: unit, + unit2: u, + })?; + } else { + unit = u } - let value: Array1<_> = Array1::from_vec(value); - let value = value.into_pyarray(py).into_any().unbind(); - Bound::new(py, PySIObject::new(value, unit)) - } else { - Ok(value.downcast_into::()?) } + let value: Array1<_> = Array1::from_vec(value); + let value = value.into_pyarray(py).into_any().unbind(); + Bound::new(py, PySIObject::new(value, unit)) + } else { + Ok(value.downcast_into::()?) } +} - /// Create a linearly spaced SIArray. - /// - /// Parameters - /// ---------- - /// start: SINumber - /// The lowest value of the Array. - /// end: SINumber - /// The highest value of the Array. - /// n: int - /// The number of points. - /// - /// Returns - /// ------- - /// SIArray1 - /// - #[staticmethod] - fn linspace( - py: Python, - start: SINumber, - end: SINumber, - n: usize, - ) -> Result { - if start.unit == end.unit { - let value = Array1::linspace(start.value, end.value, n); - let value = value.into_pyarray(py).into_any().unbind(); - Ok(PySIObject::new(value, start.unit)) - } else { - Err(QuantityError::InconsistentUnits { - unit1: start.unit, - unit2: end.unit, - }) - } +#[pyfunction] +fn linspace( + py: Python, + start: SINumber, + end: SINumber, + n: usize, +) -> Result { + if start.unit == end.unit { + let value = Array1::linspace(start.value, end.value, n); + let value = value.into_pyarray(py).into_any().unbind(); + Ok(PySIObject::new(value, start.unit)) + } else { + Err(QuantityError::InconsistentUnits { + unit1: start.unit, + unit2: end.unit, + }) } +} - /// Create a logarithmically spaced SIArray. - /// - /// Parameters - /// ---------- - /// start: SINumber - /// The lowest value of the Array. - /// end: SINumber - /// The highest value of the Array. - /// n: int - /// The number of points. - /// - /// Returns - /// ------- - /// SIArray1 - /// - #[staticmethod] - fn logspace( - py: Python, - start: SINumber, - end: SINumber, - n: usize, - ) -> Result { - if start.unit == end.unit { - let value = Array1::logspace(10.0, start.value.log10(), end.value.log10(), n); - let value = value.into_pyarray(py).into_any().unbind(); - Ok(PySIObject::new(value, start.unit)) - } else { - Err(QuantityError::InconsistentUnits { - unit1: start.unit, - unit2: end.unit, - }) - } +#[pyfunction] +fn logspace( + py: Python, + start: SINumber, + end: SINumber, + n: usize, +) -> Result { + if start.unit == end.unit { + let value = Array1::logspace(10.0, start.value.log10(), end.value.log10(), n); + let value = value.into_pyarray(py).into_any().unbind(); + Ok(PySIObject::new(value, start.unit)) + } else { + Err(QuantityError::InconsistentUnits { + unit1: start.unit, + unit2: end.unit, + }) } } @@ -501,12 +438,14 @@ pub const RONNA: f64 = 1e27; pub const QUETTA: f64 = 1e30; #[pymodule] -pub fn si_units(m: &Bound<'_, PyModule>) -> PyResult<()> { +pub fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("__version__", env!("CARGO_PKG_VERSION"))?; m.add_class::()?; m.add_class::()?; - m.add("SIArray1", SIArray1)?; + m.add_function(wrap_pyfunction!(array, m)?)?; + m.add_function(wrap_pyfunction!(linspace, m)?)?; + m.add_function(wrap_pyfunction!(logspace, m)?)?; add_constant(m, "SECOND", 1.0, _SECOND)?; add_constant(m, "METER", 1.0, _METER)?; diff --git a/si-units/src/si_units/__init__.py b/si-units/src/si_units/__init__.py new file mode 100644 index 0000000..c95390d --- /dev/null +++ b/si-units/src/si_units/__init__.py @@ -0,0 +1,171 @@ +"""Representation of quantities with SI units. + +`si_units` provides the `SIObject` class that represents physical quantities +as values with units. Values can be any Python data types that are suitable +for arithmetic operations, such as `float`, `numpy.ndarray` or `torch.tensor`. +Usually, a `SIObject` is created by multiplying a value by a `SIObject` that +represents a unit. + +Examples: + Calculate the ideal gas pressure and convert to bar. + + >>> from si_units import CELSIUS, METER, MOL, RGAS, BAR + >>> temperature = 25.0 * CELSIUS + >>> volume = 1.5 * METER**3 + >>> moles = 75.0 * MOL + >>> pressure = moles * RGAS * temperature / volume + >>> print(f"The pressure is {pressure / BAR:.2f} bar") + The pressure is 1.24 bar +""" +# from si_units._core import SIObject +from si_units._core import ( + SIObject, + Angle, + array, + linspace, + logspace, + SECOND, + METER, + KILOGRAM, + AMPERE, + KELVIN, + CELSIUS, + DEBYE, + DEGREES, + RADIANS, + DAY, + MOL, + CANDELA, + DVCS, + CLIGHT, + PLANCK, + QE, + KB, + NAV, + KCD, + HERTZ, + NEWTON, + PASCAL, + JOULE, + WATT, + COULOMB, + VOLT, + FARAD, + OHM, + SIEMENS, + WEBER, + TESLA, + HENRY, + ANGSTROM, + AMU, + AU, + BAR, + CALORIE, + GRAM, + HOUR, + LITER, + MINUTE, + G, + RGAS, + QUECTO, + RONTO, + YOCTO, + ZEPTO, + ATTO, + FEMTO, + PICO, + NANO, + MICRO, + MILLI, + CENTI, + DECI, + DECA, + HECTO, + KILO, + MEGA, + GIGA, + TERA, + PETA, + EXA, + ZETTA, + YOTTA, + RONNA, + QUETTA, + __version__, +) + +__all__ = [ + "SIObject", + "Angle", + "array", + "linspace", + "logspace", + "SECOND", + "METER", + "KILOGRAM", + "AMPERE", + "KELVIN", + "CELSIUS", + "DEBYE", + "DEGREES", + "RADIANS", + "DAY", + "MOL", + "CANDELA", + "DVCS", + "CLIGHT", + "PLANCK", + "QE", + "KB", + "NAV", + "KCD", + "HERTZ", + "NEWTON", + "PASCAL", + "JOULE", + "WATT", + "COULOMB", + "VOLT", + "FARAD", + "OHM", + "SIEMENS", + "WEBER", + "TESLA", + "HENRY", + "ANGSTROM", + "AMU", + "AU", + "BAR", + "CALORIE", + "GRAM", + "HOUR", + "LITER", + "MINUTE", + "G", + "RGAS", + "QUECTO", + "RONTO", + "YOCTO", + "ZEPTO", + "ATTO", + "FEMTO", + "PICO", + "NANO", + "MICRO", + "MILLI", + "CENTI", + "DECI", + "DECA", + "HECTO", + "KILO", + "MEGA", + "GIGA", + "TERA", + "PETA", + "EXA", + "ZETTA", + "YOTTA", + "RONNA", + "QUETTA", + "__version__", +] diff --git a/si-units/src/si_units/_core.pyi b/si-units/src/si_units/_core.pyi new file mode 100644 index 0000000..1f8d394 --- /dev/null +++ b/si-units/src/si_units/_core.pyi @@ -0,0 +1,137 @@ +from typing import Self, Any + +class SIObject: + """Combination of value and unit. + + The value can be any Python object that can be used for arithmetic + operations such as a float, numpy.ndarray or torch.tensor. + + When a SIObject is divided by its unit, the value is returned. + This is usefull to convert units or when operations are needed + that are not implemented for SIObject. + """ + + def __init__(self, value: float | Any, unit: list[int]) -> None: + """Constructs a new quantity. + + Warning: Don't use the default constructor + This constructor should not be used to construct a quantity. + Instead, multiply the value (float or array of floats) + by the appropriate unit. See example below. + + Args: + value: + The numerical value(s). Can be a scalar or an array + such as a numpy.ndarray or a torch.tensor. + unit: List of 7 exponents for SI base units. + + Raises: + RuntimeError: When unit has the wrong format. + + Examples: + >>> import si_units as si + >>> # don't do this: + >>> two_meters_init = si.SIObject(2.0, [1, 0, 0, 0, 0, 0, 0]) + >>> # instead, do this: + >>> two_meters_mul = 2.0 * si.METER + >>> assert two_meters_init == two_meters_mul + """ + ... + + def sqrt(self) -> Self: + """Calculates the square root. + + Returns: + Square root of the quantity. + + Raises: + RuntimeError: When exponents of units are not multiples of two. + AttributeError: When the inner data type has no 'sqrt' method. + + Examples: + >>> from si_units import METER + >>> square = METER**2 + >>> length = square.sqrt() + """ + ... + + def cbrt(self) -> Self: + """Calculate the cubic root. + + Returns: + Cubic root of the quantity. + + Raises: + RuntimeError: When exponents of units are not multiples of three. + AttributeError: When the inner data type has no 'cbrt' method. + + Examples: + >>> from si_units import METER + >>> volume = METER**3 + >>> length = volume.cbrt() + """ + ... + + def has_unit(self, other: Self) -> bool: + """Tests if the quantity has the same unit as the argument. + + Args: + other: The quantity to compare to. + + Returns: + Wheter the units of the compared quantities are the same or not. + """ + ... + +def array(value: SIObject | list[SIObject]) -> SIObject: + """Build SIObject from scalar or list. + + When the input is a scalar, it is stored in an array with a single element. + + Args: + value: Values to store. Must all have the same unit. + + Returns: + The quantity with values stored within array, + even if value is given as a scalar. + + Raises: + RuntimeError: If the elements of value have different units. + """ + ... + +def linspace(start: SIObject, end: SIObject, n: int) -> SIObject: + """Linearly spaced quantities. + + Args: + start: Lowest value. + end: Highest value. + n: The (positive) number of points. + + Returns: + Linearly spaced values with the same unit. + + Raises: + RuntimeError: + If start and end values are not scalars, if they don't have + the same unit, or if n is not positive. + """ + ... + +def logspace(start: SIObject, end: SIObject, n: int) -> SIObject: + """Logarithmically spaced quantities. + + Args: + start: Lowest value. + end: Highest value. + n: The (positive) number of points. + + Returns: + Logarithmically spaced values with the same unit. + + Raises: + RuntimeError: + If start and end values are not scalars, if they don't have + the same unit, or if n is not positive. + """ + ... diff --git a/si-units/src/si_units/py.typed b/si-units/src/si_units/py.typed new file mode 100644 index 0000000..e69de29