Skip to content

Commit

Permalink
Implement unit preconversion for property multiplication
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxcode123 committed Apr 9, 2024
1 parent 1e540a2 commit 0b8eb74
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 2 deletions.
116 changes: 114 additions & 2 deletions src/property_utils/properties/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}; "
Expand Down Expand Up @@ -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
91 changes: 91 additions & 0 deletions src/property_utils/tests/properties/test_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -13,6 +14,7 @@
Unit3,
Unit4,
Unit6,
Unit7,
Unit8,
generic_dimension_1,
generic_composite_dimension,
Expand Down Expand Up @@ -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):

Expand Down

0 comments on commit 0b8eb74

Please sign in to comment.