hartreez 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hartreez-0.1.0/LICENSE +21 -0
- hartreez-0.1.0/PKG-INFO +156 -0
- hartreez-0.1.0/README.md +130 -0
- hartreez-0.1.0/pyproject.toml +65 -0
- hartreez-0.1.0/src/hartreez/__init__.py +78 -0
- hartreez-0.1.0/src/hartreez/cli.py +81 -0
- hartreez-0.1.0/src/hartreez/constants.py +79 -0
- hartreez-0.1.0/src/hartreez/dimensions.py +65 -0
- hartreez-0.1.0/src/hartreez/errors.py +29 -0
- hartreez-0.1.0/src/hartreez/mapping.py +293 -0
- hartreez-0.1.0/src/hartreez/metadata.py +32 -0
- hartreez-0.1.0/src/hartreez/parser.py +112 -0
- hartreez-0.1.0/src/hartreez/py.typed +0 -0
- hartreez-0.1.0/src/hartreez/pydantic.py +91 -0
- hartreez-0.1.0/src/hartreez/systems.py +277 -0
- hartreez-0.1.0/src/hartreez/units.py +190 -0
hartreez-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Plé
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
hartreez-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hartreez
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dependency-free atomistic unit conversion for numerical Python code
|
|
5
|
+
Keywords: units,unit-conversion,atomic-units,atomistic,molecular-dynamics,physical-constants,hartree
|
|
6
|
+
Author: Thomas Plé
|
|
7
|
+
Author-email: Thomas Plé <thomas.ple@sorbonne-universite.fr>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: pydantic>=2 ; extra == 'pydantic'
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Project-URL: Documentation, https://github.com/thomasple/hartreez/tree/main/docs
|
|
22
|
+
Project-URL: Issues, https://github.com/thomasple/hartreez/issues
|
|
23
|
+
Project-URL: Repository, https://github.com/thomasple/hartreez
|
|
24
|
+
Provides-Extra: pydantic
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# hartreez
|
|
28
|
+
|
|
29
|
+
`hartreez` provides unit conversion and coherent working systems for atomistic
|
|
30
|
+
calculations. Convert lengths, energies, forces, and other quantities using
|
|
31
|
+
familiar units such as angstrom, bohr, eV, and Hartree. Values remain ordinary
|
|
32
|
+
numbers and arrays, with no quantity wrappers or core runtime dependencies.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install hartreez
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires Python 3.10 or newer. For optional Pydantic v2 input validation:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
pip install 'hartreez[pydantic]'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Work in atomic units
|
|
47
|
+
|
|
48
|
+
Choose a working system, convert input values into it, and express results in
|
|
49
|
+
the units you need. The built-in `au` system uses bohr for length and Hartree
|
|
50
|
+
for energy:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from hartreez import au
|
|
54
|
+
|
|
55
|
+
energy_eV = 1.25
|
|
56
|
+
force_eV_per_angstrom = -0.4
|
|
57
|
+
|
|
58
|
+
energy_au = energy_eV * au.EV
|
|
59
|
+
force_au = au.to_internal(force_eV_per_angstrom, "eV/angstrom")
|
|
60
|
+
|
|
61
|
+
# Express values from the working system in external units.
|
|
62
|
+
energy_out_eV = au.from_internal(energy_au, "eV")
|
|
63
|
+
force_out_eV_per_angstrom = au.from_internal(force_au, "eV/angstrom")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Factors are ordinary floats; a named factor is one external unit expressed
|
|
67
|
+
in the active system, so multiply the external value by it. `to_internal`
|
|
68
|
+
multiplies by this factor; `from_internal` applies its reciprocal.
|
|
69
|
+
Array-like inputs keep control of their arithmetic through scalar
|
|
70
|
+
multiplication; the core neither imports nor coerces NumPy, PyTorch, or other
|
|
71
|
+
array libraries.
|
|
72
|
+
|
|
73
|
+
## Choose a working system
|
|
74
|
+
|
|
75
|
+
Define a coherent system by choosing exactly three mechanical units from
|
|
76
|
+
length, time, energy, and mass. `hartreez` derives the fourth using
|
|
77
|
+
`E = M L^2 / T^2`. For example, choose angstrom, picosecond, and kcal/mol;
|
|
78
|
+
mass is derived:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from hartreez import UnitSystem
|
|
82
|
+
|
|
83
|
+
units = UnitSystem(length="angstrom", time="ps", energy="kcal/mol")
|
|
84
|
+
|
|
85
|
+
energy = units.to_internal(2.0, "eV")
|
|
86
|
+
time_ps = units.to_internal(0.5, "fs")
|
|
87
|
+
energy_out_eV = units.from_internal(energy, "eV")
|
|
88
|
+
thermal_energy = 300.0 * units.K_B # kcal/mol at 300 K
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`units.K_B` is Boltzmann's constant in this system. Other physical constants,
|
|
92
|
+
such as `au.HBAR`, are available on the corresponding system as ordinary
|
|
93
|
+
floats; see [physical constants](https://github.com/thomasple/hartreez/blob/main/docs/constants.md).
|
|
94
|
+
|
|
95
|
+
## Unit conventions
|
|
96
|
+
|
|
97
|
+
- `mol` represents Avogadro's particle count and is dimensionless, so
|
|
98
|
+
`kcal/mol` and `kJ/mol` are energy units.
|
|
99
|
+
- Spectroscopic `cm1` is cyclic frequency (`f = c*wavenumber`). Use `1/cm`
|
|
100
|
+
or `cm^-1` for geometric inverse length; multiply by `2*pi` when angular
|
|
101
|
+
frequency is needed.
|
|
102
|
+
|
|
103
|
+
See the [unit reference](https://github.com/thomasple/hartreez/blob/main/docs/units.md) for supported names, expression syntax,
|
|
104
|
+
and details of these conventions.
|
|
105
|
+
|
|
106
|
+
## Convert mappings
|
|
107
|
+
|
|
108
|
+
Convert unit-annotated values in an input mapping into your working system:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from hartreez import UnitSystem, convert_mapping
|
|
112
|
+
|
|
113
|
+
units = UnitSystem(length="angstrom", time="ps", energy="kcal/mol")
|
|
114
|
+
inputs = convert_mapping({"time[fs]": 0.5, "energy[eV]": 2.0}, units)
|
|
115
|
+
assert inputs["time"] == 0.0005
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
See [mapping conversion](https://github.com/thomasple/hartreez/blob/main/docs/mappings.md) for nested data and dimensional
|
|
119
|
+
checks.
|
|
120
|
+
|
|
121
|
+
## Validate inputs with Pydantic
|
|
122
|
+
|
|
123
|
+
If you use Pydantic to describe calculation inputs, the optional integration
|
|
124
|
+
converts unit-annotated values and checks their dimensions before validating
|
|
125
|
+
field types and constraints. Define your input model with `UnitAwareModel`
|
|
126
|
+
and supply the working system when validating:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from typing import Annotated
|
|
130
|
+
|
|
131
|
+
from hartreez import UnitDimension, au
|
|
132
|
+
from hartreez.pydantic import UnitAwareModel
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class CalculationInput(UnitAwareModel):
|
|
136
|
+
energy: Annotated[float, UnitDimension("eV")]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
inputs = CalculationInput.model_validate(
|
|
140
|
+
{"energy[eV]": 2.0}, context={"unit_system": au}
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Here, `UnitDimension("eV")` requires an energy value, and the supplied `au`
|
|
145
|
+
system makes `inputs.energy` a value in Hartree. See the
|
|
146
|
+
[Pydantic integration guide](https://github.com/thomasple/hartreez/blob/main/docs/pydantic.md) for nested models and validation
|
|
147
|
+
details.
|
|
148
|
+
|
|
149
|
+
## Use the command line
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
hartreez convert 1 eV Ha --verbose
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
See the [CLI guide](https://github.com/thomasple/hartreez/blob/main/docs/cli.md) for unit factors, constants, and vocabulary
|
|
156
|
+
queries.
|
hartreez-0.1.0/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# hartreez
|
|
2
|
+
|
|
3
|
+
`hartreez` provides unit conversion and coherent working systems for atomistic
|
|
4
|
+
calculations. Convert lengths, energies, forces, and other quantities using
|
|
5
|
+
familiar units such as angstrom, bohr, eV, and Hartree. Values remain ordinary
|
|
6
|
+
numbers and arrays, with no quantity wrappers or core runtime dependencies.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pip install hartreez
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Requires Python 3.10 or newer. For optional Pydantic v2 input validation:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
pip install 'hartreez[pydantic]'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Work in atomic units
|
|
21
|
+
|
|
22
|
+
Choose a working system, convert input values into it, and express results in
|
|
23
|
+
the units you need. The built-in `au` system uses bohr for length and Hartree
|
|
24
|
+
for energy:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from hartreez import au
|
|
28
|
+
|
|
29
|
+
energy_eV = 1.25
|
|
30
|
+
force_eV_per_angstrom = -0.4
|
|
31
|
+
|
|
32
|
+
energy_au = energy_eV * au.EV
|
|
33
|
+
force_au = au.to_internal(force_eV_per_angstrom, "eV/angstrom")
|
|
34
|
+
|
|
35
|
+
# Express values from the working system in external units.
|
|
36
|
+
energy_out_eV = au.from_internal(energy_au, "eV")
|
|
37
|
+
force_out_eV_per_angstrom = au.from_internal(force_au, "eV/angstrom")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Factors are ordinary floats; a named factor is one external unit expressed
|
|
41
|
+
in the active system, so multiply the external value by it. `to_internal`
|
|
42
|
+
multiplies by this factor; `from_internal` applies its reciprocal.
|
|
43
|
+
Array-like inputs keep control of their arithmetic through scalar
|
|
44
|
+
multiplication; the core neither imports nor coerces NumPy, PyTorch, or other
|
|
45
|
+
array libraries.
|
|
46
|
+
|
|
47
|
+
## Choose a working system
|
|
48
|
+
|
|
49
|
+
Define a coherent system by choosing exactly three mechanical units from
|
|
50
|
+
length, time, energy, and mass. `hartreez` derives the fourth using
|
|
51
|
+
`E = M L^2 / T^2`. For example, choose angstrom, picosecond, and kcal/mol;
|
|
52
|
+
mass is derived:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from hartreez import UnitSystem
|
|
56
|
+
|
|
57
|
+
units = UnitSystem(length="angstrom", time="ps", energy="kcal/mol")
|
|
58
|
+
|
|
59
|
+
energy = units.to_internal(2.0, "eV")
|
|
60
|
+
time_ps = units.to_internal(0.5, "fs")
|
|
61
|
+
energy_out_eV = units.from_internal(energy, "eV")
|
|
62
|
+
thermal_energy = 300.0 * units.K_B # kcal/mol at 300 K
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`units.K_B` is Boltzmann's constant in this system. Other physical constants,
|
|
66
|
+
such as `au.HBAR`, are available on the corresponding system as ordinary
|
|
67
|
+
floats; see [physical constants](https://github.com/thomasple/hartreez/blob/main/docs/constants.md).
|
|
68
|
+
|
|
69
|
+
## Unit conventions
|
|
70
|
+
|
|
71
|
+
- `mol` represents Avogadro's particle count and is dimensionless, so
|
|
72
|
+
`kcal/mol` and `kJ/mol` are energy units.
|
|
73
|
+
- Spectroscopic `cm1` is cyclic frequency (`f = c*wavenumber`). Use `1/cm`
|
|
74
|
+
or `cm^-1` for geometric inverse length; multiply by `2*pi` when angular
|
|
75
|
+
frequency is needed.
|
|
76
|
+
|
|
77
|
+
See the [unit reference](https://github.com/thomasple/hartreez/blob/main/docs/units.md) for supported names, expression syntax,
|
|
78
|
+
and details of these conventions.
|
|
79
|
+
|
|
80
|
+
## Convert mappings
|
|
81
|
+
|
|
82
|
+
Convert unit-annotated values in an input mapping into your working system:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from hartreez import UnitSystem, convert_mapping
|
|
86
|
+
|
|
87
|
+
units = UnitSystem(length="angstrom", time="ps", energy="kcal/mol")
|
|
88
|
+
inputs = convert_mapping({"time[fs]": 0.5, "energy[eV]": 2.0}, units)
|
|
89
|
+
assert inputs["time"] == 0.0005
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
See [mapping conversion](https://github.com/thomasple/hartreez/blob/main/docs/mappings.md) for nested data and dimensional
|
|
93
|
+
checks.
|
|
94
|
+
|
|
95
|
+
## Validate inputs with Pydantic
|
|
96
|
+
|
|
97
|
+
If you use Pydantic to describe calculation inputs, the optional integration
|
|
98
|
+
converts unit-annotated values and checks their dimensions before validating
|
|
99
|
+
field types and constraints. Define your input model with `UnitAwareModel`
|
|
100
|
+
and supply the working system when validating:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from typing import Annotated
|
|
104
|
+
|
|
105
|
+
from hartreez import UnitDimension, au
|
|
106
|
+
from hartreez.pydantic import UnitAwareModel
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class CalculationInput(UnitAwareModel):
|
|
110
|
+
energy: Annotated[float, UnitDimension("eV")]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
inputs = CalculationInput.model_validate(
|
|
114
|
+
{"energy[eV]": 2.0}, context={"unit_system": au}
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Here, `UnitDimension("eV")` requires an energy value, and the supplied `au`
|
|
119
|
+
system makes `inputs.energy` a value in Hartree. See the
|
|
120
|
+
[Pydantic integration guide](https://github.com/thomasple/hartreez/blob/main/docs/pydantic.md) for nested models and validation
|
|
121
|
+
details.
|
|
122
|
+
|
|
123
|
+
## Use the command line
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
hartreez convert 1 eV Ha --verbose
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
See the [CLI guide](https://github.com/thomasple/hartreez/blob/main/docs/cli.md) for unit factors, constants, and vocabulary
|
|
130
|
+
queries.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hartreez"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Dependency-free atomistic unit conversion for numerical Python code"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Thomas Plé", email = "thomas.ple@sorbonne-universite.fr" }
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = []
|
|
13
|
+
keywords = [
|
|
14
|
+
"units",
|
|
15
|
+
"unit-conversion",
|
|
16
|
+
"atomic-units",
|
|
17
|
+
"atomistic",
|
|
18
|
+
"molecular-dynamics",
|
|
19
|
+
"physical-constants",
|
|
20
|
+
"hartree",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 4 - Beta",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"Intended Audience :: Science/Research",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
+
"Topic :: Scientific/Engineering :: Chemistry",
|
|
30
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
31
|
+
"Typing :: Typed",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Repository = "https://github.com/thomasple/hartreez"
|
|
36
|
+
Issues = "https://github.com/thomasple/hartreez/issues"
|
|
37
|
+
Documentation = "https://github.com/thomasple/hartreez/tree/main/docs"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"pyright>=1.1.414",
|
|
42
|
+
"pytest>=9.1.1",
|
|
43
|
+
"pydantic>=2",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.scripts]
|
|
47
|
+
hartreez = "hartreez:main"
|
|
48
|
+
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
pydantic = [
|
|
51
|
+
"pydantic>=2",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[build-system]
|
|
55
|
+
requires = ["uv_build>=0.8.17,<0.9.0"]
|
|
56
|
+
build-backend = "uv_build"
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
addopts = "-ra"
|
|
61
|
+
|
|
62
|
+
[tool.pyright]
|
|
63
|
+
include = ["src", "tests"]
|
|
64
|
+
pythonVersion = "3.10"
|
|
65
|
+
typeCheckingMode = "strict"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Small, dependency-free SI unit parsing and conversion for numeric code.
|
|
2
|
+
|
|
3
|
+
Importing :mod:`hartreez` only loads its small standard-library registry. No
|
|
4
|
+
optional numerical or validation packages are imported.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from hartreez.constants import constant_names
|
|
12
|
+
from hartreez.dimensions import Dimensions
|
|
13
|
+
from hartreez.errors import (
|
|
14
|
+
IncompatibleUnitsError,
|
|
15
|
+
MappingConversionError,
|
|
16
|
+
ReferenceUnitsError,
|
|
17
|
+
UnitError,
|
|
18
|
+
UnitSyntaxError,
|
|
19
|
+
UnitSystemError,
|
|
20
|
+
UnknownUnitError,
|
|
21
|
+
)
|
|
22
|
+
from hartreez.mapping import convert_mapping
|
|
23
|
+
from hartreez.metadata import UnitDimension
|
|
24
|
+
from hartreez.parser import parse_unit
|
|
25
|
+
from hartreez.systems import UnitSystem, au
|
|
26
|
+
from hartreez.units import Unit, canonical_unit_names, unit_aliases, unit_names
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def convert(value: Any, from_unit: str | Unit, to_unit: str | Unit) -> Any:
|
|
30
|
+
"""Convert ``value`` between compatible units by multiplying by a float.
|
|
31
|
+
|
|
32
|
+
Values are not wrapped or coerced. The caller's value must support
|
|
33
|
+
multiplication by a Python ``float``.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
source = parse_unit(from_unit) if isinstance(from_unit, str) else from_unit
|
|
37
|
+
destination = parse_unit(to_unit) if isinstance(to_unit, str) else to_unit
|
|
38
|
+
if type(source) is not Unit:
|
|
39
|
+
raise TypeError("from_unit must be a unit expression or Unit")
|
|
40
|
+
if type(destination) is not Unit:
|
|
41
|
+
raise TypeError("to_unit must be a unit expression or Unit")
|
|
42
|
+
if source.dimensions != destination.dimensions:
|
|
43
|
+
raise IncompatibleUnitsError(
|
|
44
|
+
f"cannot convert from dimensions {source.dimensions} to {destination.dimensions}"
|
|
45
|
+
)
|
|
46
|
+
return value * (source.scale / destination.scale)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main() -> int:
|
|
50
|
+
"""Run the standard-library command line interface."""
|
|
51
|
+
|
|
52
|
+
from hartreez.cli import main as cli_main
|
|
53
|
+
|
|
54
|
+
return cli_main()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"Dimensions",
|
|
59
|
+
"IncompatibleUnitsError",
|
|
60
|
+
"MappingConversionError",
|
|
61
|
+
"ReferenceUnitsError",
|
|
62
|
+
"Unit",
|
|
63
|
+
"UnitDimension",
|
|
64
|
+
"UnitError",
|
|
65
|
+
"UnitSyntaxError",
|
|
66
|
+
"UnitSystem",
|
|
67
|
+
"UnitSystemError",
|
|
68
|
+
"UnknownUnitError",
|
|
69
|
+
"au",
|
|
70
|
+
"canonical_unit_names",
|
|
71
|
+
"constant_names",
|
|
72
|
+
"convert",
|
|
73
|
+
"convert_mapping",
|
|
74
|
+
"main",
|
|
75
|
+
"parse_unit",
|
|
76
|
+
"unit_aliases",
|
|
77
|
+
"unit_names",
|
|
78
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Standard-library command line interface for hartreez."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
from hartreez import au, canonical_unit_names, constant_names, convert, unit_aliases
|
|
10
|
+
from hartreez.errors import UnitError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parser() -> argparse.ArgumentParser:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="hartreez",
|
|
16
|
+
description="Convert atomistic units and query atomic-unit factors and constants.",
|
|
17
|
+
)
|
|
18
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
19
|
+
|
|
20
|
+
conversion = commands.add_parser("convert", help="convert a numeric value between units")
|
|
21
|
+
conversion.add_argument("value", type=float, help="numeric value to convert")
|
|
22
|
+
conversion.add_argument("from_unit", help="source unit expression")
|
|
23
|
+
conversion.add_argument("to_unit", help="destination unit expression")
|
|
24
|
+
conversion.add_argument(
|
|
25
|
+
"--verbose", "-v", action="store_true", help="show a readable conversion equality"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
factor = commands.add_parser("factor", help="query one unit expressed in atomic units")
|
|
29
|
+
factor.add_argument("unit", help="unit expression")
|
|
30
|
+
|
|
31
|
+
constant = commands.add_parser("constant", help="query a physical constant in atomic units")
|
|
32
|
+
constant.add_argument("name", help="constant name, for example K_B or HBAR")
|
|
33
|
+
|
|
34
|
+
commands.add_parser("units", help="list canonical unit spellings")
|
|
35
|
+
commands.add_parser(
|
|
36
|
+
"aliases", help="list aliases and their canonical spellings or equivalent expressions"
|
|
37
|
+
)
|
|
38
|
+
commands.add_parser("constants", help="list supported physical constant names")
|
|
39
|
+
return parser
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _number(value: float) -> str:
|
|
43
|
+
return format(value, ".15g")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
47
|
+
"""Run the CLI and return a process status code."""
|
|
48
|
+
|
|
49
|
+
parser = _parser()
|
|
50
|
+
args = parser.parse_args(argv)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
if args.command == "convert":
|
|
54
|
+
result = convert(args.value, args.from_unit, args.to_unit)
|
|
55
|
+
if args.verbose:
|
|
56
|
+
print(
|
|
57
|
+
f"{_number(args.value)} {args.from_unit} = "
|
|
58
|
+
f"{_number(result)} {args.to_unit}"
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
print(result)
|
|
62
|
+
elif args.command == "factor":
|
|
63
|
+
print(au.factor_from(args.unit))
|
|
64
|
+
elif args.command == "constant":
|
|
65
|
+
print(au.constant(args.name))
|
|
66
|
+
elif args.command == "units":
|
|
67
|
+
print("\n".join(canonical_unit_names()))
|
|
68
|
+
elif args.command == "aliases":
|
|
69
|
+
print("\n".join(f"{alias} -> {canonical}" for alias, canonical in sorted(unit_aliases().items())))
|
|
70
|
+
elif args.command == "constants":
|
|
71
|
+
print("\n".join(constant_names()))
|
|
72
|
+
else: # pragma: no cover - argparse enforces the command set
|
|
73
|
+
parser.error("a command is required")
|
|
74
|
+
except (UnitError, ValueError, TypeError) as error:
|
|
75
|
+
print(f"hartreez: error: {error}", file=sys.stderr)
|
|
76
|
+
return 2
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Source physical constants, independent of the unit registry.
|
|
2
|
+
|
|
3
|
+
Measured inputs are central values from CODATA 2022. Exact SI defining
|
|
4
|
+
constants are kept exact. Derived atomic units share these inputs so that
|
|
5
|
+
their identities remain internally consistent.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from math import pi
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
from typing import Mapping
|
|
12
|
+
|
|
13
|
+
from hartreez.dimensions import Dimensions
|
|
14
|
+
|
|
15
|
+
# Exact SI defining constants.
|
|
16
|
+
C_LIGHT = 299_792_458.0 # m s^-1
|
|
17
|
+
PLANCK = 6.626_070_15e-34 # J s
|
|
18
|
+
E_CHARGE = 1.602_176_634e-19 # C
|
|
19
|
+
K_B = 1.380_649e-23 # J K^-1
|
|
20
|
+
N_A = 6.022_140_76e23 # mol^-1; exposed as a dimensionless count
|
|
21
|
+
|
|
22
|
+
# CODATA 2022 measured central values.
|
|
23
|
+
ALPHA = 7.297_352_5643e-3
|
|
24
|
+
M_E = 9.109_383_7139e-31 # kg
|
|
25
|
+
M_P = 1.672_621_92595e-27 # kg
|
|
26
|
+
M_U = 1.660_539_06892e-27 # kg, atomic mass constant (Dalton)
|
|
27
|
+
|
|
28
|
+
# Derived values use the shared source inputs above.
|
|
29
|
+
HBAR = PLANCK / (2.0 * pi)
|
|
30
|
+
BOHR_RADIUS = HBAR / (M_E * C_LIGHT * ALPHA) # m
|
|
31
|
+
HARTREE_ENERGY = ALPHA**2 * M_E * C_LIGHT**2 # J
|
|
32
|
+
ATOMIC_TIME = HBAR / HARTREE_ENERGY # s
|
|
33
|
+
RYDBERG_ENERGY = HARTREE_ENERGY / 2.0 # J
|
|
34
|
+
DEBYE = 1.0e-21 / C_LIGHT # C m; exact SI conversion under definition
|
|
35
|
+
SPECTROSCOPIC_CM1 = C_LIGHT * 100.0 # Hz per cm^-1 of wavenumber
|
|
36
|
+
|
|
37
|
+
# Dimensions use the package's independent SI bases M, L, T, Q, and Θ.
|
|
38
|
+
_MASS = Dimensions(mass=1)
|
|
39
|
+
_LENGTH = Dimensions(length=1)
|
|
40
|
+
_TIME = Dimensions(time=1)
|
|
41
|
+
_CHARGE = Dimensions(charge=1)
|
|
42
|
+
_TEMPERATURE = Dimensions(temperature=1)
|
|
43
|
+
_ENERGY = _MASS * (_LENGTH**2) / (_TIME**2)
|
|
44
|
+
_DIMENSIONLESS = Dimensions()
|
|
45
|
+
|
|
46
|
+
EPSILON_0 = E_CHARGE**2 / (4.0 * pi * ALPHA * HBAR * C_LIGHT) # F m^-1
|
|
47
|
+
K_E = 1.0 / (4.0 * pi * EPSILON_0) # N m^2 C^-2
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class PhysicalConstant:
|
|
52
|
+
"""A central SI value and its physical dimensions."""
|
|
53
|
+
|
|
54
|
+
value: float
|
|
55
|
+
dimensions: Dimensions
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
CONSTANTS: Mapping[str, PhysicalConstant] = MappingProxyType(
|
|
59
|
+
{
|
|
60
|
+
"K_B": PhysicalConstant(K_B, _ENERGY / _TEMPERATURE),
|
|
61
|
+
"HBAR": PhysicalConstant(HBAR, _ENERGY * _TIME),
|
|
62
|
+
"PLANCK": PhysicalConstant(PLANCK, _ENERGY * _TIME),
|
|
63
|
+
"C_LIGHT": PhysicalConstant(C_LIGHT, _LENGTH / _TIME),
|
|
64
|
+
"ALPHA": PhysicalConstant(ALPHA, _DIMENSIONLESS),
|
|
65
|
+
"M_E": PhysicalConstant(M_E, _MASS),
|
|
66
|
+
"M_P": PhysicalConstant(M_P, _MASS),
|
|
67
|
+
"E_CHARGE": PhysicalConstant(E_CHARGE, _CHARGE),
|
|
68
|
+
# This is a particle count and remains dimensionless in every system.
|
|
69
|
+
"N_A": PhysicalConstant(N_A, _DIMENSIONLESS),
|
|
70
|
+
"EPSILON_0": PhysicalConstant(EPSILON_0, (_CHARGE**2) / _ENERGY / _LENGTH),
|
|
71
|
+
"K_E": PhysicalConstant(K_E, _ENERGY * _LENGTH / (_CHARGE**2)),
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def constant_names() -> tuple[str, ...]:
|
|
77
|
+
"""Return the curated physical constant names alphabetically."""
|
|
78
|
+
|
|
79
|
+
return tuple(sorted(CONSTANTS))
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Exact physical dimensions for the five independent hartreez dimensions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from fractions import Fraction
|
|
7
|
+
def _fraction(value: int | float | str | Fraction) -> Fraction:
|
|
8
|
+
if isinstance(value, Fraction):
|
|
9
|
+
return value
|
|
10
|
+
if isinstance(value, int):
|
|
11
|
+
return Fraction(value)
|
|
12
|
+
if isinstance(value, float):
|
|
13
|
+
return Fraction(str(value))
|
|
14
|
+
return Fraction(value)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True, init=False)
|
|
18
|
+
class Dimensions:
|
|
19
|
+
"""Exponents of mass, length, time, charge, and temperature.
|
|
20
|
+
|
|
21
|
+
Exponents are normalized to :class:`fractions.Fraction`, so dimensional
|
|
22
|
+
equality is exact even when units are raised to fractional powers.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
mass: Fraction = Fraction(0)
|
|
26
|
+
length: Fraction = Fraction(0)
|
|
27
|
+
time: Fraction = Fraction(0)
|
|
28
|
+
charge: Fraction = Fraction(0)
|
|
29
|
+
temperature: Fraction = Fraction(0)
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
mass: int | float | str | Fraction = 0,
|
|
34
|
+
length: int | float | str | Fraction = 0,
|
|
35
|
+
time: int | float | str | Fraction = 0,
|
|
36
|
+
charge: int | float | str | Fraction = 0,
|
|
37
|
+
temperature: int | float | str | Fraction = 0,
|
|
38
|
+
) -> None:
|
|
39
|
+
object.__setattr__(self, "mass", _fraction(mass))
|
|
40
|
+
object.__setattr__(self, "length", _fraction(length))
|
|
41
|
+
object.__setattr__(self, "time", _fraction(time))
|
|
42
|
+
object.__setattr__(self, "charge", _fraction(charge))
|
|
43
|
+
object.__setattr__(self, "temperature", _fraction(temperature))
|
|
44
|
+
|
|
45
|
+
def __mul__(self, other: Dimensions) -> Dimensions:
|
|
46
|
+
if type(other) is not Dimensions:
|
|
47
|
+
return NotImplemented
|
|
48
|
+
return Dimensions(*(a + b for a, b in zip(self._exponents(), other._exponents())))
|
|
49
|
+
|
|
50
|
+
def __truediv__(self, other: Dimensions) -> Dimensions:
|
|
51
|
+
if type(other) is not Dimensions:
|
|
52
|
+
return NotImplemented
|
|
53
|
+
return Dimensions(*(a - b for a, b in zip(self._exponents(), other._exponents())))
|
|
54
|
+
|
|
55
|
+
def __pow__(self, exponent: int | float | str | Fraction) -> Dimensions:
|
|
56
|
+
power = _fraction(exponent)
|
|
57
|
+
return Dimensions(*(value * power for value in self._exponents()))
|
|
58
|
+
|
|
59
|
+
def _exponents(self) -> tuple[Fraction, Fraction, Fraction, Fraction, Fraction]:
|
|
60
|
+
return self.mass, self.length, self.time, self.charge, self.temperature
|
|
61
|
+
|
|
62
|
+
def __str__(self) -> str:
|
|
63
|
+
labels = ("M", "L", "T", "Q", "Θ")
|
|
64
|
+
terms = [f"{label}^{value}" for label, value in zip(labels, self._exponents()) if value]
|
|
65
|
+
return "1" if not terms else " ".join(terms)
|