nutils-units 0.1__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.
- nutils_units-0.1/LICENSE +21 -0
- nutils_units-0.1/PKG-INFO +170 -0
- nutils_units-0.1/README.md +157 -0
- nutils_units-0.1/pyproject.toml +18 -0
- nutils_units-0.1/setup.cfg +4 -0
- nutils_units-0.1/src/nutils/units/__init__.py +3 -0
- nutils_units-0.1/src/nutils/units/_dispatch.py +70 -0
- nutils_units-0.1/src/nutils/units/_powers.py +157 -0
- nutils_units-0.1/src/nutils/units/_units.py +58 -0
- nutils_units-0.1/src/nutils/units/core.py +383 -0
- nutils_units-0.1/src/nutils/units/error.py +2 -0
- nutils_units-0.1/src/nutils/units/metric.py +138 -0
- nutils_units-0.1/src/nutils/units/typing.py +162 -0
- nutils_units-0.1/src/nutils_units.egg-info/PKG-INFO +170 -0
- nutils_units-0.1/src/nutils_units.egg-info/SOURCES.txt +15 -0
- nutils_units-0.1/src/nutils_units.egg-info/dependency_links.txt +1 -0
- nutils_units-0.1/src/nutils_units.egg-info/top_level.txt +1 -0
nutils_units-0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Evalf
|
|
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,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nutils-units
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Nutils Units: object wrappers for dimensional computations
|
|
5
|
+
Author-email: Evalf <info@evalf.com>
|
|
6
|
+
License-Expression: MIT AND (Apache-2.0 OR BSD-2-Clause)
|
|
7
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
8
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# Nutils Units
|
|
15
|
+
|
|
16
|
+
The nutils.units module provides a way to track dimensions of numerical objects
|
|
17
|
+
and their change as the objects pass through functions. Its purposes are
|
|
18
|
+
twofold:
|
|
19
|
+
|
|
20
|
+
1. Numerical type checking, safeguarding against such mistakes as adding two
|
|
21
|
+
objects of different dimensions together.
|
|
22
|
+
2. Unit consistency, ensuring that different metric systems can coexist without
|
|
23
|
+
crashing into Mars.
|
|
24
|
+
|
|
25
|
+
Units offers three API levels: core, metric and typing, with each building on
|
|
26
|
+
top of the former.
|
|
27
|
+
|
|
28
|
+
## Core
|
|
29
|
+
|
|
30
|
+
The core API offers the `Monomial` class, which wraps an object to assign it
|
|
31
|
+
physical dimensions. A dimension is identified by a string, such as "L" for
|
|
32
|
+
length. By wrapping the unit float we define the reference length for our
|
|
33
|
+
calculations to be one meter.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
>>> from nutils.units.core import Monomial
|
|
37
|
+
>>> meter = Monomial(1., "L")
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Once the initial unit is seeded, we can build on it to define derived units:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
>>> inch = meter * 0.0254
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This demonstrates that `Monomial` objects support numerical manipulation such
|
|
49
|
+
as multiplication. These manipulations extend to (supported) external packages
|
|
50
|
+
such as numpy:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
>>> import numpy as np
|
|
54
|
+
>>> v1 = np.array([1, -2]) * meter
|
|
55
|
+
>>> v2 = np.array([3, 1]) * inch
|
|
56
|
+
>>> array = np.stack([v1, v2])
|
|
57
|
+
>>> np.linalg.det(array)
|
|
58
|
+
np.float64(0.17779999999999999)[L2]
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The [L2] in the string representation indicates that the result has dimension
|
|
63
|
+
length squared, i.e. area. The value before it is the representation of the
|
|
64
|
+
wrapped object, but its value is shielded by the wrapper. The wrapper falls
|
|
65
|
+
away when an operation yields a dimensionless result, for instance by dividing
|
|
66
|
+
out a unit:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
>>> array / meter
|
|
70
|
+
array([[ 1. , -2. ],
|
|
71
|
+
[ 0.0762, 0.0254]])
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Crucially, dimensionless results do not depend on the definition of our
|
|
76
|
+
reference lengths, as any scaling cancels out by definition. We could have
|
|
77
|
+
defined our meter to be `Monomial(np.pi, "L")` instead and still obtained the
|
|
78
|
+
same result up to rounding errors. The numerical value of the reference length
|
|
79
|
+
is merely a conduit for the internal manipulations.
|
|
80
|
+
|
|
81
|
+
## Metric
|
|
82
|
+
|
|
83
|
+
The metric API adds support for units via the `parse` function. This returns a
|
|
84
|
+
`UMonomial` object that has access to a predefined set of units, using the SI
|
|
85
|
+
base units as internal reference measures.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
>>> from nutils.units.metric import parse
|
|
89
|
+
>>> length = parse('2cm')
|
|
90
|
+
>>> width = parse('3.5in')
|
|
91
|
+
>>> force = parse('5N')
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
We can then manipulate the `UMonomial` objects as before.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
>>> area = length * width
|
|
99
|
+
>>> pressure = force / area
|
|
100
|
+
>>> pressure / 'kPa'
|
|
101
|
+
2.8121484814398205
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Note that in dividing out the unit we omitted `parse`, which is a convenience
|
|
106
|
+
shorthand for this precise situation. For added convenience, the `UMonomial`
|
|
107
|
+
class also supports direct string formatting of the wrapped value.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
>>> f'pressure: {pressure:.1kPa}'
|
|
111
|
+
'pressure: 2.8kPa'
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The units registry is an append-only state machine that is part of the metric
|
|
116
|
+
module. Additional units can be added if necessary via the `units.register`
|
|
117
|
+
function.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
>>> from nutils.units.metric import units
|
|
121
|
+
>>> units.register("lbf", parse("4.448222N"), prefix="")
|
|
122
|
+
>>> units.register("psi", parse("lbf/in2"), prefix="")
|
|
123
|
+
>>> f'pressure: {pressure:.1psi}'
|
|
124
|
+
'pressure: 0.4psi'
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Typing
|
|
129
|
+
|
|
130
|
+
The typing API adds specific types for scalar quantities.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
>>> from nutils.units.typing import Length, Time, Velocity
|
|
134
|
+
>>> Velocity('.4km/h')
|
|
135
|
+
Quantity[L/T](.4km/h)
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The `Quantity` types function similarly to `parse`, with two differences: 1.
|
|
140
|
+
the object reduces back to its original string argument, with potential uses
|
|
141
|
+
for object introspection, and 2. it protects against using wrong units.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
>>> Velocity('.4km/g')
|
|
145
|
+
Traceback (most recent call last):
|
|
146
|
+
...
|
|
147
|
+
nutils.units.error.DimensionError: cannot parse .4km/g as L/T
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Derived quantities can be formed by operating directly on the types:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
>>> Velocity == Length / Time
|
|
155
|
+
True
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The quantity types can be used as function annotations for general readability
|
|
160
|
+
a for the potential aid external introspection tools.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
>>> def distance(velocity: Velocity, time: Time):
|
|
164
|
+
... return velocity * time
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Note that the return value of this function is not a `Length` but a general
|
|
169
|
+
`UMonomial`, as the result of a numerical operation does not have an inherent
|
|
170
|
+
unit.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Nutils Units
|
|
2
|
+
|
|
3
|
+
The nutils.units module provides a way to track dimensions of numerical objects
|
|
4
|
+
and their change as the objects pass through functions. Its purposes are
|
|
5
|
+
twofold:
|
|
6
|
+
|
|
7
|
+
1. Numerical type checking, safeguarding against such mistakes as adding two
|
|
8
|
+
objects of different dimensions together.
|
|
9
|
+
2. Unit consistency, ensuring that different metric systems can coexist without
|
|
10
|
+
crashing into Mars.
|
|
11
|
+
|
|
12
|
+
Units offers three API levels: core, metric and typing, with each building on
|
|
13
|
+
top of the former.
|
|
14
|
+
|
|
15
|
+
## Core
|
|
16
|
+
|
|
17
|
+
The core API offers the `Monomial` class, which wraps an object to assign it
|
|
18
|
+
physical dimensions. A dimension is identified by a string, such as "L" for
|
|
19
|
+
length. By wrapping the unit float we define the reference length for our
|
|
20
|
+
calculations to be one meter.
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
>>> from nutils.units.core import Monomial
|
|
24
|
+
>>> meter = Monomial(1., "L")
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Once the initial unit is seeded, we can build on it to define derived units:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
>>> inch = meter * 0.0254
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This demonstrates that `Monomial` objects support numerical manipulation such
|
|
36
|
+
as multiplication. These manipulations extend to (supported) external packages
|
|
37
|
+
such as numpy:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
>>> import numpy as np
|
|
41
|
+
>>> v1 = np.array([1, -2]) * meter
|
|
42
|
+
>>> v2 = np.array([3, 1]) * inch
|
|
43
|
+
>>> array = np.stack([v1, v2])
|
|
44
|
+
>>> np.linalg.det(array)
|
|
45
|
+
np.float64(0.17779999999999999)[L2]
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The [L2] in the string representation indicates that the result has dimension
|
|
50
|
+
length squared, i.e. area. The value before it is the representation of the
|
|
51
|
+
wrapped object, but its value is shielded by the wrapper. The wrapper falls
|
|
52
|
+
away when an operation yields a dimensionless result, for instance by dividing
|
|
53
|
+
out a unit:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
>>> array / meter
|
|
57
|
+
array([[ 1. , -2. ],
|
|
58
|
+
[ 0.0762, 0.0254]])
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Crucially, dimensionless results do not depend on the definition of our
|
|
63
|
+
reference lengths, as any scaling cancels out by definition. We could have
|
|
64
|
+
defined our meter to be `Monomial(np.pi, "L")` instead and still obtained the
|
|
65
|
+
same result up to rounding errors. The numerical value of the reference length
|
|
66
|
+
is merely a conduit for the internal manipulations.
|
|
67
|
+
|
|
68
|
+
## Metric
|
|
69
|
+
|
|
70
|
+
The metric API adds support for units via the `parse` function. This returns a
|
|
71
|
+
`UMonomial` object that has access to a predefined set of units, using the SI
|
|
72
|
+
base units as internal reference measures.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
>>> from nutils.units.metric import parse
|
|
76
|
+
>>> length = parse('2cm')
|
|
77
|
+
>>> width = parse('3.5in')
|
|
78
|
+
>>> force = parse('5N')
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
We can then manipulate the `UMonomial` objects as before.
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
>>> area = length * width
|
|
86
|
+
>>> pressure = force / area
|
|
87
|
+
>>> pressure / 'kPa'
|
|
88
|
+
2.8121484814398205
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Note that in dividing out the unit we omitted `parse`, which is a convenience
|
|
93
|
+
shorthand for this precise situation. For added convenience, the `UMonomial`
|
|
94
|
+
class also supports direct string formatting of the wrapped value.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
>>> f'pressure: {pressure:.1kPa}'
|
|
98
|
+
'pressure: 2.8kPa'
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The units registry is an append-only state machine that is part of the metric
|
|
103
|
+
module. Additional units can be added if necessary via the `units.register`
|
|
104
|
+
function.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
>>> from nutils.units.metric import units
|
|
108
|
+
>>> units.register("lbf", parse("4.448222N"), prefix="")
|
|
109
|
+
>>> units.register("psi", parse("lbf/in2"), prefix="")
|
|
110
|
+
>>> f'pressure: {pressure:.1psi}'
|
|
111
|
+
'pressure: 0.4psi'
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Typing
|
|
116
|
+
|
|
117
|
+
The typing API adds specific types for scalar quantities.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
>>> from nutils.units.typing import Length, Time, Velocity
|
|
121
|
+
>>> Velocity('.4km/h')
|
|
122
|
+
Quantity[L/T](.4km/h)
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The `Quantity` types function similarly to `parse`, with two differences: 1.
|
|
127
|
+
the object reduces back to its original string argument, with potential uses
|
|
128
|
+
for object introspection, and 2. it protects against using wrong units.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
>>> Velocity('.4km/g')
|
|
132
|
+
Traceback (most recent call last):
|
|
133
|
+
...
|
|
134
|
+
nutils.units.error.DimensionError: cannot parse .4km/g as L/T
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Derived quantities can be formed by operating directly on the types:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
>>> Velocity == Length / Time
|
|
142
|
+
True
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The quantity types can be used as function annotations for general readability
|
|
147
|
+
a for the potential aid external introspection tools.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
>>> def distance(velocity: Velocity, time: Time):
|
|
151
|
+
... return velocity * time
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Note that the return value of this function is not a `Length` but a general
|
|
156
|
+
`UMonomial`, as the result of a numerical operation does not have an inherent
|
|
157
|
+
unit.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nutils-units"
|
|
3
|
+
readme = "README.md"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name = "Evalf", email = "info@evalf.com" },
|
|
6
|
+
]
|
|
7
|
+
requires-python = '>=3.10'
|
|
8
|
+
description = "Nutils Units: object wrappers for dimensional computations"
|
|
9
|
+
license = "MIT AND (Apache-2.0 OR BSD-2-Clause)"
|
|
10
|
+
dynamic = ["version"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
13
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[tool.setuptools.dynamic]
|
|
17
|
+
version = {attr = "nutils.units.__version__"}
|
|
18
|
+
readme = {file = ["README"]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from inspect import getmodule
|
|
2
|
+
from functools import partial, partialmethod
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DispatchTable(dict):
|
|
6
|
+
"""Table of handler functions.
|
|
7
|
+
|
|
8
|
+
The dispatch table is a dictionary that maps the fully qualified name of a
|
|
9
|
+
callable to a handler function that should be called with the callable as
|
|
10
|
+
the first positional argument, followed by its arguments. Handlers are
|
|
11
|
+
registered via the .register method:
|
|
12
|
+
|
|
13
|
+
>>> dispatch_table = DispatchTable()
|
|
14
|
+
>>> @dispatch_table.register("math.sqrt")
|
|
15
|
+
... def handle_root(op, x):
|
|
16
|
+
... return str(op(float(x)))
|
|
17
|
+
|
|
18
|
+
In this (rather contrived) example we create a handler that extends the
|
|
19
|
+
math.sqrt function to operate on string arguments. To obtain the relevant
|
|
20
|
+
handler we use the .bind method:
|
|
21
|
+
|
|
22
|
+
>>> import math
|
|
23
|
+
>>> f = dispatch_table.bind(math.sqrt)
|
|
24
|
+
>>> f("4")
|
|
25
|
+
'2.0'
|
|
26
|
+
|
|
27
|
+
Note that by registering the fully qualified name of the callable, rather
|
|
28
|
+
than the callable itself, we are able to prepare a table for any number of
|
|
29
|
+
external functions without ever importing any third party modules.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def register(self, qualname):
|
|
33
|
+
return partial(_setitem_retvalue, self, qualname)
|
|
34
|
+
|
|
35
|
+
def bind(self, func):
|
|
36
|
+
return partial(self[get_qualname(func)], func)
|
|
37
|
+
|
|
38
|
+
def bind_method(self, func):
|
|
39
|
+
return partialmethod(self.bind(func))
|
|
40
|
+
|
|
41
|
+
def bind_method_and_reverse(self, func):
|
|
42
|
+
bound = self.bind(func)
|
|
43
|
+
return partialmethod(bound), partialmethod(_swap, bound)
|
|
44
|
+
|
|
45
|
+
def call_or_notimp(self, func, args, kwargs):
|
|
46
|
+
try:
|
|
47
|
+
bound = self.bind(func)
|
|
48
|
+
except KeyError:
|
|
49
|
+
return NotImplemented
|
|
50
|
+
else:
|
|
51
|
+
return bound(*args, **kwargs)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_qualname(func):
|
|
55
|
+
"""Get the qualified domain name of a callable.
|
|
56
|
+
|
|
57
|
+
This function returns the string to be used in DispatchTable.register to
|
|
58
|
+
identify a callable.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
return getmodule(func).__name__ + "." + func.__qualname__
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _setitem_retvalue(d, k, v):
|
|
65
|
+
d[k] = v
|
|
66
|
+
return v
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _swap(self, func, arg):
|
|
70
|
+
return func(arg, self)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from fractions import Fraction
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Powers:
|
|
5
|
+
"""Immutable counter with fractional values.
|
|
6
|
+
|
|
7
|
+
This class is similar to `collections.Counter`, with two key differences:
|
|
8
|
+
|
|
9
|
+
- Values are instances of `fractions.Fraction`;
|
|
10
|
+
- The collection is immutable.
|
|
11
|
+
|
|
12
|
+
A `Powers` instance can be created from a string, which sets its counter
|
|
13
|
+
equal to 1:
|
|
14
|
+
>>> Powers("A")
|
|
15
|
+
Powers(A)
|
|
16
|
+
|
|
17
|
+
We can then use multiplication and division to scale all counts:
|
|
18
|
+
>>> Powers("A") * 3
|
|
19
|
+
Powers(A3)
|
|
20
|
+
|
|
21
|
+
And we can use addition and subtraction to combine powers:
|
|
22
|
+
>>> Powers("A") * 3 + Powers("B") / 2
|
|
23
|
+
Powers(A3*B1/2)
|
|
24
|
+
|
|
25
|
+
Note that the different items are separated by a `*`. Note what happens
|
|
26
|
+
when we reach a negative count:
|
|
27
|
+
>>> Powers("A") * 3 - Powers("B") * 2
|
|
28
|
+
Powers(A3/B2)
|
|
29
|
+
|
|
30
|
+
Rather than making the power negatve, we switch the `*` for a `/`. This
|
|
31
|
+
notation reflects the interpretation of counts as monomial powers.
|
|
32
|
+
|
|
33
|
+
When we created a power from a string before, we actually made use of the
|
|
34
|
+
fact that we can parse back the entire string representation now
|
|
35
|
+
established:
|
|
36
|
+
>>> Powers("A3/B2")
|
|
37
|
+
Powers(A3/B2)
|
|
38
|
+
|
|
39
|
+
Alternatively we can create it from a dictionary:
|
|
40
|
+
>>> Powers({"A": Fraction(3), "B": Fraction(-2)})
|
|
41
|
+
Powers(A3/B2)
|
|
42
|
+
|
|
43
|
+
Or even from an existing powers object:
|
|
44
|
+
>>> Powers(Powers("A"))
|
|
45
|
+
Powers(A)
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, d):
|
|
49
|
+
if isinstance(d, Powers):
|
|
50
|
+
self.__d = d.__d
|
|
51
|
+
elif isinstance(d, str):
|
|
52
|
+
self.__d = _split_factors(d)
|
|
53
|
+
elif isinstance(d, dict) and all(
|
|
54
|
+
isinstance(k, str) and isinstance(v, Fraction) for k, v in d.items()
|
|
55
|
+
):
|
|
56
|
+
self.__d = {k: v for k, v in d.items() if v}
|
|
57
|
+
else:
|
|
58
|
+
raise ValueError(f"cannot create Powers from {d!r}")
|
|
59
|
+
|
|
60
|
+
def __hash__(self):
|
|
61
|
+
return hash(tuple(sorted(self.__d.items())))
|
|
62
|
+
|
|
63
|
+
def __eq__(self, other):
|
|
64
|
+
return self is other or isinstance(other, Powers) and self.__d == other.__d
|
|
65
|
+
|
|
66
|
+
def __add__(self, other):
|
|
67
|
+
if not isinstance(other, Powers):
|
|
68
|
+
return NotImplemented
|
|
69
|
+
d = self.__d.copy()
|
|
70
|
+
for name, n in other.__d.items():
|
|
71
|
+
d[name] = d.get(name, 0) + n
|
|
72
|
+
return Powers(d)
|
|
73
|
+
|
|
74
|
+
def __sub__(self, other):
|
|
75
|
+
if not isinstance(other, Powers):
|
|
76
|
+
return NotImplemented
|
|
77
|
+
d = self.__d.copy()
|
|
78
|
+
for name, n in other.__d.items():
|
|
79
|
+
d[name] = d.get(name, 0) - n
|
|
80
|
+
return Powers(d)
|
|
81
|
+
|
|
82
|
+
def __mul__(self, other):
|
|
83
|
+
if isinstance(other, float):
|
|
84
|
+
tmp = Fraction(other).limit_denominator()
|
|
85
|
+
if other == float(tmp):
|
|
86
|
+
other = tmp
|
|
87
|
+
if not isinstance(other, (int, Fraction)):
|
|
88
|
+
try:
|
|
89
|
+
other = other.__index__()
|
|
90
|
+
except Exception:
|
|
91
|
+
return NotImplemented
|
|
92
|
+
return Powers({name: n * other for name, n in self.__d.items()})
|
|
93
|
+
|
|
94
|
+
def __truediv__(self, other):
|
|
95
|
+
if isinstance(other, float):
|
|
96
|
+
tmp = Fraction(other).limit_denominator()
|
|
97
|
+
if other == float(tmp):
|
|
98
|
+
other = tmp
|
|
99
|
+
if not isinstance(other, (int, Fraction)):
|
|
100
|
+
return NotImplemented
|
|
101
|
+
return Powers({name: n / other for name, n in self.__d.items()})
|
|
102
|
+
|
|
103
|
+
def __neg__(self):
|
|
104
|
+
return Powers({name: -n for name, n in self.__d.items()})
|
|
105
|
+
|
|
106
|
+
def __bool__(self):
|
|
107
|
+
return bool(self.__d)
|
|
108
|
+
|
|
109
|
+
def items(self):
|
|
110
|
+
return self.__d.items()
|
|
111
|
+
|
|
112
|
+
def __str__(self):
|
|
113
|
+
return _join_factors(self.__d)
|
|
114
|
+
|
|
115
|
+
def __repr__(self):
|
|
116
|
+
return f"Powers({self})"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _split_factors(s):
|
|
120
|
+
items = []
|
|
121
|
+
for factors in s.split("*"):
|
|
122
|
+
numer, *denoms = factors.split("/")
|
|
123
|
+
if numer:
|
|
124
|
+
base = numer.rstrip("0123456789")
|
|
125
|
+
num = Fraction(int(numer[len(base) :] or 1))
|
|
126
|
+
items.append((base, num))
|
|
127
|
+
elif items: # s contains "*/"
|
|
128
|
+
raise ValueError
|
|
129
|
+
for denom in denoms:
|
|
130
|
+
base = denom.rstrip("0123456789")
|
|
131
|
+
if base:
|
|
132
|
+
num = -Fraction(int(denom[len(base) :] or 1))
|
|
133
|
+
else: # fractional power
|
|
134
|
+
base, num = items.pop()
|
|
135
|
+
num /= int(denom)
|
|
136
|
+
items.append((base, num))
|
|
137
|
+
return dict(items)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _join_factors(factors):
|
|
141
|
+
s = ""
|
|
142
|
+
for base, power in sorted(
|
|
143
|
+
factors.items(), key=lambda item: item[::-1], reverse=True
|
|
144
|
+
):
|
|
145
|
+
if power < 0:
|
|
146
|
+
power = -power
|
|
147
|
+
s += "/"
|
|
148
|
+
else:
|
|
149
|
+
s += "*"
|
|
150
|
+
s += base
|
|
151
|
+
if power != 1:
|
|
152
|
+
s += (
|
|
153
|
+
str(power)
|
|
154
|
+
if power.denominator == 1
|
|
155
|
+
else f"{power.numerator}/{power.denominator}"
|
|
156
|
+
)
|
|
157
|
+
return s.lstrip("*")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
PREFIX = dict(
|
|
2
|
+
Y=1e24,
|
|
3
|
+
Z=1e21,
|
|
4
|
+
E=1e18,
|
|
5
|
+
P=1e15,
|
|
6
|
+
T=1e12,
|
|
7
|
+
G=1e9,
|
|
8
|
+
M=1e6,
|
|
9
|
+
k=1e3,
|
|
10
|
+
h=1e2,
|
|
11
|
+
d=1e-1,
|
|
12
|
+
c=1e-2,
|
|
13
|
+
m=1e-3,
|
|
14
|
+
μ=1e-6,
|
|
15
|
+
n=1e-9,
|
|
16
|
+
p=1e-12,
|
|
17
|
+
f=1e-15,
|
|
18
|
+
a=1e-18,
|
|
19
|
+
z=1e-21,
|
|
20
|
+
y=1e-24,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Units:
|
|
25
|
+
"""Registry of numerical values.
|
|
26
|
+
|
|
27
|
+
The Units object is a registry of numerical values. Values can be assigned
|
|
28
|
+
under any name that is not previously used, and cannot be removed.
|
|
29
|
+
|
|
30
|
+
>>> u = Units()
|
|
31
|
+
>>> u.register("m", 5.)
|
|
32
|
+
>>> u.m
|
|
33
|
+
5.0
|
|
34
|
+
|
|
35
|
+
Intended for units, the registry by default adds all metric prefixes with
|
|
36
|
+
approprately scaled values. If "m" represents the number 5, then "km"
|
|
37
|
+
represents 1000 times that number:
|
|
38
|
+
|
|
39
|
+
>>> u.km
|
|
40
|
+
5000.0
|
|
41
|
+
|
|
42
|
+
Attempting to redefine a previously assigned value results in an error and
|
|
43
|
+
leaves the registry unmodified.
|
|
44
|
+
|
|
45
|
+
>>> u.register("am", 10.)
|
|
46
|
+
Traceback (most recent call last):
|
|
47
|
+
...
|
|
48
|
+
ValueError: registration failed: unit collides with 'am'
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def register(self, name, value, prefix=tuple(PREFIX)):
|
|
52
|
+
d = self.__dict__
|
|
53
|
+
new = [(name, value)]
|
|
54
|
+
new.extend((p + name, value * PREFIX[p]) for p in prefix)
|
|
55
|
+
collisions = ", ".join(repr(s) for s, v in new if s in d and d[s] != v)
|
|
56
|
+
if collisions:
|
|
57
|
+
raise ValueError(f"registration failed: unit collides with {collisions}")
|
|
58
|
+
d.update(new)
|