unitx 0.1.2__tar.gz → 0.1.3__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.
- {unitx-0.1.2 → unitx-0.1.3}/PKG-INFO +1 -1
- {unitx-0.1.2 → unitx-0.1.3}/pyproject.toml +1 -1
- unitx-0.1.3/src/unitx/__init__.py +5 -0
- unitx-0.1.3/src/unitx/dimension.py +135 -0
- unitx-0.1.3/src/unitx/utils/__init__.py +0 -0
- unitx-0.1.3/src/unitx/utils/number.py +28 -0
- unitx-0.1.3/src/unitx/utils/special_char.py +39 -0
- unitx-0.1.3/tests/test_dimension.py +80 -0
- unitx-0.1.2/src/unitx/__init__.py +0 -3
- unitx-0.1.2/tests/test_import.py +0 -5
- {unitx-0.1.2 → unitx-0.1.3}/.gitignore +0 -0
- {unitx-0.1.2 → unitx-0.1.3}/LICENSE +0 -0
- {unitx-0.1.2 → unitx-0.1.3}/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: unitx
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A modern, intuitive Python library for units, quantities, and dimensional analysis.
|
|
5
5
|
Project-URL: Homepage, https://github.com/DaitTHU/unitx
|
|
6
6
|
Project-URL: Issues, https://github.com/DaitTHU/unitx/issues
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fractions import Fraction
|
|
4
|
+
from typing import Iterable, Literal
|
|
5
|
+
|
|
6
|
+
from .utils.number import common_fraction, ZERO, FloatConversionWarning
|
|
7
|
+
from .utils.special_char import superscript
|
|
8
|
+
|
|
9
|
+
__all__ = ['Dimension', 'DIMENSIONLESS']
|
|
10
|
+
|
|
11
|
+
_SYMBOL = ('T', 'L', 'M', 'I', 'Θ', 'N', 'J')
|
|
12
|
+
_SYMBOL_ASCII = ('T', 'L', 'M', 'I', 'Theta', 'N', 'J')
|
|
13
|
+
_LEN = len(_SYMBOL)
|
|
14
|
+
_ALLZERO = (ZERO,) * _LEN
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Dimension:
|
|
18
|
+
'''
|
|
19
|
+
Implementation of physical dimensions.
|
|
20
|
+
- `T`: time
|
|
21
|
+
- `L`: length
|
|
22
|
+
- `M`: mass
|
|
23
|
+
- `I`: current
|
|
24
|
+
- `Θ`: temperature (use ASCII alias `Theta` for code)
|
|
25
|
+
- `N`: amount
|
|
26
|
+
- `J`: luminous
|
|
27
|
+
|
|
28
|
+
Construct
|
|
29
|
+
---
|
|
30
|
+
>>> dim_velocity = Dimension(L=1, T=-1)
|
|
31
|
+
>>> dim_force = Dimension(M=1, L=1, T=-2)
|
|
32
|
+
|
|
33
|
+
Attributes
|
|
34
|
+
---
|
|
35
|
+
You can access the exponents of each dimension by symbols or full names.
|
|
36
|
+
>>> dim_force.T
|
|
37
|
+
-2
|
|
38
|
+
>>> dim_force.mass
|
|
39
|
+
1
|
|
40
|
+
|
|
41
|
+
Operations
|
|
42
|
+
---
|
|
43
|
+
You can perform multiplication, division, and exponentiation on dimensions.
|
|
44
|
+
>>> dim_force * dim_velocity
|
|
45
|
+
Dimension(T=-3, L=2, M=1)
|
|
46
|
+
>>> dim_velocity / Dimension(T=1)
|
|
47
|
+
Dimension(T=-2, L=1)
|
|
48
|
+
>>> dim_force ** 2
|
|
49
|
+
Dimension(T=-4, L=2, M=2)
|
|
50
|
+
>>> 1 / dim_force
|
|
51
|
+
Dimension(T=2, L=-1, M=-1)
|
|
52
|
+
'''
|
|
53
|
+
|
|
54
|
+
__slots__ = ('_exponents',)
|
|
55
|
+
|
|
56
|
+
def __new__(cls, T=0, L=0, M=0, I=0, Theta=0, N=0, J=0) -> Dimension:
|
|
57
|
+
exponents = tuple(map(common_fraction, (T, L, M, I, Theta, N, J)))
|
|
58
|
+
if not any(exponents):
|
|
59
|
+
return DIMENSIONLESS
|
|
60
|
+
obj = object.__new__(cls)
|
|
61
|
+
object.__setattr__(obj, '_exponents', exponents)
|
|
62
|
+
return obj
|
|
63
|
+
|
|
64
|
+
def __setattr__(self, name: str, value) -> None:
|
|
65
|
+
raise AttributeError(
|
|
66
|
+
'Dimension is immutable and cannot set attribute.')
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def _from_iter(cls, iterable: Iterable[Fraction], /) -> Dimension:
|
|
70
|
+
'''Constructor from an iterable of exponents. Internal use only.'''
|
|
71
|
+
exponents = tuple(iterable)
|
|
72
|
+
if len(exponents) != _LEN:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f'Expected {_LEN} exponents, got {len(exponents)}.')
|
|
75
|
+
if not any(exponents):
|
|
76
|
+
return DIMENSIONLESS
|
|
77
|
+
obj = object.__new__(cls)
|
|
78
|
+
object.__setattr__(obj, '_exponents', exponents)
|
|
79
|
+
return obj
|
|
80
|
+
|
|
81
|
+
def __mul__(self, other: Dimension) -> Dimension:
|
|
82
|
+
if not isinstance(other, Dimension):
|
|
83
|
+
return NotImplemented
|
|
84
|
+
return self._from_iter(a + b for a, b in zip(self._exponents, other._exponents))
|
|
85
|
+
|
|
86
|
+
def __truediv__(self, other: Dimension) -> Dimension:
|
|
87
|
+
if not isinstance(other, Dimension):
|
|
88
|
+
return NotImplemented
|
|
89
|
+
return self._from_iter(a - b for a, b in zip(self._exponents, other._exponents))
|
|
90
|
+
|
|
91
|
+
def __pow__(self, power: int | Fraction | float) -> Dimension:
|
|
92
|
+
if isinstance(power, float):
|
|
93
|
+
import warnings
|
|
94
|
+
power_fraction = common_fraction(power)
|
|
95
|
+
warnings.warn(f'Float power {power} converted to {power_fraction}, use Fraction to specify an exact exponent.', FloatConversionWarning, stacklevel=2)
|
|
96
|
+
power = power_fraction
|
|
97
|
+
if not isinstance(power, (int, Fraction)):
|
|
98
|
+
return NotImplemented
|
|
99
|
+
return self._from_iter(a * power for a in self._exponents)
|
|
100
|
+
|
|
101
|
+
def __rtruediv__(self, other: Literal[1]) -> Dimension:
|
|
102
|
+
if other == 1:
|
|
103
|
+
return self._from_iter(-a for a in self._exponents)
|
|
104
|
+
return NotImplemented
|
|
105
|
+
|
|
106
|
+
def __eq__(self, other: object) -> bool:
|
|
107
|
+
if not isinstance(other, Dimension):
|
|
108
|
+
return NotImplemented
|
|
109
|
+
return self._exponents == other._exponents
|
|
110
|
+
|
|
111
|
+
def __hash__(self) -> int: return hash(self._exponents)
|
|
112
|
+
|
|
113
|
+
def as_tuple(self) -> tuple[Fraction, ...]: return self._exponents
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def __unpack_exponents():
|
|
117
|
+
def __getter(index: int):
|
|
118
|
+
return lambda self: self._exponents[index]
|
|
119
|
+
return (property(__getter(i)) for i in range(_LEN))
|
|
120
|
+
|
|
121
|
+
T, L, M, I, Θ, N, J = __unpack_exponents()
|
|
122
|
+
Theta = Θ # Alias for Θ
|
|
123
|
+
time, length, mass, current, temperature, amount, luminous = T, L, M, I, Θ, N, J
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
para = ', '.join(f'{s}={v}' for s, v in zip(
|
|
127
|
+
_SYMBOL_ASCII, self._exponents) if v)
|
|
128
|
+
return '{}({})'.format(self.__class__.__name__, para)
|
|
129
|
+
|
|
130
|
+
def __str__(self) -> str:
|
|
131
|
+
return ''.join(s + superscript(v) for s, v in zip(_SYMBOL, self._exponents) if v) or '1'
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
DIMENSIONLESS = object.__new__(Dimension)
|
|
135
|
+
object.__setattr__(DIMENSIONLESS, '_exponents', _ALLZERO)
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from fractions import Fraction
|
|
3
|
+
|
|
4
|
+
ZERO = Fraction(0)
|
|
5
|
+
ONE = Fraction(1)
|
|
6
|
+
TWO = Fraction(2)
|
|
7
|
+
THREE = Fraction(3)
|
|
8
|
+
TEN = Fraction(10)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def common_fraction(value: int | float | Decimal | Fraction) -> Fraction:
|
|
12
|
+
'''
|
|
13
|
+
Convert a number to a Fraction with a small denominator, like 1, 42, -2/3...
|
|
14
|
+
|
|
15
|
+
>>> Fraction(1.47)
|
|
16
|
+
Fraction(6620291452234629, 4503599627370496)
|
|
17
|
+
>>> common_fraction(1.47)
|
|
18
|
+
Fraction(147, 100)
|
|
19
|
+
'''
|
|
20
|
+
if isinstance(value, Fraction):
|
|
21
|
+
return value
|
|
22
|
+
frac = Fraction(value)
|
|
23
|
+
return frac.limit_denominator() if isinstance(value, float) else frac
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FloatConversionWarning(UserWarning):
|
|
27
|
+
'''Warning issued when a float is converted to an approximate Fraction.'''
|
|
28
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from fractions import Fraction
|
|
3
|
+
|
|
4
|
+
__all__ = ['superscript']
|
|
5
|
+
|
|
6
|
+
DIGIT = '0123456789+-=()'
|
|
7
|
+
SUPERSCRIPT = '⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾'
|
|
8
|
+
SUBSCRIPT = '₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎'
|
|
9
|
+
SUP_TRANS = str.maketrans(SUPERSCRIPT, DIGIT)
|
|
10
|
+
SUB_TRANS = str.maketrans(SUBSCRIPT, DIGIT)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def superscript(ratio: int | Fraction, /, *, omit1=True) -> str:
|
|
14
|
+
'''
|
|
15
|
+
Convert a ratio to a string of superscript characters.
|
|
16
|
+
Superscript ¹ is omitted by setting `omit1=True` (default).
|
|
17
|
+
>>> superscript(2)
|
|
18
|
+
'²'
|
|
19
|
+
>>> superscript(-1)
|
|
20
|
+
'⁻¹'
|
|
21
|
+
>>> superscript(Fraction(3, 4))
|
|
22
|
+
'³ᐟ⁴'
|
|
23
|
+
'''
|
|
24
|
+
if ratio < 0:
|
|
25
|
+
return '⁻' + superscript(-ratio, omit1=False)
|
|
26
|
+
if ratio.denominator == 1:
|
|
27
|
+
if omit1 and ratio.numerator == 1:
|
|
28
|
+
return ''
|
|
29
|
+
return _sup(ratio.numerator)
|
|
30
|
+
assert isinstance(ratio, Fraction)
|
|
31
|
+
return _sup(ratio.numerator) + 'ᐟ' + _sup(ratio.denominator)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _sup(number: int, /) -> str:
|
|
35
|
+
return ''.join(SUPERSCRIPT[int(digit)] for digit in str(number))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _sub(number: int, /) -> str:
|
|
39
|
+
return ''.join(SUBSCRIPT[int(digit)] for digit in str(number))
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from fractions import Fraction
|
|
3
|
+
from unitx import Dimension, DIMENSIONLESS
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_dimension_creation() -> None:
|
|
7
|
+
d1 = Dimension(T=1, L=2)
|
|
8
|
+
d2 = Dimension(T=1, L=2)
|
|
9
|
+
d3 = Dimension()
|
|
10
|
+
|
|
11
|
+
assert d1 == d2
|
|
12
|
+
assert d1 != d3
|
|
13
|
+
assert d3 is DIMENSIONLESS
|
|
14
|
+
assert d1.as_tuple() == (1, 2, 0, 0, 0, 0, 0)
|
|
15
|
+
assert repr(d2) == "Dimension(T=1, L=2)"
|
|
16
|
+
assert str(d3) == "1"
|
|
17
|
+
assert str(d1) == "TL²"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_temperature_alias() -> None:
|
|
21
|
+
d = Dimension(Theta=1)
|
|
22
|
+
|
|
23
|
+
assert d.Theta == 1
|
|
24
|
+
assert d.Θ == 1
|
|
25
|
+
assert d.temperature == 1
|
|
26
|
+
assert str(d) == "Θ"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_dimension_operations() -> None:
|
|
30
|
+
d1 = Dimension(T=1, L=2)
|
|
31
|
+
d2 = Dimension(M=1)
|
|
32
|
+
|
|
33
|
+
d_mul = d1 * d2
|
|
34
|
+
d_div = d1 / d2
|
|
35
|
+
d_pow = d1 ** 2
|
|
36
|
+
d_inv = 1 / d1
|
|
37
|
+
|
|
38
|
+
assert d_mul == Dimension(T=1, L=2, M=1)
|
|
39
|
+
assert d_div == Dimension(T=1, L=2, M=-1)
|
|
40
|
+
assert d_pow == Dimension(T=2, L=4)
|
|
41
|
+
assert d_inv == Dimension(T=-1, L=-2)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_fractional_exponents() -> None:
|
|
45
|
+
d = Dimension(L=1) ** Fraction(1, 2)
|
|
46
|
+
assert d.L == Fraction(1, 2)
|
|
47
|
+
assert str(d) == "L¹ᐟ²"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_invalid_operations() -> None:
|
|
51
|
+
with pytest.raises(TypeError):
|
|
52
|
+
Dimension(L=1) * 2 # type: ignore
|
|
53
|
+
|
|
54
|
+
with pytest.raises(TypeError):
|
|
55
|
+
Dimension(L=1) / 2 # type: ignore
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_hash() -> None:
|
|
59
|
+
assert hash(Dimension(L=1)) == hash(Dimension(L=1))
|
|
60
|
+
assert {Dimension(L=1), Dimension(L=1)} == {Dimension(L=1)}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_float_power_warns() -> None:
|
|
64
|
+
with pytest.warns(UserWarning):
|
|
65
|
+
Dimension(L=1) ** 0.5 # type: ignore
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_operation_returns_dimensionless_singleton() -> None:
|
|
69
|
+
d = Dimension(L=1)
|
|
70
|
+
assert d / d is DIMENSIONLESS
|
|
71
|
+
assert d ** 0 is DIMENSIONLESS
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_dimension_is_immutable() -> None:
|
|
75
|
+
d = Dimension(L=1)
|
|
76
|
+
|
|
77
|
+
with pytest.raises(AttributeError):
|
|
78
|
+
d._exponents = (1, 2, 3, 4, 5, 6, 7) # type: ignore
|
|
79
|
+
with pytest.raises(AttributeError):
|
|
80
|
+
d.L = 2 # type: ignore
|
unitx-0.1.2/tests/test_import.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|