diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 038d5eb..e523df5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,9 @@ UNRELEASED division, i.e., operations like ``a // b`` where ``a`` and ``b`` are ``Scalar`` or ``Array`` (and combinations with ``float`` or ``int``). * Add new unit category for Joule-Thomson coefficient (``K/Pa``). +* Modified comparison behavior of ``Scalar``. The previous behavior assumes that ``Scalar(1, "m") != Scalar(100, "cm")`` + and not it has been changed to ``Scalar(1, "m") == Scalar(100, "cm")``. This may affect users that rely on the previous + behavior. 1.7.1 (2019-10-03) ------------------ diff --git a/barril.spec.yml b/barril.spec.yml new file mode 100644 index 0000000..468768b --- /dev/null +++ b/barril.spec.yml @@ -0,0 +1 @@ +source_python_dir: src diff --git a/src/barril/units/_abstractvaluewithquantity.py b/src/barril/units/_abstractvaluewithquantity.py index 0c4c17f..97ea1e8 100644 --- a/src/barril/units/_abstractvaluewithquantity.py +++ b/src/barril/units/_abstractvaluewithquantity.py @@ -2,8 +2,8 @@ from barril.units.unit_database import UnitDatabase from oop_ext.interface._interface import ImplementsInterface -from .interfaces import IObjectWithQuantity, IQuantity from ._quantity import ObtainQuantity +from .interfaces import IObjectWithQuantity, IQuantity __all__ = ["AbstractValueWithQuantityObject"] @@ -201,8 +201,12 @@ def CreateCopy(self, value=None, unit=None, category=None, **kwargs): Returns a new scalar that's a copy of this scalar. """ try: + if value is None: - value = self.GetAbstractValue(unit) + if not self.HasCategory(): + value = self.GetAbstractValue() + else: + value = self.GetAbstractValue(unit) if unit is None and category is None: return self.CreateWithQuantity(self._quantity, value=value, **kwargs) diff --git a/src/barril/units/_quantity.py b/src/barril/units/_quantity.py index c754ea0..83dc508 100644 --- a/src/barril/units/_quantity.py +++ b/src/barril/units/_quantity.py @@ -9,6 +9,7 @@ UnitDatabase, UnitsError, FixUnitIfIsLegacy, + ComposedUnitError, ) from oop_ext.interface._interface import ImplementsInterface @@ -583,6 +584,9 @@ def GetUnit(self): unit = property(GetUnit) + def IsEmpty(self): + return self == self._EMPTY_QUANTITY + def GetUnitName(self): """ :rtype: str @@ -677,9 +681,14 @@ def Convert(self, value, to_unit): :returns: An object with values to the passed unit. """ - return self._unit_database.Convert( - self._composing_categories, self._composing_units, to_unit, value - ) + try: + return self._unit_database.Convert( + self._composing_categories, self._composing_units, to_unit, value + ) + except ComposedUnitError: + return self._unit_database.Convert( + self._category, self._unit, to_unit, value + ) @classmethod def _GetComparison(cls, operator, use_literals=False): diff --git a/src/barril/units/_scalar.py b/src/barril/units/_scalar.py index 81fe445..2e49edc 100644 --- a/src/barril/units/_scalar.py +++ b/src/barril/units/_scalar.py @@ -10,7 +10,7 @@ from ._abstractvaluewithquantity import AbstractValueWithQuantityObject from ._quantity import ObtainQuantity, Quantity from .interfaces import IQuantity, IScalar -from .unit_database import UnitDatabase +from .unit_database import InvalidQuantityTypeError, UnitDatabase __all__ = ["Scalar"] @@ -105,11 +105,14 @@ def GetAbstractValue(self, unit=None): """ :param unit: + + :type unit: str or None + """ if unit is None: return self._value - else: - return self._quantity.ConvertScalarValue(self._value, unit) + + return self._quantity.ConvertScalarValue(self._value, unit) GetValue = GetAbstractValue value = property(GetAbstractValue) @@ -238,23 +241,35 @@ def GetFormatted(self, unit=None, value_format=None): ) # Compare -------------------------------------------------------------------------------------- + def _GetValueInBaseUnit(self): + try: + if self._quantity.IsEmpty(): + return self._value + return self.GetValue( + self._unit_database.GetBaseUnit(self._quantity._category) + ) + except InvalidQuantityTypeError: + return (self._value, self._quantity._unit) def __eq__(self, other): return ( type(self) is type(other) - and self._value == other.value - and self._quantity == other._quantity + and self._quantity._category == other._quantity._category + and self._GetValueInBaseUnit() == other._GetValueInBaseUnit() ) def AlmostEqual(self, other, precision): return ( type(self) is type(other) - and round(self._value - other.value, precision) == 0 - and self._quantity == other._quantity + and self._quantity._category == other._quantity._category + and round( + self._GetValueInBaseUnit() - other._GetValueInBaseUnit(), precision + ) + == 0 ) def __hash__(self, *args, **kwargs): - return hash((self._value, self._quantity)) + return hash((self._quantity._category, self._GetValueInBaseUnit())) def __lt__(self, other): if self.quantity_type != other.quantity_type: diff --git a/src/barril/units/_tests/test_empty_scalar.py b/src/barril/units/_tests/test_empty_scalar.py index 03ec48e..5fbb20c 100644 --- a/src/barril/units/_tests/test_empty_scalar.py +++ b/src/barril/units/_tests/test_empty_scalar.py @@ -11,3 +11,17 @@ def testEmptyScalar(): assert scalar.GetValue() == 0.0 assert scalar == scalar.Copy() + + +def testEmptyScalarWithInitialValue(): + # An empty scalar doesn't have a category defined + scalar_1 = Scalar.CreateEmptyScalar(20.0) + scalar_2 = Scalar.CreateEmptyScalar(20.0) + + assert scalar_1 == scalar_2 + + # ETK relies on the behavior of getting a value from an empty scalar + # passing a unit + _ = scalar_1.GetValue("m") + + assert scalar_1.GetUnit() == "" diff --git a/src/barril/units/_tests/test_posc.py b/src/barril/units/_tests/test_posc.py index 197aba1..129c949 100644 --- a/src/barril/units/_tests/test_posc.py +++ b/src/barril/units/_tests/test_posc.py @@ -304,7 +304,6 @@ def testDefaultCategories(unit_database_posc): "self inductance per length", "shear rate", "status", - "thermodynamic temperature", "volume per area", "volume per length", "volume per time per area", diff --git a/src/barril/units/_tests/test_posc_additional_units.py b/src/barril/units/_tests/test_posc_additional_units.py index 2162317..7d74cad 100644 --- a/src/barril/units/_tests/test_posc_additional_units.py +++ b/src/barril/units/_tests/test_posc_additional_units.py @@ -389,3 +389,17 @@ def testPerMicrometre(db): assert approx(per_inch.GetValue()) == 25400 assert per_metre == units.Scalar("per length", 10 ** 6, "1/m") assert per_inch == units.Scalar("per length", 25400.0, "1/in") + + +def testMassTemperaturePerMol(db): + value = units.Scalar("mass temperature per mol", 3.1415, "kg.K/mol") + + assert value.GetValue("kg.K/mol") == approx(3.1415) + assert value.GetValue("kg.degC/mol") == approx(3.1415) + assert value.GetValue("g.K/mol") == approx(3.1415 * 1e3) + assert value.GetValue("g.degC/mol") == approx(3.1415 * 1e3) + assert value.GetValue("kg.K/kmol") == approx(3.1415 * 1e3) + assert value.GetValue("kg.degC/kmol") == approx(3.1415 * 1e3) + assert value.GetValue("kg.degF/mol") == approx(3.1415 * 9 / 5) + assert value.GetValue("g.degF/mol") == approx(3.1415 * 9 / 5 * 1e3) + assert value.GetValue("kg.degF/kmol") == approx(3.1415 * 9 * 1e3 / 5) diff --git a/src/barril/units/_tests/test_quantity.py b/src/barril/units/_tests/test_quantity.py index 5abbded..13bdf39 100644 --- a/src/barril/units/_tests/test_quantity.py +++ b/src/barril/units/_tests/test_quantity.py @@ -112,3 +112,10 @@ def testDivision(): assert speed.GetUnit() == "m/s" assert speed.unit == "m/s" assert no_unit.GetUnit() == "" + + +def testEmptyQuantity(): + quantity = Quantity.CreateEmpty() + assert quantity.IsEmpty() + assert quantity.GetUnit() == "" + assert quantity.GetQuantityType() == "" diff --git a/src/barril/units/_tests/test_scalar.py b/src/barril/units/_tests/test_scalar.py index 07f675e..9547afc 100644 --- a/src/barril/units/_tests/test_scalar.py +++ b/src/barril/units/_tests/test_scalar.py @@ -383,17 +383,12 @@ class MyScalar(units.Scalar): scalar = Scalar("length", 1.18, "m") assert scalar.GetFormatted() == "1.18 [m]" - -def testEmptyScalar(): - """ - ScalarMultiData exception when some of its scalars don't have a quantity_type - """ - # An empty scalar doesn't have a category defined - scalar_1 = Scalar.CreateEmptyScalar(20.0) - - # When try to retrieve scalar value or unit a exception was being raised - assert scalar_1.GetValue("m") == 20.0 - assert scalar_1.GetUnit() == "" + # Check that squared works + scalar2 = scalar * scalar + assert scalar2.GetFormatted("m2") == "1.3924 [m2]" + assert scalar2.GetFormattedValue("m2") == "1.3924" + assert scalar2.GetFormatted((("m", 2),)) == "1.3924 [('m', 2)]" + assert scalar2.GetFormattedValue((("m", 2),)) == "1.3924" def testCopyProperties(unit_database_well_length): @@ -411,6 +406,11 @@ def testCopyProperties(unit_database_well_length): assert scalar_dest.GetUnit() == unit assert scalar_dest.GetValue() == value + scalar_dest = scalar_source.CreateCopy(category=category, value=None, unit=unit) + assert scalar_dest.GetCategory() == category + assert scalar_dest.GetUnit() == unit + assert scalar_dest.GetValue() == value + def testCopyPropertiesAndValidation(unit_database_well_length): category = "well-length-with-min-and-max" # the minimum value is zero @@ -585,7 +585,10 @@ def testScalarCreationModes(): assert Scalar("length", 10, "m") == base assert Scalar((10.0, "m")) == base - with pytest.raises(AssertionError): + with pytest.raises( + AssertionError, + match="If category and value are given, the unit must be specified too.", + ): Scalar("length", 1.0) # missing unit @@ -630,3 +633,48 @@ class Fluid: assert fluid.density.GetValue("lbm/galUS") == 10 assert fluid.concentration.GetValue("%") == 1.0 + + +def testComparison(): + from math import pi + + a = Scalar(1, "m") + b = Scalar(100, "cm") + c = Scalar(99, "cm") + + assert a == b + assert a <= b + assert b >= a + assert c <= b + assert c <= a + + # Test set creation with scalars + assert {b, a, c} == {a, b, c} == {c, a, b} + + # Check Scalars with different categories + assert Scalar(99, "psi") != Scalar(100, "cm") + + assert a.AlmostEqual(b, precision=10) + assert a.AlmostEqual(c, precision=1) + + # Testing derived quantities + q = Quantity.CreateDerived( + OrderedDict([("length", ["m", 1]), ("length", ["m", 1]), ("time", ["s", -2])]) + ) + a = Scalar(q, 1.0) + b = Scalar(q * q, 1.0) + assert {a, b, b / a} == {b, a} + + for value in [0.0, pi, 0.33333333]: + a = Scalar("temperature", value, "degC") + b = Scalar("temperature", value * 1.8 + 32, "degF") + c = Scalar("temperature", value + 273.15, "K") + + assert a <= b <= c and c <= b <= a + + +def testHashing(): + m = Scalar(1, "m") + cm = Scalar(100, "cm") + assert m == cm + assert hash(m) == hash(cm) diff --git a/src/barril/units/posc.py b/src/barril/units/posc.py index e4f34a1..acddcf2 100644 --- a/src/barril/units/posc.py +++ b/src/barril/units/posc.py @@ -2665,7 +2665,7 @@ def FillUnitDatabaseWithPosc(db=None, fill_categories=True, override_categories= "degF", f_base_to_unit, f_unit_to_base, - default_category="thermodynamic temperature", + default_category=None, ) f_unit_to_base = MakeCustomaryToBase(0.0, 0.1761102, 1.0, 0.0) f_base_to_unit = MakeBaseToCustomary(0.0, 0.1761102, 1.0, 0.0) @@ -2755,7 +2755,7 @@ def FillUnitDatabaseWithPosc(db=None, fill_categories=True, override_categories= "degR", f_base_to_unit, f_unit_to_base, - default_category="thermodynamic temperature", + default_category=None, ) f_unit_to_base = MakeCustomaryToBase(0.0, 0.1, 1.0, 0.0) f_base_to_unit = MakeBaseToCustomary(0.0, 0.1, 1.0, 0.0)