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 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.
@@ -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.
@@ -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)