From 1cb236eefb42874ed621e618d3b3ae6c3692b02b Mon Sep 17 00:00:00 2001 From: Ken Kroenlein Date: Mon, 18 Dec 2023 17:17:26 -0700 Subject: [PATCH] PLA-13739 Update Cake demo to not have dimensionless absolute quantities --- gemd/demo/cake.py | 2 +- gemd/entity/object/has_quantities.py | 32 +++++- setup.py | 2 +- tests/entity/object/test_ingredient_spec.py | 105 ++++++++++++++------ 4 files changed, 103 insertions(+), 38 deletions(-) diff --git a/gemd/demo/cake.py b/gemd/demo/cake.py index aa8b05b9..055e3ecc 100644 --- a/gemd/demo/cake.py +++ b/gemd/demo/cake.py @@ -647,7 +647,7 @@ def _make_material(*, material_name, template, process_tmpl_name, process_kwargs material=eggs, labels=['wet'], process=wetmix.process, - absolute_quantity=NominalReal(nominal=4, units='') + absolute_quantity=NominalReal(nominal=4, units='count') ) vanilla = _make_material( diff --git a/gemd/entity/object/has_quantities.py b/gemd/entity/object/has_quantities.py index 67ed933d..bc7ea8ff 100644 --- a/gemd/entity/object/has_quantities.py +++ b/gemd/entity/object/has_quantities.py @@ -1,4 +1,6 @@ """For entities that hve quantities.""" +from sys import float_info + from gemd.entity.bounds.real_bounds import RealBounds from gemd.entity.value.continuous_value import ContinuousValue from gemd.entity.value.base_value import BaseValue @@ -50,7 +52,7 @@ def _check(value: BaseValue): level = get_validation_level() accept = level == WarningLevel.IGNORE or fraction_bounds.contains(value) if not accept: - message = f"Value {value} is not between 0 and 1." + message = f"Value {value} is not a dimensionless value between 0 and 1." if level == WarningLevel.WARNING: logger.warning(message) else: @@ -110,7 +112,29 @@ def absolute_quantity(self) -> ContinuousValue: def absolute_quantity(self, absolute_quantity: ContinuousValue): if absolute_quantity is None: self._absolute_quantity = None - elif isinstance(absolute_quantity, ContinuousValue): - self._absolute_quantity = absolute_quantity - else: + elif not isinstance(absolute_quantity, ContinuousValue): raise TypeError("absolute_quantity was not given as a continuous value") + else: + max_bounds = RealBounds( + lower_bound=0.0, + upper_bound=float_info.max, + default_units=absolute_quantity.units + ) + dimensionless = RealBounds( + lower_bound=0.0, + upper_bound=float_info.max, + default_units='' + ) + level = get_validation_level() + if level != WarningLevel.IGNORE: + messages = [] + if not max_bounds.contains(absolute_quantity): + messages.append(f"Value {absolute_quantity} is less than 0.0.") + if dimensionless.contains(absolute_quantity): + messages.append(f"Value {absolute_quantity} is dimensionless.") + if level == WarningLevel.WARNING: + for message in messages: + logger.warning(message) + else: + raise ValueError("; ".join(messages)) + self._absolute_quantity = absolute_quantity diff --git a/setup.py b/setup.py index 8159dd1e..ae5cc13d 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ packages.append("") setup(name='gemd', - version='1.16.9', + version='1.17.0', python_requires='>=3.8', url='http://github.com/CitrineInformatics/gemd-python', description="Python binding for Citrine's GEMD data model", diff --git a/tests/entity/object/test_ingredient_spec.py b/tests/entity/object/test_ingredient_spec.py index 62a011e8..6388720b 100644 --- a/tests/entity/object/test_ingredient_spec.py +++ b/tests/entity/object/test_ingredient_spec.py @@ -33,13 +33,30 @@ def test_ingredient_reassignment(): assert set(frying.ingredients) == {oil, potatoes} +VALID_FRACTIONS = [ + NominalReal(1.0, ''), + UniformReal(0.5, 0.6, ''), + NormalReal(0.2, 0.3, '') +] + +INVALID_FRACTIONS = [ + NominalReal(1.0, 'm'), + UniformReal(0.7, 1.1, ''), + NormalReal(-0.2, 0.3, '') +] + VALID_QUANTITIES = [ - NominalReal(14.0, ''), - UniformReal(0.5, 0.6, 'm'), - NormalReal(-0.3, 0.6, "kg") + NominalReal(14.0, 'g'), + UniformReal(0.5, 0.6, 'mol'), + NormalReal(0.3, 0.6, 'cc') ] INVALID_QUANTITIES = [ + NominalReal(14.0, ''), + UniformReal(-0.1, 0.3, 'mol'), +] + +INVALID_TYPES = [ NominalCategorical("blue"), NominalInteger(5), EmpiricalFormula("CH4"), @@ -48,55 +65,79 @@ def test_ingredient_reassignment(): ] +@pytest.mark.parametrize("valid_fraction", VALID_FRACTIONS) +def test_valid_fractions(valid_fraction, caplog): + """ + Check that all fractional quantities must be continuous values. + """ + with validation_level(WarningLevel.WARNING): + ingred = IngredientSpec(name="name", mass_fraction=valid_fraction) + assert ingred.mass_fraction == valid_fraction + ingred = IngredientSpec(name="name", volume_fraction=valid_fraction) + assert ingred.volume_fraction == valid_fraction + ingred = IngredientSpec(name="name", number_fraction=valid_fraction) + assert ingred.number_fraction == valid_fraction + assert ingred.absolute_quantity is None + assert len(caplog.records) == 0, "Warned on valid values with WARNING." + + @pytest.mark.parametrize("valid_quantity", VALID_QUANTITIES) def test_valid_quantities(valid_quantity, caplog): """ Check that all quantities must be continuous values. - - There are no restrictions on the value or the units. Although a volume fraction of -5 kg - does not make physical sense, it will not throw an error. """ - with validation_level(WarningLevel.IGNORE): - ingred = IngredientSpec(name="name", mass_fraction=valid_quantity) - assert ingred.mass_fraction == valid_quantity - ingred = IngredientSpec(name="name", volume_fraction=valid_quantity) - assert ingred.volume_fraction == valid_quantity - ingred = IngredientSpec(name="name", number_fraction=valid_quantity) - assert ingred.number_fraction == valid_quantity + with validation_level(WarningLevel.WARNING): ingred = IngredientSpec(name="name", absolute_quantity=valid_quantity) assert ingred.absolute_quantity == valid_quantity - assert len(caplog.records) == 0, "Warned when validation set to IGNORE." + assert ingred.mass_fraction is None + assert ingred.number_fraction is None + assert ingred.volume_fraction is None + assert len(caplog.records) == 0, "Warned on valid values with WARNING." -def test_validation_control(caplog): - """Verify that when validation is requested, limits are enforced.""" +@pytest.mark.parametrize("invalid_fraction", INVALID_FRACTIONS) +def test_invalid_fractions(invalid_fraction, caplog): + """ + Verify that when validation is requested, limits are enforced for fractions. + """ + with validation_level(WarningLevel.IGNORE): + IngredientSpec(name="name", mass_fraction=invalid_fraction) + assert len(caplog.records) == 0, f"Warned on invalid values with IGNORE: {invalid_fraction}" with validation_level(WarningLevel.WARNING): - IngredientSpec(name="name", mass_fraction=NominalReal(0.5, '')) - assert len(caplog.records) == 0, "Warned on valid values with WARNING." - IngredientSpec(name="name", mass_fraction=NominalReal(5, '')) - assert len(caplog.records) == 1, "Didn't warn on invalid values with WARNING." - IngredientSpec(name="name", mass_fraction=NominalReal(0.5, 'm')) - assert len(caplog.records) == 2, "Didn't warn on invalid units with WARNING." + IngredientSpec(name="name", mass_fraction=invalid_fraction) + assert len(caplog.records) == 1, f"Didn't warn on invalid values with IGNORE: {invalid_fraction}" with validation_level(WarningLevel.FATAL): - # The following should not raise an exception - IngredientSpec(name="name", mass_fraction=NominalReal(0.5, '')) - with pytest.raises(ValueError): - IngredientSpec(name="name", mass_fraction=NominalReal(5, '')) with pytest.raises(ValueError): - IngredientSpec(name="name", mass_fraction=NominalReal(0.5, 'm')) + IngredientSpec(name="name", mass_fraction=invalid_fraction) @pytest.mark.parametrize("invalid_quantity", INVALID_QUANTITIES) -def test_invalid_quantities(invalid_quantity): +def test_invalid_quantities(invalid_quantity, caplog): + """ + Verify that when validation is requested, limits are enforced for fractions. + """ + with validation_level(WarningLevel.IGNORE): + IngredientSpec(name="name", absolute_quantity=invalid_quantity) + assert len(caplog.records) == 0, f"Warned on invalid values with IGNORE: {invalid_quantity}" + with validation_level(WarningLevel.WARNING): + IngredientSpec(name="name", absolute_quantity=invalid_quantity) + assert len(caplog.records) == 1, f"Didn't warn on invalid values with IGNORE: {invalid_quantity}" + with validation_level(WarningLevel.FATAL): + with pytest.raises(ValueError): + IngredientSpec(name="name", absolute_quantity=invalid_quantity) + + +@pytest.mark.parametrize("invalid_type", INVALID_TYPES) +def test_invalid_types(invalid_type): """Check that any non-continuous value for a quantity throws a TypeError.""" with pytest.raises(TypeError): - IngredientSpec(name="name", mass_fraction=invalid_quantity) + IngredientSpec(name="name", mass_fraction=invalid_type) with pytest.raises(TypeError): - IngredientSpec(name="name", volume_fraction=invalid_quantity) + IngredientSpec(name="name", volume_fraction=invalid_type) with pytest.raises(TypeError): - IngredientSpec(name="name", number_fraction=invalid_quantity) + IngredientSpec(name="name", number_fraction=invalid_type) with pytest.raises(TypeError): - IngredientSpec(name="name", absolute_quantity=invalid_quantity) + IngredientSpec(name="name", absolute_quantity=invalid_type) def test_invalid_assignment():