Skip to content

Commit

Permalink
some tweaks to the python library add convert as method on unit object (
Browse files Browse the repository at this point in the history
#350)

* some tweaks to the python library add convert as method on unit object

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* update wheels file

* update pre-commit to add some python stuff

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* update python readme

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* update version number

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
phlptp and pre-commit-ci[bot] authored Dec 21, 2024
1 parent b8bf817 commit a713814
Show file tree
Hide file tree
Showing 11 changed files with 518 additions and 211 deletions.
68 changes: 66 additions & 2 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ name: Wheels

on:
workflow_dispatch:
pull_request:
push:
branches:
- main
Expand Down Expand Up @@ -77,4 +76,69 @@ jobs:
- uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true
repository-url: https://test.pypi.org/legacy/
repository-url: https://test.pypi.org/legacy/
publish-to-pypi:
name: >-
Publish Python 🐍 distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
needs: [build_wheels, build_sdist]
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/units_llnl # Replace <package-name> with your PyPI project name
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing

steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
pattern: dist-*
merge-multiple: true
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

github-release:
name: >-
Sign the Python 🐍 distribution 📦 with Sigstore
and upload them to GitHub Release
needs:
- publish-to-pypi
runs-on: ubuntu-latest

permissions:
contents: write # IMPORTANT: mandatory for making GitHub Releases
id-token: write # IMPORTANT: mandatory for sigstore

steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
pattern: dist-*
merge-multiple: true
path: dist/
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.0.0
with:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
- name: Create GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release create
'${{ github.ref_name }}'
--repo '${{ github.repository }}'
--notes ""
- name: Upload artifact signatures to GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
# Upload to GitHub Release using the `gh` CLI.
# `dist/` contains the built packages, and the
# sigstore-produced signatures and certificates.
run: >-
gh release upload
'${{ github.ref_name }}' dist/**
--repo '${{ github.repository }}'
12 changes: 11 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ repos:
rev: v4.0.0-alpha.8
hooks:
- id: prettier
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black
args: ["--line-length=100"]
- repo: https://github.com/asottile/blacken-docs
rev: 1.19.1
hooks:
- id: blacken-docs
additional_dependencies: [black==24.10]
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.5
hooks:
Expand Down Expand Up @@ -47,7 +57,7 @@ repos:
- id: end-of-file-fixer
- id: check-shebang-scripts-are-executable
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v19.1.4
rev: v19.1.5
hooks:
- id: clang-format
types_or: [c++, c]
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ endif()
project(
${UNITS_CMAKE_PROJECT_NAME}
LANGUAGES C CXX
VERSION 0.10.0
VERSION 0.10.2
)
include(CMakeDependentOption)
include(CTest)
Expand Down
12 changes: 3 additions & 9 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,7 @@

def which(program):
def is_exe(fpath):
return (
os.path.exists(fpath)
and os.access(fpath, os.X_OK)
and os.path.isfile(fpath)
)
return os.path.exists(fpath) and os.access(fpath, os.X_OK) and os.path.isfile(fpath)

def ext_candidates(fpath):
yield fpath
Expand Down Expand Up @@ -79,7 +75,7 @@ def ext_candidates(fpath):
# 'sphinx.ext.intersphinx',
# 'sphinx.ext.coverage',
"sphinx.ext.mathjax",
"sphinx.ext.autosectionlabel"
"sphinx.ext.autosectionlabel",
# 'sphinx.ext.viewcode',
# 'sphinx.ext.githubpages',
# 'sphinx.ext.napoleon',
Expand Down Expand Up @@ -204,9 +200,7 @@ def ext_candidates(fpath):
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, "units.tex", "Units Library Documentation", "Philip Top", "manual")
]
latex_documents = [(master_doc, "units.tex", "Units Library Documentation", "Philip Top", "manual")]

# -- Options for manual page output ---------------------------------------

Expand Down
18 changes: 14 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ build-backend = "scikit_build_core.build"

[project]
name = "units_llnl"
version = "0.10.0.post1"
description = "Python bindings for the llnl units library"
readme = "README.md"
version = "0.10.2"
description = "Python bindings for the LLNL units library"
readme = "python/README.md"
requires-python = ">=3.10"
license = {text="BSD-3-Clause"}
license-files = [
Expand All @@ -19,8 +19,18 @@ authors = [
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
"Topic :: Scientific/Engineering",
"Development Status :: 4 - Beta",
]
keywords = [
"units",
"measurement",
"power systems",
"co-simulation",
"commodities",
"science",
"python",
]

[project.urls]
Homepage = "https://github.com/llnl/units"
Documentation = "https://units.readthedocs.io/en/latest/"
Expand Down
158 changes: 158 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Units

[![codecov](https://codecov.io/gh/LLNL/units/branch/main/graph/badge.svg)](https://codecov.io/gh/LLNL/units)
[![Build Status](https://dev.azure.com/phlptp/units/_apis/build/status/LLNL.units?branchName=main)](https://dev.azure.com/phlptp/units/_build/latest?definitionId=1&branchName=main)
[![CircleCI](https://circleci.com/gh/LLNL/units.svg?style=svg)](https://circleci.com/gh/LLNL/units)
[![](https://img.shields.io/badge/License-BSD-blue.svg)](https://github.com/GMLC-TDC/HELICS-src/blob/main/LICENSE)
[![Documentation Status](https://readthedocs.org/projects/units/badge/?version=latest)](https://units.readthedocs.io/en/latest/?badge=latest)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/LLNL/units/main.svg)](https://results.pre-commit.ci/latest/github/LLNL/units/main)

[What's new](./CHANGELOG.md)

[Documentation](https://units.readthedocs.io/en/latest/)

The Units library provides a means of working with units of measurement at runtime, including conversion to and from strings. It provides a small number of types for working with units and measurements and operations necessary for user input and output with units. For additional description and discussion see [Readme](https://github.com/LLNL/units/blob/main/README.md)

## Table of contents

- [Units](#units)
- [Table of contents](#table-of-contents)
- [Purpose](#purpose)
- [Basic use case](#basic-use-case)
- [Try it out](#try-it-out)
- [Unit methods](#unit-methods)
- [Unit Operators](#unit-operators)
- [Measurement Operations](#measurement-operations)
- [Measurement operators](#measurement-operators)
- [Contributions](#contributions)
- [Project Using the Units Library](#project-using-the-units-library)
- [Release](#release)

## Purpose

A units library was needed to be able to represent units from a wide range of disciplines and be able to separate them from the numerical values for use in calculations when needed. The main drivers are

1. converting units, often represented by strings, to a standardized unit set when dealing with user input and output.
2. Being able to use the unit as a singular type that could contain any unit, and not introduce a huge number of types to represent all possible units.
3. Being able to associate a completely arbitrary unit given by users with a generic interface and support conversions between those user defined units and other units.
4. The library has its origins in power systems so support for per-unit operations was also lacking in the alternatives.
5. Capture uncertainty and uncertainty calculations directly with a measurement

The python wrapper around the library is mainly intended to be able to handle various string representations and easily handle conversions, along with some support for commodities and packaging.

### Basic use case

The primary use case for the library is string operations and conversion. For example if you have a library that does some computations with physical units. In the library code itself the units are standardized and well defined. For example take a velocity, internally everything is in meters per second, but there is a configuration file that takes in the initial data and you would like to broadly support different units on the input

```python
from units_llnl import unit

u1 = unit("m")
u2 = unit("cm")
v1 = u1.convert(10, u2)
assert v1 == 10 * 100

v2 = u1.convert(unit_out=u2, value=20)
assert v2 == 2000
```

```python
from units_llnl import measurement

m1 = measurement("10 m")
m2 = measurement("2.5 s")
m3 = m1 / m2
m4 = measurement("4.0 m/s")
assert m3 == m4
```

## Try it out

If you want to try out the string conversion components. There is server running that can do the string conversions

[Unit String Conversions](https://units.readthedocs.io/en/latest/_static/convert.html)

For more details see the [documentation](https://units.readthedocs.io/en/latest/web/index.html)

### Unit methods

These operations apply to units and precise_units

- `<unit>(<unit_data>)` construct from a base unit_data
- `<unit>(<unit_data>, double multiplier)` construct a unit from a base data and a multiplier
- `<unit>(double multiplier, <unit>)` construct from a multiplier and another unit
- also available are copy constructor and copy assignments
- `<unit> inv()` generate a new unit containing the inverse unit `m.inv()= 1/m`
- `<unit> pow(int power)` take a unit to power(NOTE: beware of limits on power representations of some units, things will always wrap so it is defined but may not produce what you expect). `power` can be negative.
- `bool is_exactly_the_same(<unit>)` compare two units and check for exact equivalence in both the unit_data and the multiplier, NOTE: this uses double equality
- `bool has_same_base(<unit>|<unit_data>)` check if the <unit_data> is the same
- `equivalent_non_counting(<unit>|<unit_data>)` check if the units are equivalent ignoring the counting bases
- `bool is_convertible(<unit>)` check if the units are convertible to each other, currently checks `equivalent_non_counting()`, but some additional conditions might be allowed in the future to better match convert.
- `int unit_type_count()` count the number of unit bases used, (does not take into consideration powers, just if the dimension is used or not.
- `bool is_per_unit()` true if the unit has the per_unit flag active
- `bool is_equation()` true if the unit has the equation flag active
- `bool has_i_flag()` true if the i_flag is marked active
- `bool has_e_flag()` true if the e_flag is marked active
- `double multiplier()` return the unit multiplier as a double

- `commodity()` get the commodity of the unit
- `commodity(int commodity)` assign a commodity to the precise_unit.

#### Unit Operators

There are also several operator overloads that apply to units and precise_units.

- `<unit>=<unit>*<unit>` generate a new unit with the units multiplied ie `m*m` does what you might expect and produces a new unit with `m^2`
- `<unit>=<unit>/<unit>` generate a new unit with the units divided ie `m/s` does what you might expect and produces a new unit with meters per second. NOTE: `m/m` will produce `1` it will not automatically produce a `pu` though we are looking at how to make a 'pu_m\*m=m' so units like strain might work smoothly.

- `bool <unit>==<unit>` compare two units. this does a rounding compare so there is some tolerance to roughly 7 significant digits for \<unit> and 13 significant digits for <precise_unit>.
- `bool <unit>!=<unit>` the opposite of `==`

precise_units can usually operate with a `precise_unit` or `unit`, `unit` usually can't operate on `precise_unit`.

### Measurement Operations

- `<measurement>(val, <unit>)` construct a unit from a value and unit object.
- `double value() const` get the measurement value as a double.
- `<measurement> convert_to(<unit>) const` convert the value in the measurement to another unit base
- `<measurement> convert_to_base() const` convert to a base unit, i.e. a unit whose multiplier is 1.0
- `<unit> units() const` get the units used as a basis for the measurement
- `<unit> as_unit() const` take the measurement as is and convert it into a single unit. For Examples say a was 10 m. calling as_unit() on that measurement would produce a unit with a multiplier of 10 and a base of meters.
- `double value_as(<unit>)` get the value of a measurement as if it were measured in \<unit\>

#### Measurement operators

There are several operator overloads which work on measurements or units to produce measurements.

- `'*', '/', '+','-'` are all defined for mathematical operations on a measurement and produce another measurement.
- `%` `*`, and `/` are defined for \<measurement>\<op>\<double>
- `*`, and `/` are defined for \<double>\<op>\<measurement>

Notes: for regular measurements, `+` and `-` are not defined for doubles due to ambiguity of what that operation means. For `fixed_measurement` types this is defined as the units are known at construction and cannot change. For `fixed_measurement` types if the operator would produce a new measurement with the same units it will be a fixed measurement, if not it reverts to a regular measurement.

- `==`, `!=`, `>`, `<`, `>=`, `<=` are defined for all measurement comparisons
- `<measurement>=<double>*<unit>`
- `<measurement>=<unit>*<double>`
- `<measurement>=<unit>/<double>`
- `<measurement>=<double>/<unit>` basically calling a number multiplied or divided by a `<unit>` produces a measurement, specifically `unit` produces a measurement and `precise_unit` produces a precise_measurement.

## Contributions

Contributions are welcome. See [Contributing](./CONTRIBUTING.md) for more details and [Contributors](./CONTRIBUTORS.md) for a list of the current and past Contributors to this project.

## Project Using the Units Library

Anyone else using the units library? Please let us know.

- [HELICS](www.helics.org)
- [GridDyn](https://github.com/LLNL/GridDyn)
- [scipp](https://scipp.github.io/)

## Release

This units library is distributed under the terms of the BSD-3 clause license. All new
contributions must be made under this license. [LICENSE](./LICENSE)

SPDX-License-Identifier: BSD-3-Clause

LLNL-CODE-773786
2 changes: 1 addition & 1 deletion python/units_llnl/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .units_llnl_ext import unit, measurement,convert,convert_pu,default_unit, __doc__
from .units_llnl_ext import unit, measurement, convert, convert_pu, default_unit, __doc__
25 changes: 24 additions & 1 deletion python/units_python.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ NB_MODULE(units_llnl_ext, mod)
},
"check if two units are equivalent in the non-counting units portion of the units (moles|radians|count)")
.def(
"is_convertible",
"is_convertible_to",
[](const units::precise_unit& type1,
const units::precise_unit& type2) {
return type1.is_convertible(type2);
Expand All @@ -108,6 +108,29 @@ NB_MODULE(units_llnl_ext, mod)
units::unit_from_string(std::string(desired_units)));
},
"check if the unit can be converted to the desired unit")
.def(
"convert",
[](units::precise_unit* unit,
double value,
const units::precise_unit& convert_to_units) {
return units::convert(value, *unit, convert_to_units);
},
"value"_a,
"unit_out"_a,
"value represented by one unit in terms of another")
.def(
"convert",
[](units::precise_unit* unit,
double value,
const char* convert_to_units) {
return units::convert(
value,
*unit,
units::unit_from_string(std::string(convert_to_units)));
},
"value"_a,
"unit_out"_a,
"value represented by one unit in terms of another")
.def(
"is_default",
&units::precise_unit::is_default,
Expand Down
Loading

0 comments on commit a713814

Please sign in to comment.