From 713ed61b5121b25ee407829708ea97ea98e6b588 Mon Sep 17 00:00:00 2001 From: JonathanSchmidt1 Date: Fri, 27 Mar 2026 20:56:23 +0100 Subject: [PATCH 01/11] add spin/charge standard inputs --- metatomic-torch/src/misc.cpp | 2 ++ metatomic-torch/tests/misc.cpp | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/metatomic-torch/src/misc.cpp b/metatomic-torch/src/misc.cpp index 647b685e6..2a8f7665c 100644 --- a/metatomic-torch/src/misc.cpp +++ b/metatomic-torch/src/misc.cpp @@ -427,6 +427,8 @@ inline std::unordered_set KNOWN_INPUTS_OUTPUTS = { "velocities", "masses", "charges", + "charge", + "spin", "heat_flux", }; diff --git a/metatomic-torch/tests/misc.cpp b/metatomic-torch/tests/misc.cpp index 55aae7d1c..c39b86ab3 100644 --- a/metatomic-torch/tests/misc.cpp +++ b/metatomic-torch/tests/misc.cpp @@ -105,3 +105,16 @@ TEST_CASE("Pick variant") { " - 'energy/foo': Variant foo of the output"; CHECK_THROWS_WITH(metatomic_torch::pick_output("energy", outputs), StartsWith(err)); } + +TEST_CASE("Standard inputs") { + // "charge" and "spin" are recognized as standard (non-namespaced) input names + auto [known_charge, base_charge, variant_charge] = metatomic_torch::details::validate_name_and_check_variant("charge"); + CHECK(known_charge == true); + CHECK(base_charge == "charge"); + CHECK(variant_charge == ""); + + auto [known_spin, base_spin, variant_spin] = metatomic_torch::details::validate_name_and_check_variant("spin"); + CHECK(known_spin == true); + CHECK(base_spin == "spin"); + CHECK(variant_spin == ""); +} From 010047020e0e197b686e80ed85ac2b62bfb7df04 Mon Sep 17 00:00:00 2001 From: JonathanSchmidt1 Date: Fri, 27 Mar 2026 21:27:02 +0100 Subject: [PATCH 02/11] export validate_name_and_check_variant for use in tests Co-Authored-By: Claude Sonnet 4.6 --- metatomic-torch/include/metatomic/torch/misc.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metatomic-torch/include/metatomic/torch/misc.hpp b/metatomic-torch/include/metatomic/torch/misc.hpp index 84fa4f215..97fc4cafa 100644 --- a/metatomic-torch/include/metatomic/torch/misc.hpp +++ b/metatomic-torch/include/metatomic/torch/misc.hpp @@ -76,7 +76,7 @@ namespace details { /// - a boolean indicating whether this is a known output/input /// - the name of the base output/input (empty if custom) /// - the name of the variant (empty if none) -std::tuple validate_name_and_check_variant( +METATOMIC_TORCH_EXPORT std::tuple validate_name_and_check_variant( const std::string& name ); } From 8f010cfa2b83de40f2419737550afd08c4d6b6cc Mon Sep 17 00:00:00 2001 From: JonathanSchmidt1 Date: Sat, 28 Mar 2026 17:12:59 +0100 Subject: [PATCH 03/11] add docs --- docs/src/index.rst | 10 +++++++++ docs/src/inputs/charge.rst | 42 ++++++++++++++++++++++++++++++++++++++ docs/src/inputs/index.rst | 37 +++++++++++++++++++++++++++++++++ docs/src/inputs/spin.rst | 42 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+) create mode 100644 docs/src/inputs/charge.rst create mode 100644 docs/src/inputs/index.rst create mode 100644 docs/src/inputs/spin.rst diff --git a/docs/src/index.rst b/docs/src/index.rst index eec45bb12..46656e6a7 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -51,6 +51,15 @@ existing trained models, look into the metatrain_ project instead. Understand the different outputs a model can have, and what the metadata should be provided for standardized outputs, such as the potential energy. + .. grid-item-card:: 📥 Standard models inputs + :link: atomistic-models-inputs + :link-type: ref + :columns: 12 12 6 6 + :margin: 0 3 0 0 + + Understand the different inputs a simulation engine can provide to a + model, such as the total charge or spin multiplicity of the system. + .. grid-item-card:: ⚙️ Simulation engines :link: engines :link-type: ref @@ -93,6 +102,7 @@ existing trained models, look into the metatrain_ project instead. installation torch/index outputs/index + inputs/index engines/index examples/index cite diff --git a/docs/src/inputs/charge.rst b/docs/src/inputs/charge.rst new file mode 100644 index 000000000..ef70ad284 --- /dev/null +++ b/docs/src/inputs/charge.rst @@ -0,0 +1,42 @@ +.. _charge-input: + +Charge +^^^^^^ + +The total charge of the system is associated with the ``"charge"`` name, and +must have the following metadata: + +.. list-table:: Metadata for charge input + :widths: 2 3 7 + :header-rows: 1 + + * - Metadata + - Names + - Description + + * - keys + - ``"_"`` + - the keys must have a single dimension named ``"_"``, with a single entry + set to ``0``. The charge is always a + :py:class:`metatensor.torch.TensorMap` with a single block. + + * - samples + - ``["system"]`` + - the samples must be named ``["system"]``, since the charge is a + per-system quantity. + + ``"system"`` must range from 0 to the number of systems given as input + to the model. + + * - components + - + - the charge must not have any components + + * - properties + - ``"charge"`` + - the charge must have a single property dimension named ``"charge"``, + with a single entry set to ``0``. + +The values are integers representing the total electric charge of the system +in units of the elementary charge :math:`e` (e.g. ``0`` for a neutral system, +``-1`` for a singly charged anion). diff --git a/docs/src/inputs/index.rst b/docs/src/inputs/index.rst new file mode 100644 index 000000000..6577b1c8d --- /dev/null +++ b/docs/src/inputs/index.rst @@ -0,0 +1,37 @@ +.. _atomistic-models-inputs: + +Standard model inputs +===================== + +Some simulation engines can provide additional per-system inputs to a model, +beyond the atomic positions and species. If your model expects one of the +inputs defined in this documentation, it should use the corresponding +standardized name and follow the metadata structure described here. + +If you need other inputs, you should use a custom name containing ``::``, +such as ``my_code::my_input``. + +.. toctree:: + :maxdepth: 1 + :hidden: + + charge + spin + +Physical quantities +^^^^^^^^^^^^^^^^^^^ + +.. grid:: 1 2 2 2 + + .. grid-item-card:: Charge + :link: charge-input + :link-type: ref + + The total electric charge of the system, in units of the elementary + charge :math:`e`. + + .. grid-item-card:: Spin + :link: spin-input + :link-type: ref + + The spin multiplicity :math:`2S + 1` of the system. diff --git a/docs/src/inputs/spin.rst b/docs/src/inputs/spin.rst new file mode 100644 index 000000000..b2122bb06 --- /dev/null +++ b/docs/src/inputs/spin.rst @@ -0,0 +1,42 @@ +.. _spin-input: + +Spin +^^^^ + +The spin multiplicity of the system is associated with the ``"spin"`` name, +and must have the following metadata: + +.. list-table:: Metadata for spin input + :widths: 2 3 7 + :header-rows: 1 + + * - Metadata + - Names + - Description + + * - keys + - ``"_"`` + - the keys must have a single dimension named ``"_"``, with a single entry + set to ``0``. The spin is always a + :py:class:`metatensor.torch.TensorMap` with a single block. + + * - samples + - ``["system"]`` + - the samples must be named ``["system"]``, since the spin multiplicity is + a per-system quantity. + + ``"system"`` must range from 0 to the number of systems given as input + to the model. + + * - components + - + - the spin must not have any components + + * - properties + - ``"spin"`` + - the spin must have a single property dimension named ``"spin"``, with a + single entry set to ``0``. + +The values are integers representing the spin multiplicity :math:`2S + 1` of +the system, where :math:`S` is the total spin quantum number (e.g. ``1`` for a +singlet, ``2`` for a doublet, ``3`` for a triplet). From 1bd871445b47064583e41f5008580e5657b34957 Mon Sep 17 00:00:00 2001 From: JonathanSchmidt1 Date: Sat, 28 Mar 2026 17:18:40 +0100 Subject: [PATCH 04/11] add docs --- docs/src/inputs/charge.rst | 26 ++++++++++++++++++++++---- docs/src/inputs/index.rst | 12 +++++------- docs/src/inputs/spin.rst | 31 +++++++++++++++++++++++++++---- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/docs/src/inputs/charge.rst b/docs/src/inputs/charge.rst index ef70ad284..99a58ae9c 100644 --- a/docs/src/inputs/charge.rst +++ b/docs/src/inputs/charge.rst @@ -23,7 +23,8 @@ must have the following metadata: * - samples - ``["system"]`` - the samples must be named ``["system"]``, since the charge is a - per-system quantity. + per-system quantity. When running a batched calculation, there will be + one row per system. ``"system"`` must range from 0 to the number of systems given as input to the model. @@ -37,6 +38,23 @@ must have the following metadata: - the charge must have a single property dimension named ``"charge"``, with a single entry set to ``0``. -The values are integers representing the total electric charge of the system -in units of the elementary charge :math:`e` (e.g. ``0`` for a neutral system, -``-1`` for a singly charged anion). +The values represent the total electric charge of the system in units of the +elementary charge :math:`e` (e.g. ``0`` for a neutral system, ``-1`` for a +singly charged anion). The values are stored as floats (matching the model's +dtype), even though they typically take integer values. The unit is always +``"e"`` (elementary charges). + +The following simulation engines support the ``"charge"`` input: + +.. grid:: 1 1 1 1 + + .. grid-item-card:: + :text-align: center + :padding: 1 + :link: engine-ase + :link-type: ref + + |ase-logo| + +In ASE, the charge is read from ``atoms.info["charge"]`` and defaults to +``0`` (neutral) if not set. diff --git a/docs/src/inputs/index.rst b/docs/src/inputs/index.rst index 6577b1c8d..a8c175787 100644 --- a/docs/src/inputs/index.rst +++ b/docs/src/inputs/index.rst @@ -3,10 +3,11 @@ Standard model inputs ===================== -Some simulation engines can provide additional per-system inputs to a model, -beyond the atomic positions and species. If your model expects one of the -inputs defined in this documentation, it should use the corresponding -standardized name and follow the metadata structure described here. +Models can receive additional per-system inputs beyond the atomic positions and +species. These inputs must be set by the user (e.g. via ``atoms.info`` in ASE) +before running a calculation. If your model expects one of the inputs defined +in this documentation, it should use the corresponding standardized name and +follow the metadata structure described here. If you need other inputs, you should use a custom name containing ``::``, such as ``my_code::my_input``. @@ -18,9 +19,6 @@ such as ``my_code::my_input``. charge spin -Physical quantities -^^^^^^^^^^^^^^^^^^^ - .. grid:: 1 2 2 2 .. grid-item-card:: Charge diff --git a/docs/src/inputs/spin.rst b/docs/src/inputs/spin.rst index b2122bb06..8611a7002 100644 --- a/docs/src/inputs/spin.rst +++ b/docs/src/inputs/spin.rst @@ -23,7 +23,8 @@ and must have the following metadata: * - samples - ``["system"]`` - the samples must be named ``["system"]``, since the spin multiplicity is - a per-system quantity. + a per-system quantity. When running a batched calculation, there will be + one row per system. ``"system"`` must range from 0 to the number of systems given as input to the model. @@ -37,6 +38,28 @@ and must have the following metadata: - the spin must have a single property dimension named ``"spin"``, with a single entry set to ``0``. -The values are integers representing the spin multiplicity :math:`2S + 1` of -the system, where :math:`S` is the total spin quantum number (e.g. ``1`` for a -singlet, ``2`` for a doublet, ``3`` for a triplet). +The values represent the spin multiplicity :math:`2S + 1` of the system, where +:math:`S` is the total spin quantum number. The values are dimensionless and +stored as floats (matching the model's dtype), even though they always take +positive integer values. The value must be at least ``1``. + +Common examples: + +- ``1`` for a singlet (:math:`S = 0`) +- ``2`` for a doublet (:math:`S = 1/2`, e.g. a radical with one unpaired electron) +- ``3`` for a triplet (:math:`S = 1`) + +The following simulation engines support the ``"spin"`` input: + +.. grid:: 1 1 1 1 + + .. grid-item-card:: + :text-align: center + :padding: 1 + :link: engine-ase + :link-type: ref + + |ase-logo| + +In ASE, the spin multiplicity is read from ``atoms.info["spin"]`` and defaults +to ``1`` (singlet) if not set. From 6477c44fdbf8e6fff46ad7d68a02e5b0ce8382e4 Mon Sep 17 00:00:00 2001 From: JonathanSchmidt1 Date: Sat, 28 Mar 2026 17:59:54 +0100 Subject: [PATCH 05/11] add mention of lattice to inputs --- docs/src/inputs/index.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/inputs/index.rst b/docs/src/inputs/index.rst index a8c175787..d810bda42 100644 --- a/docs/src/inputs/index.rst +++ b/docs/src/inputs/index.rst @@ -3,10 +3,9 @@ Standard model inputs ===================== -Models can receive additional per-system inputs beyond the atomic positions and +Models can receive additional per-system inputs beyond the atomic positions, lattice and species. These inputs must be set by the user (e.g. via ``atoms.info`` in ASE) -before running a calculation. If your model expects one of the inputs defined -in this documentation, it should use the corresponding standardized name and +before running a calculation. If your model requests one of the inputs defined in this documentation, it should use the corresponding standardized name and follow the metadata structure described here. If you need other inputs, you should use a custom name containing ``::``, From 4fddce220f7d3119f2f2d9d8f6a6cc8aa5f1cce1 Mon Sep 17 00:00:00 2001 From: JonathanSchmidt1 Date: Sun, 29 Mar 2026 13:10:31 +0200 Subject: [PATCH 06/11] add check_charge and check_spin validation in check_outputs These validate the TensorMap metadata for system-level charge and spin inputs when check_consistency=True: single block, per-system samples, no components, correct property name, no gradients. Co-Authored-By: Claude Sonnet 4.6 --- metatomic-torch/src/outputs.cpp | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/metatomic-torch/src/outputs.cpp b/metatomic-torch/src/outputs.cpp index 9ed8fcd35..745e8e13b 100644 --- a/metatomic-torch/src/outputs.cpp +++ b/metatomic-torch/src/outputs.cpp @@ -622,6 +622,66 @@ static void check_heat_flux( validate_no_gradients("heat_flux", heat_flux_block); } +/// Check input metadata for charge (per-system scalar). +static void check_charge( + const TensorMap& value, + const std::vector& systems, + const ModelOutput& request +) { + validate_single_block("charge", value); + + if (request->per_atom) { + C10_THROW_ERROR(ValueError, + "invalid 'charge' input: charge is a per-system quantity, but the request " + "indicates `per_atom=True`" + ); + } + validate_atomic_samples("charge", value, systems, request, torch::nullopt); + + auto tensor_options = torch::TensorOptions().device(value->device()); + auto charge_block = TensorMapHolder::block_by_id(value, 0); + + validate_components("charge", charge_block->components(), {}); + + auto expected_properties = torch::make_intrusive( + "charge", + torch::tensor({{0}}, tensor_options) + ); + validate_properties("charge", charge_block, expected_properties); + + validate_no_gradients("charge", charge_block); +} + +/// Check input metadata for spin (per-system scalar). +static void check_spin( + const TensorMap& value, + const std::vector& systems, + const ModelOutput& request +) { + validate_single_block("spin", value); + + if (request->per_atom) { + C10_THROW_ERROR(ValueError, + "invalid 'spin' input: spin is a per-system quantity, but the request " + "indicates `per_atom=True`" + ); + } + validate_atomic_samples("spin", value, systems, request, torch::nullopt); + + auto tensor_options = torch::TensorOptions().device(value->device()); + auto spin_block = TensorMapHolder::block_by_id(value, 0); + + validate_components("spin", spin_block->components(), {}); + + auto expected_properties = torch::make_intrusive( + "spin", + torch::tensor({{0}}, tensor_options) + ); + validate_properties("spin", spin_block, expected_properties); + + validate_no_gradients("spin", spin_block); +} + void metatomic_torch::check_outputs( const std::vector& systems, const c10::Dict& requested, @@ -694,6 +754,10 @@ void metatomic_torch::check_outputs( check_charges(value, systems, request); } else if (base == "heat_flux") { check_heat_flux(value, systems, request); + } else if (base == "charge") { + check_charge(value, systems, request); + } else if (base == "spin") { + check_spin(value, systems, request); } else if (name.find("::") != std::string::npos) { // this is a non-standard output, there is nothing to check } else { From 09451804096a36a247d51a32bd8414c6e3e1b306 Mon Sep 17 00:00:00 2001 From: JonathanSchmidt1 Date: Sun, 29 Mar 2026 13:25:49 +0200 Subject: [PATCH 07/11] add tests for check_charge/spin --- python/metatomic_torch/tests/outputs.py | 123 ++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/python/metatomic_torch/tests/outputs.py b/python/metatomic_torch/tests/outputs.py index 8e9135a97..e8cb8c067 100644 --- a/python/metatomic_torch/tests/outputs.py +++ b/python/metatomic_torch/tests/outputs.py @@ -270,3 +270,126 @@ def test_positions_momenta_model(system): assert momenta.block().properties.names == ["momenta"] assert momenta.block().components == [Labels("xyz", torch.tensor([[0], [1], [2]]))] assert len(result["momenta"].blocks()) == 1 + + +class ChargeSpinModel(torch.nn.Module): + """A model that requests charge and spin as system-level inputs.""" + + def requested_inputs(self) -> Dict[str, ModelOutput]: + return { + "charge": ModelOutput(quantity="charge", unit="e", per_atom=False), + "spin": ModelOutput(quantity="spin", unit="", per_atom=False), + } + + def forward( + self, + systems: List[System], + outputs: Dict[str, ModelOutput], + selected_atoms: Optional[Labels] = None, + ) -> Dict[str, TensorMap]: + system = systems[0] + charge = float(system.get_data("charge").block(0).values[0, 0]) + spin = float(system.get_data("spin").block(0).values[0, 0]) + energy_value = charge + 10.0 * spin + block = TensorBlock( + values=torch.tensor([[energy_value]] * len(systems), dtype=torch.float64), + samples=Labels("system", torch.arange(len(systems)).reshape(-1, 1)), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ) + return {"energy": TensorMap(Labels("_", torch.tensor([[0]])), [block])} + + +def _make_scalar_input(name, value): + """Create a valid per-system scalar TensorMap for charge or spin.""" + block = TensorBlock( + values=torch.tensor([[value]], dtype=torch.float64), + samples=Labels("system", torch.tensor([[0]])), + components=[], + properties=Labels(name, torch.tensor([[0]])), + ) + return TensorMap(Labels("_", torch.tensor([[0]])), [block]) + + +def _make_charge_spin_model(): + model = ChargeSpinModel() + capabilities = ModelCapabilities( + length_unit="angstrom", + atomic_types=[1, 2, 3], + interaction_range=4.3, + outputs={"energy": ModelOutput(per_atom=False, unit="eV")}, + supported_devices=["cpu"], + dtype="float64", + ) + return AtomisticModel(model.eval(), ModelMetadata(), capabilities) + + +def test_charge_spin_valid(system): + """check_consistency=True passes with correctly structured charge/spin.""" + atomistic = _make_charge_spin_model() + + system.add_data("charge", _make_scalar_input("charge", -1.0)) + system.add_data("spin", _make_scalar_input("spin", 3.0)) + + options = ModelEvaluationOptions(outputs={"energy": ModelOutput(per_atom=False)}) + result = atomistic([system], options, check_consistency=True) + assert "energy" in result + + +def test_charge_wrong_property_name(system): + """check_consistency catches wrong property name for charge.""" + atomistic = _make_charge_spin_model() + + # wrong property name: "wrong" instead of "charge" + bad_block = TensorBlock( + values=torch.tensor([[0.0]], dtype=torch.float64), + samples=Labels("system", torch.tensor([[0]])), + components=[], + properties=Labels("wrong", torch.tensor([[0]])), + ) + bad_charge = TensorMap(Labels("_", torch.tensor([[0]])), [bad_block]) + system.add_data("charge", bad_charge) + system.add_data("spin", _make_scalar_input("spin", 1.0)) + + options = ModelEvaluationOptions(outputs={"energy": ModelOutput(per_atom=False)}) + with pytest.raises(ValueError, match="charge"): + atomistic([system], options, check_consistency=True) + + +def test_spin_wrong_property_name(system): + """check_consistency catches wrong property name for spin.""" + atomistic = _make_charge_spin_model() + + system.add_data("charge", _make_scalar_input("charge", 0.0)) + + bad_block = TensorBlock( + values=torch.tensor([[1.0]], dtype=torch.float64), + samples=Labels("system", torch.tensor([[0]])), + components=[], + properties=Labels("wrong", torch.tensor([[0]])), + ) + bad_spin = TensorMap(Labels("_", torch.tensor([[0]])), [bad_block]) + system.add_data("spin", bad_spin) + + options = ModelEvaluationOptions(outputs={"energy": ModelOutput(per_atom=False)}) + with pytest.raises(ValueError, match="spin"): + atomistic([system], options, check_consistency=True) + + +def test_charge_with_components_error(system): + """check_consistency catches spurious components on charge.""" + atomistic = _make_charge_spin_model() + + bad_block = TensorBlock( + values=torch.tensor([[[0.0], [0.0], [0.0]]], dtype=torch.float64), + samples=Labels("system", torch.tensor([[0]])), + components=[Labels("xyz", torch.tensor([[0], [1], [2]]))], + properties=Labels("charge", torch.tensor([[0]])), + ) + bad_charge = TensorMap(Labels("_", torch.tensor([[0]])), [bad_block]) + system.add_data("charge", bad_charge) + system.add_data("spin", _make_scalar_input("spin", 1.0)) + + options = ModelEvaluationOptions(outputs={"energy": ModelOutput(per_atom=False)}) + with pytest.raises(ValueError, match="charge"): + atomistic([system], options, check_consistency=True) From b4a603ad6e74e9fd2fd21f2edb22b987dc396a94 Mon Sep 17 00:00:00 2001 From: JonathanSchmidt1 Date: Wed, 1 Apr 2026 11:57:39 +0200 Subject: [PATCH 08/11] change spin/charge from inputs to outputs --- docs/src/index.rst | 10 -------- docs/src/inputs/index.rst | 34 ------------------------- docs/src/{inputs => outputs}/charge.rst | 6 ++--- docs/src/outputs/index.rst | 15 +++++++++++ docs/src/{inputs => outputs}/spin.rst | 6 ++--- metatomic-torch/src/outputs.cpp | 8 +++--- metatomic-torch/tests/misc.cpp | 4 +-- python/metatomic_torch/tests/outputs.py | 14 +++++----- 8 files changed, 34 insertions(+), 63 deletions(-) delete mode 100644 docs/src/inputs/index.rst rename docs/src/{inputs => outputs}/charge.rst (92%) rename docs/src/{inputs => outputs}/spin.rst (93%) diff --git a/docs/src/index.rst b/docs/src/index.rst index 46656e6a7..eec45bb12 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -51,15 +51,6 @@ existing trained models, look into the metatrain_ project instead. Understand the different outputs a model can have, and what the metadata should be provided for standardized outputs, such as the potential energy. - .. grid-item-card:: 📥 Standard models inputs - :link: atomistic-models-inputs - :link-type: ref - :columns: 12 12 6 6 - :margin: 0 3 0 0 - - Understand the different inputs a simulation engine can provide to a - model, such as the total charge or spin multiplicity of the system. - .. grid-item-card:: ⚙️ Simulation engines :link: engines :link-type: ref @@ -102,7 +93,6 @@ existing trained models, look into the metatrain_ project instead. installation torch/index outputs/index - inputs/index engines/index examples/index cite diff --git a/docs/src/inputs/index.rst b/docs/src/inputs/index.rst deleted file mode 100644 index d810bda42..000000000 --- a/docs/src/inputs/index.rst +++ /dev/null @@ -1,34 +0,0 @@ -.. _atomistic-models-inputs: - -Standard model inputs -===================== - -Models can receive additional per-system inputs beyond the atomic positions, lattice and -species. These inputs must be set by the user (e.g. via ``atoms.info`` in ASE) -before running a calculation. If your model requests one of the inputs defined in this documentation, it should use the corresponding standardized name and -follow the metadata structure described here. - -If you need other inputs, you should use a custom name containing ``::``, -such as ``my_code::my_input``. - -.. toctree:: - :maxdepth: 1 - :hidden: - - charge - spin - -.. grid:: 1 2 2 2 - - .. grid-item-card:: Charge - :link: charge-input - :link-type: ref - - The total electric charge of the system, in units of the elementary - charge :math:`e`. - - .. grid-item-card:: Spin - :link: spin-input - :link-type: ref - - The spin multiplicity :math:`2S + 1` of the system. diff --git a/docs/src/inputs/charge.rst b/docs/src/outputs/charge.rst similarity index 92% rename from docs/src/inputs/charge.rst rename to docs/src/outputs/charge.rst index 99a58ae9c..012bb77eb 100644 --- a/docs/src/inputs/charge.rst +++ b/docs/src/outputs/charge.rst @@ -1,4 +1,4 @@ -.. _charge-input: +.. _charge-output: Charge ^^^^^^ @@ -6,7 +6,7 @@ Charge The total charge of the system is associated with the ``"charge"`` name, and must have the following metadata: -.. list-table:: Metadata for charge input +.. list-table:: Metadata for charge output :widths: 2 3 7 :header-rows: 1 @@ -44,7 +44,7 @@ singly charged anion). The values are stored as floats (matching the model's dtype), even though they typically take integer values. The unit is always ``"e"`` (elementary charges). -The following simulation engines support the ``"charge"`` input: +The following simulation engines support the ``"charge"`` output: .. grid:: 1 1 1 1 diff --git a/docs/src/outputs/index.rst b/docs/src/outputs/index.rst index 8a68800c1..00677db42 100644 --- a/docs/src/outputs/index.rst +++ b/docs/src/outputs/index.rst @@ -25,6 +25,8 @@ schema they need and add a new section to these pages. momenta velocities charges + charge + spin heat_flux features variants @@ -141,6 +143,19 @@ quantities, i.e. quantities with a well-defined physical meaning. Heat flux, i.e. the amount of energy transferred per unit time, i.e. :math:`\sum_i E_i \times \vec v_i` + .. grid-item-card:: Charge + :link: charge-output + :link-type: ref + + The total electric charge of the system, in units of the elementary + charge :math:`e`. + + .. grid-item-card:: Spin + :link: spin-output + :link-type: ref + + The spin multiplicity :math:`2S + 1` of the system. + Machine learning quantities ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/src/inputs/spin.rst b/docs/src/outputs/spin.rst similarity index 93% rename from docs/src/inputs/spin.rst rename to docs/src/outputs/spin.rst index 8611a7002..b3a68d0b5 100644 --- a/docs/src/inputs/spin.rst +++ b/docs/src/outputs/spin.rst @@ -1,4 +1,4 @@ -.. _spin-input: +.. _spin-output: Spin ^^^^ @@ -6,7 +6,7 @@ Spin The spin multiplicity of the system is associated with the ``"spin"`` name, and must have the following metadata: -.. list-table:: Metadata for spin input +.. list-table:: Metadata for spin output :widths: 2 3 7 :header-rows: 1 @@ -49,7 +49,7 @@ Common examples: - ``2`` for a doublet (:math:`S = 1/2`, e.g. a radical with one unpaired electron) - ``3`` for a triplet (:math:`S = 1`) -The following simulation engines support the ``"spin"`` input: +The following simulation engines support the ``"spin"`` output: .. grid:: 1 1 1 1 diff --git a/metatomic-torch/src/outputs.cpp b/metatomic-torch/src/outputs.cpp index 745e8e13b..1254adad5 100644 --- a/metatomic-torch/src/outputs.cpp +++ b/metatomic-torch/src/outputs.cpp @@ -622,7 +622,7 @@ static void check_heat_flux( validate_no_gradients("heat_flux", heat_flux_block); } -/// Check input metadata for charge (per-system scalar). +/// Check output metadata for charge (per-system scalar). static void check_charge( const TensorMap& value, const std::vector& systems, @@ -632,7 +632,7 @@ static void check_charge( if (request->per_atom) { C10_THROW_ERROR(ValueError, - "invalid 'charge' input: charge is a per-system quantity, but the request " + "invalid 'charge' output: charge is a per-system quantity, but the request " "indicates `per_atom=True`" ); } @@ -652,7 +652,7 @@ static void check_charge( validate_no_gradients("charge", charge_block); } -/// Check input metadata for spin (per-system scalar). +/// Check output metadata for spin (per-system scalar). static void check_spin( const TensorMap& value, const std::vector& systems, @@ -662,7 +662,7 @@ static void check_spin( if (request->per_atom) { C10_THROW_ERROR(ValueError, - "invalid 'spin' input: spin is a per-system quantity, but the request " + "invalid 'spin' output: spin is a per-system quantity, but the request " "indicates `per_atom=True`" ); } diff --git a/metatomic-torch/tests/misc.cpp b/metatomic-torch/tests/misc.cpp index c39b86ab3..e475dd892 100644 --- a/metatomic-torch/tests/misc.cpp +++ b/metatomic-torch/tests/misc.cpp @@ -106,8 +106,8 @@ TEST_CASE("Pick variant") { CHECK_THROWS_WITH(metatomic_torch::pick_output("energy", outputs), StartsWith(err)); } -TEST_CASE("Standard inputs") { - // "charge" and "spin" are recognized as standard (non-namespaced) input names +TEST_CASE("Standard outputs") { + // "charge" and "spin" are recognized as standard (non-namespaced) output names auto [known_charge, base_charge, variant_charge] = metatomic_torch::details::validate_name_and_check_variant("charge"); CHECK(known_charge == true); CHECK(base_charge == "charge"); diff --git a/python/metatomic_torch/tests/outputs.py b/python/metatomic_torch/tests/outputs.py index e8cb8c067..bf2db2213 100644 --- a/python/metatomic_torch/tests/outputs.py +++ b/python/metatomic_torch/tests/outputs.py @@ -273,7 +273,7 @@ def test_positions_momenta_model(system): class ChargeSpinModel(torch.nn.Module): - """A model that requests charge and spin as system-level inputs.""" + """A model that requests charge and spin as system-level outputs.""" def requested_inputs(self) -> Dict[str, ModelOutput]: return { @@ -300,7 +300,7 @@ def forward( return {"energy": TensorMap(Labels("_", torch.tensor([[0]])), [block])} -def _make_scalar_input(name, value): +def _make_scalar_output(name, value): """Create a valid per-system scalar TensorMap for charge or spin.""" block = TensorBlock( values=torch.tensor([[value]], dtype=torch.float64), @@ -328,8 +328,8 @@ def test_charge_spin_valid(system): """check_consistency=True passes with correctly structured charge/spin.""" atomistic = _make_charge_spin_model() - system.add_data("charge", _make_scalar_input("charge", -1.0)) - system.add_data("spin", _make_scalar_input("spin", 3.0)) + system.add_data("charge", _make_scalar_output("charge", -1.0)) + system.add_data("spin", _make_scalar_output("spin", 3.0)) options = ModelEvaluationOptions(outputs={"energy": ModelOutput(per_atom=False)}) result = atomistic([system], options, check_consistency=True) @@ -349,7 +349,7 @@ def test_charge_wrong_property_name(system): ) bad_charge = TensorMap(Labels("_", torch.tensor([[0]])), [bad_block]) system.add_data("charge", bad_charge) - system.add_data("spin", _make_scalar_input("spin", 1.0)) + system.add_data("spin", _make_scalar_output("spin", 1.0)) options = ModelEvaluationOptions(outputs={"energy": ModelOutput(per_atom=False)}) with pytest.raises(ValueError, match="charge"): @@ -360,7 +360,7 @@ def test_spin_wrong_property_name(system): """check_consistency catches wrong property name for spin.""" atomistic = _make_charge_spin_model() - system.add_data("charge", _make_scalar_input("charge", 0.0)) + system.add_data("charge", _make_scalar_output("charge", 0.0)) bad_block = TensorBlock( values=torch.tensor([[1.0]], dtype=torch.float64), @@ -388,7 +388,7 @@ def test_charge_with_components_error(system): ) bad_charge = TensorMap(Labels("_", torch.tensor([[0]])), [bad_block]) system.add_data("charge", bad_charge) - system.add_data("spin", _make_scalar_input("spin", 1.0)) + system.add_data("spin", _make_scalar_output("spin", 1.0)) options = ModelEvaluationOptions(outputs={"energy": ModelOutput(per_atom=False)}) with pytest.raises(ValueError, match="charge"): From 65736fc2c3dc309f35c423e831d6b080927364f5 Mon Sep 17 00:00:00 2001 From: JonathanSchmidt1 Date: Wed, 8 Apr 2026 14:52:04 +0200 Subject: [PATCH 09/11] remove charge, rename spin to spin_multiplicity Drop charge as a standard name. Rename spin to spin_multiplicity everywhere (C++ registration, validation, tests, docs). The ASE info key remains atoms.info["spin"] for user convenience. Co-Authored-By: Claude Opus 4.6 --- docs/src/outputs/charge.rst | 60 --------------- docs/src/outputs/index.rst | 14 +--- .../{spin.rst => spin_multiplicity.rst} | 24 +++--- metatomic-torch/src/misc.cpp | 3 +- metatomic-torch/src/outputs.cpp | 56 +++----------- metatomic-torch/tests/misc.cpp | 15 ++-- python/metatomic_torch/tests/outputs.py | 76 ++++++------------- 7 files changed, 58 insertions(+), 190 deletions(-) delete mode 100644 docs/src/outputs/charge.rst rename docs/src/outputs/{spin.rst => spin_multiplicity.rst} (72%) diff --git a/docs/src/outputs/charge.rst b/docs/src/outputs/charge.rst deleted file mode 100644 index 012bb77eb..000000000 --- a/docs/src/outputs/charge.rst +++ /dev/null @@ -1,60 +0,0 @@ -.. _charge-output: - -Charge -^^^^^^ - -The total charge of the system is associated with the ``"charge"`` name, and -must have the following metadata: - -.. list-table:: Metadata for charge output - :widths: 2 3 7 - :header-rows: 1 - - * - Metadata - - Names - - Description - - * - keys - - ``"_"`` - - the keys must have a single dimension named ``"_"``, with a single entry - set to ``0``. The charge is always a - :py:class:`metatensor.torch.TensorMap` with a single block. - - * - samples - - ``["system"]`` - - the samples must be named ``["system"]``, since the charge is a - per-system quantity. When running a batched calculation, there will be - one row per system. - - ``"system"`` must range from 0 to the number of systems given as input - to the model. - - * - components - - - - the charge must not have any components - - * - properties - - ``"charge"`` - - the charge must have a single property dimension named ``"charge"``, - with a single entry set to ``0``. - -The values represent the total electric charge of the system in units of the -elementary charge :math:`e` (e.g. ``0`` for a neutral system, ``-1`` for a -singly charged anion). The values are stored as floats (matching the model's -dtype), even though they typically take integer values. The unit is always -``"e"`` (elementary charges). - -The following simulation engines support the ``"charge"`` output: - -.. grid:: 1 1 1 1 - - .. grid-item-card:: - :text-align: center - :padding: 1 - :link: engine-ase - :link-type: ref - - |ase-logo| - -In ASE, the charge is read from ``atoms.info["charge"]`` and defaults to -``0`` (neutral) if not set. diff --git a/docs/src/outputs/index.rst b/docs/src/outputs/index.rst index 00677db42..6a6f00b93 100644 --- a/docs/src/outputs/index.rst +++ b/docs/src/outputs/index.rst @@ -25,8 +25,7 @@ schema they need and add a new section to these pages. momenta velocities charges - charge - spin + spin_multiplicity heat_flux features variants @@ -143,15 +142,8 @@ quantities, i.e. quantities with a well-defined physical meaning. Heat flux, i.e. the amount of energy transferred per unit time, i.e. :math:`\sum_i E_i \times \vec v_i` - .. grid-item-card:: Charge - :link: charge-output - :link-type: ref - - The total electric charge of the system, in units of the elementary - charge :math:`e`. - - .. grid-item-card:: Spin - :link: spin-output + .. grid-item-card:: Spin multiplicity + :link: spin-multiplicity-output :link-type: ref The spin multiplicity :math:`2S + 1` of the system. diff --git a/docs/src/outputs/spin.rst b/docs/src/outputs/spin_multiplicity.rst similarity index 72% rename from docs/src/outputs/spin.rst rename to docs/src/outputs/spin_multiplicity.rst index b3a68d0b5..3d1aeeddc 100644 --- a/docs/src/outputs/spin.rst +++ b/docs/src/outputs/spin_multiplicity.rst @@ -1,12 +1,12 @@ -.. _spin-output: +.. _spin-multiplicity-output: -Spin -^^^^ +Spin multiplicity +^^^^^^^^^^^^^^^^^ -The spin multiplicity of the system is associated with the ``"spin"`` name, -and must have the following metadata: +The spin multiplicity of the system is associated with the ``"spin_multiplicity"`` +name, and must have the following metadata: -.. list-table:: Metadata for spin output +.. list-table:: Metadata for spin_multiplicity output :widths: 2 3 7 :header-rows: 1 @@ -17,7 +17,7 @@ and must have the following metadata: * - keys - ``"_"`` - the keys must have a single dimension named ``"_"``, with a single entry - set to ``0``. The spin is always a + set to ``0``. The spin multiplicity is always a :py:class:`metatensor.torch.TensorMap` with a single block. * - samples @@ -31,12 +31,12 @@ and must have the following metadata: * - components - - - the spin must not have any components + - the spin multiplicity must not have any components * - properties - - ``"spin"`` - - the spin must have a single property dimension named ``"spin"``, with a - single entry set to ``0``. + - ``"spin_multiplicity"`` + - the spin multiplicity must have a single property dimension named + ``"spin_multiplicity"``, with a single entry set to ``0``. The values represent the spin multiplicity :math:`2S + 1` of the system, where :math:`S` is the total spin quantum number. The values are dimensionless and @@ -49,7 +49,7 @@ Common examples: - ``2`` for a doublet (:math:`S = 1/2`, e.g. a radical with one unpaired electron) - ``3`` for a triplet (:math:`S = 1`) -The following simulation engines support the ``"spin"`` output: +The following simulation engines support the ``"spin_multiplicity"`` output: .. grid:: 1 1 1 1 diff --git a/metatomic-torch/src/misc.cpp b/metatomic-torch/src/misc.cpp index 2a8f7665c..f19de58aa 100644 --- a/metatomic-torch/src/misc.cpp +++ b/metatomic-torch/src/misc.cpp @@ -427,8 +427,7 @@ inline std::unordered_set KNOWN_INPUTS_OUTPUTS = { "velocities", "masses", "charges", - "charge", - "spin", + "spin_multiplicity", "heat_flux", }; diff --git a/metatomic-torch/src/outputs.cpp b/metatomic-torch/src/outputs.cpp index 1254adad5..16226a993 100644 --- a/metatomic-torch/src/outputs.cpp +++ b/metatomic-torch/src/outputs.cpp @@ -622,64 +622,34 @@ static void check_heat_flux( validate_no_gradients("heat_flux", heat_flux_block); } -/// Check output metadata for charge (per-system scalar). -static void check_charge( +/// Check output metadata for spin_multiplicity (per-system scalar). +static void check_spin_multiplicity( const TensorMap& value, const std::vector& systems, const ModelOutput& request ) { - validate_single_block("charge", value); + validate_single_block("spin_multiplicity", value); if (request->per_atom) { C10_THROW_ERROR(ValueError, - "invalid 'charge' output: charge is a per-system quantity, but the request " - "indicates `per_atom=True`" - ); - } - validate_atomic_samples("charge", value, systems, request, torch::nullopt); - - auto tensor_options = torch::TensorOptions().device(value->device()); - auto charge_block = TensorMapHolder::block_by_id(value, 0); - - validate_components("charge", charge_block->components(), {}); - - auto expected_properties = torch::make_intrusive( - "charge", - torch::tensor({{0}}, tensor_options) - ); - validate_properties("charge", charge_block, expected_properties); - - validate_no_gradients("charge", charge_block); -} - -/// Check output metadata for spin (per-system scalar). -static void check_spin( - const TensorMap& value, - const std::vector& systems, - const ModelOutput& request -) { - validate_single_block("spin", value); - - if (request->per_atom) { - C10_THROW_ERROR(ValueError, - "invalid 'spin' output: spin is a per-system quantity, but the request " - "indicates `per_atom=True`" + "invalid 'spin_multiplicity' output: spin_multiplicity is a per-system quantity, " + "but the request indicates `per_atom=True`" ); } - validate_atomic_samples("spin", value, systems, request, torch::nullopt); + validate_atomic_samples("spin_multiplicity", value, systems, request, torch::nullopt); auto tensor_options = torch::TensorOptions().device(value->device()); auto spin_block = TensorMapHolder::block_by_id(value, 0); - validate_components("spin", spin_block->components(), {}); + validate_components("spin_multiplicity", spin_block->components(), {}); auto expected_properties = torch::make_intrusive( - "spin", + "spin_multiplicity", torch::tensor({{0}}, tensor_options) ); - validate_properties("spin", spin_block, expected_properties); + validate_properties("spin_multiplicity", spin_block, expected_properties); - validate_no_gradients("spin", spin_block); + validate_no_gradients("spin_multiplicity", spin_block); } void metatomic_torch::check_outputs( @@ -754,10 +724,8 @@ void metatomic_torch::check_outputs( check_charges(value, systems, request); } else if (base == "heat_flux") { check_heat_flux(value, systems, request); - } else if (base == "charge") { - check_charge(value, systems, request); - } else if (base == "spin") { - check_spin(value, systems, request); + } else if (base == "spin_multiplicity") { + check_spin_multiplicity(value, systems, request); } else if (name.find("::") != std::string::npos) { // this is a non-standard output, there is nothing to check } else { diff --git a/metatomic-torch/tests/misc.cpp b/metatomic-torch/tests/misc.cpp index e475dd892..fb321b1e4 100644 --- a/metatomic-torch/tests/misc.cpp +++ b/metatomic-torch/tests/misc.cpp @@ -107,14 +107,9 @@ TEST_CASE("Pick variant") { } TEST_CASE("Standard outputs") { - // "charge" and "spin" are recognized as standard (non-namespaced) output names - auto [known_charge, base_charge, variant_charge] = metatomic_torch::details::validate_name_and_check_variant("charge"); - CHECK(known_charge == true); - CHECK(base_charge == "charge"); - CHECK(variant_charge == ""); - - auto [known_spin, base_spin, variant_spin] = metatomic_torch::details::validate_name_and_check_variant("spin"); - CHECK(known_spin == true); - CHECK(base_spin == "spin"); - CHECK(variant_spin == ""); + // "spin_multiplicity" is recognized as a standard (non-namespaced) output name + auto [known, base, variant] = metatomic_torch::details::validate_name_and_check_variant("spin_multiplicity"); + CHECK(known == true); + CHECK(base == "spin_multiplicity"); + CHECK(variant == ""); } diff --git a/python/metatomic_torch/tests/outputs.py b/python/metatomic_torch/tests/outputs.py index bf2db2213..ed89e0962 100644 --- a/python/metatomic_torch/tests/outputs.py +++ b/python/metatomic_torch/tests/outputs.py @@ -272,13 +272,12 @@ def test_positions_momenta_model(system): assert len(result["momenta"].blocks()) == 1 -class ChargeSpinModel(torch.nn.Module): - """A model that requests charge and spin as system-level outputs.""" +class SpinMultiplicityModel(torch.nn.Module): + """A model that requests spin_multiplicity as a system-level output.""" def requested_inputs(self) -> Dict[str, ModelOutput]: return { - "charge": ModelOutput(quantity="charge", unit="e", per_atom=False), - "spin": ModelOutput(quantity="spin", unit="", per_atom=False), + "spin_multiplicity": ModelOutput(quantity="spin_multiplicity", unit="", per_atom=False), } def forward( @@ -288,9 +287,8 @@ def forward( selected_atoms: Optional[Labels] = None, ) -> Dict[str, TensorMap]: system = systems[0] - charge = float(system.get_data("charge").block(0).values[0, 0]) - spin = float(system.get_data("spin").block(0).values[0, 0]) - energy_value = charge + 10.0 * spin + spin = float(system.get_data("spin_multiplicity").block(0).values[0, 0]) + energy_value = 10.0 * spin block = TensorBlock( values=torch.tensor([[energy_value]] * len(systems), dtype=torch.float64), samples=Labels("system", torch.arange(len(systems)).reshape(-1, 1)), @@ -301,7 +299,7 @@ def forward( def _make_scalar_output(name, value): - """Create a valid per-system scalar TensorMap for charge or spin.""" + """Create a valid per-system scalar TensorMap for spin_multiplicity.""" block = TensorBlock( values=torch.tensor([[value]], dtype=torch.float64), samples=Labels("system", torch.tensor([[0]])), @@ -311,8 +309,8 @@ def _make_scalar_output(name, value): return TensorMap(Labels("_", torch.tensor([[0]])), [block]) -def _make_charge_spin_model(): - model = ChargeSpinModel() +def _make_spin_multiplicity_model(): + model = SpinMultiplicityModel() capabilities = ModelCapabilities( length_unit="angstrom", atomic_types=[1, 2, 3], @@ -324,43 +322,20 @@ def _make_charge_spin_model(): return AtomisticModel(model.eval(), ModelMetadata(), capabilities) -def test_charge_spin_valid(system): - """check_consistency=True passes with correctly structured charge/spin.""" - atomistic = _make_charge_spin_model() +def test_spin_multiplicity_valid(system): + """check_consistency=True passes with correctly structured spin_multiplicity.""" + atomistic = _make_spin_multiplicity_model() - system.add_data("charge", _make_scalar_output("charge", -1.0)) - system.add_data("spin", _make_scalar_output("spin", 3.0)) + system.add_data("spin_multiplicity", _make_scalar_output("spin_multiplicity", 3.0)) options = ModelEvaluationOptions(outputs={"energy": ModelOutput(per_atom=False)}) result = atomistic([system], options, check_consistency=True) assert "energy" in result -def test_charge_wrong_property_name(system): - """check_consistency catches wrong property name for charge.""" - atomistic = _make_charge_spin_model() - - # wrong property name: "wrong" instead of "charge" - bad_block = TensorBlock( - values=torch.tensor([[0.0]], dtype=torch.float64), - samples=Labels("system", torch.tensor([[0]])), - components=[], - properties=Labels("wrong", torch.tensor([[0]])), - ) - bad_charge = TensorMap(Labels("_", torch.tensor([[0]])), [bad_block]) - system.add_data("charge", bad_charge) - system.add_data("spin", _make_scalar_output("spin", 1.0)) - - options = ModelEvaluationOptions(outputs={"energy": ModelOutput(per_atom=False)}) - with pytest.raises(ValueError, match="charge"): - atomistic([system], options, check_consistency=True) - - -def test_spin_wrong_property_name(system): - """check_consistency catches wrong property name for spin.""" - atomistic = _make_charge_spin_model() - - system.add_data("charge", _make_scalar_output("charge", 0.0)) +def test_spin_multiplicity_wrong_property_name(system): + """check_consistency catches wrong property name for spin_multiplicity.""" + atomistic = _make_spin_multiplicity_model() bad_block = TensorBlock( values=torch.tensor([[1.0]], dtype=torch.float64), @@ -369,27 +344,26 @@ def test_spin_wrong_property_name(system): properties=Labels("wrong", torch.tensor([[0]])), ) bad_spin = TensorMap(Labels("_", torch.tensor([[0]])), [bad_block]) - system.add_data("spin", bad_spin) + system.add_data("spin_multiplicity", bad_spin) options = ModelEvaluationOptions(outputs={"energy": ModelOutput(per_atom=False)}) - with pytest.raises(ValueError, match="spin"): + with pytest.raises(ValueError, match="spin_multiplicity"): atomistic([system], options, check_consistency=True) -def test_charge_with_components_error(system): - """check_consistency catches spurious components on charge.""" - atomistic = _make_charge_spin_model() +def test_spin_multiplicity_with_components_error(system): + """check_consistency catches spurious components on spin_multiplicity.""" + atomistic = _make_spin_multiplicity_model() bad_block = TensorBlock( - values=torch.tensor([[[0.0], [0.0], [0.0]]], dtype=torch.float64), + values=torch.tensor([[[1.0], [1.0], [1.0]]], dtype=torch.float64), samples=Labels("system", torch.tensor([[0]])), components=[Labels("xyz", torch.tensor([[0], [1], [2]]))], - properties=Labels("charge", torch.tensor([[0]])), + properties=Labels("spin_multiplicity", torch.tensor([[0]])), ) - bad_charge = TensorMap(Labels("_", torch.tensor([[0]])), [bad_block]) - system.add_data("charge", bad_charge) - system.add_data("spin", _make_scalar_output("spin", 1.0)) + bad_spin = TensorMap(Labels("_", torch.tensor([[0]])), [bad_block]) + system.add_data("spin_multiplicity", bad_spin) options = ModelEvaluationOptions(outputs={"energy": ModelOutput(per_atom=False)}) - with pytest.raises(ValueError, match="charge"): + with pytest.raises(ValueError, match="spin_multiplicity"): atomistic([system], options, check_consistency=True) From 26bc4eb77c9c928c2438d71e22b05eaa95036134 Mon Sep 17 00:00:00 2001 From: JonathanSchmidt1 Date: Wed, 8 Apr 2026 15:35:48 +0200 Subject: [PATCH 10/11] fix lint and register spin_multiplicity as dimensionless quantity Format long ModelOutput line per ruff. Add spin_multiplicity to QUANTITY_DIMS in units.cpp as a dimensionless quantity to avoid unknown-quantity warning. Co-Authored-By: Claude Opus 4.6 --- metatomic-torch/src/units.cpp | 3 ++- python/metatomic_torch/tests/outputs.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/metatomic-torch/src/units.cpp b/metatomic-torch/src/units.cpp index bd74e1ac6..1aa50d372 100644 --- a/metatomic-torch/src/units.cpp +++ b/metatomic-torch/src/units.cpp @@ -594,7 +594,8 @@ static const auto QUANTITY_DIMS = std::unordered_map{ {"mass", DIM_MASS}, {"velocity", {{1, -1, 0, 0, 0}}}, // length/time {"charge", DIM_CHARGE}, - {"heat_flux", {{3, -3, 1, 0, 0}}}, // energy*velocity + {"heat_flux", {{3, -3, 1, 0, 0}}}, // energy*velocity + {"spin_multiplicity", {{0, 0, 0, 0, 0}}}, // dimensionless }; diff --git a/python/metatomic_torch/tests/outputs.py b/python/metatomic_torch/tests/outputs.py index ed89e0962..5d4e4f85c 100644 --- a/python/metatomic_torch/tests/outputs.py +++ b/python/metatomic_torch/tests/outputs.py @@ -277,7 +277,9 @@ class SpinMultiplicityModel(torch.nn.Module): def requested_inputs(self) -> Dict[str, ModelOutput]: return { - "spin_multiplicity": ModelOutput(quantity="spin_multiplicity", unit="", per_atom=False), + "spin_multiplicity": ModelOutput( + quantity="spin_multiplicity", unit="", per_atom=False + ), } def forward( From 33fca964ae655cb0311eb886628da4ce3745365e Mon Sep 17 00:00:00 2001 From: JonathanSchmidt1 Date: Wed, 8 Apr 2026 14:06:46 +0000 Subject: [PATCH 11/11] fix C++ test for known quantities list after adding spin_multiplicity Co-Authored-By: Claude Opus 4.6 --- metatomic-torch/tests/models.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metatomic-torch/tests/models.cpp b/metatomic-torch/tests/models.cpp index c319b6593..ec74bcc98 100644 --- a/metatomic-torch/tests/models.cpp +++ b/metatomic-torch/tests/models.cpp @@ -111,7 +111,7 @@ TEST_CASE("Models metadata") { void process(const torch::Warning& warning) override { auto expected = std::string( "unknown quantity 'unknown', only [charge energy force heat_flux " - "length mass momentum pressure velocity] are supported" + "length mass momentum pressure spin_multiplicity velocity] are supported" ); CHECK(warning.msg() == expected); }