Skip to content

Commit 5c4fd26

Browse files
committed
Implement unit preconversion for property multiplication
1 parent e69e3c1 commit 5c4fd26

File tree

2 files changed

+205
-2
lines changed

2 files changed

+205
-2
lines changed

src/property_utils/properties/property.py

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
This module defines the Property class and property arithmetics.
33
"""
44

5-
from dataclasses import dataclass
5+
from dataclasses import dataclass, replace
66
from typing import Type, Optional
77
from math import isclose
88

@@ -209,8 +209,9 @@ def __mul__(self, other) -> "Property":
209209
if isinstance(other, (float, int)):
210210
return Property(self.value * other, self.unit)
211211
if isinstance(other, Property):
212+
_other = self._unit_preconversion(other)
212213
return Property(
213-
self.value * other.value, (self.unit * other.unit).simplified()
214+
self.value * _other.value, (self.unit * _other.unit).simplified()
214215
)
215216
raise PropertyBinaryOperationError(
216217
f"cannot multiply {self} with {other}; "
@@ -609,3 +610,114 @@ def _validate_comparison_input(self, other) -> None:
609610
f"cannot compare ({other}) to ({self}); "
610611
f"({other}) must have ({self.unit.to_generic()}) units. "
611612
)
613+
614+
def _unit_preconversion(self, prop: "Property") -> "Property":
615+
"""
616+
Applies a conversion to the given property's units before it is multiplied or
617+
divided with this unit.
618+
619+
The preconversion is needed to produce simplified units from the multiplication/
620+
division.
621+
For example, if you multiply 5 cm with 2.02 m you don't want to get the result
622+
in cm * m; in order to get the result in cm^2, 2.02 m (the right operand) is
623+
converted to cm first.
624+
"""
625+
if isinstance(prop.unit, CompositeDimension):
626+
return prop.to_unit(self._composite_unit_preconversion(prop.unit))
627+
628+
if isinstance(prop.unit, Dimension):
629+
return prop.to_unit(self._dimension_unit_preconversion(prop.unit))
630+
631+
if isinstance(prop.unit, MeasurementUnit):
632+
return prop.to_unit(self._simple_unit_preconversion(prop.unit))
633+
634+
return prop
635+
636+
# pylint: disable=too-many-branches
637+
def _composite_unit_preconversion(
638+
self, unit: CompositeDimension
639+
) -> CompositeDimension:
640+
"""
641+
Returns the composite dimension that the given dimension should be converted to
642+
before multiplication or division with this property.
643+
"""
644+
other = replace(unit).simplified()
645+
646+
if isinstance(self.unit, CompositeDimension):
647+
self.unit.simplify()
648+
649+
for i, num in enumerate(other.numerator):
650+
_n = self.unit.get_numerator(num.to_generic(), None)
651+
if _n is not None:
652+
other.numerator[i] = replace(_n)
653+
654+
d = self.unit.get_denominator(num.to_generic(), None)
655+
if d is not None:
656+
other.numerator[i] = replace(d)
657+
658+
for i, d in enumerate(other.denominator):
659+
_d = self.unit.get_denominator(d.to_generic(), None)
660+
if _d is not None:
661+
other.denominator[i] = replace(_d)
662+
663+
n = self.unit.get_numerator(d.to_generic(), None)
664+
if n is not None:
665+
other.denominator[i] = replace(n)
666+
667+
return other
668+
669+
_self: UnitDescriptor
670+
if isinstance(self.unit, MeasurementUnit):
671+
_self = self.unit**1
672+
elif isinstance(self.unit, Dimension):
673+
_self = replace(self.unit)
674+
else:
675+
_self = self.unit
676+
677+
if isinstance(_self, Dimension):
678+
for i, n in enumerate(other.numerator):
679+
if n.unit.isinstance(_self.unit.to_generic()):
680+
other.numerator[i] = _self.unit ** other.numerator[i].power
681+
return other
682+
683+
for i, d in enumerate(other.denominator):
684+
if d.unit.isinstance(_self.unit.to_generic()):
685+
other.denominator[i] = _self.unit ** other.denominator[i].power
686+
return other
687+
688+
return unit
689+
690+
def _dimension_unit_preconversion(self, unit: Dimension) -> Dimension:
691+
"""
692+
Returns the dimension that the given dimension should be converted to before
693+
multiplication or division with this property.
694+
"""
695+
if isinstance(self.unit, CompositeDimension):
696+
self.unit.simplify()
697+
698+
for d in self.unit.denominator:
699+
if d.unit.isinstance(unit.unit.to_generic()):
700+
return d.unit**unit.power
701+
702+
for n in self.unit.numerator:
703+
if n.unit.isinstance(unit.unit.to_generic()):
704+
return n.unit**unit.power
705+
706+
_self: UnitDescriptor
707+
if isinstance(self.unit, Dimension):
708+
_self = self.unit.unit
709+
else:
710+
_self = self.unit
711+
712+
if isinstance(_self, MeasurementUnit):
713+
if _self.isinstance(unit.unit.to_generic()):
714+
return _self**unit.power
715+
716+
return unit
717+
718+
def _simple_unit_preconversion(self, unit: MeasurementUnit) -> MeasurementUnit:
719+
"""
720+
Returns the unit that the given unit should be converted to before
721+
multiplication or division with this property.
722+
"""
723+
return self._dimension_unit_preconversion(unit**1).unit

src/property_utils/tests/properties/test_property.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from unittest_extensions import args, TestCase
44

55
from property_utils.properties.property import Property, p
6+
from property_utils.units.descriptors import CompositeDimension
67
from property_utils.units.units import NonDimensionalUnit, PressureUnit
78
from property_utils.exceptions.properties.property import (
89
PropertyExponentError,
@@ -13,6 +14,7 @@
1314
Unit3,
1415
Unit4,
1516
Unit6,
17+
Unit7,
1618
Unit8,
1719
generic_dimension_1,
1820
generic_composite_dimension,
@@ -332,6 +334,95 @@ def test_with_zero_value_property(self):
332334
self.assert_result("0.0 (A^3) / (B^3)")
333335

334336

337+
@add_to(property_test_suite, "__mul__")
338+
class TestCompositeDimensionPropertyUnitPreconversionMultiplication(TestProperty):
339+
340+
def build_property(self) -> Property:
341+
return Property(1, Unit1.A * Unit4.d**2 / Unit6.F / Unit8.H**3)
342+
343+
@args({"other": Property(1, Unit6.f / Unit1.a)})
344+
def test_with_composite_unit_simplify_numerator_and_denominator(self):
345+
self.assert_result("5.0 (d^2) / (H^3)")
346+
347+
@args({"other": Property(1, Unit1.a / Unit6.f)})
348+
def test_with_composite_unit_add_to_numerator_and_denominator(self):
349+
self.assert_result("0.2 (A^2) * (d^2) / (F^2) / (H^3)")
350+
351+
@args({"other": Property(64, Unit8.h**3)})
352+
def test_with_dimension_same_denominator(self):
353+
self.assert_result("1.0 (d^2) * A / F")
354+
355+
@args({"other": Property(16, Unit8.h**2)})
356+
def test_with_dimension_denominator(self):
357+
self.assert_result("1.0 (d^2) * A / F / H")
358+
359+
@args({"other": Property(100, Unit1.a**2)})
360+
def test_with_dimension_numerator(self):
361+
self.assert_result_almost("1.0 (A^3) * (d^2) / (H^3) / F")
362+
363+
@args({"other": Property(1, Unit4.D)})
364+
def test_with_unit_same_numerator(self):
365+
self.assert_result("5.0 (d^3) * A / (H^3) / F")
366+
367+
@args({"other": Property(2, Unit6.f)})
368+
def test_with_unit_same_denominator(self):
369+
self.assert_result("1.0 (d^2) * A / (H^3)")
370+
371+
372+
@add_to(property_test_suite, "__mul__")
373+
class TestDimensionPropertyUnitPreconversionMultiplication(TestProperty):
374+
375+
def build_property(self) -> Property:
376+
return Property(1, Unit1.A**2)
377+
378+
@args({"other": Property(1, Unit4.d / Unit1.a)})
379+
def test_with_composite_dimension_denominator(self):
380+
self.assert_result("10.0 A * d")
381+
382+
@args({"other": Property(10, Unit1.a / Unit4.d)})
383+
def test_with_composite_dimension_numerator(self):
384+
self.assert_result("1.0 (A^3) / d")
385+
386+
@args({"other": Property(1, Unit4.d / Unit1.a**2)})
387+
def test_with_composite_dimension_same_denominator(self):
388+
self.assert_result_almost("100.0 d")
389+
390+
@args({"other": Property(1000, Unit1.a**3)})
391+
def test_with_same_unit_dimension(self):
392+
self.assert_result_almost("1.0 (A^5)")
393+
394+
@args({"other": Property(10, Unit1.a)})
395+
def test_with_same_unit(self):
396+
self.assert_result("1.0 (A^3)")
397+
398+
399+
@add_to(property_test_suite, "__mul__")
400+
class TestUnitPropertyUnitPreconversionMultiplication(TestProperty):
401+
402+
def build_property(self) -> Property:
403+
return Property(1, Unit1.A)
404+
405+
@args({"other": Property(1, Unit4.d / Unit1.a)})
406+
def test_with_composite_dimension_same_denominator(self):
407+
self.assert_result("10.0 d")
408+
409+
@args({"other": Property(10, Unit1.a / Unit4.d)})
410+
def test_with_composite_dimension_same_numerator(self):
411+
self.assert_result("1.0 (A^2) / d")
412+
413+
@args({"other": Property(1, Unit4.d / Unit1.a**2)})
414+
def test_with_composite_dimension(self):
415+
self.assert_result_almost("100.0 d / A")
416+
417+
@args({"other": Property(100, Unit1.a**2)})
418+
def test_with_dimension_same_unit(self):
419+
self.assert_result_almost("1.0 (A^3)")
420+
421+
@args({"other": Property(10, Unit1.a)})
422+
def test_with_same_unit(self):
423+
self.assert_result("1.0 (A^2)")
424+
425+
335426
@add_to(property_test_suite, "__truediv__")
336427
class TestPropertyDivision(TestProperty):
337428

0 commit comments

Comments
 (0)