Skip to content

Commit

Permalink
Support converting from aliased to alias units
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxcode123 committed Apr 6, 2024
1 parent eafc3bb commit 48f92ce
Show file tree
Hide file tree
Showing 2 changed files with 241 additions and 12 deletions.
140 changes: 140 additions & 0 deletions src/property_utils/tests/units/test_converter_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@
Unit3,
Unit4,
Unit5,
Unit6,
Unit7,
Unit8,
Unit1Converter,
UnregisteredConverter,
Unit1_2Converter,
Unit1_314Converter,
Unit2_4Converter,
Unit3_2Converter,
Expand All @@ -38,6 +42,8 @@
Unit1Unit4FractionConverter,
Unit1_2Unit4_3Converter,
Unit5Converter,
Unit1Unit4_2Converter,
Unit6Unit4_2Converter,
)


Expand Down Expand Up @@ -377,6 +383,28 @@ def test_from_B_to_b(self):
self.assertResultRaises(UnsupportedConverterError)


@add_to(ExponentiatedUnitConverter_test_suite)
class TestAliasExponentiatedUnitConverterConvert(TestCase):
def subject(self, value, from_descriptor, to_descriptor):
return Unit1_2Converter.convert(value, from_descriptor, to_descriptor)

@args({"value": 2, "from_descriptor": Unit1.A**2, "to_descriptor": Unit1.A**2})
def test_with_same_unit(self):
self.assertResult(2)

@args({"value": 5, "from_descriptor": Unit1.A**2, "to_descriptor": Unit1.a**2})
def test_with_same_unit_type(self):
self.assertResult(500)

@args({"value": 5, "from_descriptor": Unit1.A**2, "to_descriptor": Unit6.F})
def test_with_alias_units(self):
self.assertResult(250)

@args({"value": 10, "from_descriptor": Unit1.a**2, "to_descriptor": Unit6.f})
def test_with_alias_si_units(self):
self.assertResult(10)


@add_to(ExponentiatedUnitConverter_test_suite)
class TestExponentiatedUnitConverterGetFactor(TestCase):
def subject(self, from_descriptor, to_descriptor):
Expand Down Expand Up @@ -685,6 +713,118 @@ def test_valid_conversion_from_A2D3_to_A2d3(self):
self.assertResult(10 / 125)


@add_to(CompositeUnitConverter_test_suite)
class TestAliasCompositeUnitConverterConvert(TestCase):
def subject(self, value, from_descriptor, to_descriptor):
return Unit1Unit4_2Converter.convert(value, from_descriptor, to_descriptor)

@args(
{
"value": 2,
"from_descriptor": Unit1.A / (Unit4.D**2),
"to_descriptor": Unit1.A / (Unit4.D**2),
}
)
def test_with_same_units(self):
self.assertResult(2)

@args(
{
"value": 2,
"from_descriptor": Unit1.A / (Unit4.D**2),
"to_descriptor": Unit1.a / (Unit4.d**2),
}
)
def test_with_si_units(self):
self.assertResult(0.8)

@args(
{
"value": 3,
"from_descriptor": Unit1.A / (Unit4.D**2),
"to_descriptor": Unit5.E,
}
)
def test_with_alias_unit(self):
self.assertResult(3 * 10 / 15 / 25)

@args(
{
"value": 7,
"from_descriptor": Unit1.a / (Unit4.d**2),
"to_descriptor": Unit5.e,
}
)
def test_with_alias_si_unit(self):
self.assertResult(7)


@add_to(CompositeUnitConverter_test_suite)
class TestTwiceAliasCompositeUnitConverterConvert(TestCase):
def subject(self, value, from_descriptor, to_descriptor):
return Unit6Unit4_2Converter.convert(value, from_descriptor, to_descriptor)

@args(
{
"value": 2,
"from_descriptor": Unit6.F / (Unit4.D**2),
"to_descriptor": Unit6.F / (Unit4.D**2),
}
)
def test_with_same_units(self):
self.assertResult(2)

@args(
{
"value": 2,
"from_descriptor": Unit6.F / (Unit4.D**2),
"to_descriptor": Unit6.f / (Unit4.d**2),
}
)
def test_with_si_units(self):
self.assertResult(2 * 2 / 25)

@args(
{
"value": 3,
"from_descriptor": Unit6.F / (Unit4.D**2),
"to_descriptor": Unit8.H,
}
)
def test_with_alias_unit(self):
self.assertResult(0.06)

@args(
{
"value": 7,
"from_descriptor": Unit6.f / (Unit4.d**2),
"to_descriptor": Unit8.h,
}
)
def test_with_alias_si_unit(self):
self.assertResult(7)

@args(
{
"value": 3,
"from_descriptor": Unit6.F / (Unit4.D**2),
"to_descriptor": Unit1.A**2 / (Unit4.D**2),
}
)
def test_with_aliased_units(self):
self.assertResult(0.06)

@args(
{
"value": 3,
"from_descriptor": Unit6.f / (Unit4.d**2),
"to_descriptor": Unit1.a**2 / (Unit4.d**2),
}
)
def test_with_aliased_si_units(self):
self.assertResult(3)


@add_to(CompositeUnitConverter_test_suite)
class TestCompositeUnitConverterGetFactor(TestCase):
def subject(self, from_descriptor, to_descriptor):
Expand Down
113 changes: 101 additions & 12 deletions src/property_utils/units/converter_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,13 @@ def get_factor(
>>> assert LengthUnitConverter.get_factor(LengthUnit.CENTI_METER, LengthUnit.INCH) == 1/2.54
"""
if not from_descriptor.isinstance(cls.generic_unit_descriptor):
if not from_descriptor.isinstance_equivalent(cls.generic_unit_descriptor):
raise UnitConversionError(
f"invalid 'from_descriptor; expected an instance of {cls.generic_unit_descriptor}. "
f"invalid 'from_descriptor; expected an instance-equivalent of {cls.generic_unit_descriptor}. "
)
if not to_descriptor.isinstance(cls.generic_unit_descriptor):
if not to_descriptor.isinstance_equivalent(cls.generic_unit_descriptor):
raise UnitConversionError(
f"invalid 'to_descriptor'; expected an instance of {cls.generic_unit_descriptor}. "
f"invalid 'to_descriptor'; expected an instance-equivalent of {cls.generic_unit_descriptor}. "
)
from_unit = MeasurementUnit.from_descriptor(from_descriptor)

Expand Down Expand Up @@ -483,16 +483,22 @@ def get_factor(
>>> assert AreaUnitConverter.get_factor(LengthUnit.INCH**2, LengthUnit.CENTI_METER**2) == 6.4516
"""
if not from_descriptor.isinstance(cls.generic_unit_descriptor):
if not from_descriptor.isinstance_equivalent(cls.generic_unit_descriptor):
raise UnitConversionError(
f"invalid 'from_descriptor; expected an instance of {cls.generic_unit_descriptor}. "
f"invalid 'from_descriptor; expected an instance-equivalent of {cls.generic_unit_descriptor}. "
)
if not to_descriptor.isinstance(cls.generic_unit_descriptor):
if not to_descriptor.isinstance_equivalent(cls.generic_unit_descriptor):
raise UnitConversionError(
f"invalid 'to_descriptor'; expected an instance of {cls.generic_unit_descriptor}. "
f"invalid 'to_descriptor'; expected an instance-equivalent of {cls.generic_unit_descriptor}. "
)
from_dimension = Dimension.from_descriptor(from_descriptor)

if not to_descriptor.isinstance(from_descriptor.to_generic()):
if isinstance(to_descriptor, AliasMeasurementUnit):
return cls._get_aliased_factor(from_dimension, to_descriptor)

to_dimension = Dimension.from_descriptor(to_descriptor)

try:
converter = get_converter(cls.generic_unit_descriptor.unit_type)
except UndefinedConverterError:
Expand Down Expand Up @@ -657,15 +663,24 @@ def get_factor(
>>> assert VelocityUnitConverter.get_factor(LengthUnit.INCH/TimeUnit.MINUTE, LengthUnit.CENTI_METER/TimeUnit.SECOND) == 2.54/60
"""
if not from_descriptor.isinstance(cls.generic_unit_descriptor):
if not from_descriptor.isinstance_equivalent(cls.generic_unit_descriptor):
raise UnitConversionError(
f"invalid 'from_descriptor; expected an instance of {cls.generic_unit_descriptor}. "
f"invalid 'from_descriptor; expected an instance-equivalent of {cls.generic_unit_descriptor}. "
)
if not to_descriptor.isinstance(cls.generic_unit_descriptor):
if not to_descriptor.isinstance_equivalent(cls.generic_unit_descriptor):
raise UnitConversionError(
f"invalid 'to_descriptor'; expected an instance of {cls.generic_unit_descriptor}. "
f"invalid 'to_descriptor'; expected an instance-equivalent of {cls.generic_unit_descriptor}. "
)

from_dimension = CompositeDimension.from_descriptor(from_descriptor)

if not to_descriptor.isinstance(from_descriptor.to_generic()):

if cls._is_alias(from_dimension, to_descriptor) or (
cls._is_aliased(from_dimension)
):
return cls._get_aliased_factor(from_dimension, to_descriptor)

to_dimension = CompositeDimension.from_descriptor(to_descriptor)
return cls._get_numerator_factor(
from_dimension, to_dimension
Expand Down Expand Up @@ -731,3 +746,77 @@ def _get_denominator_factor(
factor = (converter.get_factor(from_d.unit, to_d.unit)) ** from_d.power
denominator_factor *= factor
return denominator_factor

@staticmethod
def _is_alias(
from_dimension: CompositeDimension, descriptor: UnitDescriptor
) -> bool:
"""
Returns True if the descriptor is an alias of the from_dimension.
Assumes that from_dimension and descriptor are both an instance of the
converter's generic unit descriptor.
"""
if isinstance(descriptor, AliasMeasurementUnit):
return True

if isinstance(descriptor, Dimension):
if isinstance(descriptor.unit, AliasMeasurementUnit):
return True

return False

if isinstance(descriptor, CompositeDimension):
for n in descriptor.numerator:
if from_dimension.get_numerator(n.to_generic(), None) is None:
return True

for d in descriptor.denominator:
if from_dimension.get_denominator(d.to_generic(), None) is None:
return True

return False

@staticmethod
def _is_aliased(dimension: CompositeDimension) -> bool:
"""
Returns True if the dimension contains an alias, False otherwise.
"""
for n in dimension.numerator:
if isinstance(n.unit, AliasMeasurementUnit):
return True

for d in dimension.denominator:
if isinstance(d.unit, AliasMeasurementUnit):
return True

return False

@classmethod
def _get_aliased_factor(
cls, from_dimension: CompositeDimension, to_descriptor: UnitDescriptor
) -> float:
"""
Returns the conversion factor from an alias unit to its aliased.
The conversion happens in three steps:
1. Convert from the alias unit to the SI unit.
2. Convert from the SI unit to the aliased SI units (this step is not
implemented in code, because the conversion factor is 1)
3. Convert from the SI units to the target units.
e.g. if you want to convert from cal/K/s to kW/K:
1. cal/K/s -> J/K/s
2. J/K/s -> W/K (conversion factor 1)
3. W/K -> kW/K
"""
step_1_factor = cls.get_factor(from_dimension, from_dimension.si())

converter = get_converter(to_descriptor.to_generic())

step_3_factor = converter.convert(
1, to_descriptor.to_generic().to_si(), to_descriptor
)

return step_1_factor * step_3_factor

0 comments on commit 48f92ce

Please sign in to comment.