From 45feaab52d4d3b1e606b8726cb8ebd3b624de87e Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 10 Jun 2026 13:30:47 -0700 Subject: [PATCH 1/4] update: add formation energy contributions --- .../non_scalar/non_scalar_property_context.py | 29 ++++++++ express/settings.py | 3 + .../test_formation_energy_contributions.py | 72 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 express/properties/non_scalar/non_scalar_property_context.py create mode 100644 tests/unit/properties/non_scalar/test_formation_energy_contributions.py diff --git a/express/properties/non_scalar/non_scalar_property_context.py b/express/properties/non_scalar/non_scalar_property_context.py new file mode 100644 index 00000000..737a5718 --- /dev/null +++ b/express/properties/non_scalar/non_scalar_property_context.py @@ -0,0 +1,29 @@ +from typing import Any, Type + +from express.parsers import BaseParser +from express.properties.non_scalar import NonScalarProperty + + +class NonScalarPropertyFromContext(NonScalarProperty): + def __init__( + self, + name: str, + parser: Type[BaseParser], + data: Any = None, + context: dict[str, Any] | None = None, + context_key: str | None = None, + *args, + **kwargs, + ): + super().__init__(name, parser, *args, **kwargs) + if data is not None: + self.data = data + elif "value" in kwargs: + self.data = kwargs["value"] + else: + self.data = context[context_key or name] + + def _serialize(self): + if isinstance(self.data, dict): + return {"name": self.name, **self.data} + return {"name": self.name, "values": self.data} diff --git a/express/settings.py b/express/settings.py index 4e545d90..550af34f 100644 --- a/express/settings.py +++ b/express/settings.py @@ -36,6 +36,9 @@ "total_energy_contributions": { "reference": "express.properties.non_scalar.total_energy_contributions.TotalEnergyContributions" }, + "formation_energy_contributions": { + "reference": "express.properties.non_scalar.non_scalar_property_context.NonScalarPropertyFromContext" + }, "material": {"reference": "express.properties.material.Material"}, "symmetry": {"reference": "express.properties.non_scalar.symmetry.Symmetry"}, "workflow:pyml_predict": {"reference": "express.properties.workflow.PyMLTrainAndPredictWorkflow"}, diff --git a/tests/unit/properties/non_scalar/test_formation_energy_contributions.py b/tests/unit/properties/non_scalar/test_formation_energy_contributions.py new file mode 100644 index 00000000..5c408bbc --- /dev/null +++ b/tests/unit/properties/non_scalar/test_formation_energy_contributions.py @@ -0,0 +1,72 @@ +from tests.unit import UnitTestBase + +from express.properties.non_scalar.non_scalar_property_context import NonScalarPropertyFromContext + +COMPOUND_CONTRIBUTION = { + "formula": "SiC", + "n_atoms": 4, + "is_elemental": False, + "total_energy": -520.003969643439, + "total_energy_per_atom": -130.00099241085975, + "precision_value": 8192, + "precision_metric": "KPPRA", +} +SILICON_CONTRIBUTION = { + "formula": "Si", + "n_atoms": 2, + "is_elemental": True, + "total_energy": -261.003969643439, + "total_energy_per_atom": -130.5019848217195, + "precision_value": 8192, + "precision_metric": "KPPRA", +} +FORMATION_ENERGY_CONTRIBUTIONS_VALUES = [COMPOUND_CONTRIBUTION, SILICON_CONTRIBUTION] +FORMATION_ENERGY_CONTRIBUTIONS = { + "name": "formation_energy_contributions", + "values": FORMATION_ENERGY_CONTRIBUTIONS_VALUES, +} + + +class FormationEnergyContributionsTest(UnitTestBase): + def setUp(self): + super().setUp() + + def tearDown(self): + super().tearDown() + + def test_formation_energy_contributions_from_context_values(self): + property_ = NonScalarPropertyFromContext( + "formation_energy_contributions", + None, + data=FORMATION_ENERGY_CONTRIBUTIONS_VALUES, + ) + + self.assertDeepAlmostEqual(property_.serialize_and_validate(), FORMATION_ENERGY_CONTRIBUTIONS) + + def test_formation_energy_contributions_from_context_data(self): + property_ = NonScalarPropertyFromContext( + "formation_energy_contributions", + None, + data={"values": FORMATION_ENERGY_CONTRIBUTIONS_VALUES}, + ) + + self.assertDeepAlmostEqual(property_.serialize_and_validate(), FORMATION_ENERGY_CONTRIBUTIONS) + + def test_formation_energy_contributions_from_value_alias(self): + property_ = NonScalarPropertyFromContext( + "formation_energy_contributions", + None, + value=FORMATION_ENERGY_CONTRIBUTIONS_VALUES, + ) + + self.assertDeepAlmostEqual(property_.serialize_and_validate(), FORMATION_ENERGY_CONTRIBUTIONS) + + def test_formation_energy_contributions_from_context_key(self): + property_ = NonScalarPropertyFromContext( + "formation_energy_contributions", + None, + context={"FORMATION_ENERGY_REFERENCES": FORMATION_ENERGY_CONTRIBUTIONS_VALUES}, + context_key="FORMATION_ENERGY_REFERENCES", + ) + + self.assertDeepAlmostEqual(property_.serialize_and_validate(), FORMATION_ENERGY_CONTRIBUTIONS) From a06d653b43deb949e583b5eca1edf04c843283a0 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 10 Jun 2026 13:33:56 -0700 Subject: [PATCH 2/4] update: esse --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0f3a4d81..eb6fd0da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ dependencies = [ "pymatgen>=2023.8.10", "ase>=3.17.0", - "mat3ra-esse>=2026.5.30.post0", + "mat3ra-esse @ git+https://github.com/Exabyte-io/esse.git@bf3c8c51a83b6fbd3cf2105bdb69a5d1b50c743e", "jarvis-tools>=2023.12.12", # To avoid module 'numpy.linalg._umath_linalg' has no attribute '_ilp64' in Colab "numpy>=1.24.4,<2", From cfc6ab30b94defbd08d4ceb80c9b30fe39ae063f Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 11 Jun 2026 20:00:51 -0700 Subject: [PATCH 3/4] update: use defaults automatically --- .../non_scalar/non_scalar_property_context.py | 9 ++++++ pyproject.toml | 2 +- .../test_formation_energy_contributions.py | 29 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/express/properties/non_scalar/non_scalar_property_context.py b/express/properties/non_scalar/non_scalar_property_context.py index 737a5718..1ba57e79 100644 --- a/express/properties/non_scalar/non_scalar_property_context.py +++ b/express/properties/non_scalar/non_scalar_property_context.py @@ -1,5 +1,7 @@ from typing import Any, Type +from mat3ra.esse.utils import validate_and_clean + from express.parsers import BaseParser from express.properties.non_scalar import NonScalarProperty @@ -27,3 +29,10 @@ def _serialize(self): if isinstance(self.data, dict): return {"name": self.name, **self.data} return {"name": self.name, "values": self.data} + + def serialize_and_validate(self): + instance = self._serialize() + result = validate_and_clean(instance, self.schema) + if not result["is_valid"]: + raise result["errors"][0] + return instance diff --git a/pyproject.toml b/pyproject.toml index eb6fd0da..ce48d4c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ dependencies = [ "pymatgen>=2023.8.10", "ase>=3.17.0", - "mat3ra-esse @ git+https://github.com/Exabyte-io/esse.git@bf3c8c51a83b6fbd3cf2105bdb69a5d1b50c743e", + "mat3ra-esse @ git+https://github.com/Exabyte-io/esse.git@75c6729da64a4cd77f4ae25bdb5246ecb6c737d2", "jarvis-tools>=2023.12.12", # To avoid module 'numpy.linalg._umath_linalg' has no attribute '_ilp64' in Colab "numpy>=1.24.4,<2", diff --git a/tests/unit/properties/non_scalar/test_formation_energy_contributions.py b/tests/unit/properties/non_scalar/test_formation_energy_contributions.py index 5c408bbc..d4da8765 100644 --- a/tests/unit/properties/non_scalar/test_formation_energy_contributions.py +++ b/tests/unit/properties/non_scalar/test_formation_energy_contributions.py @@ -25,6 +25,20 @@ "name": "formation_energy_contributions", "values": FORMATION_ENERGY_CONTRIBUTIONS_VALUES, } +COMPOUND_CONTRIBUTION_WITHOUT_PRECISION = { + "formula": "SiC", + "n_atoms": 4, + "is_elemental": False, + "total_energy": -520.003969643439, + "total_energy_per_atom": -130.00099241085975, +} +DEFAULT_PRECISION_VALUE = -1 +DEFAULT_PRECISION_METRIC = "unknown" +COMPOUND_CONTRIBUTION_WITH_DEFAULT_PRECISION = { + **COMPOUND_CONTRIBUTION_WITHOUT_PRECISION, + "precision_value": DEFAULT_PRECISION_VALUE, + "precision_metric": DEFAULT_PRECISION_METRIC, +} class FormationEnergyContributionsTest(UnitTestBase): @@ -61,6 +75,21 @@ def test_formation_energy_contributions_from_value_alias(self): self.assertDeepAlmostEqual(property_.serialize_and_validate(), FORMATION_ENERGY_CONTRIBUTIONS) + def test_formation_energy_contributions_applies_schema_defaults(self): + property_ = NonScalarPropertyFromContext( + "formation_energy_contributions", + None, + data=[COMPOUND_CONTRIBUTION_WITHOUT_PRECISION], + ) + + self.assertDeepAlmostEqual( + property_.serialize_and_validate(), + { + "name": "formation_energy_contributions", + "values": [COMPOUND_CONTRIBUTION_WITH_DEFAULT_PRECISION], + }, + ) + def test_formation_energy_contributions_from_context_key(self): property_ = NonScalarPropertyFromContext( "formation_energy_contributions", From 6fc1ffca35e3ca54316a6149187cd5034c70dd67 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 11 Jun 2026 20:20:51 -0700 Subject: [PATCH 4/4] update: esse --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ce48d4c9..89653c2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ dependencies = [ "pymatgen>=2023.8.10", "ase>=3.17.0", - "mat3ra-esse @ git+https://github.com/Exabyte-io/esse.git@75c6729da64a4cd77f4ae25bdb5246ecb6c737d2", + "mat3ra-esse>=2026.6.12", "jarvis-tools>=2023.12.12", # To avoid module 'numpy.linalg._umath_linalg' has no attribute '_ilp64' in Colab "numpy>=1.24.4,<2",