Skip to content

Commit a713814

Browse files
some tweaks to the python library add convert as method on unit object (#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>
1 parent b8bf817 commit a713814

11 files changed

+518
-211
lines changed

.github/workflows/wheels.yml

+66-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ name: Wheels
22

33
on:
44
workflow_dispatch:
5-
pull_request:
65
push:
76
branches:
87
- main
@@ -77,4 +76,69 @@ jobs:
7776
- uses: pypa/gh-action-pypi-publish@release/v1
7877
with:
7978
verbose: true
80-
repository-url: https://test.pypi.org/legacy/
79+
repository-url: https://test.pypi.org/legacy/
80+
publish-to-pypi:
81+
name: >-
82+
Publish Python 🐍 distribution 📦 to PyPI
83+
if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
84+
needs: [build_wheels, build_sdist]
85+
runs-on: ubuntu-latest
86+
environment:
87+
name: pypi
88+
url: https://pypi.org/p/units_llnl # Replace <package-name> with your PyPI project name
89+
permissions:
90+
id-token: write # IMPORTANT: mandatory for trusted publishing
91+
92+
steps:
93+
- name: Download all the dists
94+
uses: actions/download-artifact@v4
95+
with:
96+
pattern: dist-*
97+
merge-multiple: true
98+
path: dist/
99+
- name: Publish distribution 📦 to PyPI
100+
uses: pypa/gh-action-pypi-publish@release/v1
101+
102+
github-release:
103+
name: >-
104+
Sign the Python 🐍 distribution 📦 with Sigstore
105+
and upload them to GitHub Release
106+
needs:
107+
- publish-to-pypi
108+
runs-on: ubuntu-latest
109+
110+
permissions:
111+
contents: write # IMPORTANT: mandatory for making GitHub Releases
112+
id-token: write # IMPORTANT: mandatory for sigstore
113+
114+
steps:
115+
- name: Download all the dists
116+
uses: actions/download-artifact@v4
117+
with:
118+
pattern: dist-*
119+
merge-multiple: true
120+
path: dist/
121+
- name: Sign the dists with Sigstore
122+
uses: sigstore/gh-action-sigstore-python@v3.0.0
123+
with:
124+
inputs: >-
125+
./dist/*.tar.gz
126+
./dist/*.whl
127+
- name: Create GitHub Release
128+
env:
129+
GITHUB_TOKEN: ${{ github.token }}
130+
run: >-
131+
gh release create
132+
'${{ github.ref_name }}'
133+
--repo '${{ github.repository }}'
134+
--notes ""
135+
- name: Upload artifact signatures to GitHub Release
136+
env:
137+
GITHUB_TOKEN: ${{ github.token }}
138+
# Upload to GitHub Release using the `gh` CLI.
139+
# `dist/` contains the built packages, and the
140+
# sigstore-produced signatures and certificates.
141+
run: >-
142+
gh release upload
143+
'${{ github.ref_name }}' dist/**
144+
--repo '${{ github.repository }}'

.pre-commit-config.yaml

+11-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ repos:
1919
rev: v4.0.0-alpha.8
2020
hooks:
2121
- id: prettier
22+
- repo: https://github.com/psf/black
23+
rev: 24.10.0
24+
hooks:
25+
- id: black
26+
args: ["--line-length=100"]
27+
- repo: https://github.com/asottile/blacken-docs
28+
rev: 1.19.1
29+
hooks:
30+
- id: blacken-docs
31+
additional_dependencies: [black==24.10]
2232
- repo: https://github.com/Lucas-C/pre-commit-hooks
2333
rev: v1.5.5
2434
hooks:
@@ -47,7 +57,7 @@ repos:
4757
- id: end-of-file-fixer
4858
- id: check-shebang-scripts-are-executable
4959
- repo: https://github.com/pre-commit/mirrors-clang-format
50-
rev: v19.1.4
60+
rev: v19.1.5
5161
hooks:
5262
- id: clang-format
5363
types_or: [c++, c]

CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ endif()
2323
project(
2424
${UNITS_CMAKE_PROJECT_NAME}
2525
LANGUAGES C CXX
26-
VERSION 0.10.0
26+
VERSION 0.10.2
2727
)
2828
include(CMakeDependentOption)
2929
include(CTest)

docs/conf.py

+3-9
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,7 @@
3838

3939
def which(program):
4040
def is_exe(fpath):
41-
return (
42-
os.path.exists(fpath)
43-
and os.access(fpath, os.X_OK)
44-
and os.path.isfile(fpath)
45-
)
41+
return os.path.exists(fpath) and os.access(fpath, os.X_OK) and os.path.isfile(fpath)
4642

4743
def ext_candidates(fpath):
4844
yield fpath
@@ -79,7 +75,7 @@ def ext_candidates(fpath):
7975
# 'sphinx.ext.intersphinx',
8076
# 'sphinx.ext.coverage',
8177
"sphinx.ext.mathjax",
82-
"sphinx.ext.autosectionlabel"
78+
"sphinx.ext.autosectionlabel",
8379
# 'sphinx.ext.viewcode',
8480
# 'sphinx.ext.githubpages',
8581
# 'sphinx.ext.napoleon',
@@ -204,9 +200,7 @@ def ext_candidates(fpath):
204200
# Grouping the document tree into LaTeX files. List of tuples
205201
# (source start file, target name, title,
206202
# author, documentclass [howto, manual, or own class]).
207-
latex_documents = [
208-
(master_doc, "units.tex", "Units Library Documentation", "Philip Top", "manual")
209-
]
203+
latex_documents = [(master_doc, "units.tex", "Units Library Documentation", "Philip Top", "manual")]
210204

211205
# -- Options for manual page output ---------------------------------------
212206

pyproject.toml

+14-4
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ build-backend = "scikit_build_core.build"
44

55
[project]
66
name = "units_llnl"
7-
version = "0.10.0.post1"
8-
description = "Python bindings for the llnl units library"
9-
readme = "README.md"
7+
version = "0.10.2"
8+
description = "Python bindings for the LLNL units library"
9+
readme = "python/README.md"
1010
requires-python = ">=3.10"
1111
license = {text="BSD-3-Clause"}
1212
license-files = [
@@ -19,8 +19,18 @@ authors = [
1919
classifiers = [
2020
"Programming Language :: Python :: 3",
2121
"Operating System :: OS Independent",
22+
"Topic :: Scientific/Engineering",
23+
"Development Status :: 4 - Beta",
24+
]
25+
keywords = [
26+
"units",
27+
"measurement",
28+
"power systems",
29+
"co-simulation",
30+
"commodities",
31+
"science",
32+
"python",
2233
]
23-
2434
[project.urls]
2535
Homepage = "https://github.com/llnl/units"
2636
Documentation = "https://units.readthedocs.io/en/latest/"

python/README.md

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Units
2+
3+
[![codecov](https://codecov.io/gh/LLNL/units/branch/main/graph/badge.svg)](https://codecov.io/gh/LLNL/units)
4+
[![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)
5+
[![CircleCI](https://circleci.com/gh/LLNL/units.svg?style=svg)](https://circleci.com/gh/LLNL/units)
6+
[![](https://img.shields.io/badge/License-BSD-blue.svg)](https://github.com/GMLC-TDC/HELICS-src/blob/main/LICENSE)
7+
[![Documentation Status](https://readthedocs.org/projects/units/badge/?version=latest)](https://units.readthedocs.io/en/latest/?badge=latest)
8+
[![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)
9+
10+
[What's new](./CHANGELOG.md)
11+
12+
[Documentation](https://units.readthedocs.io/en/latest/)
13+
14+
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)
15+
16+
## Table of contents
17+
18+
- [Units](#units)
19+
- [Table of contents](#table-of-contents)
20+
- [Purpose](#purpose)
21+
- [Basic use case](#basic-use-case)
22+
- [Try it out](#try-it-out)
23+
- [Unit methods](#unit-methods)
24+
- [Unit Operators](#unit-operators)
25+
- [Measurement Operations](#measurement-operations)
26+
- [Measurement operators](#measurement-operators)
27+
- [Contributions](#contributions)
28+
- [Project Using the Units Library](#project-using-the-units-library)
29+
- [Release](#release)
30+
31+
## Purpose
32+
33+
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
34+
35+
1. converting units, often represented by strings, to a standardized unit set when dealing with user input and output.
36+
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.
37+
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.
38+
4. The library has its origins in power systems so support for per-unit operations was also lacking in the alternatives.
39+
5. Capture uncertainty and uncertainty calculations directly with a measurement
40+
41+
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.
42+
43+
### Basic use case
44+
45+
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
46+
47+
```python
48+
from units_llnl import unit
49+
50+
u1 = unit("m")
51+
u2 = unit("cm")
52+
v1 = u1.convert(10, u2)
53+
assert v1 == 10 * 100
54+
55+
v2 = u1.convert(unit_out=u2, value=20)
56+
assert v2 == 2000
57+
```
58+
59+
```python
60+
from units_llnl import measurement
61+
62+
m1 = measurement("10 m")
63+
m2 = measurement("2.5 s")
64+
m3 = m1 / m2
65+
m4 = measurement("4.0 m/s")
66+
assert m3 == m4
67+
```
68+
69+
## Try it out
70+
71+
If you want to try out the string conversion components. There is server running that can do the string conversions
72+
73+
[Unit String Conversions](https://units.readthedocs.io/en/latest/_static/convert.html)
74+
75+
For more details see the [documentation](https://units.readthedocs.io/en/latest/web/index.html)
76+
77+
### Unit methods
78+
79+
These operations apply to units and precise_units
80+
81+
- `<unit>(<unit_data>)` construct from a base unit_data
82+
- `<unit>(<unit_data>, double multiplier)` construct a unit from a base data and a multiplier
83+
- `<unit>(double multiplier, <unit>)` construct from a multiplier and another unit
84+
- also available are copy constructor and copy assignments
85+
- `<unit> inv()` generate a new unit containing the inverse unit `m.inv()= 1/m`
86+
- `<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.
87+
- `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
88+
- `bool has_same_base(<unit>|<unit_data>)` check if the <unit_data> is the same
89+
- `equivalent_non_counting(<unit>|<unit_data>)` check if the units are equivalent ignoring the counting bases
90+
- `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.
91+
- `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.
92+
- `bool is_per_unit()` true if the unit has the per_unit flag active
93+
- `bool is_equation()` true if the unit has the equation flag active
94+
- `bool has_i_flag()` true if the i_flag is marked active
95+
- `bool has_e_flag()` true if the e_flag is marked active
96+
- `double multiplier()` return the unit multiplier as a double
97+
98+
- `commodity()` get the commodity of the unit
99+
- `commodity(int commodity)` assign a commodity to the precise_unit.
100+
101+
#### Unit Operators
102+
103+
There are also several operator overloads that apply to units and precise_units.
104+
105+
- `<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`
106+
- `<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.
107+
108+
- `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>.
109+
- `bool <unit>!=<unit>` the opposite of `==`
110+
111+
precise_units can usually operate with a `precise_unit` or `unit`, `unit` usually can't operate on `precise_unit`.
112+
113+
### Measurement Operations
114+
115+
- `<measurement>(val, <unit>)` construct a unit from a value and unit object.
116+
- `double value() const` get the measurement value as a double.
117+
- `<measurement> convert_to(<unit>) const` convert the value in the measurement to another unit base
118+
- `<measurement> convert_to_base() const` convert to a base unit, i.e. a unit whose multiplier is 1.0
119+
- `<unit> units() const` get the units used as a basis for the measurement
120+
- `<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.
121+
- `double value_as(<unit>)` get the value of a measurement as if it were measured in \<unit\>
122+
123+
#### Measurement operators
124+
125+
There are several operator overloads which work on measurements or units to produce measurements.
126+
127+
- `'*', '/', '+','-'` are all defined for mathematical operations on a measurement and produce another measurement.
128+
- `%` `*`, and `/` are defined for \<measurement>\<op>\<double>
129+
- `*`, and `/` are defined for \<double>\<op>\<measurement>
130+
131+
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.
132+
133+
- `==`, `!=`, `>`, `<`, `>=`, `<=` are defined for all measurement comparisons
134+
- `<measurement>=<double>*<unit>`
135+
- `<measurement>=<unit>*<double>`
136+
- `<measurement>=<unit>/<double>`
137+
- `<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.
138+
139+
## Contributions
140+
141+
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.
142+
143+
## Project Using the Units Library
144+
145+
Anyone else using the units library? Please let us know.
146+
147+
- [HELICS](www.helics.org)
148+
- [GridDyn](https://github.com/LLNL/GridDyn)
149+
- [scipp](https://scipp.github.io/)
150+
151+
## Release
152+
153+
This units library is distributed under the terms of the BSD-3 clause license. All new
154+
contributions must be made under this license. [LICENSE](./LICENSE)
155+
156+
SPDX-License-Identifier: BSD-3-Clause
157+
158+
LLNL-CODE-773786

python/units_llnl/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .units_llnl_ext import unit, measurement,convert,convert_pu,default_unit, __doc__
1+
from .units_llnl_ext import unit, measurement, convert, convert_pu, default_unit, __doc__

python/units_python.cpp

+24-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ NB_MODULE(units_llnl_ext, mod)
9595
},
9696
"check if two units are equivalent in the non-counting units portion of the units (moles|radians|count)")
9797
.def(
98-
"is_convertible",
98+
"is_convertible_to",
9999
[](const units::precise_unit& type1,
100100
const units::precise_unit& type2) {
101101
return type1.is_convertible(type2);
@@ -108,6 +108,29 @@ NB_MODULE(units_llnl_ext, mod)
108108
units::unit_from_string(std::string(desired_units)));
109109
},
110110
"check if the unit can be converted to the desired unit")
111+
.def(
112+
"convert",
113+
[](units::precise_unit* unit,
114+
double value,
115+
const units::precise_unit& convert_to_units) {
116+
return units::convert(value, *unit, convert_to_units);
117+
},
118+
"value"_a,
119+
"unit_out"_a,
120+
"value represented by one unit in terms of another")
121+
.def(
122+
"convert",
123+
[](units::precise_unit* unit,
124+
double value,
125+
const char* convert_to_units) {
126+
return units::convert(
127+
value,
128+
*unit,
129+
units::unit_from_string(std::string(convert_to_units)));
130+
},
131+
"value"_a,
132+
"unit_out"_a,
133+
"value represented by one unit in terms of another")
111134
.def(
112135
"is_default",
113136
&units::precise_unit::is_default,

0 commit comments

Comments
 (0)