From 2ec1f59bcf61587a205e0e17e5770665a0dd76e8 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Fri, 30 Sep 2022 14:04:48 +0200 Subject: [PATCH 01/17] fix pyproject to allow uploading to pypi --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9e0ae0d..5698919 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" [project] -name = "nutils.SI" +name = "nutils-SI" authors = [{name = "Evalf", email = "info@evalf.com"}] readme = "README.md" license = {file = "LICENSE"} @@ -12,3 +12,6 @@ dynamic = ["version", "description"] [project.urls] Home = "https://github.com/evalf/nutils-SI/" + +[tool.flit.module] +name = "nutils.SI" From eaf8098811e24ba5363a3b4edaed5c79e383dbc3 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Sat, 10 Sep 2022 15:17:12 +0200 Subject: [PATCH 02/17] fix handling of TypeError during dispatch --- nutils/SI.py | 7 ++++++- tests/test_SI.py | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/nutils/SI.py b/nutils/SI.py index 5a0f5e1..f910692 100644 --- a/nutils/SI.py +++ b/nutils/SI.py @@ -208,7 +208,12 @@ def _dispatch(op, *args, **kwargs): else: return NotImplemented assert isinstance(Dim, Dimension) - return Dim.__wrap__(op(*(arg.__value if isinstance(arg, Quantity) else arg for arg in args), **kwargs)) + try: + retval = op(*(arg.__value if isinstance(arg, Quantity) else arg for arg in args), **kwargs) + except TypeError: + return NotImplemented + else: + return Dim.__wrap__(retval) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): if method != '__call__': diff --git a/tests/test_SI.py b/tests/test_SI.py index d899c19..420387f 100644 --- a/tests/test_SI.py +++ b/tests/test_SI.py @@ -246,3 +246,8 @@ def test_pickle(self): def test_string_representation(self): F = SI.Force('2N') self.assertEqual(str(F), '2.0[M*L/T2]') + + def test_type_error(self): + for F in SI.Force('2N'), numpy.array([1,2,3]) * SI.Force('N'): + with self.assertRaisesRegex(TypeError, r"unsupported operand type\(s\) for /: '\[M\*L/T2\]' and 'object'"): + F / object() From 0c6e2d9dea0d5245be98b8c6cbbfce18e5e476f1 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Fri, 14 Oct 2022 13:09:00 +0200 Subject: [PATCH 03/17] replace type tests by assertIsInstance --- tests/test_SI.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_SI.py b/tests/test_SI.py index 420387f..24f3f14 100644 --- a/tests/test_SI.py +++ b/tests/test_SI.py @@ -46,15 +46,15 @@ class Quantity(unittest.TestCase): def test_fromstring(self): F = SI.parse('5kN') - self.assertEqual(type(F), SI.Force) + self.assertIsInstance(F, SI.Force) self.assertEqual(F / 'N', 5000) v = SI.parse('-864km/24h') - self.assertEqual(type(v), SI.Velocity) + self.assertIsInstance(v, SI.Velocity) self.assertEqual(v / 'm/s', -10) def test_fromvalue(self): F = SI.Force('10N') - self.assertEqual(type(F), SI.Force) + self.assertIsInstance(F, SI.Force) self.assertEqual(F / SI.Force('2N'), 5) def test_array(self): From 6a7a6a4a57d340fd8ff38e621e7fe950060af2e5 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Thu, 29 Sep 2022 15:22:26 +0200 Subject: [PATCH 04/17] remove Dimension.__bool__ --- nutils/SI.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/nutils/SI.py b/nutils/SI.py index f910692..7bcea79 100644 --- a/nutils/SI.py +++ b/nutils/SI.py @@ -59,9 +59,6 @@ def __getattr__(cls, attr): for base, power, isnumer in _split_factors(attr[1:-1]) if power}) raise AttributeError(attr) - def __bool__(cls) -> bool: - return bool(cls.__powers) - def __or__(cls, other): return typing.Union[cls, other] From c9c32bb30d7bb6045117df8ad189c61a1444b3d3 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Fri, 14 Oct 2022 13:20:40 +0200 Subject: [PATCH 05/17] remove instantiation of Quantity from Quantity --- nutils/SI.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nutils/SI.py b/nutils/SI.py index 7bcea79..eebb94c 100644 --- a/nutils/SI.py +++ b/nutils/SI.py @@ -108,8 +108,6 @@ def __stringly_dumps__(cls, v): def __call__(cls, value): if cls is Quantity: raise Exception('Quantity base class cannot be instantiated') - if isinstance(value, cls): - return value if not isinstance(value, str): raise ValueError(f'expected a str, got {type(value).__name__}') q = parse(value) From 2bddd33aaaaa3a1d0c1f06e03e4bdcea0d5b94cd Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Sat, 10 Sep 2022 14:51:21 +0200 Subject: [PATCH 06/17] restrict automatic string parsing to division --- nutils/SI.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nutils/SI.py b/nutils/SI.py index eebb94c..c48b83f 100644 --- a/nutils/SI.py +++ b/nutils/SI.py @@ -169,7 +169,6 @@ def __str__(self): @staticmethod def _dispatch(op, *args, **kwargs): name = op.__name__ - args = [parse(arg) if isinstance(arg, str) else arg for arg in args] if name in ('add', 'sub', 'subtract', 'hypot'): Dim = type(args[0]) if type(args[1]) != Dim: @@ -249,10 +248,15 @@ def _binary_r(name): __sub__, __rsub__ = _binary_r('sub') __mul__, __rmul__ = _binary_r('mul') __matmul__, __rmatmul__ = _binary_r('matmul') - __truediv__, __rtruediv__ = _binary_r('truediv') + __truediv, __rtruediv__ = _binary_r('truediv') __mod__, __rmod__ = _binary_r('mod') __pow__, __rpow__ = _binary_r('pow') + def __truediv__(self, other): + if type(other) is str: + return self.__value / self.__class__(other).__value + return self.__truediv(other) + def _attr(name): return property(lambda self: getattr(self.__value, name)) From b8e202b64e1fa6256bf2312e086dd53896f1dcce Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Thu, 29 Sep 2022 15:35:45 +0200 Subject: [PATCH 07/17] add Quantity.__repr__ --- nutils/SI.py | 3 +++ tests/test_SI.py | 1 + 2 files changed, 4 insertions(+) diff --git a/nutils/SI.py b/nutils/SI.py index c48b83f..3086d08 100644 --- a/nutils/SI.py +++ b/nutils/SI.py @@ -166,6 +166,9 @@ def __format__(self, format_spec): def __str__(self): return str(self.__value) + type(self).__name__ + def __repr__(self): + return repr(self.__value) + type(self).__name__ + @staticmethod def _dispatch(op, *args, **kwargs): name = op.__name__ diff --git a/tests/test_SI.py b/tests/test_SI.py index 24f3f14..f4c098e 100644 --- a/tests/test_SI.py +++ b/tests/test_SI.py @@ -246,6 +246,7 @@ def test_pickle(self): def test_string_representation(self): F = SI.Force('2N') self.assertEqual(str(F), '2.0[M*L/T2]') + self.assertEqual(repr(F), '2.0[M*L/T2]') def test_type_error(self): for F in SI.Force('2N'), numpy.array([1,2,3]) * SI.Force('N'): From e30c84c7056603aa52353ac5d379d1dfd21e7ed3 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Fri, 9 Sep 2022 12:03:42 +0200 Subject: [PATCH 08/17] add support for more operations --- nutils/SI.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/nutils/SI.py b/nutils/SI.py index 3086d08..6a785eb 100644 --- a/nutils/SI.py +++ b/nutils/SI.py @@ -172,25 +172,29 @@ def __repr__(self): @staticmethod def _dispatch(op, *args, **kwargs): name = op.__name__ - if name in ('add', 'sub', 'subtract', 'hypot'): + if name in ('add', 'sub', 'subtract', 'hypot', 'minimum', 'maximum', 'remainder', 'divmod'): Dim = type(args[0]) if type(args[1]) != Dim: raise TypeError(f'incompatible arguments for {name}: ' + ', '.join(type(arg).__name__ for arg in args)) - elif name in ('mul', 'multiply', 'matmul'): + elif name == 'reciprocal': + Dim = type(args[0])**-1 + elif name in ('mul', 'multiply', 'matmul', 'dot'): Dim = type(args[0]) * type(args[1]) elif name in ('truediv', 'true_divide', 'divide'): Dim = type(args[0]) / type(args[1]) - elif name in ('neg', 'negative', 'pos', 'positive', 'abs', 'absolute', 'sum', 'mean', 'broadcast_to', 'transpose', 'trace', 'take', 'ptp', 'getitem', 'amax', 'amin'): + elif name in ('neg', 'negative', 'pos', 'positive', 'abs', 'absolute', 'sum', 'cumsum', 'mean', 'broadcast_to', 'transpose', 'trace', 'take', 'compress', 'ptp', 'getitem', 'amax', 'amin', 'diff', 'reshape', 'ravel', 'repeat', 'swapaxes'): Dim = type(args[0]) elif name == 'sqrt': Dim = type(args[0])**fractions.Fraction(1,2) + elif name == 'square': + Dim = type(args[0])**2 elif name == 'setitem': Dim = type(args[0]) if type(args[2]) != Dim: raise TypeError(f'cannot assign {type(args[2]).__name__} to {Dim.__name__}') elif name in ('pow', 'power'): Dim = type(args[0])**args[1] - elif name in ('lt', 'le', 'eq', 'ne', 'gt', 'ge', 'equal', 'not_equal', 'less', 'less_equal', 'greater', 'greater_equal', 'isfinite', 'isnan'): + elif name in ('lt', 'le', 'eq', 'ne', 'gt', 'ge', 'equal', 'not_equal', 'less', 'less_equal', 'greater', 'greater_equal', 'isfinite', 'isinf', 'isnan'): if any(type(q) != type(args[0]) for q in args[1:]): raise TypeError(f'incompatible arguments for {name}: ' + ', '.join(type(arg).__name__ for arg in args)) Dim = Dimension.from_powers({}) @@ -200,7 +204,7 @@ def _dispatch(op, *args, **kwargs): if any(type(q) != Dim for q in stack_args[1:]): raise TypeError(f'incompatible arguments for {name}: ' + ', '.join(type(arg).__name__ for arg in stack_args)) args = [q.__value for q in stack_args], - elif name in ('shape', 'ndim', 'size'): + elif name in ('shape', 'ndim', 'size', 'sign'): Dim = Dimension.from_powers({}) else: return NotImplemented @@ -209,8 +213,9 @@ def _dispatch(op, *args, **kwargs): retval = op(*(arg.__value if isinstance(arg, Quantity) else arg for arg in args), **kwargs) except TypeError: return NotImplemented - else: - return Dim.__wrap__(retval) + if name == 'divmod': + return retval[0], Dim.__wrap__(retval[1]) + return Dim.__wrap__(retval) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): if method != '__call__': From f089a6f5cc1f866cfb818a52cf311956809a9f97 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Fri, 30 Sep 2022 12:59:25 +0200 Subject: [PATCH 09/17] add Angle dimension --- nutils/SI.py | 11 +++++++++++ tests/test_SI.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/nutils/SI.py b/nutils/SI.py index 6a785eb..6e77fdc 100644 --- a/nutils/SI.py +++ b/nutils/SI.py @@ -206,6 +206,14 @@ def _dispatch(op, *args, **kwargs): args = [q.__value for q in stack_args], elif name in ('shape', 'ndim', 'size', 'sign'): Dim = Dimension.from_powers({}) + elif name in ('sin', 'cos', 'tan'): + if not isinstance(args[0], Angle): + raise TypeError(f'trigonometric functions require angle {Angle.__name__}, got {type(args[0]).__name__}') + Dim = Dimension.from_powers({}) + elif name == 'arctan2': + if type(args[0]) != type(args[1]): + raise TypeError(f'arguments of arctan2 must have equal dimension, got {type(args[0]).__name__} and {type(args[1]).__name__}') + Dim = Angle else: return NotImplemented assert isinstance(Dim, Dimension) @@ -319,6 +327,7 @@ def _split_factors(s): Temperature = Dimension.create('θ') AmountOfSubstance = Dimension.create('N') LuminousFlux = LuminousIntensity = Dimension.create('J') +Angle = Dimension.create('A') Area = Length**2 Volume = Length**3 @@ -362,6 +371,7 @@ def _split_factors(s): units.K = Temperature.reference_quantity units.mol = AmountOfSubstance.reference_quantity units.cd = LuminousIntensity.reference_quantity +units.rad = Angle.reference_quantity units.N = 'kg*m/s2' # newton units.Pa = 'N/m2' # pascal @@ -392,3 +402,4 @@ def _split_factors(s): units.t = '1000kg' # ton units.Da = '1.66053904020yg' # dalton units.eV = '.1602176634aJ' # electronvolt +units.deg = '0.017453292519943295rad' # degree diff --git a/tests/test_SI.py b/tests/test_SI.py index f4c098e..12ba5da 100644 --- a/tests/test_SI.py +++ b/tests/test_SI.py @@ -252,3 +252,18 @@ def test_type_error(self): for F in SI.Force('2N'), numpy.array([1,2,3]) * SI.Force('N'): with self.assertRaisesRegex(TypeError, r"unsupported operand type\(s\) for /: '\[M\*L/T2\]' and 'object'"): F / object() + + def test_angle(self): + φ = SI.Angle('30deg') + self.assertTrue(numpy.isclose(numpy.sin(φ), .5)) + self.assertTrue(numpy.isclose(numpy.cos(φ), numpy.sqrt(3)/2)) + self.assertTrue(numpy.isclose(numpy.tan(φ), 1/numpy.sqrt(3))) + with self.assertRaisesRegex(TypeError, r'trigonometric functions require angle \[A\], got \[L\]'): + numpy.sin(SI.parse('2m')) + a = SI.Length('1m') + b = SI.Length('-1m') + φ = numpy.arctan2(a, b) + self.assertIsInstance(φ, SI.Angle) + self.assertTrue(numpy.isclose(φ / 'deg', 135)) + with self.assertRaisesRegex(TypeError, r'arguments of arctan2 must have equal dimension, got \[L\] and \[M\]'): + numpy.arctan2(SI.parse('2m'), SI.parse('1kg')) From 0f97574627e5b4cb7d04f0824cd868f5d66f0867 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Fri, 30 Sep 2022 13:41:55 +0200 Subject: [PATCH 10/17] move unittests to project root --- tests/test_SI.py => tests.py | 0 tests/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/test_SI.py => tests.py (100%) delete mode 100644 tests/__init__.py diff --git a/tests/test_SI.py b/tests.py similarity index 100% rename from tests/test_SI.py rename to tests.py diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 From 41bc29610d1c7fdf323ee777c21e006a48fa2a9c Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Sun, 9 Jan 2022 15:47:56 +0100 Subject: [PATCH 11/17] update readme The patch removes left-over references to Quantity as a univeral constructor, a role that has been taken over by the parse function. --- README.md | 54 +++++++++++++++++++++++++----------------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index a45f498..35d8196 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,15 @@ components of the Nutils suite, from where it should be imortable as `SI`: ## Usage The SI module defines all base units and derived units of the International -System of Units (SI) are predefined, as well as the full set of metric -prefixes. Dimensional values are generated primarily by instantiating the -Quantity type with a string value. +System of Units (SI) plus an Angle dimension, as well as the full set of metric +prefixes. Dimensional values are generated primarily by parsing a string value. >>> v = SI.parse('7μN*5h/6g') -The Quantity constructor recognizes the multiplication (\*) and division (/) -operators to separate factors. Every factor can be prefixed with a scale and -suffixed with a power. The remainder must be either a unit, or else a unit with -a metric prefix. +The parser recognizes the multiplication (\*) and division (/) operators to +separate factors. Every factor can be prefixed with a scale and suffixed with a +power. The remainder must be either a unit, or else a unit with a metric +prefix. In this example, the resulting object is of type "L/T", i.e. length over time, which is a subtype of Quantity that stores the powers L=1 and T=-1. Many @@ -43,9 +42,9 @@ through manipulation. >>> type(v) == SI.Velocity == SI.Length / SI.Time True -While Quantity can instantiate any subtype, we could have created the same -object by instantiating Velocity directly, which has the advantage of verifying -that the specified quantity is indeed of the desired dimension. +While `parse` can instantiate any subtype of Quantity, we could have created +the same object by instantiating Velocity directly, which has the advantage of +verifying that the specified quantity is indeed of the desired dimension. >>> w = SI.Velocity('8km') Traceback (most recent call last): @@ -56,7 +55,7 @@ Explicit subtypes can also be used in function annotations: >>> def f(size: SI.Length, load: SI.Force): pass -The Quantity type acts as an opaque container. As long as a quantity has a +The Quantity types act as an opaque container. As long as a quantity has a physical dimension, its value is inaccessible. The value can only be retrieved by dividing out a reference quantity, so that the result becomes dimensionless and the Quantity wrapper falls away. @@ -64,8 +63,9 @@ and the Quantity wrapper falls away. >>> v / SI.parse('m/s') 21.0 -To simplify this fairly common situation, any operation involving a Quantity -and a string is handled by parsing the latter automatically. +To simplify the fairly common situation that the reference quantity is a unit +or another parsed value, any operation involving a Quantity and a string is +handled by parsing the latter automatically. >>> v / 'm/s' 21.0 @@ -101,36 +101,32 @@ In case the predefined set of dimensions and units are insufficient, both can be extended. For instance, though it is not part of the official SI system, it might be desirable to add an angular dimension. This is done by creating a new Dimension instance, using a symbol that avoids the existing symbols T, L, M, I, -Θ, N and J: +Θ, N, J and A: - >>> Angle = SI.Dimension.create('Φ') + >>> Xonon = SI.Dimension.create('X') At this point, the dimension is not very useful yet as it lacks units. To -rectify this we define the radian by its abbreviation 'rad' in terms of the -provided reference quantity, and assign it to the global table of units: +rectify this we define the unit yam as being internally represented by a unit +value: - >>> SI.units.rad = Angle.reference_quantity + >>> SI.units.yam = Xonon.reference_quantity -Additional units can be defined by relating them to pre-existing ones: +Additional units can be defined by relating them to pre-existing ones. Here we +define the unit zop as the equal of 15 kilo-yam: >>> import math - >>> SI.units.deg = math.pi / 180 * SI.units.rad + >>> SI.units.zop = 15 * SI.units.kyam Alternatively, units can be defined using the same string syntax that is used by the Quantity constructor. Nevertheless, the following statement fails as we cannot define the same unit twice. - >>> SI.units.deg = '0.017453292519943295rad' + >>> SI.units.zop = '15kyam' Traceback (most recent call last): ... - ValueError: cannot define 'deg': unit is already defined + ValueError: cannot define 'zop': unit is already defined Having defined the new units we can directly use them: - >>> angle = SI.parse('30deg') - -Any function that accepts angular values will expect to receive them in a -specific unit. The new Angle dimension makes this unit explicit: - - >>> math.sin(angle / SI.units.rad) - 0.49999999999999994 + >>> SI.parse('24mzop/h') + 0.1[X/T] From 638efbea2de396c99b25a141b8bef9f87e66702e Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Fri, 14 Oct 2022 15:46:24 +0200 Subject: [PATCH 12/17] move cancellation logic to dispatch --- nutils/SI.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/nutils/SI.py b/nutils/SI.py index 6e77fdc..239087e 100644 --- a/nutils/SI.py +++ b/nutils/SI.py @@ -111,14 +111,11 @@ def __call__(cls, value): if not isinstance(value, str): raise ValueError(f'expected a str, got {type(value).__name__}') q = parse(value) - expect = float if not cls.__powers else cls - if type(q) != expect: - raise TypeError(f'expected {expect.__name__}, got {type(q).__name__}') + if not isinstance(q, cls): + raise TypeError(f'expected {cls.__name__}, got {type(q).__name__}') return q def __wrap__(cls, value): - if not cls.__powers: - return value return super().__call__(value) @property @@ -138,6 +135,8 @@ def parse(s): except (ValueError, AttributeError): raise ValueError(f'invalid (sub)expression {expr!r}') from None q = q * v if isnumer else q / v + if not isinstance(q, Quantity): + q = Dimensionless.__wrap__(q) q._parsed_from = s return q @@ -197,7 +196,7 @@ def _dispatch(op, *args, **kwargs): elif name in ('lt', 'le', 'eq', 'ne', 'gt', 'ge', 'equal', 'not_equal', 'less', 'less_equal', 'greater', 'greater_equal', 'isfinite', 'isinf', 'isnan'): if any(type(q) != type(args[0]) for q in args[1:]): raise TypeError(f'incompatible arguments for {name}: ' + ', '.join(type(arg).__name__ for arg in args)) - Dim = Dimension.from_powers({}) + Dim = Dimensionless elif name in ('stack', 'concatenate'): stack_args, = args Dim = type(stack_args[0]) @@ -205,11 +204,11 @@ def _dispatch(op, *args, **kwargs): raise TypeError(f'incompatible arguments for {name}: ' + ', '.join(type(arg).__name__ for arg in stack_args)) args = [q.__value for q in stack_args], elif name in ('shape', 'ndim', 'size', 'sign'): - Dim = Dimension.from_powers({}) + Dim = Dimensionless elif name in ('sin', 'cos', 'tan'): if not isinstance(args[0], Angle): raise TypeError(f'trigonometric functions require angle {Angle.__name__}, got {type(args[0]).__name__}') - Dim = Dimension.from_powers({}) + Dim = Dimensionless elif name == 'arctan2': if type(args[0]) != type(args[1]): raise TypeError(f'arguments of arctan2 must have equal dimension, got {type(args[0]).__name__} and {type(args[1]).__name__}') @@ -221,6 +220,8 @@ def _dispatch(op, *args, **kwargs): retval = op(*(arg.__value if isinstance(arg, Quantity) else arg for arg in args), **kwargs) except TypeError: return NotImplemented + if Dim == Dimensionless: + return retval if name == 'divmod': return retval[0], Dim.__wrap__(retval[1]) return Dim.__wrap__(retval) @@ -320,6 +321,8 @@ def _split_factors(s): ## SI DIMENSIONS +Dimensionless = Dimension.from_powers({}) + Time = Dimension.create('T') Length = Dimension.create('L') Mass = Dimension.create('M') From 5ae9d05d063ad79fe28f971d664c7f9a5711c072 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Fri, 21 Oct 2022 00:50:00 +0200 Subject: [PATCH 13/17] add inch unit --- nutils/SI.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nutils/SI.py b/nutils/SI.py index 239087e..79de3ee 100644 --- a/nutils/SI.py +++ b/nutils/SI.py @@ -406,3 +406,4 @@ def _split_factors(s): units.Da = '1.66053904020yg' # dalton units.eV = '.1602176634aJ' # electronvolt units.deg = '0.017453292519943295rad' # degree +units['in'] = 25.4 * units.mm # inch From d824e72891ec57e7aa84fac20ff40d714c4d8201 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Wed, 9 Nov 2022 12:47:28 +0100 Subject: [PATCH 14/17] add hash wip --- nutils/SI.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nutils/SI.py b/nutils/SI.py index 79de3ee..0d00ce9 100644 --- a/nutils/SI.py +++ b/nutils/SI.py @@ -52,6 +52,9 @@ def from_powers(mcls, arg): mcls.__cache[name] = cls return cls + def __hash__(cls): + return hash(tuple(sorted(cls.__powers.items()))) + def __getattr__(cls, attr): if attr.startswith('[') and attr.endswith(']'): # this, together with __qualname__, is what makes pickle work @@ -168,6 +171,9 @@ def __str__(self): def __repr__(self): return repr(self.__value) + type(self).__name__ + def __hash__(self): + return hash((type(self), self.__value)) + @staticmethod def _dispatch(op, *args, **kwargs): name = op.__name__ From 9c8978b13bbdbd2f6ae1cd269fb1551d11ab0d20 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Thu, 19 Jan 2023 16:26:28 +0100 Subject: [PATCH 15/17] rename to nutils_SI --- README.md | 28 ++++++++++++++-------------- nutils/SI.py => nutils_SI.py | 0 pyproject.toml | 2 +- tests.py | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) rename nutils/SI.py => nutils_SI.py (100%) diff --git a/README.md b/README.md index 35d8196..a0c9e30 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,9 @@ The SI module is most conveniently installed using Python's pip installer: Alternatively the package can be installed from source by calling `python -m pip install .` (note the dot) from inside the source directory. -The module will be installed in the nutils namespace, alongside other -components of the Nutils suite, from where it should be imortable as `SI`: +After installation the module is imortable as `nutils_SI`: - >>> from nutils import SI + >>> from nutils_SI import parse, Velocity, Length, Time, Force ## Usage @@ -27,7 +26,7 @@ The SI module defines all base units and derived units of the International System of Units (SI) plus an Angle dimension, as well as the full set of metric prefixes. Dimensional values are generated primarily by parsing a string value. - >>> v = SI.parse('7μN*5h/6g') + >>> v = parse('7μN*5h/6g') The parser recognizes the multiplication (\*) and division (/) operators to separate factors. Every factor can be prefixed with a scale and suffixed with a @@ -39,28 +38,28 @@ which is a subtype of Quantity that stores the powers L=1 and T=-1. Many subtypes are readily defined by their physical names; others can be created through manipulation. - >>> type(v) == SI.Velocity == SI.Length / SI.Time + >>> type(v) == Velocity == Length / Time True While `parse` can instantiate any subtype of Quantity, we could have created the same object by instantiating Velocity directly, which has the advantage of verifying that the specified quantity is indeed of the desired dimension. - >>> w = SI.Velocity('8km') + >>> w = Velocity('8km') Traceback (most recent call last): ... TypeError: expected [L/T], got [L] Explicit subtypes can also be used in function annotations: - >>> def f(size: SI.Length, load: SI.Force): pass + >>> def f(size: Length, load: Force): pass The Quantity types act as an opaque container. As long as a quantity has a physical dimension, its value is inaccessible. The value can only be retrieved by dividing out a reference quantity, so that the result becomes dimensionless and the Quantity wrapper falls away. - >>> v / SI.parse('m/s') + >>> v / parse('m/s') 21.0 To simplify the fairly common situation that the reference quantity is a unit @@ -83,7 +82,7 @@ idiomatic way is to rely on multiplication so as not to depend on the specifics of the internal reference system. >>> import numpy - >>> F = numpy.array([1,2,3]) * SI.parse('N') + >>> F = numpy.array([1,2,3]) * parse('N') For convenience, Quantity objects define the shape, ndim and size attributes. Beyond this, however, no Numpy specific methods or attributes are defined. @@ -103,30 +102,31 @@ might be desirable to add an angular dimension. This is done by creating a new Dimension instance, using a symbol that avoids the existing symbols T, L, M, I, Θ, N, J and A: - >>> Xonon = SI.Dimension.create('X') + >>> from nutils_SI import Dimension, units + >>> Xonon = Dimension.create('X') At this point, the dimension is not very useful yet as it lacks units. To rectify this we define the unit yam as being internally represented by a unit value: - >>> SI.units.yam = Xonon.reference_quantity + >>> units.yam = Xonon.reference_quantity Additional units can be defined by relating them to pre-existing ones. Here we define the unit zop as the equal of 15 kilo-yam: >>> import math - >>> SI.units.zop = 15 * SI.units.kyam + >>> units.zop = 15 * units.kyam Alternatively, units can be defined using the same string syntax that is used by the Quantity constructor. Nevertheless, the following statement fails as we cannot define the same unit twice. - >>> SI.units.zop = '15kyam' + >>> units.zop = '15kyam' Traceback (most recent call last): ... ValueError: cannot define 'zop': unit is already defined Having defined the new units we can directly use them: - >>> SI.parse('24mzop/h') + >>> parse('24mzop/h') 0.1[X/T] diff --git a/nutils/SI.py b/nutils_SI.py similarity index 100% rename from nutils/SI.py rename to nutils_SI.py diff --git a/pyproject.toml b/pyproject.toml index 5698919..e9f6349 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,4 +14,4 @@ dynamic = ["version", "description"] Home = "https://github.com/evalf/nutils-SI/" [tool.flit.module] -name = "nutils.SI" +name = "nutils_SI" diff --git a/tests.py b/tests.py index 12ba5da..dcf07e0 100644 --- a/tests.py +++ b/tests.py @@ -1,4 +1,4 @@ -from nutils import SI +import nutils_SI as SI import numpy import pickle From b59503d1ac6939ee23736b9ca5e6f96f4ae5a3b9 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Thu, 26 Jan 2023 08:47:38 +0100 Subject: [PATCH 16/17] dispatch fractional power as float --- nutils_SI.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nutils_SI.py b/nutils_SI.py index 0d00ce9..3f4a6ec 100644 --- a/nutils_SI.py +++ b/nutils_SI.py @@ -199,6 +199,7 @@ def _dispatch(op, *args, **kwargs): raise TypeError(f'cannot assign {type(args[2]).__name__} to {Dim.__name__}') elif name in ('pow', 'power'): Dim = type(args[0])**args[1] + args = args[0], float(args[1]) elif name in ('lt', 'le', 'eq', 'ne', 'gt', 'ge', 'equal', 'not_equal', 'less', 'less_equal', 'greater', 'greater_equal', 'isfinite', 'isinf', 'isnan'): if any(type(q) != type(args[0]) for q in args[1:]): raise TypeError(f'incompatible arguments for {name}: ' + ', '.join(type(arg).__name__ for arg in args)) From f66bd96b48069fcc4bb59af683f02f34704a6dbd Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Mon, 10 Apr 2023 11:49:44 +0200 Subject: [PATCH 17/17] add getnewarg --- nutils_SI.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nutils_SI.py b/nutils_SI.py index 3f4a6ec..d2bcb32 100644 --- a/nutils_SI.py +++ b/nutils_SI.py @@ -149,6 +149,9 @@ class Quantity(metaclass=Dimension): def __init__(self, value): self.__value = value + def __getnewargs__(self): + return self.__value, + def __bool__(self): return bool(self.__value)