From 0b8eb740153c3cadab1f5922323223d68de6e490 Mon Sep 17 00:00:00 2001 From: Maximos Nikiforakis Date: Tue, 9 Apr 2024 15:38:37 +0300 Subject: [PATCH] Implement unit preconversion for property multiplication --- src/property_utils/properties/property.py | 116 +++++++++++++++++- .../tests/properties/test_property.py | 91 ++++++++++++++ 2 files changed, 205 insertions(+), 2 deletions(-) diff --git a/src/property_utils/properties/property.py b/src/property_utils/properties/property.py index 0868471..73ee9e2 100644 --- a/src/property_utils/properties/property.py +++ b/src/property_utils/properties/property.py @@ -2,7 +2,7 @@ This module defines the Property class and property arithmetics. """ -from dataclasses import dataclass +from dataclasses import dataclass, replace from typing import Type, Optional from math import isclose @@ -209,8 +209,9 @@ def __mul__(self, other) -> "Property": if isinstance(other, (float, int)): return Property(self.value * other, self.unit) if isinstance(other, Property): + _other = self._unit_preconversion(other) return Property( - self.value * other.value, (self.unit * other.unit).simplified() + self.value * _other.value, (self.unit * _other.unit).simplified() ) raise PropertyBinaryOperationError( f"cannot multiply {self} with {other}; " @@ -609,3 +610,114 @@ def _validate_comparison_input(self, other) -> None: f"cannot compare ({other}) to ({self}); " f"({other}) must have ({self.unit.to_generic()}) units. " ) + + def _unit_preconversion(self, prop: "Property") -> "Property": + """ + Applies a conversion to the given property's units before it is multiplied or + divided with this unit. + + The preconversion is needed to produce simplified units from the multiplication/ + division. + For example, if you multiply 5 cm with 2.02 m you don't want to get the result + in cm * m; in order to get the result in cm^2, 2.02 m (the right operand) is + converted to cm first. + """ + if isinstance(prop.unit, CompositeDimension): + return prop.to_unit(self._composite_unit_preconversion(prop.unit)) + + if isinstance(prop.unit, Dimension): + return prop.to_unit(self._dimension_unit_preconversion(prop.unit)) + + if isinstance(prop.unit, MeasurementUnit): + return prop.to_unit(self._simple_unit_preconversion(prop.unit)) + + return prop + + # pylint: disable=too-many-branches + def _composite_unit_preconversion( + self, unit: CompositeDimension + ) -> CompositeDimension: + """ + Returns the composite dimension that the given dimension should be converted to + before multiplication or division with this property. + """ + other = replace(unit).simplified() + + if isinstance(self.unit, CompositeDimension): + self.unit.simplify() + + for i, num in enumerate(other.numerator): + _n = self.unit.get_numerator(num.to_generic(), None) + if _n is not None: + other.numerator[i] = replace(_n) + + d = self.unit.get_denominator(num.to_generic(), None) + if d is not None: + other.numerator[i] = replace(d) + + for i, d in enumerate(other.denominator): + _d = self.unit.get_denominator(d.to_generic(), None) + if _d is not None: + other.denominator[i] = replace(_d) + + n = self.unit.get_numerator(d.to_generic(), None) + if n is not None: + other.denominator[i] = replace(n) + + return other + + _self: UnitDescriptor + if isinstance(self.unit, MeasurementUnit): + _self = self.unit**1 + elif isinstance(self.unit, Dimension): + _self = replace(self.unit) + else: + _self = self.unit + + if isinstance(_self, Dimension): + for i, n in enumerate(other.numerator): + if n.unit.isinstance(_self.unit.to_generic()): + other.numerator[i] = _self.unit ** other.numerator[i].power + return other + + for i, d in enumerate(other.denominator): + if d.unit.isinstance(_self.unit.to_generic()): + other.denominator[i] = _self.unit ** other.denominator[i].power + return other + + return unit + + def _dimension_unit_preconversion(self, unit: Dimension) -> Dimension: + """ + Returns the dimension that the given dimension should be converted to before + multiplication or division with this property. + """ + if isinstance(self.unit, CompositeDimension): + self.unit.simplify() + + for d in self.unit.denominator: + if d.unit.isinstance(unit.unit.to_generic()): + return d.unit**unit.power + + for n in self.unit.numerator: + if n.unit.isinstance(unit.unit.to_generic()): + return n.unit**unit.power + + _self: UnitDescriptor + if isinstance(self.unit, Dimension): + _self = self.unit.unit + else: + _self = self.unit + + if isinstance(_self, MeasurementUnit): + if _self.isinstance(unit.unit.to_generic()): + return _self**unit.power + + return unit + + def _simple_unit_preconversion(self, unit: MeasurementUnit) -> MeasurementUnit: + """ + Returns the unit that the given unit should be converted to before + multiplication or division with this property. + """ + return self._dimension_unit_preconversion(unit**1).unit diff --git a/src/property_utils/tests/properties/test_property.py b/src/property_utils/tests/properties/test_property.py index 5c6049b..89deb32 100644 --- a/src/property_utils/tests/properties/test_property.py +++ b/src/property_utils/tests/properties/test_property.py @@ -3,6 +3,7 @@ from unittest_extensions import args, TestCase from property_utils.properties.property import Property, p +from property_utils.units.descriptors import CompositeDimension from property_utils.units.units import NonDimensionalUnit, PressureUnit from property_utils.exceptions.properties.property import ( PropertyExponentError, @@ -13,6 +14,7 @@ Unit3, Unit4, Unit6, + Unit7, Unit8, generic_dimension_1, generic_composite_dimension, @@ -332,6 +334,95 @@ def test_with_zero_value_property(self): self.assert_result("0.0 (A^3) / (B^3)") +@add_to(property_test_suite, "__mul__") +class TestCompositeDimensionPropertyUnitPreconversionMultiplication(TestProperty): + + def build_property(self) -> Property: + return Property(1, Unit1.A * Unit4.d**2 / Unit6.F / Unit8.H**3) + + @args({"other": Property(1, Unit6.f / Unit1.a)}) + def test_with_composite_unit_simplify_numerator_and_denominator(self): + self.assert_result("5.0 (d^2) / (H^3)") + + @args({"other": Property(1, Unit1.a / Unit6.f)}) + def test_with_composite_unit_add_to_numerator_and_denominator(self): + self.assert_result("0.2 (A^2) * (d^2) / (F^2) / (H^3)") + + @args({"other": Property(64, Unit8.h**3)}) + def test_with_dimension_same_denominator(self): + self.assert_result("1.0 (d^2) * A / F") + + @args({"other": Property(16, Unit8.h**2)}) + def test_with_dimension_denominator(self): + self.assert_result("1.0 (d^2) * A / F / H") + + @args({"other": Property(100, Unit1.a**2)}) + def test_with_dimension_numerator(self): + self.assert_result_almost("1.0 (A^3) * (d^2) / (H^3) / F") + + @args({"other": Property(1, Unit4.D)}) + def test_with_unit_same_numerator(self): + self.assert_result("5.0 (d^3) * A / (H^3) / F") + + @args({"other": Property(2, Unit6.f)}) + def test_with_unit_same_denominator(self): + self.assert_result("1.0 (d^2) * A / (H^3)") + + +@add_to(property_test_suite, "__mul__") +class TestDimensionPropertyUnitPreconversionMultiplication(TestProperty): + + def build_property(self) -> Property: + return Property(1, Unit1.A**2) + + @args({"other": Property(1, Unit4.d / Unit1.a)}) + def test_with_composite_dimension_denominator(self): + self.assert_result("10.0 A * d") + + @args({"other": Property(10, Unit1.a / Unit4.d)}) + def test_with_composite_dimension_numerator(self): + self.assert_result("1.0 (A^3) / d") + + @args({"other": Property(1, Unit4.d / Unit1.a**2)}) + def test_with_composite_dimension_same_denominator(self): + self.assert_result_almost("100.0 d") + + @args({"other": Property(1000, Unit1.a**3)}) + def test_with_same_unit_dimension(self): + self.assert_result_almost("1.0 (A^5)") + + @args({"other": Property(10, Unit1.a)}) + def test_with_same_unit(self): + self.assert_result("1.0 (A^3)") + + +@add_to(property_test_suite, "__mul__") +class TestUnitPropertyUnitPreconversionMultiplication(TestProperty): + + def build_property(self) -> Property: + return Property(1, Unit1.A) + + @args({"other": Property(1, Unit4.d / Unit1.a)}) + def test_with_composite_dimension_same_denominator(self): + self.assert_result("10.0 d") + + @args({"other": Property(10, Unit1.a / Unit4.d)}) + def test_with_composite_dimension_same_numerator(self): + self.assert_result("1.0 (A^2) / d") + + @args({"other": Property(1, Unit4.d / Unit1.a**2)}) + def test_with_composite_dimension(self): + self.assert_result_almost("100.0 d / A") + + @args({"other": Property(100, Unit1.a**2)}) + def test_with_dimension_same_unit(self): + self.assert_result_almost("1.0 (A^3)") + + @args({"other": Property(10, Unit1.a)}) + def test_with_same_unit(self): + self.assert_result("1.0 (A^2)") + + @add_to(property_test_suite, "__truediv__") class TestPropertyDivision(TestProperty):