From 63be7179feb8cda7ced97faf0ea6978ecfee9513 Mon Sep 17 00:00:00 2001 From: Joseph Abbott Date: Tue, 12 Aug 2025 13:58:48 +0200 Subject: [PATCH 1/8] Refactor the loss. Co-authored-by: Paolo Pegolo Co-authored-by: Sanggyu Chong --- docs/src/advanced-concepts/index.rst | 1 + docs/src/advanced-concepts/loss-functions.rst | 123 +++ docs/src/architectures/nanopet.rst | 16 +- docs/src/architectures/pet.rst | 16 +- docs/src/architectures/soap-bpnn.rst | 16 +- docs/src/dev-docs/index.rst | 1 + docs/src/dev-docs/new-loss.rst | 61 ++ src/metatrain/cli/train.py | 12 +- .../experimental/nanopet/checkpoints.py | 20 + .../experimental/nanopet/default-hypers.yaml | 4 - .../experimental/nanopet/schema-hypers.json | 104 +- ...v1.ckpt.gz => model-v1-trainer-v1.ckpt.gz} | Bin .../checkpoints/model-v1-trainer-v2.ckpt.gz | Bin 0 -> 12025 bytes .../nanopet/tests/test_checkpoints.py | 6 + .../nanopet/tests/test_continue.py | 5 + .../nanopet/tests/test_regression.py | 4 + src/metatrain/experimental/nanopet/trainer.py | 60 +- src/metatrain/gap/tests/test_torchscript.py | 2 - src/metatrain/pet/checkpoints.py | 25 +- src/metatrain/pet/default-hypers.yaml | 5 - src/metatrain/pet/model.py | 8 +- src/metatrain/pet/schema-hypers.json | 110 +- .../checkpoints/model-v1-trainer-v1.ckpt.gz | Bin 0 -> 15018 bytes .../checkpoints/model-v1-trainer-v2.ckpt.gz | Bin 0 -> 15075 bytes .../checkpoints/model-v2-trainer-v1.ckpt.gz | Bin 0 -> 15018 bytes .../checkpoints/model-v2-trainer-v2.ckpt.gz | Bin 0 -> 15066 bytes .../checkpoints/model-v3-trainer-v2.ckpt.gz | Bin 0 -> 14861 bytes .../pet/tests/checkpoints/v1.ckpt.gz | Bin 15037 -> 0 bytes .../pet/tests/checkpoints/v2.ckpt.gz | Bin 15268 -> 0 bytes .../pet/tests/checkpoints/v3.ckpt.gz | Bin 14820 -> 0 bytes src/metatrain/pet/tests/test_checkpoints.py | 6 + src/metatrain/pet/tests/test_continue.py | 5 + src/metatrain/pet/tests/test_finetuning.py | 5 + src/metatrain/pet/tests/test_regression.py | 9 +- src/metatrain/pet/trainer.py | 50 +- src/metatrain/soap_bpnn/checkpoints.py | 23 +- src/metatrain/soap_bpnn/default-hypers.yaml | 4 - src/metatrain/soap_bpnn/model.py | 4 +- src/metatrain/soap_bpnn/schema-hypers.json | 101 +- ...v1.ckpt.gz => model-v1-trainer-v1.ckpt.gz} | Bin .../checkpoints/model-v1-trainer-v2.ckpt.gz | Bin 0 -> 60149 bytes .../checkpoints/model-v2-trainer-v2.ckpt.gz | Bin 0 -> 35707 bytes .../soap_bpnn/tests/checkpoints/v2.ckpt.gz | Bin 35711 -> 0 bytes .../soap_bpnn/tests/test_checkpoints.py | 6 + .../soap_bpnn/tests/test_continue.py | 5 + .../soap_bpnn/tests/test_regression.py | 43 +- src/metatrain/soap_bpnn/trainer.py | 48 +- src/metatrain/utils/augmentation.py | 12 + src/metatrain/utils/io.py | 13 +- src/metatrain/utils/loss.py | 992 ++++++++++++------ src/metatrain/utils/old_loss.py | 348 ++++++ src/metatrain/utils/omegaconf.py | 169 ++- src/metatrain/utils/testing/checkpoints.py | 21 +- src/metatrain/utils/transfer.py | 10 +- tests/utils/test_io.py | 3 +- tests/utils/test_llpr.py | 4 + tests/utils/test_loss.py | 709 +++++++++---- tests/utils/test_old_loss.py | 389 +++++++ tests/utils/test_omegaconf.py | 235 ++++- tests/utils/test_transfer.py | 68 +- 60 files changed, 2983 insertions(+), 898 deletions(-) create mode 100644 docs/src/advanced-concepts/loss-functions.rst create mode 100644 docs/src/dev-docs/new-loss.rst create mode 100644 src/metatrain/experimental/nanopet/checkpoints.py rename src/metatrain/experimental/nanopet/tests/checkpoints/{v1.ckpt.gz => model-v1-trainer-v1.ckpt.gz} (100%) create mode 100644 src/metatrain/experimental/nanopet/tests/checkpoints/model-v1-trainer-v2.ckpt.gz create mode 100644 src/metatrain/pet/tests/checkpoints/model-v1-trainer-v1.ckpt.gz create mode 100644 src/metatrain/pet/tests/checkpoints/model-v1-trainer-v2.ckpt.gz create mode 100644 src/metatrain/pet/tests/checkpoints/model-v2-trainer-v1.ckpt.gz create mode 100644 src/metatrain/pet/tests/checkpoints/model-v2-trainer-v2.ckpt.gz create mode 100644 src/metatrain/pet/tests/checkpoints/model-v3-trainer-v2.ckpt.gz delete mode 100644 src/metatrain/pet/tests/checkpoints/v1.ckpt.gz delete mode 100644 src/metatrain/pet/tests/checkpoints/v2.ckpt.gz delete mode 100644 src/metatrain/pet/tests/checkpoints/v3.ckpt.gz rename src/metatrain/soap_bpnn/tests/checkpoints/{v1.ckpt.gz => model-v1-trainer-v1.ckpt.gz} (100%) create mode 100644 src/metatrain/soap_bpnn/tests/checkpoints/model-v1-trainer-v2.ckpt.gz create mode 100644 src/metatrain/soap_bpnn/tests/checkpoints/model-v2-trainer-v2.ckpt.gz delete mode 100644 src/metatrain/soap_bpnn/tests/checkpoints/v2.ckpt.gz create mode 100644 src/metatrain/utils/old_loss.py create mode 100644 tests/utils/test_old_loss.py diff --git a/docs/src/advanced-concepts/index.rst b/docs/src/advanced-concepts/index.rst index 22b3ff3f6..40d08a5d1 100644 --- a/docs/src/advanced-concepts/index.rst +++ b/docs/src/advanced-concepts/index.rst @@ -9,6 +9,7 @@ such as output naming, auxiliary outputs, and wrapper models. :maxdepth: 1 output-naming + loss-functions auxiliary-outputs multi-gpu auto-restarting diff --git a/docs/src/advanced-concepts/loss-functions.rst b/docs/src/advanced-concepts/loss-functions.rst new file mode 100644 index 000000000..9bddba9ea --- /dev/null +++ b/docs/src/advanced-concepts/loss-functions.rst @@ -0,0 +1,123 @@ +.. _loss-functions: + +Loss functions +============== + +``metatrain`` supports a variety of loss functions, which can be configured +in the ``loss`` subsection of the ``training`` section for each ``architecture`` +in the options file. The loss functions are designed to be flexible and can be +tailored to the specific needs of the dataset and the targets being predicted. + +The ``loss`` subsection describes the loss functions to be used. The most basic +configuration is + +.. code-block:: yaml + + loss: mse + +which sets the loss function to mean squared error (MSE) for all targets. +When training a potential energy surface on energy, forces, and virial, +for example, this configuration is internally expanded to + +.. code-block:: yaml + + loss: + energy: + type: mse + weight: 1.0 + reduction: mean + forces: + type: mse + weight: 1.0 + reduction: mean + virial: + type: mse + weight: 1.0 + reduction: mean + +This internal, more detailed configuration can be used in the options file +to specify different loss functions for each target, or to override default +values for the parameters. The parameters accepted by each loss function are + +1. ``type``. This controls the type of loss to be used. The default value is ``mse``, + and other standard options are ``mae`` and ``huber``, which implement the equivalent + PyTorch loss functions + `MSELoss `_, + `L1Loss `_, + and + `HuberLoss `_, + respectively. + There are also "masked" versions of these losses, which are useful when using + padded targets with values that should be masked before computing the loss. The + masked losses are named ``masked_mse``, ``masked_mae``, and ``masked_huber``. + +2. ``weight``. This controls the weighting of different contributions to the loss + (e.g., energy, forces, virial, etc.). The default value of 1.0 for all targets + works well for most datasets, but can be adjusted if required. + +3. ``reduction``. This controls how the overall loss is computed across batches. + The default for this is to use the ``mean`` of the batch losses. The ``sum`` + function is also supported. + +Some losses, like ``huber``, require additional parameters to be specified. Below is +a table summarizing losses that require or allow additional parameters: + +.. list-table:: Loss Functions and Parameters + :header-rows: 1 + :widths: 20 30 50 + + * - Loss Type + - Description + - Additional Parameters + * - ``mse`` + - Mean squared error + - N/A + * - ``mae`` + - Mean absolute error + - N/A + * - ``mse_masked`` + - Masked mean squared error + - N/A + * - ``mae_masked`` + - Masked mean absolute error + - N/A + * - ``huber`` + - Huber loss + - ``delta``: Threshold at which to switch from squared error to absolute error. + + +Masked loss functions +--------------------- + +Masked loss functions are particularly useful when dealing with datasets that contain +padded targets. In such cases, the loss function can be configured to ignore the padded +values during the loss computation. This is done by using the ``masked_`` prefix in +the loss type. For example, if the target contains padded values, you can use +``masked_mse`` or ``masked_mae`` to ensure that the loss is computed only on the +valid (non-padded) values. The values of the masks must be passed as ``extra_data`` +in the training set, and the loss function will automatically apply the mask to +the target values. An example configuration for a masked loss is as follows: + + .. code-block:: yaml + + loss: + energy: + type: masked_mse + weight: 1.0 + reduction: sum + forces: + type: masked_mae + weight: 0.1 + reduction: sum + ... + + training_set: + systems: + ... + targets: + mtt::my_target: + ... + ... + extra_data: + mtt::my_target_mask: + read_from: my_target_mask.mts diff --git a/docs/src/architectures/nanopet.rst b/docs/src/architectures/nanopet.rst index f4ec15949..55d6d140b 100644 --- a/docs/src/architectures/nanopet.rst +++ b/docs/src/architectures/nanopet.rst @@ -60,19 +60,9 @@ hyperparameters to tune are (in decreasing order of importance): - ``num_attention_layers``: The number of attention layers in each layer of the graph neural network. Depending on the dataset, increasing this hyperparameter might lead to better accuracy, at the cost of increased training and evaluation time. -- ``loss``: This section describes the loss function to be used, and it has three - subsections. 1. ``weights``. This controls the weighting of different contributions - to the loss (e.g., energy, forces, virial, etc.). The default values of 1.0 for all - targets work well for most datasets, but they might need to be adjusted. For example, - to set a weight of 1.0 for the energy and 0.1 for the forces, you can set the - following in the ``options.yaml`` file under ``loss``: - ``weights: {"energy": 1.0, "forces": 0.1}``. 2. ``type``. This controls the type of - loss to be used. The default value is ``mse``, and other options are ``mae`` and - ``huber``. ``huber`` is a subsection of its own, and it requires the user to specify - the ``deltas`` parameters in a similar way to how the ``weights`` are specified (e.g., - ``deltas: {"energy": 0.1, "forces": 0.01}``). 3. ``reduction``. This controls how the - loss is reduced over batches. The default value is ``mean``, and the other allowed - option is ``sum``. +- ``loss``: This section describes the loss function to be used. See the + :doc:`dedicated documentation page <../advanced-concepts/loss-functions>` for more + details. - ``long_range``: In some systems and datasets, enabling long-range Coulomb interactions might be beneficial for the accuracy of the model and/or its physical correctness. See below for a breakdown of the long-range section of the model hyperparameters. diff --git a/docs/src/architectures/pet.rst b/docs/src/architectures/pet.rst index 6b2e09449..3f893d80f 100644 --- a/docs/src/architectures/pet.rst +++ b/docs/src/architectures/pet.rst @@ -58,19 +58,9 @@ hyperparameters to tune are (in decreasing order of importance): - ``num_attention_layers``: The number of attention layers in each layer of the graph neural network. Depending on the dataset, increasing this hyperparameter might lead to better accuracy, at the cost of increased training and evaluation time. -- ``loss``: This section describes the loss function to be used, and it has three - subsections. 1. ``weights``. This controls the weighting of different contributions - to the loss (e.g., energy, forces, virial, etc.). The default values of 1.0 for all - targets work well for most datasets, but they might need to be adjusted. For example, - to set a weight of 1.0 for the energy and 0.1 for the forces, you can set the - following in the ``options.yaml`` file under ``loss``: - ``weights: {"energy": 1.0, "forces": 0.1}``. 2. ``type``. This controls the type of - loss to be used. The default value is ``mse``, and other options are ``mae`` and - ``huber``. ``huber`` is a subsection of its own, and it requires the user to specify - the ``deltas`` parameters in a similar way to how the ``weights`` are specified (e.g., - ``deltas: {"energy": 0.1, "forces": 0.01}``). 3. ``reduction``. This controls how the - loss is reduced over batches. The default value is ``mean``, and the other allowed - option is ``sum``. +- ``loss``: This section describes the loss function to be used. See the + :doc:`dedicated documentation page <../advanced-concepts/loss-functions>` for more + details. - ``long_range``: In some systems and datasets, enabling long-range Coulomb interactions might be beneficial for the accuracy of the model and/or its physical correctness. See below for a breakdown of the long-range section of the model hyperparameters. diff --git a/docs/src/architectures/soap-bpnn.rst b/docs/src/architectures/soap-bpnn.rst index 9ee96426e..88bee0194 100644 --- a/docs/src/architectures/soap-bpnn.rst +++ b/docs/src/architectures/soap-bpnn.rst @@ -54,19 +54,9 @@ hyperparameters to tune are (in decreasing order of importance): - ``layernorm``: Whether to use layer normalization before the neural network. Setting this hyperparameter to ``false`` will lead to slower convergence of training, but might lead to better generalization outside of the training set distribution. -- ``loss``: This section describes the loss function to be used, and it has three - subsections. 1. ``weights``. This controls the weighting of different contributions - to the loss (e.g., energy, forces, virial, etc.). The default values of 1.0 for all - targets work well for most datasets, but they might need to be adjusted. For example, - to set a weight of 1.0 for the energy and 0.1 for the forces, you can set the - following in the ``options.yaml`` file under ``loss``: - ``weights: {"energy": 1.0, "forces": 0.1}``. 2. ``type``. This controls the type of - loss to be used. The default value is ``mse``, and other options are ``mae`` and - ``huber``. ``huber`` is a subsection of its own, and it requires the user to specify - the ``deltas`` parameters in a similar way to how the ``weights`` are specified (e.g., - ``deltas: {"energy": 0.1, "forces": 0.01}``). 3. ``reduction``. This controls how the - loss is reduced over batches. The default value is ``mean``, and the other allowed - option is ``sum``. +- ``loss``: This section describes the loss function to be used. See the + :doc:`dedicated documentation page <../advanced-concepts/loss-functions>` for more + details. - ``long_range``: In some systems and datasets, enabling long-range Coulomb interactions might be beneficial for the accuracy of the model and/or its physical correctness. See below for a breakdown of the long-range section of the model hyperparameters. diff --git a/docs/src/dev-docs/index.rst b/docs/src/dev-docs/index.rst index 5dff20c61..a91847fa7 100644 --- a/docs/src/dev-docs/index.rst +++ b/docs/src/dev-docs/index.rst @@ -13,6 +13,7 @@ module. architecture-life-cycle new-architecture dataset-information + new-loss cli/index utils/index changelog diff --git a/docs/src/dev-docs/new-loss.rst b/docs/src/dev-docs/new-loss.rst new file mode 100644 index 000000000..dc49cc1d7 --- /dev/null +++ b/docs/src/dev-docs/new-loss.rst @@ -0,0 +1,61 @@ +.. _adding-new-loss: + +Adding a new loss function +========================== + +This page describes the required classes and files necessary for adding a new +loss function to ``metatrain``. Defining a new loss can be useful in case some extra +data has to be used to compute the loss. + +Loss functions in ``metatrain`` are implemented as subclasses of +:py:class:`metatrain.utils.loss.LossInterface`. This interface defines the +required method :py:meth:`compute`, which takes the model predictions and +the ground truth values as input and returns the computed loss value. The +:py:meth:`compute` method accepts an additional argument ``extra_data`` on top of +``predictions`` and ``targets``, that can be used to pass any extra information needed +for the loss computation. + +.. code-block:: python + + from typing import Dict, Optional + import torch + from metatrain.utils.loss import LossInterface + from metatensor.torch import TensorMap + + class NewLoss(LossInterface): + def __init__( + self, + name: str, + gradient: Optional[str], + weight: float, + reduction: str, + ) -> None: + ... + + def compute( + self, + predictions: Dict[str, TensorMap], + targets: Dict[str, TensorMap], + extra_data: Dict[str, TensorMap] + ) -> torch.Tensor: + ... + + +Examples of loss functions already implemented in ``metatrain`` are +:py:class:`metatrain.utils.loss.TensorMapMSELoss` and +:py:class:`metatrain.utils.loss.TensorMapMAELoss`. They both inherit from the +:py:class:`metatrain.utils.loss.BaseTensorMapLoss` class, which implements pointwise +losses for :py:class:`metatensor.torch.TensorMap` objects. + + +Loss weight scheduling +---------------------- + +Currently, only one loss weight scheduler is implemented in ``metatrain``, which is +:py:class:`metatrain.utils.loss.EMAScheduler`. This class is used to schedule the weight +of a loss function based on the Exponential Moving Average (EMA) of the loss value. +The EMA scheduler is useful to adapt the loss weight during training, allowing for a +more dynamic adjustment of the loss contribution based on the training progress. +New schedulers can be implemented by inheriting from the +:py:class:`metatrain.utils.loss.WeightScheduler` abstract class, which defines the +:py:meth:`initialize` and :py:meth:`update` methods that need to be implemented. diff --git a/src/metatrain/cli/train.py b/src/metatrain/cli/train.py index 9a393ad6c..f04ab7bd8 100644 --- a/src/metatrain/cli/train.py +++ b/src/metatrain/cli/train.py @@ -39,7 +39,12 @@ ) from ..utils.jsonschema import validate from ..utils.logging import ROOT_LOGGER, WandbHandler, human_readable -from ..utils.omegaconf import BASE_OPTIONS, check_units, expand_dataset_config +from ..utils.omegaconf import ( + BASE_OPTIONS, + check_units, + expand_dataset_config, + expand_loss_config, +) from .eval import _eval_targets from .export import _has_extensions from .formatter import CustomHelpFormatter @@ -205,7 +210,6 @@ def train_model( {"architecture": get_default_hypers(architecture_name)}, options, ) - hypers = OmegaConf.to_container(options["architecture"]) ########################### # PROCESS BASE PARAMETERS # @@ -386,6 +390,10 @@ def train_model( test_datasets.append(dataset) test_indices.append(None) + # Expand loss options and finalize the hypers + options = expand_loss_config(options) + hypers = OmegaConf.to_container(options["architecture"]) + ############################################ # SAVE TRAIN, VALIDATION, TEST INDICES ##### ############################################ diff --git a/src/metatrain/experimental/nanopet/checkpoints.py b/src/metatrain/experimental/nanopet/checkpoints.py new file mode 100644 index 000000000..4a83f08f3 --- /dev/null +++ b/src/metatrain/experimental/nanopet/checkpoints.py @@ -0,0 +1,20 @@ +# ===== Model checkpoint updates ===== + +# ... + +# ===== Trainer checkpoint updates ===== + + +def trainer_update_v1_v2(checkpoint): + old_loss_hypers = checkpoint["train_hypers"]["loss"].copy() + dataset_info = checkpoint["model_data"]["dataset_info"] + new_loss_hypers = {} + + for target_name in dataset_info.targets.keys(): + new_loss_hypers[target_name] = { + "type": old_loss_hypers["type"], + "weight": old_loss_hypers["weights"].get(target_name, 1.0), + "reduction": old_loss_hypers["reduction"], + "sliding_factor": old_loss_hypers.get("sliding_factor", None), + } + checkpoint["train_hypers"]["loss"] = new_loss_hypers diff --git a/src/metatrain/experimental/nanopet/default-hypers.yaml b/src/metatrain/experimental/nanopet/default-hypers.yaml index 38f01381b..06d57e83e 100644 --- a/src/metatrain/experimental/nanopet/default-hypers.yaml +++ b/src/metatrain/experimental/nanopet/default-hypers.yaml @@ -31,7 +31,3 @@ architecture: log_mae: false log_separate_blocks: false best_model_metric: rmse_prod - loss: - type: mse - weights: {} - reduction: mean diff --git a/src/metatrain/experimental/nanopet/schema-hypers.json b/src/metatrain/experimental/nanopet/schema-hypers.json index 0b389739a..7f23b0388 100644 --- a/src/metatrain/experimental/nanopet/schema-hypers.json +++ b/src/metatrain/experimental/nanopet/schema-hypers.json @@ -137,59 +137,13 @@ ] }, "loss": { - "type": "object", - "properties": { - "weights": { - "type": "object", - "patternProperties": { - ".*": { - "type": "number" - } - }, - "additionalProperties": false - }, - "reduction": { - "type": "string", - "enum": [ - "sum", - "mean", - "none" - ] - }, - "type": { - "oneOf": [ - { - "type": "string", - "enum": [ - "mse", - "mae" - ] - }, - { - "type": "object", - "properties": { - "huber": { - "type": "object", - "properties": { - "deltas": { - "type": "object", - "patternProperties": { - ".*": { - "type": "number" - } - }, - "additionalProperties": false - } - }, - "required": [ - "deltas" - ], - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] + "type": [ + "object", + "string" + ], + "patternProperties": { + "^.+$": { + "$ref": "#/definitions/lossTerm" } }, "additionalProperties": false @@ -206,5 +160,47 @@ "uniqueItems": true } }, - "additionalProperties": false + "additionalProperties": false, + "definitions": { + "lossTerm": { + "type": [ + "object", + "string" + ], + "properties": { + "type": { + "type": "string" + }, + "weight": { + "type": "number", + "minimum": 0.0 + }, + "reduction": { + "type": "string", + "enum": [ + "none", + "mean", + "sum" + ] + }, + "sliding_factor": { + "type": [ + "number", + "null" + ], + "minimum": 0.0 + }, + "gradients": { + "type": "object", + "patternProperties": { + "^.+$": { + "$ref": "#/definitions/lossTerm" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true + } + } } diff --git a/src/metatrain/experimental/nanopet/tests/checkpoints/v1.ckpt.gz b/src/metatrain/experimental/nanopet/tests/checkpoints/model-v1-trainer-v1.ckpt.gz similarity index 100% rename from src/metatrain/experimental/nanopet/tests/checkpoints/v1.ckpt.gz rename to src/metatrain/experimental/nanopet/tests/checkpoints/model-v1-trainer-v1.ckpt.gz diff --git a/src/metatrain/experimental/nanopet/tests/checkpoints/model-v1-trainer-v2.ckpt.gz b/src/metatrain/experimental/nanopet/tests/checkpoints/model-v1-trainer-v2.ckpt.gz new file mode 100644 index 0000000000000000000000000000000000000000..0f5d58e58f5d87de3e3cdd9af0a6e7b47e327a97 GIT binary patch literal 12025 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 literal 0 HcmV?d00001 diff --git a/src/metatrain/experimental/nanopet/tests/test_checkpoints.py b/src/metatrain/experimental/nanopet/tests/test_checkpoints.py index 2df080223..6e790e90f 100644 --- a/src/metatrain/experimental/nanopet/tests/test_checkpoints.py +++ b/src/metatrain/experimental/nanopet/tests/test_checkpoints.py @@ -2,10 +2,12 @@ import pytest import torch +from omegaconf import OmegaConf from metatrain.experimental.nanopet import NanoPET, Trainer from metatrain.utils.data import DatasetInfo, get_atomic_types, get_dataset from metatrain.utils.data.target_info import get_energy_target_info +from metatrain.utils.omegaconf import CONF_LOSS from metatrain.utils.testing.checkpoints import ( checkpoint_did_not_change, make_checkpoint_load_tests, @@ -59,6 +61,10 @@ def model_trainer(): hypers = copy.deepcopy(DEFAULT_HYPERS) hypers["training"]["num_epochs"] = 1 + loss_hypers = OmegaConf.create({"energy": CONF_LOSS.copy()}) + loss_hypers = OmegaConf.to_container(loss_hypers, resolve=True) + hypers["training"]["loss"] = loss_hypers + trainer = Trainer(hypers["training"]) trainer.train( diff --git a/src/metatrain/experimental/nanopet/tests/test_continue.py b/src/metatrain/experimental/nanopet/tests/test_continue.py index 28812398c..ec67c3280 100644 --- a/src/metatrain/experimental/nanopet/tests/test_continue.py +++ b/src/metatrain/experimental/nanopet/tests/test_continue.py @@ -10,6 +10,7 @@ from metatrain.utils.data.target_info import get_energy_target_info from metatrain.utils.io import model_from_checkpoint from metatrain.utils.neighbor_lists import get_system_with_neighbor_lists +from metatrain.utils.omegaconf import CONF_LOSS from . import DATASET_PATH, DEFAULT_HYPERS, MODEL_HYPERS @@ -58,6 +59,10 @@ def test_continue(monkeypatch, tmp_path): hypers = DEFAULT_HYPERS.copy() hypers["training"]["num_epochs"] = 0 + loss_conf = OmegaConf.create({"mtt::U0": CONF_LOSS.copy()}) + OmegaConf.resolve(loss_conf) + hypers["training"]["loss"] = loss_conf + trainer = Trainer(hypers["training"]) trainer.train( model=model, diff --git a/src/metatrain/experimental/nanopet/tests/test_regression.py b/src/metatrain/experimental/nanopet/tests/test_regression.py index dd7ee6d2d..cddb4a72a 100644 --- a/src/metatrain/experimental/nanopet/tests/test_regression.py +++ b/src/metatrain/experimental/nanopet/tests/test_regression.py @@ -10,6 +10,7 @@ from metatrain.utils.data.readers import read_systems, read_targets from metatrain.utils.data.target_info import get_energy_target_info from metatrain.utils.neighbor_lists import get_system_with_neighbor_lists +from metatrain.utils.omegaconf import CONF_LOSS from . import DATASET_PATH, DEFAULT_HYPERS, MODEL_HYPERS @@ -84,6 +85,9 @@ def test_regression_train(): hypers = DEFAULT_HYPERS.copy() hypers["training"]["num_epochs"] = 2 + loss_conf = OmegaConf.create({"mtt::U0": CONF_LOSS.copy()}) + OmegaConf.resolve(loss_conf) + hypers["training"]["loss"] = loss_conf dataset_info = DatasetInfo( length_unit="Angstrom", atomic_types=[1, 6, 7, 8], targets=target_info_dict diff --git a/src/metatrain/experimental/nanopet/trainer.py b/src/metatrain/experimental/nanopet/trainer.py index 8f3719a3e..00a123f39 100644 --- a/src/metatrain/experimental/nanopet/trainer.py +++ b/src/metatrain/experimental/nanopet/trainer.py @@ -21,10 +21,9 @@ ) from metatrain.utils.distributed.slurm import DistributedEnvironment from metatrain.utils.evaluate_model import evaluate_model -from metatrain.utils.external_naming import to_external_name from metatrain.utils.io import check_file_extension from metatrain.utils.logging import ROOT_LOGGER, MetricLogger -from metatrain.utils.loss import TensorMapDictLoss +from metatrain.utils.loss import LossAggregator from metatrain.utils.metrics import MAEAccumulator, RMSEAccumulator, get_selected_metric from metatrain.utils.neighbor_lists import ( get_requested_neighbor_lists, @@ -34,11 +33,12 @@ from metatrain.utils.scaler import remove_scale from metatrain.utils.transfer import batch_to +from . import checkpoints from .model import NanoPET class Trainer(TrainerInterface): - __checkpoint_version__ = 1 + __checkpoint_version__ = 2 def __init__(self, hypers): super().__init__(hypers) @@ -232,29 +232,23 @@ def train( outputs_list.append(target_name) for gradient_name in target_info.gradients: outputs_list.append(f"{target_name}_{gradient_name}_gradients") - # Create a loss weight dict: - loss_weights_dict = {} - for output_name in outputs_list: - loss_weights_dict[output_name] = ( - self.hypers["loss"]["weights"][ - to_external_name(output_name, train_targets) - ] - if to_external_name(output_name, train_targets) - in self.hypers["loss"]["weights"] - else 1.0 - ) - loss_weights_dict_external = { - to_external_name(key, train_targets): value - for key, value in loss_weights_dict.items() - } - loss_hypers = copy.deepcopy(self.hypers["loss"]) - loss_hypers["weights"] = loss_weights_dict - logging.info(f"Training with loss weights: {loss_weights_dict_external}") # Create a loss function: - loss_fn = TensorMapDictLoss( - **loss_hypers, + loss_hypers = self.hypers["loss"] + loss_fn = LossAggregator( + targets=train_targets, + config=loss_hypers, ) + logging.info("Using the following loss functions:") + for name, info in loss_fn.metadata.items(): + logging.info(f"{name}:") + main = {k: v for k, v in info.items() if k != "gradients"} + logging.info(main) + if "gradients" not in info or len(info["gradients"]) == 0: + continue + logging.info("With gradients:") + for grad, ginfo in info["gradients"].items(): + logging.info(f"\t{name}::{grad}: {ginfo}") # Create an optimizer: optimizer = torch.optim.Adam( @@ -346,7 +340,7 @@ def train( ) targets = average_by_num_atoms(targets, systems, per_structure_targets) - train_loss_batch = loss_fn(predictions, targets) + train_loss_batch = loss_fn(predictions, targets, extra_data) train_loss_batch.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() @@ -408,7 +402,7 @@ def train( ) targets = average_by_num_atoms(targets, systems, per_structure_targets) - val_loss_batch = loss_fn(predictions, targets) + val_loss_batch = loss_fn(predictions, targets, extra_data) if is_distributed: # sum the loss over all processes @@ -562,6 +556,16 @@ def load_checkpoint( return trainer - @staticmethod - def upgrade_checkpoint(checkpoint: Dict) -> Dict: - raise NotImplementedError("checkpoint upgrade is not implemented for NanoPET") + @classmethod + def upgrade_checkpoint(cls, checkpoint: Dict) -> Dict: + if checkpoint["trainer_ckpt_version"] == 1: + checkpoints.trainer_update_v1_v2(checkpoint) + checkpoint["trainer_ckpt_version"] = 2 + + if checkpoint["trainer_ckpt_version"] != cls.__checkpoint_version__: + raise RuntimeError( + f"Unable to upgrade the checkpoint: the checkpoint is using " + f"trainer version {checkpoint['trainer_ckpt_version']}, while the " + f"current trainer version is {cls.__checkpoint_version__}." + ) + return checkpoint diff --git a/src/metatrain/gap/tests/test_torchscript.py b/src/metatrain/gap/tests/test_torchscript.py index 2705ed831..869ac5d2d 100644 --- a/src/metatrain/gap/tests/test_torchscript.py +++ b/src/metatrain/gap/tests/test_torchscript.py @@ -93,8 +93,6 @@ def test_torchscript_integers(): new_hypers["soap"]["density"]["scaling"]["scale"] = 2 new_hypers["soap"]["density"]["scaling"]["exponent"] = 7 - # print(new_hypers) - target_info_dict = {} target_info_dict["mtt::U0"] = get_energy_target_info({"unit": "eV"}) diff --git a/src/metatrain/pet/checkpoints.py b/src/metatrain/pet/checkpoints.py index 6431df401..cb4a4150f 100644 --- a/src/metatrain/pet/checkpoints.py +++ b/src/metatrain/pet/checkpoints.py @@ -1,4 +1,7 @@ -def update_v1_v2(state_dict): +# ===== Model checkpoint updates ===== + + +def model_update_v1_v2(state_dict): # This if-statement is necessary to handle cases when # best_model_state_dict and model_state_dict are the same. # In that case, the both are updated within the first call of @@ -12,10 +15,28 @@ def update_v1_v2(state_dict): ) -def update_v2_v3(state_dict): +def model_update_v2_v3(state_dict): if state_dict is not None: if "train_hypers" in state_dict: finetune_config = state_dict["train_hypers"].get("finetune", {}) else: finetune_config = {} state_dict["finetune_config"] = finetune_config + + +# ===== Trainer checkpoint updates ===== + + +def trainer_update_v1_v2(checkpoint): + old_loss_hypers = checkpoint["train_hypers"]["loss"].copy() + dataset_info = checkpoint["model_data"]["dataset_info"] + new_loss_hypers = {} + + for target_name in dataset_info.targets.keys(): + new_loss_hypers[target_name] = { + "type": old_loss_hypers["type"], + "weight": old_loss_hypers["weights"].get(target_name, 1.0), + "reduction": old_loss_hypers["reduction"], + "sliding_factor": old_loss_hypers.get("sliding_factor", None), + } + checkpoint["train_hypers"]["loss"] = new_loss_hypers diff --git a/src/metatrain/pet/default-hypers.yaml b/src/metatrain/pet/default-hypers.yaml index eb43d62ff..c73f3de39 100644 --- a/src/metatrain/pet/default-hypers.yaml +++ b/src/metatrain/pet/default-hypers.yaml @@ -34,8 +34,3 @@ architecture: log_separate_blocks: false best_model_metric: rmse_prod grad_clip_norm: .inf - loss: - type: mse - weights: {} - reduction: mean - sliding_factor: null diff --git a/src/metatrain/pet/model.py b/src/metatrain/pet/model.py index b3ad748a5..2495fcb32 100644 --- a/src/metatrain/pet/model.py +++ b/src/metatrain/pet/model.py @@ -891,12 +891,12 @@ def _get_system_indices_and_labels( @classmethod def upgrade_checkpoint(cls, checkpoint: Dict) -> Dict: if checkpoint["model_ckpt_version"] == 1: - checkpoints.update_v1_v2(checkpoint["model_state_dict"]) - checkpoints.update_v1_v2(checkpoint["best_model_state_dict"]) + checkpoints.model_update_v1_v2(checkpoint["model_state_dict"]) + checkpoints.model_update_v1_v2(checkpoint["best_model_state_dict"]) checkpoint["model_ckpt_version"] = 2 if checkpoint["model_ckpt_version"] == 2: - checkpoints.update_v2_v3(checkpoint["model_state_dict"]) - checkpoints.update_v2_v3(checkpoint["best_model_state_dict"]) + checkpoints.model_update_v2_v3(checkpoint["model_state_dict"]) + checkpoints.model_update_v2_v3(checkpoint["best_model_state_dict"]) checkpoint["model_ckpt_version"] = 3 if checkpoint["model_ckpt_version"] != cls.__checkpoint_version__: diff --git a/src/metatrain/pet/schema-hypers.json b/src/metatrain/pet/schema-hypers.json index 4c1854303..1ec8cf875 100644 --- a/src/metatrain/pet/schema-hypers.json +++ b/src/metatrain/pet/schema-hypers.json @@ -198,65 +198,13 @@ "type": "number" }, "loss": { - "type": "object", - "properties": { - "weights": { - "type": "object", - "patternProperties": { - ".*": { - "type": "number" - } - }, - "additionalProperties": false - }, - "reduction": { - "type": "string", - "enum": [ - "sum", - "mean", - "none" - ] - }, - "type": { - "oneOf": [ - { - "type": "string", - "enum": [ - "mse", - "mae" - ] - }, - { - "type": "object", - "properties": { - "huber": { - "type": "object", - "properties": { - "deltas": { - "type": "object", - "patternProperties": { - ".*": { - "type": "number" - } - }, - "additionalProperties": false - } - }, - "required": [ - "deltas" - ], - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "sliding_factor": { - "type": [ - "number", - "null" - ] + "type": [ + "object", + "string" + ], + "patternProperties": { + "^.+$": { + "$ref": "#/definitions/lossTerm" } }, "additionalProperties": false @@ -273,5 +221,47 @@ "uniqueItems": true } }, - "additionalProperties": false + "additionalProperties": false, + "definitions": { + "lossTerm": { + "type": [ + "object", + "string" + ], + "properties": { + "type": { + "type": "string" + }, + "weight": { + "type": "number", + "minimum": 0.0 + }, + "reduction": { + "type": "string", + "enum": [ + "none", + "mean", + "sum" + ] + }, + "sliding_factor": { + "type": [ + "number", + "null" + ], + "minimum": 0.0 + }, + "gradients": { + "type": "object", + "patternProperties": { + "^.+$": { + "$ref": "#/definitions/lossTerm" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true + } + } } diff --git a/src/metatrain/pet/tests/checkpoints/model-v1-trainer-v1.ckpt.gz b/src/metatrain/pet/tests/checkpoints/model-v1-trainer-v1.ckpt.gz new file mode 100644 index 0000000000000000000000000000000000000000..1d4f7a8f16a096483fe151c064ddc50f7411240b GIT binary patch literal 15018 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 W4)+GF9cp(F*iY)!CYFk@F#iYIxGsnQ literal 0 HcmV?d00001 diff --git a/src/metatrain/pet/tests/checkpoints/model-v1-trainer-v2.ckpt.gz b/src/metatrain/pet/tests/checkpoints/model-v1-trainer-v2.ckpt.gz new file mode 100644 index 0000000000000000000000000000000000000000..67e9f5e31fc36c1fa5dede590ff36632322b23b2 GIT binary patch literal 15075 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

ieD4{ zJNA>NTo1nxCP~O!Dt5mX{H@UsdDRzYiaU)_2XJQS)4n{IdIh#|w(gh{yPz~=bNImgN8aKrjc(#mFxh@!A9>{vSt1TF}8lypVZ;O)TcT{P!P|hFvhd9X}UmnhSK;KW3 zeX5^P%71uPozYz*aI4CeCl3iYyo}$ydum& zcvY^&am#*0jXR%i$7vD{N|p7|EZgQ7OBA7~5t`yRV)`Nkxysic)5#g}%81Q9vEpQI0oyY44EHsIoqV5X*RRCLEx8kk_v6$N=kA_$t5{*#9Ir(Kk+>s~Gg zzS44P+UiBf_=de=20N zL=q#irk0!Hf0&NNtP2iw$Goyi8`Kd|rFN^)3@s`YL|;L)L*8HMj_OHK9tR@GgU|W! zU6RVyeueMOCTdZ--C|S@CP0d?^c%^-Ta-4?t%(I4VW1!JKIt`tet(2!MP1mxo>wrq z3ztMx)bZGOEJ?Ket{8tzmK2$g2mj7Nn2o?fB&qgh5XNmQ4UtSLV+;q%|ZAAH~r5SV9OQDf3F`eb}aJ2 zyt@*x-B#Rr#Mhl$D~LiYK^D;fIFAj(wL0mMDtJa>uN`+j{;@oOl+u7StT&neHIM5Fh=x1=pAbTJK0?xGd_`ciE2nkugp!pLI4XCX{dzw0 zNmH8KjShk5#GSVX(#)qI;`R^Nc;ShcP-pfZF#QQw_kZmeLX3T3f`Oxm{G+NV7PX?z zvki(b932(?9rGCS%FpMIy7R_cU87}yp$=OkNztU>VMp#Zy479U;Gq;<`bQBeV`6xk eP3YnCxtbQ;D>yO|5*&_-blaKI@>3HT>Hh(J@G8^* literal 0 HcmV?d00001 diff --git a/src/metatrain/pet/tests/checkpoints/model-v2-trainer-v1.ckpt.gz b/src/metatrain/pet/tests/checkpoints/model-v2-trainer-v1.ckpt.gz new file mode 100644 index 0000000000000000000000000000000000000000..eddb3200681eeab316378edb9e423b814d3e9bf8 GIT binary patch literal 15018 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 Isv#r&A3J5vEC2ui literal 0 HcmV?d00001 diff --git a/src/metatrain/pet/tests/checkpoints/model-v2-trainer-v2.ckpt.gz b/src/metatrain/pet/tests/checkpoints/model-v2-trainer-v2.ckpt.gz new file mode 100644 index 0000000000000000000000000000000000000000..0708363c31309a335c2bc86147ef3955b0e8f866 GIT binary patch literal 15066 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 MQ2#1RALGIQ1%O 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 OvQA3bO|C8?!v6vj^Xv@( diff --git a/src/metatrain/pet/tests/checkpoints/v2.ckpt.gz b/src/metatrain/pet/tests/checkpoints/v2.ckpt.gz deleted file mode 100644 index 676a75ac948de3579ab1e312a48ebfed6b6e5699..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15268 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 diff --git a/src/metatrain/pet/tests/checkpoints/v3.ckpt.gz b/src/metatrain/pet/tests/checkpoints/v3.ckpt.gz deleted file mode 100644 index 5723aa12672a2dda07b5d7ac232da9df880ea658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14820 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 d+XEwi?v9rq|7Hx Dict: + if checkpoint["trainer_ckpt_version"] == 1: + checkpoints.trainer_update_v1_v2(checkpoint) + checkpoint["trainer_ckpt_version"] = 2 + if checkpoint["trainer_ckpt_version"] != cls.__checkpoint_version__: raise RuntimeError( f"Unable to upgrade the checkpoint: the checkpoint is using " diff --git a/src/metatrain/soap_bpnn/checkpoints.py b/src/metatrain/soap_bpnn/checkpoints.py index 8041658f8..bc6f032c2 100644 --- a/src/metatrain/soap_bpnn/checkpoints.py +++ b/src/metatrain/soap_bpnn/checkpoints.py @@ -1,4 +1,7 @@ -def update_v1_v2(state_dict): +# ===== Model checkpoint updates ===== + + +def model_update_v1_v2(state_dict): # This if-statement is necessary to handle cases when # best_model_state_dict and model_state_dict are the same. # In that case, the both are updated within the first call of @@ -10,3 +13,21 @@ def update_v1_v2(state_dict): state_dict["additive_models.0.model.type_to_index"] = state_dict.pop( "additive_models.0.type_to_index" ) + + +# ===== Trainer checkpoint updates ===== + + +def trainer_update_v1_v2(checkpoint): + old_loss_hypers = checkpoint["train_hypers"]["loss"].copy() + dataset_info = checkpoint["model_data"]["dataset_info"] + new_loss_hypers = {} + + for target_name in dataset_info.targets.keys(): + new_loss_hypers[target_name] = { + "type": old_loss_hypers["type"], + "weight": old_loss_hypers["weights"].get(target_name, 1.0), + "reduction": old_loss_hypers["reduction"], + "sliding_factor": old_loss_hypers.get("sliding_factor", None), + } + checkpoint["train_hypers"]["loss"] = new_loss_hypers diff --git a/src/metatrain/soap_bpnn/default-hypers.yaml b/src/metatrain/soap_bpnn/default-hypers.yaml index 5b5082232..2bb5b6943 100644 --- a/src/metatrain/soap_bpnn/default-hypers.yaml +++ b/src/metatrain/soap_bpnn/default-hypers.yaml @@ -37,7 +37,3 @@ architecture: log_mae: false log_separate_blocks: false best_model_metric: rmse_prod - loss: - type: mse - weights: {} - reduction: mean diff --git a/src/metatrain/soap_bpnn/model.py b/src/metatrain/soap_bpnn/model.py index 567bf32da..c549bd172 100644 --- a/src/metatrain/soap_bpnn/model.py +++ b/src/metatrain/soap_bpnn/model.py @@ -859,8 +859,8 @@ def _add_output(self, target_name: str, target: TargetInfo) -> None: @classmethod def upgrade_checkpoint(cls, checkpoint: Dict) -> Dict: if checkpoint["model_ckpt_version"] == 1: - checkpoints.update_v1_v2(checkpoint["model_state_dict"]) - checkpoints.update_v1_v2(checkpoint["best_model_state_dict"]) + checkpoints.model_update_v1_v2(checkpoint["model_state_dict"]) + checkpoints.model_update_v1_v2(checkpoint["best_model_state_dict"]) checkpoint["model_ckpt_version"] = 2 if checkpoint["model_ckpt_version"] != cls.__checkpoint_version__: diff --git a/src/metatrain/soap_bpnn/schema-hypers.json b/src/metatrain/soap_bpnn/schema-hypers.json index 5534cac93..8872ca26c 100644 --- a/src/metatrain/soap_bpnn/schema-hypers.json +++ b/src/metatrain/soap_bpnn/schema-hypers.json @@ -164,56 +164,13 @@ ] }, "loss": { - "type": "object", - "properties": { - "weights": { - "type": "object", - "patternProperties": { - ".*": { - "type": "number" - } - }, - "additionalProperties": false - }, - "reduction": { - "type": "string", - "enum": [ - "sum", - "mean", - "none" - ] - }, - "type": { - "oneOf": [ - { - "type": "string", - "enum": [ - "mse", - "mae" - ] - }, - { - "type": "object", - "properties": { - "huber": { - "type": "object", - "properties": { - "deltas": { - "type": "object", - "patternProperties": { - ".*": { - "type": "number" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] + "type": [ + "object", + "string" + ], + "patternProperties": { + "^.+$": { + "$ref": "#/definitions/lossTerm" } }, "additionalProperties": false @@ -230,5 +187,47 @@ "uniqueItems": true } }, - "additionalProperties": false + "additionalProperties": false, + "definitions": { + "lossTerm": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "weight": { + "type": "number", + "minimum": 0.0 + }, + "reduction": { + "type": "string", + "enum": [ + "none", + "mean", + "sum" + ] + }, + "sliding_factor": { + "type": [ + "number", + "null" + ], + "minimum": 0.0 + }, + "gradients": { + "type": [ + "object", + "string" + ], + "patternProperties": { + "^.+$": { + "$ref": "#/definitions/lossTerm" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true + } + } } diff --git a/src/metatrain/soap_bpnn/tests/checkpoints/v1.ckpt.gz b/src/metatrain/soap_bpnn/tests/checkpoints/model-v1-trainer-v1.ckpt.gz similarity index 100% rename from src/metatrain/soap_bpnn/tests/checkpoints/v1.ckpt.gz rename to src/metatrain/soap_bpnn/tests/checkpoints/model-v1-trainer-v1.ckpt.gz diff --git a/src/metatrain/soap_bpnn/tests/checkpoints/model-v1-trainer-v2.ckpt.gz b/src/metatrain/soap_bpnn/tests/checkpoints/model-v1-trainer-v2.ckpt.gz new file mode 100644 index 0000000000000000000000000000000000000000..938c48b88194b05cff47576009ef0e4d1331c2ff GIT binary patch literal 60149 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 DwERm# literal 0 HcmV?d00001 diff --git a/src/metatrain/soap_bpnn/tests/checkpoints/v2.ckpt.gz b/src/metatrain/soap_bpnn/tests/checkpoints/v2.ckpt.gz deleted file mode 100644 index 863005b00f0473f8a98676fa5ce0527566855d9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35711 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 Dict: + if checkpoint["trainer_ckpt_version"] == 1: + checkpoints.trainer_update_v1_v2(checkpoint) + checkpoint["trainer_ckpt_version"] = 2 + if checkpoint["trainer_ckpt_version"] != cls.__checkpoint_version__: raise RuntimeError( f"Unable to upgrade the checkpoint: the checkpoint is using trainer " diff --git a/src/metatrain/utils/augmentation.py b/src/metatrain/utils/augmentation.py index 14936d2f5..1bc6994a5 100644 --- a/src/metatrain/utils/augmentation.py +++ b/src/metatrain/utils/augmentation.py @@ -133,6 +133,9 @@ def apply_random_augmentations( ) for tensormap_dict, info_dict in zip(tensormap_dicts, info_dicts): for name in tensormap_dict.keys(): + if name.endswith("_mask"): + # skip loss masks + continue tensormap_info = info_dict[name] if tensormap_info.is_spherical: for block in tensormap_info.layout.blocks(): @@ -246,6 +249,15 @@ def _apply_random_augmentations( new_targets: Dict[str, TensorMap] = {} new_extra_data: Dict[str, TensorMap] = {} + # Do not transform any masks present in extra_data + if extra_data is not None: + mask_keys: List[str] = [] + for key in extra_data.keys(): + if key.endswith("_mask"): + mask_keys.append(key) + for key in mask_keys: + new_extra_data[key] = extra_data.pop(key) + for tensormap_dict, new_dict in zip( [targets, extra_data], [new_targets, new_extra_data] ): diff --git a/src/metatrain/utils/io.py b/src/metatrain/utils/io.py index 6fe56b417..284837bd2 100644 --- a/src/metatrain/utils/io.py +++ b/src/metatrain/utils/io.py @@ -181,15 +181,15 @@ def model_from_checkpoint( architecture = import_architecture(architecture_name) model_ckpt_version = checkpoint.get("model_ckpt_version") - ckpt_before_versionning = model_ckpt_version is None - if ckpt_before_versionning: + ckpt_before_versioning = model_ckpt_version is None + if ckpt_before_versioning: # assume version 1 and try our best model_ckpt_version = 1 checkpoint["model_ckpt_version"] = model_ckpt_version if model_ckpt_version != architecture.__model__.__checkpoint_version__: try: - if ckpt_before_versionning: + if ckpt_before_versioning: warnings.warn( "trying to upgrade an old model checkpoint with unknown " "version, this might fail and require manual modifications", @@ -229,15 +229,16 @@ def trainer_from_checkpoint( architecture = import_architecture(architecture_name) trainer_ckpt_version = checkpoint.get("trainer_ckpt_version") - ckpt_before_versionning = trainer_ckpt_version is None - if ckpt_before_versionning: + + ckpt_before_versioning = trainer_ckpt_version is None + if ckpt_before_versioning: # assume version 1 and try our best trainer_ckpt_version = 1 checkpoint["trainer_ckpt_version"] = trainer_ckpt_version if trainer_ckpt_version != architecture.__trainer__.__checkpoint_version__: try: - if ckpt_before_versionning: + if ckpt_before_versioning: warnings.warn( "trying to upgrade an old trainer checkpoint with unknown " "version, this might fail and require manual modifications", diff --git a/src/metatrain/utils/loss.py b/src/metatrain/utils/loss.py index 726e3bab4..da33e3509 100644 --- a/src/metatrain/utils/loss.py +++ b/src/metatrain/utils/loss.py @@ -1,348 +1,748 @@ -from typing import Dict, Optional, Tuple, Union +from abc import ABC, abstractmethod +from enum import Enum +from typing import Any, Dict, Optional, Type +import metatensor.torch as mts import torch from metatensor.torch import TensorMap -from omegaconf import DictConfig from torch.nn.modules.loss import _Loss -from metatrain.utils.external_naming import to_internal_name +from metatrain.utils.data import TargetInfo -class TensorMapLoss: - """A loss function that operates on two ``metatensor.torch.TensorMap``. - - The loss is computed as the sum of the loss on the block values and - the loss on the gradients, with weights specified at initialization. - - At the moment, this loss function assumes that all the gradients - declared at initialization are present in both TensorMaps. - - :param reduction: The reduction to apply to the loss. - See :py:class:`torch.nn.MSELoss`. - :param weight: The weight to apply to the loss on the block values. - :param gradient_weights: The weights to apply to the loss on the gradients. - :param sliding_factor: The factor to apply to the exponential moving average - of the "sliding" weights. These are weights that act on different components of - the loss (for example, energies and forces), based on their individual recent - history. If ``None``, no sliding weights are used in the computation of the - loss. - :param type: The type of loss to use. This can be either "mse" or "mae". - A Huber loss can also be requested as a dictionary with the key "huber" and - the value must be a dictionary with the key "deltas" and the value - must be a dictionary with the keys "values" and the gradient keys. - The values of the dictionary must be the deltas to use for the - Huber loss. +class LossInterface(ABC): + """ + Abstract base for all loss functions. - :returns: The loss as a zero-dimensional :py:class:`torch.Tensor` - (with one entry). + Subclasses must implement the ``compute`` method. """ + weight: float + reduction: str + loss_kwargs: Dict[str, Any] + target: str + gradient: Optional[str] + def __init__( self, - reduction: str = "mean", - weight: float = 1.0, - gradient_weights: Optional[Dict[str, float]] = None, - sliding_factor: Optional[float] = None, - type: Union[str, dict] = "mse", - ): - if gradient_weights is None: - gradient_weights = {} - - losses = {} - if type == "mse": - losses["values"] = torch.nn.MSELoss(reduction=reduction) - for key in gradient_weights.keys(): - losses[key] = torch.nn.MSELoss(reduction=reduction) - elif type == "mae": - losses["values"] = torch.nn.L1Loss(reduction=reduction) - for key in gradient_weights.keys(): - losses[key] = torch.nn.L1Loss(reduction=reduction) - elif isinstance(type, dict) and "huber" in type: - # Huber loss - deltas = type["huber"]["deltas"] - losses["values"] = torch.nn.HuberLoss( - reduction=reduction, delta=deltas["values"] - ) - for key in gradient_weights.keys(): - losses[key] = torch.nn.HuberLoss(reduction=reduction, delta=deltas[key]) - else: - raise ValueError(f"Unknown loss type: {type}") - - self.losses = losses + name: str, + gradient: Optional[str], + weight: float, + reduction: str, + ) -> None: + """ + :param name: key in the predictions/targets dict to select the TensorMap. + :param gradient: optional name of a gradient field to extract. + :param weight: multiplicative weight (used by ScheduledLoss). + :param reduction: reduction mode for torch losses ("mean", "sum", etc.). + """ + self.target = name + self.gradient = gradient self.weight = weight - self.gradient_weights = gradient_weights - self.sliding_factor = sliding_factor - self.sliding_weights: Optional[Dict[str, TensorMap]] = None + self.reduction = reduction + self.loss_kwargs = {} + super().__init__() + + @abstractmethod + def compute( + self, + predictions: Dict[str, TensorMap], + targets: Dict[str, TensorMap], + extra_data: Optional[Any] = None, + ) -> torch.Tensor: + """ + Compute the loss. + + :param predictions: mapping from target names to :py:class:`TensorMap`. + :param targets: mapping from target names to :py:class:`TensorMap`. + :param extra_data: optional additional data (e.g., masks). + :return: scalar torch.Tensor representing the loss. + """ + ... def __call__( self, - predictions_tensor_map: TensorMap, - targets_tensor_map: TensorMap, - ) -> Tuple[torch.Tensor, Dict[str, Tuple[float, int]]]: - # Check that the two have the same metadata, except for the samples, - # which can be different due to batching, but must have the same size: - if predictions_tensor_map.keys != targets_tensor_map.keys: - raise ValueError( - "TensorMapLoss requires the two TensorMaps to have the same keys." - ) - for block_1, block_2 in zip( - predictions_tensor_map.blocks(), targets_tensor_map.blocks() - ): - if block_1.properties != block_2.properties: - raise ValueError( - "TensorMapLoss requires the two TensorMaps to have the same " - "properties." - ) - if block_1.components != block_2.components: - raise ValueError( - "TensorMapLoss requires the two TensorMaps to have the same " - "components." + predictions: Dict[str, TensorMap], + targets: Dict[str, TensorMap], + extra_data: Optional[Any] = None, + ) -> torch.Tensor: + """ + Alias to compute() for direct invocation. + """ + return self.compute(predictions, targets, extra_data) + + @classmethod + def from_config(cls, cfg: Dict[str, Any]) -> "LossInterface": + """ + Instantiate a loss from a config dict. + + :param cfg: keyword args matching the loss constructor. + :return: instance of a LossInterface subclass. + """ + return cls(**cfg) + + +# --- scheduler interface and implementations ------------------------------------------ + + +class WeightScheduler(ABC): + """ + Abstract interface for scheduling a weight for a :py:class:`LossInterface`. + """ + + initialized: bool = False + + @abstractmethod + def initialize( + self, loss_fn: LossInterface, targets: Dict[str, TensorMap] + ) -> float: + """ + Compute and return the initial weight. + + :param loss_fn: the base loss to initialize. + :param targets: mapping of target names to :py:class:`TensorMap`. + :return: initial weight as a float. + """ + + @abstractmethod + def update( + self, + loss_fn: LossInterface, + predictions: Dict[str, TensorMap], + targets: Dict[str, TensorMap], + ) -> float: + """ + Update and return the new weight after a batch. + + :param loss_fn: the base loss. + :param predictions: mapping of target names to :py:class:`TensorMap`. + :param targets: mapping of target names to :py:class:`TensorMap`. + :return: updated weight as a float. + """ + + +class EMAScheduler(WeightScheduler): + """ + Exponential moving average scheduler for loss weights. + """ + + EPSILON = 1e-6 + + def __init__(self, sliding_factor: Optional[float]) -> None: + """ + :param sliding_factor: factor in [0,1] for EMA (0 disables scheduling). + """ + self.sliding_factor = float(sliding_factor or 0.0) + self.current_weight = 1.0 + self.initialized = False + + def initialize( + self, loss_fn: LossInterface, targets: Dict[str, TensorMap] + ) -> float: + # If scheduling disabled, keep weight = 1.0 + if self.sliding_factor <= 0.0: + self.current_weight = 1.0 + else: + # Compute a baseline loss against a constant mean or zero-gradient map + target_name = loss_fn.target + gradient_name = getattr(loss_fn, "gradient", None) + tensor_map_for_target = targets[target_name] + + if gradient_name is None: + # Create a baseline TensorMap with all values = mean over samples + mean_tensor_map = mts.mean_over_samples( + tensor_map_for_target, tensor_map_for_target.sample_names ) - if len(block_1.samples) != len(block_2.samples): - raise ValueError( - "TensorMapLoss requires the two TensorMaps " - "to have the same number of samples." + baseline_tensor_map = TensorMap( + keys=tensor_map_for_target.keys, + blocks=[ + mts.TensorBlock( + samples=block.samples, + components=block.components, + properties=block.properties, + values=torch.ones_like(block.values) * mean_block.values, + ) + for block, mean_block in zip( + tensor_map_for_target, mean_tensor_map + ) + ], ) - for gradient_name in block_2.gradients_list(): - if len(block_1.gradient(gradient_name).samples) != len( - block_2.gradient(gradient_name).samples - ): - raise ValueError( - "TensorMapLoss requires the two TensorMaps " - "to have the same number of gradient samples." - ) - if ( - block_1.gradient(gradient_name).properties - != block_2.gradient(gradient_name).properties - ): - raise ValueError( - "TensorMapLoss requires the two TensorMaps " - "to have the same gradient properties." - ) - if ( - block_1.gradient(gradient_name).components - != block_2.gradient(gradient_name).components - ): - raise ValueError( - "TensorMapLoss requires the two TensorMaps " - "to have the same gradient components." - ) + else: + # Zero baseline for gradient-based losses + baseline_tensor_map = mts.zeros_like(tensor_map_for_target) - # First time the function is called: compute the sliding weights only - # from the targets (if they are enabled) - if self.sliding_factor is not None and self.sliding_weights is None: - self.sliding_weights = get_sliding_weights( - self.losses, - self.sliding_factor, - targets_tensor_map, + initial_loss_value = loss_fn.compute( + {target_name: tensor_map_for_target}, {target_name: baseline_tensor_map} ) + self.current_weight = float(initial_loss_value.clamp_min(self.EPSILON)) + + self.initialized = True + return self.current_weight - # Compute the loss: - loss = torch.zeros( - (), - dtype=predictions_tensor_map.block(0).values.dtype, - device=predictions_tensor_map.block(0).values.device, + def update( + self, + loss_fn: LossInterface, + predictions: Dict[str, TensorMap], + targets: Dict[str, TensorMap], + ) -> float: + # If scheduling disabled, return fixed weight + if self.sliding_factor <= 0.0: + return self.current_weight + + # Compute the instantaneous error + instantaneous_error = loss_fn.compute(predictions, targets).detach().item() + # EMA update + new_weight = ( + self.sliding_factor * self.current_weight + + (1.0 - self.sliding_factor) * instantaneous_error ) - for key in targets_tensor_map.keys: - block_1 = predictions_tensor_map.block(key) - block_2 = targets_tensor_map.block(key) - values_1 = block_1.values - values_2 = block_2.values - # sliding weights: default to 1.0 if not used/provided for this target - sliding_weight = ( - 1.0 - if self.sliding_weights is None - else self.sliding_weights.get("values", 1.0) - ) - loss += ( - self.weight * self.losses["values"](values_1, values_2) / sliding_weight - ) - for gradient_name in block_2.gradients_list(): - gradient_weight = self.gradient_weights[gradient_name] - values_1 = block_1.gradient(gradient_name).values - values_2 = block_2.gradient(gradient_name).values - # sliding weights: default to 1.0 if not used/provided for this target - sliding_weigths_value = ( - 1.0 - if self.sliding_weights is None - else self.sliding_weights.get(gradient_name, 1.0) - ) - loss += ( - gradient_weight - * self.losses[gradient_name](values_1, values_2) - / sliding_weigths_value - ) - if self.sliding_factor is not None: - self.sliding_weights = get_sliding_weights( - self.losses, - self.sliding_factor, - targets_tensor_map, - predictions_tensor_map, - self.sliding_weights, + self.current_weight = max(new_weight, self.EPSILON) + return self.current_weight + + +class ScheduledLoss(LossInterface): + """ + Wrap a base :py:class:`LossInterface` with a :py:class:`WeightScheduler`. + After each compute, the scheduler updates the loss weight. + """ + + def __init__(self, base_loss: LossInterface, weight_scheduler: WeightScheduler): + """ + :param base_loss: underlying LossInterface to wrap. + :param weight_scheduler: scheduler that controls the multiplier. + """ + super().__init__( + base_loss.target, + base_loss.gradient, + base_loss.weight, + base_loss.reduction, + ) + self.base_loss = base_loss + self.scheduler = weight_scheduler + self.loss_kwargs = getattr(base_loss, "loss_kwargs", {}) + + def compute( + self, + predictions: Dict[str, TensorMap], + targets: Dict[str, TensorMap], + extra_data: Optional[Any] = None, + ) -> torch.Tensor: + # Initialize scheduler on first call + if not self.scheduler.initialized: + self.normalization_factor = self.scheduler.initialize( + self.base_loss, targets ) - return loss + # compute the raw loss using the base loss function + raw_loss_value = self.base_loss.compute(predictions, targets, extra_data) + + # scale by the fixed weight and divide by the sliding weight + weighted_loss_value = raw_loss_value * ( + self.base_loss.weight / self.normalization_factor + ) + + # update the sliding weight + self.normalization_factor = self.scheduler.update( + self.base_loss, predictions, targets + ) -class TensorMapDictLoss: - """A loss function that operates on two ``Dict[str, metatensor.torch.TensorMap]``. + return weighted_loss_value - At initialization, the user specifies a list of keys to use for the loss, - along with a weight for each key. - The loss is then computed as a weighted sum. Any keys that are not present - in the dictionaries are ignored. +# --- specific losses ------------------------------------------------------------------ - :param weights: A dictionary mapping keys to weights. This might contain - gradient keys, in the form ``__gradients``. - :param sliding_factor: The factor to apply to the exponential moving average - of the "sliding" weights. These are weights that act on different components of - the loss (for example, energies and forces), based on their individual recent - history. If ``None``, no sliding weights are used in the computation of the - loss. - :param reduction: The reduction to apply to the loss. - See :py:class:`torch.nn.MSELoss`. - :returns: The loss as a zero-dimensional :py:class:`torch.Tensor` - (with one entry). +class BaseTensorMapLoss(LossInterface): + """ + Backbone for pointwise losses on :py:class:`TensorMap` entries. + + Provides a compute_flattened() helper that extracts values or gradients, + flattens them, applies an optional mask, and computes the torch loss. """ def __init__( self, - weights: Dict[str, float], - sliding_factor: Optional[float] = None, - reduction: str = "mean", - type: Union[str, dict] = "mse", + name: str, + gradient: Optional[str], + weight: float, + reduction: str, + *, + loss_fn: _Loss, ): - outputs = [key for key in weights.keys() if "gradients" not in key] - self.losses = {} - for output in outputs: - value_weight = weights[output] - gradient_weights = {} - for key, weight in weights.items(): - if key.startswith(output) and key.endswith("_gradients"): - gradient_name = key.replace(f"{output}_", "").replace( - "_gradients", "" - ) - gradient_weights[gradient_name] = weight - type_output = _process_type(type, output) - if output == "energy" and sliding_factor is not None: - self.losses[output] = TensorMapLoss( - reduction=reduction, - weight=value_weight, - gradient_weights=gradient_weights, - sliding_factor=sliding_factor, - type=type_output, - ) + """ + :param name: key in the predictions/targets dict. + :param gradient: optional gradient field name. + :param weight: dummy here; real weighting in ScheduledLoss. + :param reduction: reduction mode for torch loss. + :param loss_fn: pre-instantiated torch.nn loss (e.g. MSELoss). + """ + super().__init__(name, gradient, weight, reduction) + self.torch_loss = loss_fn + + def compute_flattened( + self, + tensor_map_predictions_for_target: TensorMap, + tensor_map_targets_for_target: TensorMap, + tensor_map_mask_for_target: Optional[TensorMap] = None, + ) -> torch.Tensor: + """ + Flatten prediction and target blocks (and optional mask), then + apply the torch loss. + + :param tensor_map_predictions_for_target: predicted :py:class:`TensorMap`. + :param tensor_map_targets_for_target: target :py:class:`TensorMap`. + :param tensor_map_mask_for_target: optional mask :py:class:`TensorMap`. + :return: scalar torch.Tensor of the computed loss. + """ + list_of_prediction_segments = [] + list_of_target_segments = [] + + def extract_flattened_values_from_block( + tensor_block: mts.TensorBlock, + ) -> torch.Tensor: + """ + Extract values or gradients from a block, flatten to 1D. + """ + if self.gradient is not None: + values = tensor_block.gradient(self.gradient).values else: - self.losses[output] = TensorMapLoss( - reduction=reduction, - weight=value_weight, - gradient_weights=gradient_weights, - type=type_output, - ) + values = tensor_block.values + return values.reshape(-1) - def __call__( + # Loop over each key in the TensorMap + for single_key in tensor_map_predictions_for_target.keys: + block_for_prediction = tensor_map_predictions_for_target.block(single_key) + block_for_target = tensor_map_targets_for_target.block(single_key) + + flattened_prediction = extract_flattened_values_from_block( + block_for_prediction + ) + flattened_target = extract_flattened_values_from_block(block_for_target) + + if tensor_map_mask_for_target is not None: + # Apply boolean mask if provided + block_for_mask = tensor_map_mask_for_target.block(single_key) + flattened_mask = extract_flattened_values_from_block( + block_for_mask + ).bool() + flattened_prediction = flattened_prediction[flattened_mask] + flattened_target = flattened_target[flattened_mask] + + list_of_prediction_segments.append(flattened_prediction) + list_of_target_segments.append(flattened_target) + + # Concatenate all segments and apply the torch loss + all_predictions_flattened = torch.cat(list_of_prediction_segments) + all_targets_flattened = torch.cat(list_of_target_segments) + return self.torch_loss(all_predictions_flattened, all_targets_flattened) + + def compute( self, - tensor_map_dict_1: Dict[str, TensorMap], - tensor_map_dict_2: Dict[str, TensorMap], + predictions: Dict[str, TensorMap], + targets: Dict[str, TensorMap], + extra_data: Optional[Any] = None, ) -> torch.Tensor: - # Assert that the two have the keys: - assert set(tensor_map_dict_1.keys()) == set(tensor_map_dict_2.keys()) + """ + Compute the unmasked pointwise loss. + + :param predictions: mapping of names to :py:class:`TensorMap`. + :param targets: mapping of names to :py:class:`TensorMap`. + :param extra_data: ignored for unmasked losses. + :return: scalar torch.Tensor loss. + """ + tensor_map_pred = predictions[self.target] + tensor_map_targ = targets[self.target] + + # Check gradients are present in the target TensorMap + if self.gradient is not None: + if self.gradient not in tensor_map_targ[0].gradients_list(): + # Skip loss computation if block gradient is missing in the dataset + # Tensor gradients are not tracked + return torch.zeros( + (), dtype=torch.float, device=tensor_map_targ[0].values.device + ) + return self.compute_flattened(tensor_map_pred, tensor_map_targ) + - # Initialize the loss: - first_values = next(iter(tensor_map_dict_1.values())).block(0).values - loss = torch.zeros((), dtype=first_values.dtype, device=first_values.device) +class MaskedTensorMapLoss(BaseTensorMapLoss): + """ + Pointwise masked loss on :py:class:`TensorMap` entries. + + Inherits flattening and torch-loss logic from BaseTensorMapLoss. + """ - # Compute the loss: - for target in tensor_map_dict_1.keys(): - target_loss = self.losses[target]( - tensor_map_dict_1[target], tensor_map_dict_2[target] + def compute( + self, + predictions: Dict[str, TensorMap], + targets: Dict[str, TensorMap], + extra_data: Optional[Dict[str, TensorMap]] = None, + ) -> torch.Tensor: + """ + Gather and flatten target and prediction blocks, then compute loss. + + :param predictions: Mapping from target names to TensorMaps. + :param targets: Mapping from target names to TensorMaps. + :param extra_data: Additional data for loss computation. Assumes that, for the + target ``name`` used in the constructor, there is a corresponding data field + ``name + "_mask"`` that contains the tensor to be used for masking. It + should have the same metadata as the target and prediction tensors. + :return: Scalar loss tensor. + """ + mask_key = f"{self.target}_mask" + if extra_data is None or mask_key not in extra_data: + raise ValueError( + f"Expected extra_data to contain TensorMap under '{mask_key}'" ) - loss += target_loss + tensor_map_pred = predictions[self.target] + tensor_map_targ = targets[self.target] + tensor_map_mask = extra_data[mask_key] + return self.compute_flattened(tensor_map_pred, tensor_map_targ, tensor_map_mask) + + +# ------------------------------------------------------------------------ +# Simple explicit subclasses for common pointwise losses +# ------------------------------------------------------------------------ + + +class TensorMapMSELoss(BaseTensorMapLoss): + """ + Unmasked mean-squared error on :py:class:`TensorMap` entries. + """ + + def __init__( + self, + name: str, + gradient: Optional[str], + weight: float, + reduction: str, + ): + super().__init__( + name, + gradient, + weight, + reduction, + loss_fn=torch.nn.MSELoss(reduction=reduction), + ) + + +class TensorMapMAELoss(BaseTensorMapLoss): + """ + Unmasked mean-absolute error on :py:class:`TensorMap` entries. + """ + + def __init__( + self, + name: str, + gradient: Optional[str], + weight: float, + reduction: str, + ): + super().__init__( + name, + gradient, + weight, + reduction, + loss_fn=torch.nn.L1Loss(reduction=reduction), + ) + + +class TensorMapHuberLoss(BaseTensorMapLoss): + """ + Unmasked Huber loss on :py:class:`TensorMap` entries. + + :param delta: threshold parameter for HuberLoss. + """ + + def __init__( + self, + name: str, + gradient: Optional[str], + weight: float, + reduction: str, + delta: float, + ): + super().__init__( + name, + gradient, + weight, + reduction, + loss_fn=torch.nn.HuberLoss(reduction=reduction, delta=delta), + ) + + +class TensorMapMaskedMSELoss(MaskedTensorMapLoss): + """ + Masked mean-squared error on :py:class:`TensorMap` entries. + """ + + def __init__( + self, + name: str, + gradient: Optional[str], + weight: float, + reduction: str, + ): + super().__init__( + name, + gradient, + weight, + reduction, + loss_fn=torch.nn.MSELoss(reduction=reduction), + ) - return loss +class TensorMapMaskedMAELoss(MaskedTensorMapLoss): + """ + Masked mean-absolute error on :py:class:`TensorMap` entries. + """ + + def __init__( + self, + name: str, + gradient: Optional[str], + weight: float, + reduction: str, + ): + super().__init__( + name, + gradient, + weight, + reduction, + loss_fn=torch.nn.L1Loss(reduction=reduction), + ) + + +class TensorMapMaskedHuberLoss(MaskedTensorMapLoss): + """ + Masked Huber loss on :py:class:`TensorMap` entries. -def get_sliding_weights( - losses: Dict[str, _Loss], - sliding_factor: float, - targets: TensorMap, - predictions: Optional[TensorMap] = None, - previous_sliding_weights: Optional[Dict[str, float]] = None, -) -> Dict[str, float]: + :param delta: threshold parameter for HuberLoss. """ - Compute the sliding weights for the loss function. - The sliding weights are computed as the absolute difference between the - predictions and the targets. + def __init__( + self, + name: str, + gradient: Optional[str], + weight: float, + reduction: str, + delta: float, + ): + super().__init__( + name, + gradient, + weight, + reduction, + loss_fn=torch.nn.HuberLoss(reduction=reduction, delta=delta), + ) + - :param predictions: The predictions. - :param targets: The targets. +# --- aggregator ----------------------------------------------------------------------- - :return: The sliding weights. + +class LossAggregator(LossInterface): + """ + Aggregate multiple :py:class:`LossInterface` terms with scheduled weights and + metadata. """ - sliding_weights = {} - if predictions is None: - for block in targets.blocks(): - values = block.values - sliding_weights["values"] = ( - losses["values"](values, values.mean() * torch.ones_like(values)) + 1e-6 + + def __init__( + self, targets: Dict[str, TargetInfo], config: Dict[str, Dict[str, Any]] + ): + """ + :param targets: mapping from target names to :py:class:`TargetInfo`. + :param config: per-target configuration dict. + """ + super().__init__(name="", gradient=None, weight=0.0, reduction="mean") + self.scheduled_losses: Dict[str, ScheduledLoss] = {} + self.metadata: Dict[str, Dict[str, Any]] = {} + + for target_name, target_info in targets.items(): + target_config = config[target_name] + + # Create main loss and its scheduler + base_loss = create_loss( + target_config["type"], + name=target_name, + gradient=None, + weight=target_config["weight"], + reduction=target_config["reduction"], + **{ + pname: pval + for pname, pval in target_config.items() + if pname + not in ( + "type", + "weight", + "reduction", + "sliding_factor", + "gradients", + ) + }, ) - for gradient_name, gradient_block in block.gradients(): - values = gradient_block.values - sliding_weights[gradient_name] = losses[gradient_name]( - values, torch.zeros_like(values) + ema_scheduler = EMAScheduler(target_config["sliding_factor"]) + scheduled_main_loss = ScheduledLoss(base_loss, ema_scheduler) + self.scheduled_losses[target_name] = scheduled_main_loss + self.metadata[target_name] = { + "type": target_config["type"], + "weight": base_loss.weight, + "reduction": base_loss.reduction, + "sliding_factor": target_config["sliding_factor"], + "gradients": {}, + } + for pname, pval in target_config.items(): + if pname not in ( + "type", + "weight", + "reduction", + "sliding_factor", + "gradients", + ): + self.metadata[target_name][pname] = pval + + # Create gradient-based losses + gradient_config = target_config["gradients"] + for gradient_name in target_info.layout[0].gradients_list(): + gradient_key = f"{target_name}_grad_{gradient_name}" + + gradient_specific_config = gradient_config[gradient_name] + + grad_loss = create_loss( + gradient_specific_config["type"], + name=target_name, + gradient=gradient_name, + weight=gradient_specific_config["weight"], + reduction=gradient_specific_config["reduction"], + **{ + pname: pval + for pname, pval in gradient_specific_config.items() + if pname + not in ( + "type", + "weight", + "reduction", + "sliding_factor", + "gradients", + ) + }, ) - elif predictions is not None: - if previous_sliding_weights is None: - raise RuntimeError( - "previous_sliding_weights must be provided if predictions is not None" + ema_scheduler_for_grad = EMAScheduler(target_config["sliding_factor"]) + scheduled_grad_loss = ScheduledLoss(grad_loss, ema_scheduler_for_grad) + self.scheduled_losses[gradient_key] = scheduled_grad_loss + self.metadata[target_name]["gradients"][gradient_name] = { + "type": gradient_specific_config["type"], + "weight": grad_loss.weight, + "reduction": grad_loss.reduction, + "sliding_factor": target_config["sliding_factor"], + } + for pname, pval in gradient_specific_config.items(): + if pname not in ( + "type", + "weight", + "reduction", + "sliding_factor", + "gradients", + ): + self.metadata[target_name]["gradients"][gradient_name][ + pname + ] = pval + + def compute( + self, + predictions: Dict[str, TensorMap], + targets: Dict[str, TensorMap], + extra_data: Optional[Any] = None, + ) -> torch.Tensor: + """ + Sum over all scheduled losses present in the predictions. + """ + # Initialize a zero tensor matching the dtype and device of the first block + first_tensor_map = next(iter(predictions.values())) + first_block = first_tensor_map.block(first_tensor_map.keys[0]) + total_loss = torch.zeros( + (), dtype=first_block.values.dtype, device=first_block.values.device + ) + + # Sum each scheduled term that has a matching prediction + for scheduled_term in self.scheduled_losses.values(): + if scheduled_term.target not in predictions: + continue + total_loss = total_loss + scheduled_term.compute( + predictions, targets, extra_data ) - else: - for predictions_block, target_block in zip( - predictions.blocks(), targets.blocks() - ): - target_values = target_block.values - predictions_values = predictions_block.values - sliding_weights["values"] = ( - sliding_factor * previous_sliding_weights["values"] - + (1 - sliding_factor) - * losses["values"](predictions_values, target_values).detach() - ) - for gradient_name, gradient_block in target_block.gradients(): - target_values = gradient_block.values - predictions_values = predictions_block.gradient( - gradient_name - ).values - sliding_weights[gradient_name] = ( - sliding_factor * previous_sliding_weights[gradient_name] - + (1 - sliding_factor) - * losses[gradient_name]( - predictions_values, target_values - ).detach() - ) - return sliding_weights - - -def _process_type(type: Union[str, DictConfig], output: str) -> Union[str, dict]: - if not isinstance(type, str): - assert "huber" in type - # we process the Huber loss delta dict to make it similar to the - # `weights` dict - type_output = {"huber": {"deltas": {}}} # type: ignore - for key, delta in type["huber"]["deltas"].items(): - key_internal = to_internal_name(key) - if key_internal == output: - type_output["huber"]["deltas"]["values"] = delta - elif key_internal.startswith(output) and key_internal.endswith( - "_gradients" - ): - gradient_name = key_internal.replace(f"{output}_", "").replace( - "_gradients", "" - ) - type_output["huber"]["deltas"][gradient_name] = delta - else: - pass - else: - type_output = type # type: ignore - return type_output + + return total_loss + + +class LossType(Enum): + """ + Enumeration of available loss types and their implementing classes. + """ + + MSE = ("mse", TensorMapMSELoss) + MAE = ("mae", TensorMapMAELoss) + HUBER = ("huber", TensorMapHuberLoss) + MASKED_MSE = ("masked_mse", TensorMapMaskedMSELoss) + MASKED_MAE = ("masked_mae", TensorMapMaskedMAELoss) + MASKED_HUBER = ("masked_huber", TensorMapMaskedHuberLoss) + POINTWISE = ("pointwise", BaseTensorMapLoss) + MASKED_POINTWISE = ("masked_pointwise", MaskedTensorMapLoss) + + def __init__(self, key: str, cls: Type[LossInterface]): + self._key = key + self._cls = cls + + @property + def key(self) -> str: + """String key for this loss type.""" + return self._key + + @property + def cls(self) -> Type[LossInterface]: + """Class implementing this loss type.""" + return self._cls + + @classmethod + def from_key(cls, key: str) -> "LossType": + """ + Look up a LossType by its string key. + + :raises ValueError: if the key is not valid. + """ + for loss_type in cls: + if loss_type.key == key: + return loss_type + valid_keys = ", ".join(loss_type.key for loss_type in cls) + raise ValueError(f"Unknown loss '{key}'. Valid types: {valid_keys}") + + +def create_loss( + loss_type: str, + *, + name: str, + gradient: Optional[str], + weight: float, + reduction: str, + **extra_kwargs: Any, +) -> LossInterface: + """ + Factory to instantiate a concrete :py:class:`LossInterface` given its string key. + + :param loss_type: string key matching one of the members of :py:class:`LossType`. + :param name: target name for the loss. + :param gradient: gradient name, if present. + :param weight: weight for the loss contribution. + :param reduction: reduction mode for the torch loss. + :param extra_kwargs: additional hyperparameters specific to the loss type. + :return: instance of the selected loss. + """ + loss_type_entry = LossType.from_key(loss_type) + try: + return loss_type_entry.cls( + name=name, + gradient=gradient, + weight=weight, + reduction=reduction, + **extra_kwargs, + ) + except TypeError as e: + raise TypeError(f"Error constructing loss '{loss_type}': {e}") from e diff --git a/src/metatrain/utils/old_loss.py b/src/metatrain/utils/old_loss.py new file mode 100644 index 000000000..726e3bab4 --- /dev/null +++ b/src/metatrain/utils/old_loss.py @@ -0,0 +1,348 @@ +from typing import Dict, Optional, Tuple, Union + +import torch +from metatensor.torch import TensorMap +from omegaconf import DictConfig +from torch.nn.modules.loss import _Loss + +from metatrain.utils.external_naming import to_internal_name + + +class TensorMapLoss: + """A loss function that operates on two ``metatensor.torch.TensorMap``. + + The loss is computed as the sum of the loss on the block values and + the loss on the gradients, with weights specified at initialization. + + At the moment, this loss function assumes that all the gradients + declared at initialization are present in both TensorMaps. + + :param reduction: The reduction to apply to the loss. + See :py:class:`torch.nn.MSELoss`. + :param weight: The weight to apply to the loss on the block values. + :param gradient_weights: The weights to apply to the loss on the gradients. + :param sliding_factor: The factor to apply to the exponential moving average + of the "sliding" weights. These are weights that act on different components of + the loss (for example, energies and forces), based on their individual recent + history. If ``None``, no sliding weights are used in the computation of the + loss. + :param type: The type of loss to use. This can be either "mse" or "mae". + A Huber loss can also be requested as a dictionary with the key "huber" and + the value must be a dictionary with the key "deltas" and the value + must be a dictionary with the keys "values" and the gradient keys. + The values of the dictionary must be the deltas to use for the + Huber loss. + + :returns: The loss as a zero-dimensional :py:class:`torch.Tensor` + (with one entry). + """ + + def __init__( + self, + reduction: str = "mean", + weight: float = 1.0, + gradient_weights: Optional[Dict[str, float]] = None, + sliding_factor: Optional[float] = None, + type: Union[str, dict] = "mse", + ): + if gradient_weights is None: + gradient_weights = {} + + losses = {} + if type == "mse": + losses["values"] = torch.nn.MSELoss(reduction=reduction) + for key in gradient_weights.keys(): + losses[key] = torch.nn.MSELoss(reduction=reduction) + elif type == "mae": + losses["values"] = torch.nn.L1Loss(reduction=reduction) + for key in gradient_weights.keys(): + losses[key] = torch.nn.L1Loss(reduction=reduction) + elif isinstance(type, dict) and "huber" in type: + # Huber loss + deltas = type["huber"]["deltas"] + losses["values"] = torch.nn.HuberLoss( + reduction=reduction, delta=deltas["values"] + ) + for key in gradient_weights.keys(): + losses[key] = torch.nn.HuberLoss(reduction=reduction, delta=deltas[key]) + else: + raise ValueError(f"Unknown loss type: {type}") + + self.losses = losses + self.weight = weight + self.gradient_weights = gradient_weights + self.sliding_factor = sliding_factor + self.sliding_weights: Optional[Dict[str, TensorMap]] = None + + def __call__( + self, + predictions_tensor_map: TensorMap, + targets_tensor_map: TensorMap, + ) -> Tuple[torch.Tensor, Dict[str, Tuple[float, int]]]: + # Check that the two have the same metadata, except for the samples, + # which can be different due to batching, but must have the same size: + if predictions_tensor_map.keys != targets_tensor_map.keys: + raise ValueError( + "TensorMapLoss requires the two TensorMaps to have the same keys." + ) + for block_1, block_2 in zip( + predictions_tensor_map.blocks(), targets_tensor_map.blocks() + ): + if block_1.properties != block_2.properties: + raise ValueError( + "TensorMapLoss requires the two TensorMaps to have the same " + "properties." + ) + if block_1.components != block_2.components: + raise ValueError( + "TensorMapLoss requires the two TensorMaps to have the same " + "components." + ) + if len(block_1.samples) != len(block_2.samples): + raise ValueError( + "TensorMapLoss requires the two TensorMaps " + "to have the same number of samples." + ) + for gradient_name in block_2.gradients_list(): + if len(block_1.gradient(gradient_name).samples) != len( + block_2.gradient(gradient_name).samples + ): + raise ValueError( + "TensorMapLoss requires the two TensorMaps " + "to have the same number of gradient samples." + ) + if ( + block_1.gradient(gradient_name).properties + != block_2.gradient(gradient_name).properties + ): + raise ValueError( + "TensorMapLoss requires the two TensorMaps " + "to have the same gradient properties." + ) + if ( + block_1.gradient(gradient_name).components + != block_2.gradient(gradient_name).components + ): + raise ValueError( + "TensorMapLoss requires the two TensorMaps " + "to have the same gradient components." + ) + + # First time the function is called: compute the sliding weights only + # from the targets (if they are enabled) + if self.sliding_factor is not None and self.sliding_weights is None: + self.sliding_weights = get_sliding_weights( + self.losses, + self.sliding_factor, + targets_tensor_map, + ) + + # Compute the loss: + loss = torch.zeros( + (), + dtype=predictions_tensor_map.block(0).values.dtype, + device=predictions_tensor_map.block(0).values.device, + ) + for key in targets_tensor_map.keys: + block_1 = predictions_tensor_map.block(key) + block_2 = targets_tensor_map.block(key) + values_1 = block_1.values + values_2 = block_2.values + # sliding weights: default to 1.0 if not used/provided for this target + sliding_weight = ( + 1.0 + if self.sliding_weights is None + else self.sliding_weights.get("values", 1.0) + ) + loss += ( + self.weight * self.losses["values"](values_1, values_2) / sliding_weight + ) + for gradient_name in block_2.gradients_list(): + gradient_weight = self.gradient_weights[gradient_name] + values_1 = block_1.gradient(gradient_name).values + values_2 = block_2.gradient(gradient_name).values + # sliding weights: default to 1.0 if not used/provided for this target + sliding_weigths_value = ( + 1.0 + if self.sliding_weights is None + else self.sliding_weights.get(gradient_name, 1.0) + ) + loss += ( + gradient_weight + * self.losses[gradient_name](values_1, values_2) + / sliding_weigths_value + ) + if self.sliding_factor is not None: + self.sliding_weights = get_sliding_weights( + self.losses, + self.sliding_factor, + targets_tensor_map, + predictions_tensor_map, + self.sliding_weights, + ) + return loss + + +class TensorMapDictLoss: + """A loss function that operates on two ``Dict[str, metatensor.torch.TensorMap]``. + + At initialization, the user specifies a list of keys to use for the loss, + along with a weight for each key. + + The loss is then computed as a weighted sum. Any keys that are not present + in the dictionaries are ignored. + + :param weights: A dictionary mapping keys to weights. This might contain + gradient keys, in the form ``__gradients``. + :param sliding_factor: The factor to apply to the exponential moving average + of the "sliding" weights. These are weights that act on different components of + the loss (for example, energies and forces), based on their individual recent + history. If ``None``, no sliding weights are used in the computation of the + loss. + :param reduction: The reduction to apply to the loss. + See :py:class:`torch.nn.MSELoss`. + + :returns: The loss as a zero-dimensional :py:class:`torch.Tensor` + (with one entry). + """ + + def __init__( + self, + weights: Dict[str, float], + sliding_factor: Optional[float] = None, + reduction: str = "mean", + type: Union[str, dict] = "mse", + ): + outputs = [key for key in weights.keys() if "gradients" not in key] + self.losses = {} + for output in outputs: + value_weight = weights[output] + gradient_weights = {} + for key, weight in weights.items(): + if key.startswith(output) and key.endswith("_gradients"): + gradient_name = key.replace(f"{output}_", "").replace( + "_gradients", "" + ) + gradient_weights[gradient_name] = weight + type_output = _process_type(type, output) + if output == "energy" and sliding_factor is not None: + self.losses[output] = TensorMapLoss( + reduction=reduction, + weight=value_weight, + gradient_weights=gradient_weights, + sliding_factor=sliding_factor, + type=type_output, + ) + else: + self.losses[output] = TensorMapLoss( + reduction=reduction, + weight=value_weight, + gradient_weights=gradient_weights, + type=type_output, + ) + + def __call__( + self, + tensor_map_dict_1: Dict[str, TensorMap], + tensor_map_dict_2: Dict[str, TensorMap], + ) -> torch.Tensor: + # Assert that the two have the keys: + assert set(tensor_map_dict_1.keys()) == set(tensor_map_dict_2.keys()) + + # Initialize the loss: + first_values = next(iter(tensor_map_dict_1.values())).block(0).values + loss = torch.zeros((), dtype=first_values.dtype, device=first_values.device) + + # Compute the loss: + for target in tensor_map_dict_1.keys(): + target_loss = self.losses[target]( + tensor_map_dict_1[target], tensor_map_dict_2[target] + ) + loss += target_loss + + return loss + + +def get_sliding_weights( + losses: Dict[str, _Loss], + sliding_factor: float, + targets: TensorMap, + predictions: Optional[TensorMap] = None, + previous_sliding_weights: Optional[Dict[str, float]] = None, +) -> Dict[str, float]: + """ + Compute the sliding weights for the loss function. + + The sliding weights are computed as the absolute difference between the + predictions and the targets. + + :param predictions: The predictions. + :param targets: The targets. + + :return: The sliding weights. + """ + sliding_weights = {} + if predictions is None: + for block in targets.blocks(): + values = block.values + sliding_weights["values"] = ( + losses["values"](values, values.mean() * torch.ones_like(values)) + 1e-6 + ) + for gradient_name, gradient_block in block.gradients(): + values = gradient_block.values + sliding_weights[gradient_name] = losses[gradient_name]( + values, torch.zeros_like(values) + ) + elif predictions is not None: + if previous_sliding_weights is None: + raise RuntimeError( + "previous_sliding_weights must be provided if predictions is not None" + ) + else: + for predictions_block, target_block in zip( + predictions.blocks(), targets.blocks() + ): + target_values = target_block.values + predictions_values = predictions_block.values + sliding_weights["values"] = ( + sliding_factor * previous_sliding_weights["values"] + + (1 - sliding_factor) + * losses["values"](predictions_values, target_values).detach() + ) + for gradient_name, gradient_block in target_block.gradients(): + target_values = gradient_block.values + predictions_values = predictions_block.gradient( + gradient_name + ).values + sliding_weights[gradient_name] = ( + sliding_factor * previous_sliding_weights[gradient_name] + + (1 - sliding_factor) + * losses[gradient_name]( + predictions_values, target_values + ).detach() + ) + return sliding_weights + + +def _process_type(type: Union[str, DictConfig], output: str) -> Union[str, dict]: + if not isinstance(type, str): + assert "huber" in type + # we process the Huber loss delta dict to make it similar to the + # `weights` dict + type_output = {"huber": {"deltas": {}}} # type: ignore + for key, delta in type["huber"]["deltas"].items(): + key_internal = to_internal_name(key) + if key_internal == output: + type_output["huber"]["deltas"]["values"] = delta + elif key_internal.startswith(output) and key_internal.endswith( + "_gradients" + ): + gradient_name = key_internal.replace(f"{output}_", "").replace( + "_gradients", "" + ) + type_output["huber"]["deltas"][gradient_name] = delta + else: + pass + else: + type_output = type # type: ignore + return type_output diff --git a/src/metatrain/utils/omegaconf.py b/src/metatrain/utils/omegaconf.py index 4c6d72db0..1f5b282bc 100644 --- a/src/metatrain/utils/omegaconf.py +++ b/src/metatrain/utils/omegaconf.py @@ -56,15 +56,19 @@ def default_precision(_root_: BaseContainer) -> int: ) -def default_random_seed() -> int: - """Return session seed in the range [0, 2**32).""" - return RANDOM_SEED +def default_huber_loss_delta() -> float: + """Return the default delta for the huber loss.""" + return 1.0 # Register custom resolvers OmegaConf.register_new_resolver("default_device", default_device) OmegaConf.register_new_resolver("default_precision", default_precision) -OmegaConf.register_new_resolver("default_random_seed", default_random_seed) +OmegaConf.register_new_resolver("default_random_seed", lambda: RANDOM_SEED) +OmegaConf.register_new_resolver("default_loss_type", lambda: "mse") +OmegaConf.register_new_resolver("default_loss_reduction", lambda: "mean") +OmegaConf.register_new_resolver("default_loss_sliding_factor", lambda: None) +OmegaConf.register_new_resolver("default_loss_weight", lambda: 1.0) def _resolve_single_str(config: str) -> DictConfig: @@ -124,6 +128,16 @@ def _resolve_single_str(config: str) -> DictConfig: } ) +CONF_LOSS = OmegaConf.create( + { + "type": "${default_loss_type:}", + "weight": "${default_loss_weight:}", + "reduction": "${default_loss_reduction:}", + "sliding_factor": "${default_loss_sliding_factor:}", + "gradients": {}, + } +) + KNOWN_GRADIENTS = list(CONF_GRADIENTS.keys()) # Merge configs to get default configs for energies and other targets @@ -363,6 +377,153 @@ def expand_dataset_config(conf: Union[str, DictConfig, ListConfig]) -> ListConfi return conf +def expand_loss_config(conf: DictConfig) -> DictConfig: + """Expand the loss configuration to a list of configurations. + + :param conf: The loss configuration to expand. + :returns: A list of expanded loss configurations. + """ + + training_confs = conf["training_set"] + + if not isinstance(training_confs, ListConfig): + training_confs = OmegaConf.create([training_confs]) + + # initialize + loss_dict: dict = {} + conf_loss = CONF_LOSS.copy() + OmegaConf.resolve(conf_loss) + train_on_forces = False + train_on_stress_or_virial = False + + # fill loss_dict with default values + for tc in training_confs: + for target_name, opts in tc["targets"].items(): + if target_name == "energy": + f, s = _process_energy(loss_dict, opts, conf_loss) + train_on_forces |= f + train_on_stress_or_virial |= s + else: + loss_dict[target_name] = conf_loss.copy() + + train_hypers = conf["architecture"]["training"] + if "loss" not in train_hypers: + # Use default loss configuration + train_hypers["loss"] = OmegaConf.create(loss_dict) + else: + # Expand str -> DictConfig + if isinstance(train_hypers["loss"], str): + # TODO: add test + # the string must be the loss type, which is going to be used + # for all targets + for t in loss_dict.keys(): + loss_dict[t]["type"] = train_hypers["loss"] + if train_hypers["loss"] == "huber": + loss_dict[t]["delta"] = default_huber_loss_delta() + train_hypers["loss"] = OmegaConf.create(loss_dict) + + else: + # Expand per-target str loss configurations + for t in loss_dict.keys(): + if t in train_hypers["loss"]: + if isinstance(train_hypers["loss"][t], str): + train_hypers["loss"][t] = {"type": train_hypers["loss"][t]} + if train_hypers["loss"][t]["type"] == "huber": + train_hypers["loss"][t]["delta"] = ( + default_huber_loss_delta() + ) + + # Adapt the loss configuration to the internal structure + if train_on_forces: + _migrate_gradient_key(train_hypers["loss"], "forces", "positions") + else: + if "forces" in train_hypers["loss"]: + del train_hypers["loss"]["forces"] + + if train_on_stress_or_virial: + for legacy in ["stress", "virial"]: + _migrate_gradient_key(train_hypers["loss"], legacy, "strain") + else: + if "stress" in train_hypers["loss"]: + del train_hypers["loss"]["stress"] + if "virial" in train_hypers["loss"]: + del train_hypers["loss"]["virial"] + + # Add default delta for huber loss if not present + for t in train_hypers["loss"].keys(): + if "type" in train_hypers["loss"][t]: + if train_hypers["loss"][t]["type"] == "huber": + if "delta" not in train_hypers["loss"][t]: + train_hypers["loss"][t]["delta"] = ( + default_huber_loss_delta() + ) + if "gradients" in train_hypers["loss"][t]: + for grad_key in train_hypers["loss"][t]["gradients"].keys(): + if "type" in train_hypers["loss"][t]["gradients"][grad_key]: + if ( + train_hypers["loss"][t]["gradients"][grad_key]["type"] + == "huber" + ): + if ( + "delta" + not in train_hypers["loss"][t]["gradients"][ + grad_key + ] + ): + train_hypers["loss"][t]["gradients"][grad_key][ + "delta" + ] = default_huber_loss_delta() + + train_hypers["loss"] = OmegaConf.merge(loss_dict, train_hypers["loss"]) + + conf["architecture"]["training"] = train_hypers + return conf + + +def _migrate_gradient_key(loss_dict: dict, old_key: str, grad_key: str): + """ + If `old_key` exists in `loss_dict`, move it under + loss_dict['energy']['gradients'][grad_key], creating the necessary nested dicts + along the way. + """ + if old_key in loss_dict: + if "energy" not in loss_dict: + loss_dict["energy"] = {} + if "gradients" not in loss_dict["energy"]: + loss_dict["energy"]["gradients"] = {} + loss_dict["energy"]["gradients"][grad_key] = loss_dict[old_key] + del loss_dict[old_key] + + +def _process_energy( + loss_dict: dict, + opts: dict, + template: dict, +) -> tuple[bool, bool]: + """ + Ensure `loss_dict["energy"]` exists, reset its gradients, and add 'positions' / + 'strain' entries if requested by opts. + Returns (added_forces, added_strain) bools. + """ + if "energy" not in loss_dict: + loss_dict["energy"] = template.copy() + # start with an empty gradients dict each time + loss_dict["energy"]["gradients"] = {} + + added_forces = False + added_strain = False + + if opts.get("forces", False): + loss_dict["energy"]["gradients"]["positions"] = template.copy() + added_forces = True + + if opts.get("stress", False) or opts.get("virial", False): + loss_dict["energy"]["gradients"]["strain"] = template.copy() + added_strain = True + + return added_forces, added_strain + + def check_units( actual_options: Union[DictConfig, ListConfig], desired_options: Union[DictConfig, ListConfig], diff --git a/src/metatrain/utils/testing/checkpoints.py b/src/metatrain/utils/testing/checkpoints.py index d38878c85..10d955586 100644 --- a/src/metatrain/utils/testing/checkpoints.py +++ b/src/metatrain/utils/testing/checkpoints.py @@ -1,6 +1,7 @@ import glob import gzip import os +from pathlib import Path import pytest import torch @@ -34,21 +35,25 @@ def checkpoint_did_not_change(monkeypatch, tmp_path, model_trainer): checkpoint = torch.load("checkpoint.ckpt", weights_only=False) monkeypatch.chdir(cwd) - version = model.__checkpoint_version__ - if not os.path.exists(f"checkpoints/v{version}.ckpt.gz"): - with gzip.open(f"v{version}.ckpt.gz", "wb") as output: + model_version = model.__checkpoint_version__ + trainer_version = trainer.__checkpoint_version__ + + filename = Path( + f"checkpoints/model-v{model_version}-trainer-v{trainer_version}.ckpt.gz" + ) + if not filename.exists(): + with gzip.open(filename, "wb") as output: with open(os.path.join(tmp_path, "checkpoint.ckpt"), "rb") as input: output.write(input.read()) raise ValueError( - f"missing reference checkpoint for v{version}, " - "we created one for you with the current state of the code. " - f"Please move it to `checkpoints/v{version}.ckpt.gz` if you " - "have no other changes to do" + f"missing reference checkpoint for {filename.name}, we created one for you" + "with the current state of the code. " + f"Please move it to `{filename}` if you have no other changes to do" ) else: - with gzip.open(f"checkpoints/v{version}.ckpt.gz", "rb") as fd: + with gzip.open(filename, "rb") as fd: reference = torch.load(fd, weights_only=False) try: diff --git a/src/metatrain/utils/transfer.py b/src/metatrain/utils/transfer.py index 91b96dce6..8b2e6d008 100644 --- a/src/metatrain/utils/transfer.py +++ b/src/metatrain/utils/transfer.py @@ -28,9 +28,15 @@ def batch_to( key: value.to(dtype=dtype, device=device) for key, value in targets.items() } if extra_data is not None: + new_dtypes: List[Optional[int]] = [] + for key in extra_data.keys(): + if key.endswith("_mask"): # masks should always be boolean + new_dtypes.append(torch.bool) + else: + new_dtypes.append(dtype) extra_data = { - key: value.to(dtype=dtype, device=device) - for key, value in extra_data.items() + key: value.to(dtype=_dtype, device=device) + for (key, value), _dtype in zip(extra_data.items(), new_dtypes) } return systems, targets, extra_data diff --git a/tests/utils/test_io.py b/tests/utils/test_io.py index a21b1c9a9..f3edaf152 100644 --- a/tests/utils/test_io.py +++ b/tests/utils/test_io.py @@ -88,8 +88,9 @@ def test_load_trainer_checkpoint_wrong_version(monkeypatch, tmp_path): message = ( "Unable to load the trainer checkpoint " "for the 'soap_bpnn' architecture: the checkpoint is using version 5000000, " - "while the current version is 1; and trying to upgrade the checkpoint failed." + "while the current version is 2; and trying to upgrade the checkpoint failed." ) + with pytest.raises(RuntimeError, match=message): checkpoint = torch.load(file, weights_only=False, map_location="cpu") trainer_from_checkpoint(checkpoint, context="restart", hypers={}) diff --git a/tests/utils/test_llpr.py b/tests/utils/test_llpr.py index 88b4e8eb4..416302c3a 100644 --- a/tests/utils/test_llpr.py +++ b/tests/utils/test_llpr.py @@ -25,6 +25,7 @@ get_requested_neighbor_lists, get_system_with_neighbor_lists, ) +from metatrain.utils.omegaconf import CONF_LOSS from . import RESOURCES_PATH @@ -297,6 +298,9 @@ def test_llpr_finetuning(tmpdir): }, } + hypers["training"]["loss"] = {"energy": CONF_LOSS} + hypers["training"]["loss"]["energy"]["gradients"] = {"positions": CONF_LOSS} + trainer = Trainer(hypers["training"]) trainer.train( model=model, diff --git a/tests/utils/test_loss.py b/tests/utils/test_loss.py index 3bb572207..5561e9524 100644 --- a/tests/utils/test_loss.py +++ b/tests/utils/test_loss.py @@ -1,10 +1,26 @@ +# tests/test_losses.py + from pathlib import Path +import metatensor.torch as mts import pytest import torch from metatensor.torch import Labels, TensorBlock, TensorMap -from metatrain.utils.loss import TensorMapDictLoss, TensorMapLoss +from metatrain.utils.data import TargetInfo +from metatrain.utils.loss import ( + EMAScheduler, + LossAggregator, + LossType, + TensorMapHuberLoss, + TensorMapMAELoss, + TensorMapMaskedHuberLoss, + TensorMapMaskedMAELoss, + TensorMapMaskedMSELoss, + TensorMapMSELoss, + create_loss, +) +from metatrain.utils.old_loss import TensorMapLoss RESOURCES_PATH = Path(__file__).parents[1] / "resources" @@ -14,12 +30,12 @@ def tensor_map_with_grad_1(): block = TensorBlock( values=torch.tensor([[1.0], [2.0], [3.0]]), - samples=Labels.range("samples", 3), + samples=Labels.range("sample", 3), components=[], properties=Labels("energy", torch.tensor([[0]])), ) block.add_gradient( - "gradient", + "positions", TensorBlock( values=torch.tensor([[1.0], [2.0], [3.0]]), samples=Labels.range("sample", 3), @@ -35,12 +51,12 @@ def tensor_map_with_grad_1(): def tensor_map_with_grad_2(): block = TensorBlock( values=torch.tensor([[1.0], [1.0], [3.0]]), - samples=Labels.range("samples", 3), + samples=Labels.range("sample", 3), components=[], properties=Labels("energy", torch.tensor([[0]])), ) block.add_gradient( - "gradient", + "positions", TensorBlock( values=torch.tensor([[1.0], [0.0], [3.0]]), samples=Labels.range("sample", 3), @@ -56,12 +72,12 @@ def tensor_map_with_grad_2(): def tensor_map_with_grad_3(): block = TensorBlock( values=torch.tensor([[0.0], [1.0], [3.0]]), - samples=Labels.range("samples", 3), + samples=Labels.range("sample", 3), components=[], properties=Labels("energy", torch.tensor([[0]])), ) block.add_gradient( - "gradient", + "positions", TensorBlock( values=torch.tensor([[1.0], [0.0], [3.0]]), samples=Labels.range("sample", 3), @@ -77,12 +93,12 @@ def tensor_map_with_grad_3(): def tensor_map_with_grad_4(): block = TensorBlock( values=torch.tensor([[0.0], [1.0], [3.0]]), - samples=Labels.range("samples", 3), + samples=Labels.range("sample", 3), components=[], properties=Labels("energy", torch.tensor([[0]])), ) block.add_gradient( - "gradient", + "positions", TensorBlock( values=torch.tensor([[1.0], [0.0], [2.0]]), samples=Labels.range("sample", 3), @@ -94,113 +110,379 @@ def tensor_map_with_grad_4(): return tensor_map -@pytest.mark.parametrize("type", ["mse", {"huber": {"deltas": {"values": 3.0}}}]) -def test_tmap_loss_no_gradients(type): - """Test that the loss is computed correctly when there are no gradients.""" - loss = TensorMapLoss(type=type, reduction="sum") - - tensor_map_1 = TensorMap( - keys=Labels.single(), - blocks=[ - TensorBlock( - values=torch.tensor([[1.0], [2.0], [3.0]]), - samples=Labels.range("samples", 3), - components=[], - properties=Labels("energy", torch.tensor([[0]])), - ) - ], +@pytest.fixture +def tensor_map_with_grad_1_with_strain(): + block = TensorBlock( + values=torch.tensor([[0.0], [1.0], [3.0]]), + samples=Labels.range("sample", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ) + block.add_gradient( + "positions", + TensorBlock( + values=torch.tensor([[1.0], [0.0], [3.0]]), + samples=Labels.range("sample", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ), ) - tensor_map_2 = TensorMap( - keys=Labels.single(), - blocks=[ - TensorBlock( - values=torch.tensor([[0.0], [2.0], [3.0]]), - samples=Labels.range("samples", 3), - components=[], - properties=Labels("energy", torch.tensor([[0]])), - ) - ], + block.add_gradient( + "strain", + TensorBlock( + values=torch.tensor([[1.0], [0.0], [3.0]]), + samples=Labels.range("sample", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ), ) + tensor_map = TensorMap(keys=Labels.single(), blocks=[block]) + return tensor_map - loss_value = loss(tensor_map_1, tensor_map_1) - torch.testing.assert_close(loss_value, torch.tensor(0.0)) - # Expected result: 1.0 - loss_value = loss(tensor_map_1, tensor_map_2) - # Huber loss is scaled by 0.5 due to torch implementation - torch.testing.assert_close( - loss_value, (1.0 if type == "mse" else 0.5) * torch.tensor(1.0) +@pytest.fixture +def tensor_map_with_grad_3_with_strain(): + block = TensorBlock( + values=torch.tensor([[0.0], [1.0], [3.0]]), + samples=Labels.range("sample", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), ) + block.add_gradient( + "positions", + TensorBlock( + values=torch.tensor([[1.0], [0.0], [3.0]]), + samples=Labels.range("sample", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ), + ) + block.add_gradient( + "strain", + TensorBlock( + values=torch.tensor([[1.0], [0.0], [3.0]]), + samples=Labels.range("sample", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ), + ) + tensor_map = TensorMap(keys=Labels.single(), blocks=[block]) + return tensor_map +# Pointwise losses must return zero when predictions == targets @pytest.mark.parametrize( - "type", ["mse", {"huber": {"deltas": {"values": 3.0, "gradient": 3.0}}}] + "LossCls", + [ + TensorMapMSELoss, + TensorMapMAELoss, + TensorMapHuberLoss, + ], ) -def test_tmap_loss_with_gradients(tensor_map_with_grad_1, tensor_map_with_grad_2, type): - """Test that the loss is computed correctly when there are gradients.""" - loss = TensorMapLoss(type=type, gradient_weights={"gradient": 0.5}, reduction="sum") - - loss_value = loss(tensor_map_with_grad_1, tensor_map_with_grad_1) - torch.testing.assert_close(loss_value, torch.tensor(0.0)) - - # Expected result: 1.0 + 0.5 * 4.0 - loss_value = loss(tensor_map_with_grad_1, tensor_map_with_grad_2) - torch.testing.assert_close( - loss_value, - # Huber loss is scaled by 0.5 due to torch implementation - (1.0 if type == "mse" else 0.5) * torch.tensor(1.0 + 0.5 * 4.0), +def test_pointwise_zero_loss(tensor_map_with_grad_1, LossCls): + tm = tensor_map_with_grad_1 + key = tm.keys.names[0] + if LossCls == TensorMapHuberLoss: + loss = LossCls(name=key, gradient=None, weight=1.0, reduction="mean", delta=1.0) + else: + loss = LossCls(name=key, gradient=None, weight=1.0, reduction="mean") + pred = {key: tm} + targ = {key: tm} + assert loss(pred, targ).item() == pytest.approx(0.0) + + +# Check consistency between old and new loss implementations +def test_check_old_and_new_loss_consistency(tensor_map_with_grad_2): + tensor_1 = mts.remove_gradients(tensor_map_with_grad_2) + tensor_2 = mts.random_uniform_like(tensor_1) + loss_fn_1 = TensorMapLoss() + loss_fn_2 = TensorMapMSELoss( + name="", + gradient=None, + weight=1.0, + reduction="mean", ) + assert loss_fn_1(tensor_1, tensor_2) == loss_fn_2({"": tensor_1}, {"": tensor_2}) -def test_tmap_dict_loss( - tensor_map_with_grad_1, - tensor_map_with_grad_2, - tensor_map_with_grad_3, - tensor_map_with_grad_4, +# Masked losses must error if no mask is supplied +@pytest.mark.parametrize( + "MaskedCls", + [ + TensorMapMaskedMSELoss, + TensorMapMaskedMAELoss, + TensorMapMaskedHuberLoss, + ], +) +def test_masked_loss_error_on_missing_mask(tensor_map_with_grad_1, MaskedCls): + tm = tensor_map_with_grad_1 + key = tm.keys.names[0] + if MaskedCls == TensorMapMaskedHuberLoss: + loss = MaskedCls( + name=key, gradient=None, weight=1.0, reduction="mean", delta=1.0 + ) + else: + loss = MaskedCls(name=key, gradient=None, weight=1.0, reduction="mean") + with pytest.raises(ValueError): + loss({key: tm}, {key: tm}) + + +# Functional test for masked MSE: only unmasked element contributes +def test_masked_mse_behavior(tensor_map_with_grad_1, tensor_map_with_grad_2): + tm1 = tensor_map_with_grad_1 + tm2 = tensor_map_with_grad_2 + key = tm1.keys.names[0] + + # Construct a mask TensorMap: only index 1 is True + mask_vals = torch.tensor([[False], [True], [False]], dtype=torch.bool) + mask_block = TensorBlock( + values=mask_vals, + samples=tm1.block(0).samples, + components=tm1.block(0).components, + properties=tm1.block(0).properties, + ) + mask_map = TensorMap(keys=tm1.keys, blocks=[mask_block]) + extra_data = {f"{key}_mask": mask_map} + + loss = TensorMapMaskedMSELoss(name=key, gradient=None, weight=1.0, reduction="mean") + # Only element 1 contributes: (1-2)^2 = 1 + result = loss({key: tm2}, {key: tm1}, extra_data) + assert result.item() == pytest.approx(1.0) + + +# EMA scheduler: test both no-sliding and sliding-factor cases +@pytest.mark.parametrize( + "sf, expected_init, expected_update", + [ + (0.0, 1.0, 1.0), + (0.5, 2 / 3, (2 / 3) * 0.5), + ], +) +def test_ema_scheduler( + tensor_map_with_grad_1, tensor_map_with_grad_2, sf, expected_init, expected_update ): - """Test that the dict loss is computed correctly.""" - - loss_rmse = TensorMapDictLoss( - weights={ - "output_1": 0.6, - "output_2": 1.0, - "output_1_gradient_gradients": 0.5, - "output_2_gradient_gradients": 0.5, - }, - reduction="sum", + tm1 = tensor_map_with_grad_1 + tm2 = tensor_map_with_grad_2 + key = tm1.keys.names[0] + loss = TensorMapMSELoss(name=key, gradient=None, weight=1.0, reduction="mean") + sched = EMAScheduler(sliding_factor=sf) + + init_w = sched.initialize(loss, {key: tm1}) + assert init_w == pytest.approx(expected_init) + + new_w = sched.update(loss, {key: tm2}, {key: tm2}) + assert new_w == pytest.approx(expected_update) + + +# Factory and enum resolution +def test_loss_type_and_factory(): + mapping = { + "mse": TensorMapMSELoss, + "mae": TensorMapMAELoss, + "huber": TensorMapHuberLoss, + "masked_mse": TensorMapMaskedMSELoss, + "masked_mae": TensorMapMaskedMAELoss, + "masked_huber": TensorMapMaskedHuberLoss, + } + for key, cls in mapping.items(): + # LossType.from_key should return enum with .key + lt = LossType.from_key(key) + assert lt.key == key + # Factory should produce correct class + extra_kwargs = {} + if key == "huber" or key == "masked_huber": + extra_kwargs = {"delta": 1.0} + loss = create_loss( + key, + name="dummy", + gradient=None, + weight=1.0, + reduction="mean", + **extra_kwargs, + ) + assert isinstance(loss, cls) + + # Invalid keys raise ValueError + with pytest.raises(ValueError): + LossType.from_key("invalid_key") + with pytest.raises(ValueError): + create_loss( + "invalid_key", + name="dummy", + gradient=None, + weight=1.0, + reduction="mean", + ) + + +# Point-wise gradient-only +@pytest.mark.parametrize( + "LossCls, expected", + [ + (TensorMapMSELoss, 1 / 3), # MSEGradient: one error squared -> 1/3 + (TensorMapMAELoss, 1 / 3), # MAEGradient: one abs error -> 1/3 + (TensorMapHuberLoss, 1 / 6), # HuberGradient: 0.5*1^2 /3 = 1/6 + ], +) +def test_pointwise_gradient_loss( + tensor_map_with_grad_3, tensor_map_with_grad_4, LossCls, expected +): + tm3 = tensor_map_with_grad_3 + tm4 = tensor_map_with_grad_4 + key = tm3.keys.names[0] + # instantiate with gradient extraction + if LossCls == TensorMapHuberLoss: + loss = LossCls( + name=key, gradient="positions", weight=1.0, reduction="mean", delta=1.0 + ) + else: + loss = LossCls(name=key, gradient="positions", weight=1.0, reduction="mean") + val = loss({key: tm3}, {key: tm4}).item() + assert val == pytest.approx(expected) + + +def test_create_loss_invalid_kwargs(): + # Passing `foo` into an MSELoss constructor will cause + # a TypeError inside create_loss, which should be caught + # and re-raised with our custom message. + with pytest.raises(TypeError) as exc: + create_loss( + "mse", name="dummy", gradient=None, weight=1.0, reduction="mean", foo=123 + ) + msg = str(exc.value) + assert "Error constructing loss 'mse'" in msg + assert ( + "foo" in msg + ) # original constructor error should mention the unexpected 'foo' + + +def test_masked_pointwise_gradient_branch( + tensor_map_with_grad_3, tensor_map_with_grad_4 +): + tm3 = tensor_map_with_grad_3 + tm4 = tensor_map_with_grad_4 + key = tm3.keys.names[0] + + # Build a mask that selects all entries + mask_vals = torch.tensor([[True], [True], [True]], dtype=torch.bool) + mask_block = TensorBlock( + values=mask_vals, + samples=tm3.block(0).samples, + components=tm3.block(0).components, + properties=tm3.block(0).properties, + ) + + # Add a gradient-block to the mask, so grab(mask_block, "gradient") works + grad_block_for_mask = TensorBlock( + values=mask_vals, + samples=tm3.block(0).samples, + components=tm3.block(0).components, + properties=tm3.block(0).properties, ) - loss_huber = TensorMapDictLoss( - weights={ - "output_1": 0.6, - "output_2": 1.0, - "output_1_gradient_gradients": 0.5, - "output_2_gradient_gradients": 0.5, + mask_block.add_gradient("positions", grad_block_for_mask) + + mask_map = TensorMap(keys=tm3.keys, blocks=[mask_block]) + extra = {f"{key}_mask": mask_map} + + # Create the masked-pointwise loss on the 'positions' channel + loss = TensorMapMaskedMSELoss( + name=key, gradient="positions", weight=1.0, reduction="mean" + ) + + # The gradient values in tm3: [1, 0, 3]; in tm4: [1, 0, 2] + # Only one difference of 1 -> MSE mean = 1/3 + result = loss({key: tm3}, {key: tm4}, extra).item() + assert result == pytest.approx(1 / 3) + + +def test_ema_initialize_gradient_branch(tensor_map_with_grad_1): + tm = tensor_map_with_grad_1 + key = tm.keys.names[0] + + # gradient block values [1,2,3], zero baseline -> MSE = (1+4+9)/3 + loss = TensorMapMSELoss( + name=key, gradient="positions", weight=1.0, reduction="mean" + ) + sched = EMAScheduler(sliding_factor=0.5) + init_w = sched.initialize(loss, {key: tm}) + + assert init_w == pytest.approx((1 + 4 + 9) / 3) + + +def test_tmap_loss_subset(tensor_map_with_grad_1, tensor_map_with_grad_3): + """Test that the loss is computed correctly when only a subset + of the possible targets is present both in outputs and targets.""" + + block = TensorBlock( + values=torch.empty(0, 1), + samples=Labels( + names=["system"], + values=torch.empty((0, 1), dtype=torch.int32), + ), + components=[], + properties=Labels.range("property", 1), + ) + block.add_gradient( + "positions", + TensorBlock( + values=torch.empty(0, 1), + samples=Labels( + names=["sample"], + values=torch.empty((0, 1), dtype=torch.int32), + ), + components=[], + properties=Labels.range("property", 1), + ), + ) + layout = TensorMap(keys=Labels.single(), blocks=[block]) + + target_info = TargetInfo(quantity="energy", unit="eV", layout=layout) + loss_hypers = { + "output_1": { + "type": "mse", + "weight": 1.0, + "reduction": "sum", + "sliding_factor": None, + "gradients": { + "positions": { + "type": "mse", + "weight": 0.5, + "reduction": "sum", + "sliding_factor": None, + }, + }, }, - type={ - "huber": { - "deltas": { - "output_1": 0.1, - "output_2": 0.1, - "output_1_gradient_gradients": 0.1, - "output_2_gradient_gradients": 0.1, - } - } + "output_2": { + "type": "mse", + "weight": 1.0, + "reduction": "sum", + "sliding_factor": None, + "gradients": { + "positions": { + "type": "mse", + "weight": 0.5, + "reduction": "sum", + "sliding_factor": None, + }, + }, }, - reduction="sum", + } + + loss = LossAggregator( + targets={"output_1": target_info, "output_2": target_info}, + config=loss_hypers, ) output_dict = { "output_1": tensor_map_with_grad_1, - "output_2": tensor_map_with_grad_2, } target_dict = { "output_1": tensor_map_with_grad_3, - "output_2": tensor_map_with_grad_4, } expected_result = ( - 0.6 + 1.0 * ( tensor_map_with_grad_1.block().values - tensor_map_with_grad_3.block().values @@ -209,71 +491,111 @@ def test_tmap_dict_loss( .sum() + 0.5 * ( - tensor_map_with_grad_1.block().gradient("gradient").values - - tensor_map_with_grad_3.block().gradient("gradient").values - ) - .pow(2) - .sum() - + 1.0 - * ( - tensor_map_with_grad_2.block().values - - tensor_map_with_grad_4.block().values - ) - .pow(2) - .sum() - + 0.5 - * ( - tensor_map_with_grad_2.block().gradient("gradient").values - - tensor_map_with_grad_4.block().gradient("gradient").values + tensor_map_with_grad_1.block().gradient("positions").values + - tensor_map_with_grad_3.block().gradient("positions").values ) .pow(2) .sum() ) - loss_value = loss_rmse(output_dict, target_dict) + loss_value = loss(output_dict, target_dict) torch.testing.assert_close(loss_value, expected_result) - # Huber loss should be lower than RMSE - # (scaled by 0.5 due to torch implementation of Huber) - assert loss_huber(output_dict, target_dict) < 0.5 * loss_rmse( - output_dict, target_dict - ) - -def test_tmap_dict_loss_subset(tensor_map_with_grad_1, tensor_map_with_grad_3): - """Test that the dict loss is computed correctly when only a subset - of the possible targets is present both in outputs and targets.""" +def test_tmap_loss_multiple_datasets_same_target_different_gradients( + tensor_map_with_grad_1, + tensor_map_with_grad_1_with_strain, + tensor_map_with_grad_3_with_strain, +): + """Test that the loss is computed correctly when two datasets have the same target, + but different gradients.""" - loss = TensorMapDictLoss( - weights={ - "output_1": 1.0, - "output_2": 1.0, - "output_1_gradient_gradients": 0.5, - "output_2_gradient_gradients": 0.5, + block = TensorBlock( + values=torch.empty(0, 1), + samples=Labels( + names=["system"], + values=torch.empty((0, 1), dtype=torch.int32), + ), + components=[], + properties=Labels.range("property", 1), + ) + block.add_gradient( + "positions", + TensorBlock( + values=torch.empty(0, 1), + samples=Labels( + names=["sample"], + values=torch.empty((0, 1), dtype=torch.int32), + ), + components=[], + properties=Labels.range("property", 1), + ), + ) + block.add_gradient( + "strain", + TensorBlock( + values=torch.empty(0, 1), + samples=Labels( + names=["sample"], + values=torch.empty((0, 1), dtype=torch.int32), + ), + components=[], + properties=Labels.range("property", 1), + ), + ) + layout = TensorMap(keys=Labels.single(), blocks=[block]) + + target_info = TargetInfo(quantity="energy", unit="eV", layout=layout) + loss_hypers = { + "output": { + "type": "mse", + "weight": 1.0, + "reduction": "sum", + "sliding_factor": None, + "gradients": { + "positions": { + "type": "mse", + "weight": 0.5, + "reduction": "sum", + "sliding_factor": None, + }, + "strain": { + "type": "mse", + "weight": 0.3, + "reduction": "sum", + "sliding_factor": None, + }, + }, }, - reduction="sum", + } + + loss = LossAggregator( + targets={"output": target_info}, + config=loss_hypers, ) + # Test a case where the target has only one gradient output_dict = { - "output_1": tensor_map_with_grad_1, + "output": tensor_map_with_grad_3_with_strain, } + # The target has no `other_gradient` target_dict = { - "output_1": tensor_map_with_grad_3, + "output": tensor_map_with_grad_1, } expected_result = ( 1.0 * ( tensor_map_with_grad_1.block().values - - tensor_map_with_grad_3.block().values + - tensor_map_with_grad_3_with_strain.block().values ) .pow(2) .sum() + 0.5 * ( - tensor_map_with_grad_1.block().gradient("gradient").values - - tensor_map_with_grad_3.block().gradient("gradient").values + tensor_map_with_grad_1.block().gradient("positions").values + - tensor_map_with_grad_3_with_strain.block().gradient("positions").values ) .pow(2) .sum() @@ -282,108 +604,39 @@ def test_tmap_dict_loss_subset(tensor_map_with_grad_1, tensor_map_with_grad_3): loss_value = loss(output_dict, target_dict) torch.testing.assert_close(loss_value, expected_result) + # Test a case where the target has both gradients + output_dict = { + "output": tensor_map_with_grad_3_with_strain, + } -def test_tmap_loss_mae(): - """Test that the MAE loss is computed correctly.""" - loss = TensorMapLoss(type="mae", reduction="mean") - - tensor_map_1 = TensorMap( - keys=Labels.single(), - blocks=[ - TensorBlock( - values=torch.tensor([[2.0], [2.0], [3.0]]), - samples=Labels.range("samples", 3), - components=[], - properties=Labels("energy", torch.tensor([[0]])), - ) - ], - ) - tensor_map_2 = TensorMap( - keys=Labels.single(), - blocks=[ - TensorBlock( - values=torch.tensor([[0.0], [3.0], [3.0]]), - samples=Labels.range("samples", 3), - components=[], - properties=Labels("energy", torch.tensor([[0]])), - ) - ], - ) - - loss_value = loss(tensor_map_1, tensor_map_1) - torch.testing.assert_close(loss_value, torch.tensor(0.0)) - - # Expected result: 1.0 - loss_value = loss(tensor_map_1, tensor_map_2) - torch.testing.assert_close(loss_value, torch.tensor(1.0)) - - -def test_tmap_loss_huber(): - """Test that the Huber loss is computed correctly.""" - loss_mse = TensorMapLoss(type="mse", reduction="mean") - loss_huber = TensorMapLoss( - type={"huber": {"deltas": {"values": 3.0}}}, reduction="mean" - ) - - tensor_map_1 = TensorMap( - keys=Labels.single(), - blocks=[ - TensorBlock( - values=torch.tensor([[2.0], [2.0], [3.0]]), - samples=Labels.range("samples", 3), - components=[], - properties=Labels("energy", torch.tensor([[0]])), - ) - ], - ) - tensor_map_2 = TensorMap( - keys=Labels.single(), - blocks=[ - TensorBlock( - values=torch.tensor([[0.0], [3.0], [3.0]]), - samples=Labels.range("samples", 3), - components=[], - properties=Labels("energy", torch.tensor([[0]])), - ) - ], - ) - - loss_value = loss_huber(tensor_map_1, tensor_map_1) - torch.testing.assert_close(loss_value, torch.tensor(0.0)) - - # No outliers, should be equal to MSE (scaled by 0.5 due to torch implementation) - loss_value_huber = loss_huber(tensor_map_1, tensor_map_2) - loss_value_mse = loss_mse(tensor_map_1, tensor_map_2) - torch.testing.assert_close(loss_value_huber, 0.5 * loss_value_mse) - - tensor_map_with_outlier = TensorMap( - keys=Labels.single(), - blocks=[ - TensorBlock( - values=torch.tensor([[0.0], [100.0], [3.0]]), - samples=Labels.range("samples", 3), - components=[], - properties=Labels("energy", torch.tensor([[0]])), - ) - ], - ) - - loss_value_huber = loss_huber(tensor_map_1, tensor_map_with_outlier) - loss_value_mse = loss_mse(tensor_map_1, tensor_map_with_outlier) - # Huber loss is lower due to the outlier - assert loss_value_huber < 0.5 * loss_value_mse - + # The target has no `other_gradient` + target_dict = { + "output": tensor_map_with_grad_1_with_strain, + } -def test_tmap_loss_with_sliding_weights(tensor_map_with_grad_1, tensor_map_with_grad_2): - """Test that the loss behaves as expected with sliding weights.""" - loss = TensorMapLoss( - type="mse", gradient_weights={"gradient": 1.0}, sliding_factor=0.7 + expected_result = ( + 1.0 + * ( + tensor_map_with_grad_1_with_strain.block().values + - tensor_map_with_grad_3_with_strain.block().values + ) + .pow(2) + .sum() + + 0.5 + * ( + tensor_map_with_grad_1_with_strain.block().gradient("positions").values + - tensor_map_with_grad_3_with_strain.block().gradient("positions").values + ) + .pow(2) + .sum() + + 0.3 + * ( + tensor_map_with_grad_1_with_strain.block().gradient("strain").values + - tensor_map_with_grad_3_with_strain.block().gradient("strain").values + ) + .pow(2) + .sum() ) - for _ in range(5): - loss(tensor_map_with_grad_1, tensor_map_with_grad_2) - - # in the two TensorMaps above, the loss on the gradients is larger than the loss on - # the values, therefore we should expect a larger sliding weight for the gradients - - assert loss.sliding_weights["gradient"] > loss.sliding_weights["values"] + loss_value = loss(output_dict, target_dict) + torch.testing.assert_close(loss_value, expected_result) diff --git a/tests/utils/test_old_loss.py b/tests/utils/test_old_loss.py new file mode 100644 index 000000000..46df7958a --- /dev/null +++ b/tests/utils/test_old_loss.py @@ -0,0 +1,389 @@ +from pathlib import Path + +import pytest +import torch +from metatensor.torch import Labels, TensorBlock, TensorMap + +from metatrain.utils.old_loss import TensorMapDictLoss, TensorMapLoss + + +RESOURCES_PATH = Path(__file__).parents[1] / "resources" + + +@pytest.fixture +def tensor_map_with_grad_1(): + block = TensorBlock( + values=torch.tensor([[1.0], [2.0], [3.0]]), + samples=Labels.range("samples", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ) + block.add_gradient( + "gradient", + TensorBlock( + values=torch.tensor([[1.0], [2.0], [3.0]]), + samples=Labels.range("sample", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ), + ) + tensor_map = TensorMap(keys=Labels.single(), blocks=[block]) + return tensor_map + + +@pytest.fixture +def tensor_map_with_grad_2(): + block = TensorBlock( + values=torch.tensor([[1.0], [1.0], [3.0]]), + samples=Labels.range("samples", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ) + block.add_gradient( + "gradient", + TensorBlock( + values=torch.tensor([[1.0], [0.0], [3.0]]), + samples=Labels.range("sample", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ), + ) + tensor_map = TensorMap(keys=Labels.single(), blocks=[block]) + return tensor_map + + +@pytest.fixture +def tensor_map_with_grad_3(): + block = TensorBlock( + values=torch.tensor([[0.0], [1.0], [3.0]]), + samples=Labels.range("samples", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ) + block.add_gradient( + "gradient", + TensorBlock( + values=torch.tensor([[1.0], [0.0], [3.0]]), + samples=Labels.range("sample", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ), + ) + tensor_map = TensorMap(keys=Labels.single(), blocks=[block]) + return tensor_map + + +@pytest.fixture +def tensor_map_with_grad_4(): + block = TensorBlock( + values=torch.tensor([[0.0], [1.0], [3.0]]), + samples=Labels.range("samples", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ) + block.add_gradient( + "gradient", + TensorBlock( + values=torch.tensor([[1.0], [0.0], [2.0]]), + samples=Labels.range("sample", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ), + ) + tensor_map = TensorMap(keys=Labels.single(), blocks=[block]) + return tensor_map + + +@pytest.mark.parametrize("type", ["mse", {"huber": {"deltas": {"values": 3.0}}}]) +def test_tmap_loss_no_gradients(type): + """Test that the loss is computed correctly when there are no gradients.""" + loss = TensorMapLoss(type=type, reduction="sum") + + tensor_map_1 = TensorMap( + keys=Labels.single(), + blocks=[ + TensorBlock( + values=torch.tensor([[1.0], [2.0], [3.0]]), + samples=Labels.range("samples", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ) + ], + ) + tensor_map_2 = TensorMap( + keys=Labels.single(), + blocks=[ + TensorBlock( + values=torch.tensor([[0.0], [2.0], [3.0]]), + samples=Labels.range("samples", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ) + ], + ) + + loss_value = loss(tensor_map_1, tensor_map_1) + torch.testing.assert_close(loss_value, torch.tensor(0.0)) + + # Expected result: 1.0 + loss_value = loss(tensor_map_1, tensor_map_2) + # Huber loss is scaled by 0.5 due to torch implementation + torch.testing.assert_close( + loss_value, (1.0 if type == "mse" else 0.5) * torch.tensor(1.0) + ) + + +@pytest.mark.parametrize( + "type", ["mse", {"huber": {"deltas": {"values": 3.0, "gradient": 3.0}}}] +) +def test_tmap_loss_with_gradients(tensor_map_with_grad_1, tensor_map_with_grad_2, type): + """Test that the loss is computed correctly when there are gradients.""" + loss = TensorMapLoss(type=type, gradient_weights={"gradient": 0.5}, reduction="sum") + + loss_value = loss(tensor_map_with_grad_1, tensor_map_with_grad_1) + torch.testing.assert_close(loss_value, torch.tensor(0.0)) + + # Expected result: 1.0 + 0.5 * 4.0 + loss_value = loss(tensor_map_with_grad_1, tensor_map_with_grad_2) + torch.testing.assert_close( + loss_value, + # Huber loss is scaled by 0.5 due to torch implementation + (1.0 if type == "mse" else 0.5) * torch.tensor(1.0 + 0.5 * 4.0), + ) + + +def test_tmap_dict_loss( + tensor_map_with_grad_1, + tensor_map_with_grad_2, + tensor_map_with_grad_3, + tensor_map_with_grad_4, +): + """Test that the dict loss is computed correctly.""" + + loss_rmse = TensorMapDictLoss( + weights={ + "output_1": 0.6, + "output_2": 1.0, + "output_1_gradient_gradients": 0.5, + "output_2_gradient_gradients": 0.5, + }, + reduction="sum", + ) + loss_huber = TensorMapDictLoss( + weights={ + "output_1": 0.6, + "output_2": 1.0, + "output_1_gradient_gradients": 0.5, + "output_2_gradient_gradients": 0.5, + }, + type={ + "huber": { + "deltas": { + "output_1": 0.1, + "output_2": 0.1, + "output_1_gradient_gradients": 0.1, + "output_2_gradient_gradients": 0.1, + } + } + }, + reduction="sum", + ) + + output_dict = { + "output_1": tensor_map_with_grad_1, + "output_2": tensor_map_with_grad_2, + } + + target_dict = { + "output_1": tensor_map_with_grad_3, + "output_2": tensor_map_with_grad_4, + } + + expected_result = ( + 0.6 + * ( + tensor_map_with_grad_1.block().values + - tensor_map_with_grad_3.block().values + ) + .pow(2) + .sum() + + 0.5 + * ( + tensor_map_with_grad_1.block().gradient("gradient").values + - tensor_map_with_grad_3.block().gradient("gradient").values + ) + .pow(2) + .sum() + + 1.0 + * ( + tensor_map_with_grad_2.block().values + - tensor_map_with_grad_4.block().values + ) + .pow(2) + .sum() + + 0.5 + * ( + tensor_map_with_grad_2.block().gradient("gradient").values + - tensor_map_with_grad_4.block().gradient("gradient").values + ) + .pow(2) + .sum() + ) + + loss_value = loss_rmse(output_dict, target_dict) + torch.testing.assert_close(loss_value, expected_result) + + # Huber loss should be lower than RMSE + # (scaled by 0.5 due to torch implementation of Huber) + assert loss_huber(output_dict, target_dict) < 0.5 * loss_rmse( + output_dict, target_dict + ) + + +def test_tmap_dict_loss_subset(tensor_map_with_grad_1, tensor_map_with_grad_3): + """Test that the dict loss is computed correctly when only a subset + of the possible targets is present both in outputs and targets.""" + + loss = TensorMapDictLoss( + weights={ + "output_1": 1.0, + "output_2": 1.0, + "output_1_gradient_gradients": 0.5, + "output_2_gradient_gradients": 0.5, + }, + reduction="sum", + ) + + output_dict = { + "output_1": tensor_map_with_grad_1, + } + + target_dict = { + "output_1": tensor_map_with_grad_3, + } + + expected_result = ( + 1.0 + * ( + tensor_map_with_grad_1.block().values + - tensor_map_with_grad_3.block().values + ) + .pow(2) + .sum() + + 0.5 + * ( + tensor_map_with_grad_1.block().gradient("gradient").values + - tensor_map_with_grad_3.block().gradient("gradient").values + ) + .pow(2) + .sum() + ) + + loss_value = loss(output_dict, target_dict) + torch.testing.assert_close(loss_value, expected_result) + + +def test_tmap_loss_mae(): + """Test that the MAE loss is computed correctly.""" + loss = TensorMapLoss(type="mae", reduction="mean") + + tensor_map_1 = TensorMap( + keys=Labels.single(), + blocks=[ + TensorBlock( + values=torch.tensor([[2.0], [2.0], [3.0]]), + samples=Labels.range("samples", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ) + ], + ) + tensor_map_2 = TensorMap( + keys=Labels.single(), + blocks=[ + TensorBlock( + values=torch.tensor([[0.0], [3.0], [3.0]]), + samples=Labels.range("samples", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ) + ], + ) + + loss_value = loss(tensor_map_1, tensor_map_1) + torch.testing.assert_close(loss_value, torch.tensor(0.0)) + + # Expected result: 1.0 + loss_value = loss(tensor_map_1, tensor_map_2) + torch.testing.assert_close(loss_value, torch.tensor(1.0)) + + +def test_tmap_loss_huber(): + """Test that the Huber loss is computed correctly.""" + loss_mse = TensorMapLoss(type="mse", reduction="mean") + loss_huber = TensorMapLoss( + type={"huber": {"deltas": {"values": 3.0}}}, reduction="mean" + ) + + tensor_map_1 = TensorMap( + keys=Labels.single(), + blocks=[ + TensorBlock( + values=torch.tensor([[2.0], [2.0], [3.0]]), + samples=Labels.range("samples", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ) + ], + ) + tensor_map_2 = TensorMap( + keys=Labels.single(), + blocks=[ + TensorBlock( + values=torch.tensor([[0.0], [3.0], [3.0]]), + samples=Labels.range("samples", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ) + ], + ) + + loss_value = loss_huber(tensor_map_1, tensor_map_1) + torch.testing.assert_close(loss_value, torch.tensor(0.0)) + + # No outliers, should be equal to MSE (scaled by 0.5 due to torch implementation) + loss_value_huber = loss_huber(tensor_map_1, tensor_map_2) + loss_value_mse = loss_mse(tensor_map_1, tensor_map_2) + torch.testing.assert_close(loss_value_huber, 0.5 * loss_value_mse) + + tensor_map_with_outlier = TensorMap( + keys=Labels.single(), + blocks=[ + TensorBlock( + values=torch.tensor([[0.0], [100.0], [3.0]]), + samples=Labels.range("samples", 3), + components=[], + properties=Labels("energy", torch.tensor([[0]])), + ) + ], + ) + + loss_value_huber = loss_huber(tensor_map_1, tensor_map_with_outlier) + loss_value_mse = loss_mse(tensor_map_1, tensor_map_with_outlier) + # Huber loss is lower due to the outlier + assert loss_value_huber < 0.5 * loss_value_mse + + +def test_tmap_loss_with_sliding_weights(tensor_map_with_grad_1, tensor_map_with_grad_2): + """Test that the loss behaves as expected with sliding weights.""" + loss = TensorMapLoss( + type="mse", gradient_weights={"gradient": 1.0}, sliding_factor=0.7 + ) + + for _ in range(5): + loss(tensor_map_with_grad_1, tensor_map_with_grad_2) + + # in the two TensorMaps above, the loss on the gradients is larger than the loss on + # the values, therefore we should expect a larger sliding weight for the gradients + + assert loss.sliding_weights["gradient"] > loss.sliding_weights["values"] diff --git a/tests/utils/test_omegaconf.py b/tests/utils/test_omegaconf.py index 3f02d4478..9c85b0577 100644 --- a/tests/utils/test_omegaconf.py +++ b/tests/utils/test_omegaconf.py @@ -2,7 +2,7 @@ import pytest import torch -from omegaconf import ListConfig, OmegaConf +from omegaconf import DictConfig, ListConfig, OmegaConf from metatrain import soap_bpnn from metatrain.utils import omegaconf @@ -10,6 +10,7 @@ check_dataset_options, check_units, expand_dataset_config, + expand_loss_config, ) @@ -233,6 +234,238 @@ def test_expand_dataset_gradient(): conf_expanded["targets"]["my_energy"]["virial"]["read_from"] +def test_expand_loss_config_default(): + """ + When no custom loss is provided, architecture.training.loss + should be created from the default template for each target in training_set. + """ + conf = OmegaConf.create( + { + "training_set": { + "targets": { + "energy": {}, # no gradients requested + "dipole": {}, # non-energy target + } + }, + "architecture": {"training": {}}, + } + ) + expanded = expand_loss_config(conf) + loss = expanded["architecture"]["training"]["loss"] + # top-level loss must be a DictConfig with exactly the two keys + assert isinstance(loss, DictConfig) + assert set(loss.keys()) == {"energy", "dipole"} + + # energy should have an empty gradients dict + assert isinstance(loss["energy"]["gradients"], DictConfig) + assert len(loss["energy"]["gradients"]) == 0 + + # non-energy target gets the default loss template + assert isinstance(loss["dipole"], DictConfig) + + +def test_expand_loss_config_migrates_forces(monkeypatch): + """ + If the training hyperparams include a top-level 'forces' block, + and the dataset requests forces, it should be moved into + energy.gradients.positions. + """ + conf = OmegaConf.create( + { + "training_set": {"targets": {"energy": {"forces": True}}}, + "architecture": { + "training": { + "loss": { + "forces": {"weight": 2.0}, + # also supply some default energy block to be merged + "energy": {"weight": 3.0}, + } + } + }, + } + ) + expanded = expand_loss_config(conf) + loss = expanded["architecture"]["training"]["loss"] + + # no top-level 'forces' or 'stress' or 'virial' + assert "forces" not in loss + assert "stress" not in loss + assert "virial" not in loss + + # custom 'scale' should appear under energy.gradients.positions + pos = loss["energy"]["gradients"]["positions"] + assert isinstance(pos, DictConfig) + assert pos["weight"] == 2.0 + + # custom energy.weight should have been merged + assert loss["energy"]["weight"] == 3.0 + + +def test_expand_loss_config_migrates_virial_to_strain(): + """ + Legacy 'virial' in loss hyperparams should migrate to + energy.gradients.strain when the dataset requests virials. + """ + conf = OmegaConf.create( + { + "training_set": {"targets": {"energy": {"virial": True}}}, + "architecture": { + "training": { + "loss": {"virial": {"weight": 0.5}, "energy": {"type": "huber"}} + } + }, + } + ) + expanded = expand_loss_config(conf) + loss = expanded["architecture"]["training"]["loss"] + + # no top-level 'virial' or 'stress' + assert "virial" not in loss + assert "stress" not in loss + + # migrated into energy.gradients.strain + strain = loss["energy"]["gradients"]["strain"] + assert isinstance(strain, DictConfig) + assert strain["weight"] == 0.5 + + # original energy.type preserved + assert loss["energy"]["type"] == "huber" + + +def test_expand_loss_config_removes_unused_legacy_keys(): + """ + If the dataset does not request a given gradient, any legacy key + (forces, stress, virial) in the loss hyperparams must be deleted. + """ + conf = OmegaConf.create( + { + "training_set": { + "targets": {"energy": {}} # no forces, stress, nor virial + }, + "architecture": { + "training": { + "loss": { + "forces": {"scale": 9.9}, + "stress": {"scale": 8.8}, + "virial": {"scale": 7.7}, + } + } + }, + } + ) + expanded = expand_loss_config(conf) + loss = expanded["architecture"]["training"]["loss"] + + # none of the legacy keys should survive at top level + for legacy in ("forces", "stress", "virial"): + assert legacy not in loss + + # and energy.gradients remains empty + assert loss["energy"]["gradients"] == {} + + +def test_expand_loss_config_non_energy_only(): + """ + If the training_set contains only non-energy targets, no 'energy' + block should appear in the final loss, only the non-energy ones. + """ + conf = OmegaConf.create( + { + "training_set": {"targets": {"dipole": {}, "foo": {}}}, + "architecture": {"training": {}}, + } + ) + expanded = expand_loss_config(conf) + loss = expanded["architecture"]["training"]["loss"] + + # energy should not appear + assert "energy" not in loss + # both non-energy targets must appear, with default template + assert set(loss.keys()) == {"dipole", "foo"} + for target in ("dipole", "foo"): + assert isinstance(loss[target], DictConfig) + + +def test_expand_loss_config_single_string(): + """ + When the loss is given as a single string, it should be expanded into a DictConfig + with the default template for all targets. + """ + conf = OmegaConf.create( + { + "training_set": { + "targets": { + "energy": {}, # no gradients requested + "dipole": {}, # non-energy target + } + }, + "architecture": {"training": {"loss": "mae"}}, + } + ) + expanded = expand_loss_config(conf) + loss = expanded["architecture"]["training"]["loss"] + # top-level loss must be a DictConfig with exactly the two keys + assert isinstance(loss, DictConfig) + assert set(loss.keys()) == {"energy", "dipole"} + + # energy should have an empty gradients dict + assert isinstance(loss["energy"]["gradients"], DictConfig) + assert len(loss["energy"]["gradients"]) == 0 + + # the type of the energy loss should be 'mae' + assert loss["energy"]["type"] == "mae" + + # non-energy target gets the default loss template + assert isinstance(loss["dipole"], DictConfig) + + # the type of the dipole loss should be 'mae' + assert loss["dipole"]["type"] == "mae" + + +def test_expand_loss_config_per_target_string(): + """ + When the loss is given as a string per target, it should be expanded into a + DictConfig with the default template for each target, but with the type + set to the given string. + """ + conf = OmegaConf.create( + { + "training_set": { + "targets": { + "energy": {}, # no gradients requested + "dipole": {}, # non-energy target + } + }, + "architecture": { + "training": {"loss": {"energy": "mse", "dipole": "huber"}} + }, + } + ) + expanded = expand_loss_config(conf) + loss = expanded["architecture"]["training"]["loss"] + # top-level loss must be a DictConfig with exactly the two keys + assert isinstance(loss, DictConfig) + assert set(loss.keys()) == {"energy", "dipole"} + + # energy should have an empty gradients dict + assert isinstance(loss["energy"]["gradients"], DictConfig) + assert len(loss["energy"]["gradients"]) == 0 + + # the type of the energy loss should be 'mse' + assert loss["energy"]["type"] == "mse" + assert loss["energy"]["weight"] == 1.0 + assert loss["energy"]["reduction"] == "mean" + + # non-energy target gets the default loss template + assert isinstance(loss["dipole"], DictConfig) + + # the type of the dipole loss should be 'huber' + assert loss["dipole"]["type"] == "huber" + assert loss["dipole"]["weight"] == 1.0 + assert loss["dipole"]["reduction"] == "mean" + assert loss["dipole"]["delta"] == 1.0 + + def test_check_units(): file_name = "foo.xyz" system_section = {"read_from": file_name, "length_unit": "angstrom"} diff --git a/tests/utils/test_transfer.py b/tests/utils/test_transfer.py index a1021a0c1..4c792d5a4 100644 --- a/tests/utils/test_transfer.py +++ b/tests/utils/test_transfer.py @@ -1,4 +1,5 @@ import metatensor.torch as mts +import pytest import torch from metatensor.torch import Labels, TensorMap from metatomic.torch import System @@ -6,20 +7,27 @@ from metatrain.utils.transfer import batch_to -def test_batch_to_dtype(): - system = System( +@pytest.fixture +def simple_tensormap(): + return TensorMap( + keys=Labels.single(), + blocks=[mts.block_from_array(torch.tensor([[1.0]]))], + ) + + +@pytest.fixture +def simple_system(): + return System( positions=torch.tensor([[1.0, 1.0, 1.0]]), cell=torch.tensor([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]), types=torch.tensor([1]), pbc=torch.tensor([True, True, True]), ) - targets = TensorMap( - keys=Labels.single(), - blocks=[mts.block_from_array(torch.tensor([[1.0]]))], - ) - systems = [system] - targets = {"energy": targets} + +def test_batch_to_dtype(simple_system, simple_tensormap): + systems = [simple_system] + targets = {"energy": simple_tensormap} assert systems[0].positions.dtype == torch.float32 assert systems[0].cell.dtype == torch.float32 @@ -32,20 +40,9 @@ def test_batch_to_dtype(): assert targets["energy"].block().values.dtype == torch.float64 -def test_batch_to_device(): - system = System( - positions=torch.tensor([[1.0, 1.0, 1.0]]), - cell=torch.tensor([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]), - types=torch.tensor([1]), - pbc=torch.tensor([True, True, True]), - ) - targets = TensorMap( - keys=Labels.single(), - blocks=[mts.block_from_array(torch.tensor([[1.0]]))], - ) - - systems = [system] - targets = {"energy": targets} +def test_batch_to_device(simple_system, simple_tensormap): + systems = [simple_system] + targets = {"energy": simple_tensormap} assert systems[0].positions.device == torch.device("cpu") assert systems[0].types.device == torch.device("cpu") @@ -56,3 +53,30 @@ def test_batch_to_device(): assert systems[0].positions.device == torch.device("meta") assert systems[0].types.device == torch.device("meta") assert targets["energy"].block().values.device == torch.device("meta") + + +def test_batch_to_extra_data_mask_branch(simple_system, simple_tensormap): + system = simple_system + targets = {"energy": simple_tensormap} + + # extra_data with one normal key and one mask key + extra_data = { + "feature": simple_tensormap, + "feature_mask": TensorMap( + keys=Labels.single(), + blocks=[mts.block_from_array(torch.tensor([[1]], dtype=torch.int64))], + ), + } + + # Apply batch_to requesting float64 + _, _, extra_out = batch_to( + [system], targets, extra_data=extra_data, dtype=torch.float64 + ) + + # The non-mask TensorMap should be float64 + feat_tm = extra_out["feature"] + assert feat_tm.block().values.dtype == torch.float64 + + # The mask TensorMap should be bool, despite original dtype and requested dtype + mask_tm = extra_out["feature_mask"] + assert mask_tm.block().values.dtype == torch.bool From 5ff1585e86bf40c1326e9723a280817808b9e3a9 Mon Sep 17 00:00:00 2001 From: ppegolo Date: Wed, 20 Aug 2025 16:50:46 +0200 Subject: [PATCH 2/8] Change trainer checkpoint version --- src/metatrain/pet/checkpoints.py | 2 +- src/metatrain/pet/trainer.py | 8 +++++--- src/metatrain/soap_bpnn/checkpoints.py | 2 +- src/metatrain/soap_bpnn/trainer.py | 8 +++++--- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/metatrain/pet/checkpoints.py b/src/metatrain/pet/checkpoints.py index cb4a4150f..74831fed8 100644 --- a/src/metatrain/pet/checkpoints.py +++ b/src/metatrain/pet/checkpoints.py @@ -27,7 +27,7 @@ def model_update_v2_v3(state_dict): # ===== Trainer checkpoint updates ===== -def trainer_update_v1_v2(checkpoint): +def trainer_update_v3_v4(checkpoint): old_loss_hypers = checkpoint["train_hypers"]["loss"].copy() dataset_info = checkpoint["model_data"]["dataset_info"] new_loss_hypers = {} diff --git a/src/metatrain/pet/trainer.py b/src/metatrain/pet/trainer.py index 33f00b8f9..e22994006 100644 --- a/src/metatrain/pet/trainer.py +++ b/src/metatrain/pet/trainer.py @@ -51,7 +51,7 @@ def func_lr_scheduler(epoch): class Trainer(TrainerInterface): - __checkpoint_version__ = 2 + __checkpoint_version__ = 4 def __init__(self, hypers): super().__init__(hypers) @@ -206,11 +206,13 @@ def train( sampler=train_sampler, shuffle=( # the sampler takes care of this (if present) - train_sampler is None + train_sampler + is None ), drop_last=( # the sampler takes care of this (if present) - train_sampler is None + train_sampler + is None ), collate_fn=collate_fn, ) diff --git a/src/metatrain/soap_bpnn/checkpoints.py b/src/metatrain/soap_bpnn/checkpoints.py index bc6f032c2..a45de66dd 100644 --- a/src/metatrain/soap_bpnn/checkpoints.py +++ b/src/metatrain/soap_bpnn/checkpoints.py @@ -18,7 +18,7 @@ def model_update_v1_v2(state_dict): # ===== Trainer checkpoint updates ===== -def trainer_update_v1_v2(checkpoint): +def trainer_update_v2_v3(checkpoint): old_loss_hypers = checkpoint["train_hypers"]["loss"].copy() dataset_info = checkpoint["model_data"]["dataset_info"] new_loss_hypers = {} diff --git a/src/metatrain/soap_bpnn/trainer.py b/src/metatrain/soap_bpnn/trainer.py index 048ab3c57..13ae1a2e8 100644 --- a/src/metatrain/soap_bpnn/trainer.py +++ b/src/metatrain/soap_bpnn/trainer.py @@ -39,7 +39,7 @@ class Trainer(TrainerInterface): - __checkpoint_version__ = 2 + __checkpoint_version__ = 3 def __init__(self, hypers): super().__init__(hypers) @@ -193,11 +193,13 @@ def train( sampler=train_sampler, shuffle=( # the sampler takes care of this (if present) - train_sampler is None + train_sampler + is None ), drop_last=( # the sampler takes care of this (if present) - train_sampler is None + train_sampler + is None ), collate_fn=collate_fn, ) From a4faa4c66d9ae41249d9e5b517266d2c88357ef5 Mon Sep 17 00:00:00 2001 From: ppegolo Date: Wed, 20 Aug 2025 17:23:28 +0200 Subject: [PATCH 3/8] Update trainer chekpoints --- src/metatrain/pet/checkpoints.py | 4 +++- .../checkpoints/model-v4_trainer-v4.ckpt.gz | Bin 0 -> 14886 bytes src/metatrain/pet/trainer.py | 8 +++----- src/metatrain/soap_bpnn/trainer.py | 6 ++---- src/metatrain/utils/testing/checkpoints.py | 2 +- 5 files changed, 9 insertions(+), 11 deletions(-) create mode 100644 src/metatrain/pet/tests/checkpoints/model-v4_trainer-v4.ckpt.gz diff --git a/src/metatrain/pet/checkpoints.py b/src/metatrain/pet/checkpoints.py index 306353045..070741981 100644 --- a/src/metatrain/pet/checkpoints.py +++ b/src/metatrain/pet/checkpoints.py @@ -35,7 +35,9 @@ def model_update_v3_v4(checkpoint): def trainer_update_v1_v2(checkpoint): - checkpoint["train_hypers"] = checkpoint["train_hypers"].get("scheduler_factor", 0.5) + checkpoint["train_hypers"]["scheduler_factor"] = checkpoint["train_hypers"].get( + "scheduler_factor", 0.5 + ) def trainer_update_v2_v3(checkpoint): diff --git a/src/metatrain/pet/tests/checkpoints/model-v4_trainer-v4.ckpt.gz b/src/metatrain/pet/tests/checkpoints/model-v4_trainer-v4.ckpt.gz new file mode 100644 index 0000000000000000000000000000000000000000..4bcfcbd042abf72ead7d1d9e92ebb75b38f5bcd9 GIT binary patch literal 14886 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 literal 0 HcmV?d00001 diff --git a/src/metatrain/pet/trainer.py b/src/metatrain/pet/trainer.py index 0289d9f5f..1f95c983e 100644 --- a/src/metatrain/pet/trainer.py +++ b/src/metatrain/pet/trainer.py @@ -33,7 +33,6 @@ from metatrain.utils.scaler import remove_scale from metatrain.utils.transfer import batch_to -from . import checkpoints from . import checkpoints from .model import PET from .modules.finetuning import apply_finetuning_strategy @@ -208,13 +207,11 @@ def train( sampler=train_sampler, shuffle=( # the sampler takes care of this (if present) - train_sampler - is None + train_sampler is None ), drop_last=( # the sampler takes care of this (if present) - train_sampler - is None + train_sampler is None ), collate_fn=collate_fn, ) @@ -582,6 +579,7 @@ def load_checkpoint( def upgrade_checkpoint(cls, checkpoint: Dict) -> Dict: for v in range(1, cls.__checkpoint_version__): if checkpoint["trainer_ckpt_version"] == v: + print(v, checkpoint["train_hypers"]) update = getattr(checkpoints, f"trainer_update_v{v}_v{v + 1}") update(checkpoint) checkpoint["trainer_ckpt_version"] = v + 1 diff --git a/src/metatrain/soap_bpnn/trainer.py b/src/metatrain/soap_bpnn/trainer.py index c5787632b..66d073c54 100644 --- a/src/metatrain/soap_bpnn/trainer.py +++ b/src/metatrain/soap_bpnn/trainer.py @@ -194,13 +194,11 @@ def train( sampler=train_sampler, shuffle=( # the sampler takes care of this (if present) - train_sampler - is None + train_sampler is None ), drop_last=( # the sampler takes care of this (if present) - train_sampler - is None + train_sampler is None ), collate_fn=collate_fn, ) diff --git a/src/metatrain/utils/testing/checkpoints.py b/src/metatrain/utils/testing/checkpoints.py index b4a34d785..0720ea8d3 100644 --- a/src/metatrain/utils/testing/checkpoints.py +++ b/src/metatrain/utils/testing/checkpoints.py @@ -1,7 +1,6 @@ import glob import gzip import os -from pathlib import Path import pytest import torch @@ -82,6 +81,7 @@ def test_loading_old_checkpoints(model_trainer, context): if context != "export": if checkpoint["trainer_ckpt_version"] != trainer.__checkpoint_version__: + print(context) checkpoint = trainer.__class__.upgrade_checkpoint(checkpoint) trainer.load_checkpoint(checkpoint, DEFAULT_HYPERS, context) From ab696de71bbb9816be402d10f60a76098f4afd6e Mon Sep 17 00:00:00 2001 From: ppegolo Date: Wed, 20 Aug 2025 17:26:52 +0200 Subject: [PATCH 4/8] Add checkpoint file for soap-bpnn --- .../checkpoints/model-v3_trainer-v3.ckpt.gz | Bin 0 -> 35819 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/metatrain/soap_bpnn/tests/checkpoints/model-v3_trainer-v3.ckpt.gz diff --git a/src/metatrain/soap_bpnn/tests/checkpoints/model-v3_trainer-v3.ckpt.gz b/src/metatrain/soap_bpnn/tests/checkpoints/model-v3_trainer-v3.ckpt.gz new file mode 100644 index 0000000000000000000000000000000000000000..5ecc76db0bb1ab3c66a145c4d50f866f74299365 GIT binary patch literal 35819 zcmV)sK$yQDiwFqK=%r`^|7~w%Wo#{WGhcLaVQFqwZ3A2DG5{(q*tQawiISXdYxBB$Z z3<~bA?tS*|UD@ZinF{auPE~bP^`UFl#Yst7K|xhj;fJ4w3iTAc{YBnm#i0QqQez)a zsi%>6Y*0_fS_&g7eqseNjDo(K2Cd+Uz5N5EB5!FJ7P*Ib28$R)H#M3r3H21adx=9r z7=c>@`Z+k%M-=2vxl;FV5tammhA>J_N^Y!#V3E|5HN+@;OG7D6eULW}5K9Mm1yZa! zqhc_Lso|EWsGtz7u0wxxJ;S8_p;)48r!#W2A(!MGC9v}43t*A22#Z3zMJ$>#&Iyq7nN`8(K<=OcU0v*S{H0Q{#Ln2* zCp5q)6#E&Qni!dyn3x$eVV*{27Pe;AG9!F7N0FYDmAjTF*3EVvM@2#IYaPUjCu@H)6JN6qPshka(6d^s}b~wNMgixt~4|_ zz}u)&*D$JajGC95IzKWXkm^%7y%Cb?#)Qw^4>ATfpOL<1P zdxrRh1$knozLO${qiNXFC%`j^(V$5+Xp(oBG}PCZX%NFS)Q1E%KTN_j>gCXg8}@E0 z^q+A7K2m?CaW7QNC5DB8O&G*YVi-+*7}!Ccks=%tii4S^GvE#g2@7`j5Ag93g}6hW zglR_YHkRdth{AAah{T;b!eujBPKse}_1Rm;$A=~bd--^}dwEI%Buw)epu=C}=_6rU z#4y@!O7vrtR}j-O9()T54e_JSh4_h>RxykY=Zq-C(+^o36L@!+}KuUI=aOecLOMW-50 zHJwyNZZ&BEsVDXmNhOR?3}Y;72NCv*WK62o!W9R(>Vx5TPL3&+V-~}hbDbL?ahG^| zvct(@2DnKRygjj0Bnj{gVJzdz(1`b%4Uk4M*8V1-hW2%6n3S>c zXQwY)ZelZX2SMtz(Y?X~f_&Viq7X?ab`LjGXYBO#!7T|b=}G&yvurMOjFgH7acSyI z7dJW!Dwr@r7#QB-Fvi}wgR+!C1DzD;FVh$YC&g(@*LZL5(4ZhXKhchps1LwCbQbgJ z9>7kfjs~twH!0KImFeMT_^)nTP-iq4F3$AE>lw%N@`8H;`m48_fNl1nOdn_#ub@!x zv9xLYJ;frXZw%uELze3mcXxLarr&7B*-6n;66Pi0ve@~TyYc(SFayH8!eID|#G&5) zjEj>36!H>Dr0!f2brS9yZf=mU0^NP6SB$Ga>((HBH^=gYhxUv&ofW80MpggH%@9Ts zF`Z~dSax*}_Y4Z7qiwK1?G69(9_St#M(;1a4~9S=m@-5C?VS!wnPF6wlcSST9OLGs z5wwENCWIZmp&UspWf)fmLx;r$VhWp(makdU&<_SWfp#?$c$68s1nDPN#Hn%GR|U_ zvxMU;l`_j*nWXO&nU^bC&Xd^t-PD9x!J$^lP^(zfY7UhwW!AVdYrm6Z&PiHV8Dz?= z=Rg}|pp7hO69?KXWl~(3RJVGv6_|ECKaA2UbIh1694DRSC_1^sFPDWmdu3U72OU?BQ5@c~(BhA{?tg z%ItGx_E)gXD_93Avn-i|9P1FzI?S<-aIB+J=9nvU+|3jkp3a%vUc`u-e~l`pVIw+y zh6Rb-gFVF+)=pHmX2qQ3?(I{YHEQNGcR9mZE0i*4U72&gW{tn;&R4c(&0OHDUF5A@ z;x0v;waZfGiYs&V*R1h3)V0diY?$kuwHv&(o809VXRTPu+;(N|{F*iXX1QD0nk{pW zvv!}i_JF%QFyQgOCLm-r29nB^$!xrq+33RAFlFBO+dGe-Ta35t z7Gnh6V!UImyyvWxNtq9>%ttr9Dy_gTM(hTTZZhc3%SRN!e5$mx_M`h*eqsI0nKWa* zP`A{i%vaVewQ0;ZY7i^XNepaaMf#f*3+TVDSc(1%yG3@>#x7XwMS_)Sa^rOaFQ)a=H5xaB8ja*@=>5}G9XE!`P2?*Z{^FJZ zYXYOGJj3EQw-vY898ca;>VY zO&5M_4f!_JGNPX_b~4p76K24xEGY~EnmKIr8tATugFsxh6AGLecJ~L z_mwZq1`;54Avek;*a-^u`$5I`1!e0Hc82o(<;%DENqM;?7!QD|F7j1P{#jL{a1px- zaiJGH5IC;#9N}+p+{F@6m`^D8999c@{zBk{|g&IEQ0#J^7T9X(v}?i0m1*b38pvz z2!X#%FvDYk5cJywa~uqW5P3qqA9sm36pD$zpHqI=OkxHKV)=ra)$cm71d2<)pJusz zA`XMxaJk$ti5JX#5N`TDz=X$@@Ao3u{ocPR%!DI<*rrS2wyLr0_VeEuYQmF#&}NAz^V+7!Yn#ezo5pL418vhm+YDCQzbOWV zf!(};Jz!uj7|8$8 zhw|rtOnxw7jth7b`zlQAmzg-gn>Yw24uOfoKl;E0(eRNU3|Qc!yn$mC29C=NoZtW zDuQ(Q;SV-!@FU*FV|g1-cpFc78_&STbFlG(weefQJ6!UE3R_&t&xtR271Z`C7!&Mo zuXz=3K*d{7@$Q#Z@DtGc?^Rfs;4)st2YD4Ac@>{{6`w)H7f|u_msRjn$hRL5 z9pYx3aW;C0wPeRYnop|IngVSf6iEgNhdIAftEW>4re>1au<-L=lbYC6JFBnh0cZLsP+b z8|3d}g=PYFt7T<2}h1y#ig`$OIULDjKB7swqbs4iCM3i*Qs)x`=2L;es!)v>~%kULCJU98Xz z@`nqmixrN5{E>pHV}+w2ceJ3oSm7ARcNbI_EA)VTPeIkOLNCbm7E~82^nrYlpt@M0 zFXa0P{%NexAIb*^{!y$j5DJeK{L5Hj5EKj+{L@%r2$T;M{6?%$3{@GypT-I?a3q3% zYphTT&@jQjKUNqHUSm6XHGg0u9SYa#_ znI!nvvBJqva*E)WVue$o(lo*E#tP%0{&d0b#|mcvAztwNvBH@^m?ilASmA6S%n|&4 ztZ*(6<_Z2qtZ+UQTOjzCvBHH=aFO62#tIXlc%q={Sm9#GT_UI)D`ZC_ykjVPM-?xH zV$0Zl9SlRC0ErX_c!fztJ~)Zp7{b%MT&lY`6ie}Pn&rGHiF?(}Q|j&SE@9`-73`K$ z6;j!kNa6iA30?^sP1vdii9B&g0L;8n5ne?zdLb7t=W{PpK7+3`J1Ej!A`KOb*-~OU zm$NT2LCie0w5?Db8&o@UH&Yev(310rav$Uk0iPg4@S0oryWyG>_T z9QIXO+Kge`JLlz~O*DHn-Yk&)9i9~z>?y)2(Dn^wDH4&`lWj3~?qx-s3b`7U1HZu{ z+DG0vO(6TbKMt0N+{HN52XA4Mlz}CY;B=6+m3<^IiNV{T>)A~9Aq8i!McKDBX=S{f z{lJbX+4Es0v}LL6-D9vp8=IRKv&wA*i#$VcHv6eA+iF1pK5S3QdLxIX_lKH(?2cI! zLR%mg^yIP68E0K!CIG=x3lD*l{Gbq$kTK;MWCaf3m2xW~6_(!OHf=bca{qWFq03MnS zmUp8lLhSAt?uQTk@GxeB4-3j8Ek^`=)a$71VeA8^jRK#4$$WTeSvLvC&d_ik!_m5iWUjvy|*;Mmw4}sDtmt!P*(tTm7Q#U z@YvE6U*lz6uaI>^ChI0I>lUzzfpwc@{otad8NS1_?pCnw$yoP!)&pQY1lA*PQ4oV4 zSMQ=Ze!{!>w6crO0QDSDFRFIY0+;ZzN-Jc&l*xL<%X$s0H^6#ZwTqVc9nX4S!77un zKJcuM!1@HN&)}j`4E|ERi&pq6@8Y-0E-EOoC`Ba}B~Yr;MQg02R4z+diI=6K#L23m zR4%I~uvCGirc|YiHdvi!)v93Cma*#ath&Ie2dw%^tc%JqSVO5=7j1C^rE(V=Dphi^ z5uh3as!5eDTA5%?URKizSI*CRq(K{dpG$RCdt?Py+$wTD6O| zcn~jZaD}WPGFd};S;K(k2CU&#x@c{JNARqX6|7M*)@YtJ23YRE@&FfA=|g_CE?S#n zFWyD($}aiH+D&sD!t+Aqd19W& z@H`AW3Gk%gsv3R9t=?4&9L~EsuCl8UfQkfERMoCp;%HvlczJ0tytE0tw28or1>U5p zUA4lKdEOLx-c+79jpxMyZ#wX1fUD~C!K`{$t?^9W)mfEYoeiitfSOyit2THZFKxcO zv<1Ahg}k&yz)JvLV%4tN;>A2~i9By9&s)axl7P1ycq_owS}}NKwXWKj;8nbZq=?@;5@$e4kfui675tf|0B^ZUfOQp?E&82s$I3j z`8^v0Iv{uXRCJA2A|`3=jC}9c-}>xcL{h!z`G2t){Vhes&~~EU*+M~D!Y0eP&WW| zvr1QOP4F$gcCoy)+q|?pytKQ(y9d1cRk~_xiXZU2hw{8fJnu2jdjhzTV)vj9Lk38>_Jnu8l z`@-|S0`D8}6qMokE*qs_ANYga{cvcQSb`Om*&u;HnSGXJkHG7Logj>rmE|KSDi9Em zdP=Ygq(3{!{+#NCO5UHo(qwwaI9x+n_INL%)S7G|`yP>V@0iTC1Ebt?zPpde+cOfY z(rhR0an>_f!alEKHDxyLpvoOr<{rV-A&D*I8;V7q-u}3jGW(3rKCOCtilt$gJ?4vR zL$SILOY()|>Cynmr~?_Sa^EnC$OqSj+D+JkB5&$^kjTfqlB{}Ap+Id?_GS69m_;LH z`3MrYAoBGL3zE7A2ZXrOPH0@wP+W=xHvvtWkl_gp#s{mKDnqb}P4f;74e|*c7lNB{ zg!*!6?$miU3Wc?}JZ0GlQrx_}3#l#KLRl7wk@{ni#Gh8h+LeYxUHLo+6ycWT04xf^ zt>}lq$^${YJPHcoJFKG&(K}YJI~N?qxHY(^D{sk@KNRmiE)+{dxDA^-u{>C$EQ?j) zwrsM4L38HqmBA?>kBt<1dHR$`3-!R?Fo;HS(Jd~n)efwc+;{lW-gE>+-XOQiSWtWD zNqx2_1Gxy0fwC+D)Iqu0NyHL&R4%`54V5cRBAt}ko76~|y-AIgtDHovu!(Z{0&S|y zZ=}tXxs9~Ba`_E!0W3>kSt(aJfmmZ}o@Gz7=R<(<^xH~VaM}@4OGFiQNS-pYP2UvZ7a8U`4W13(m9@eh{<}8Et=V1c? z<^r&R%Cb<7Yn487WK%qdcXDuLCx-xQD6oe8;3QYw47>5dhF1t1Arm%|7d8rDqX9Oi z8ZXVUI}h`yfO*PbUOdbjU_Jm7Da)cczCZEO0{ih^`d9Wc09b**8e5H*mNm!@WTIS*S=0b419 zt>R&;0hSD~HOjKE&f1@NX^z+NUaqg~yMVR38ZWK!9$whq z3Ss#&VT2b}0I+=k+h2{BHuwM!J6HibB!eC1VMhRV6kx}cWx<`}Kk?EQpWwYbS=q}| zz&Z`AGgW(OW`YZOVP}=)-_2F#-_7O4T>#)k0A8xvQ!`Us!~-wO1F!JFt32=;0Ivh^ zhO#WabF+GH&CKvE-rM5J-rfe*9bnz9##?iIj~91eUfcs-+(Ta6BLF@I;FD^+wZKn# z;4^vPa~}AD2bKV^6o4<4Wg(tdKk?QQzvjJtQ`y_Mzbzj><;B(G#nlI(1^^qVROzj`32w*(8_5G3^S~xNP!oVn z0oY7M7Us4Ujz96%8h7LA-79#Ypfg&F0 z%LDxY=nudE*4wZ^F2)n60xuwf-<5(pfow=8u*MIM>w#RDCJ;h1fe?)ega}L^gj@n4 zz7hygl|Trc1ak3`AeDa@F9|bpajL00F2b$7^*{v$_CJ?uzlKpup{xAYFdF%W;$Tny z6^@>cLiulF{PiDGR!LsTb@m(|`^_3ZQKGG2QAvsH`^nrrg97|Q*e@6C>1guHVyrA! z0caeJM68Bs!n@7b5LJzzYS2*V z{Qb9L8k_WVwEkTItR>ku4=S|Q_g0H&@lE!lO8p%xij)|Ih$9sh>MKl@l`6k3>OaKU zWw@e3xI(l}d-z_<_I5fW_1n9*H`Hn0CBUM+fuYW5`l~Pda+zm{J2Vw7($h0YBBJT= z?QHb3zNw*(fliFhFaEHhDTT`Frz~*{E?3`>s^{LUGBz>hPPvvl5-Y0^`!h${ck#7p z|4-DL80!3<1H2=ZojCgKSi^EB8p%5$@npY5^Vhtf-64sTNJYWF+9xKz>e6>!ctxH| zDfdEC-U~4f6^pPmK=jMwihE`A|N7vPjVncgky7RRVyaA|{7t!j68mrYUGjY`c09() zx~O5LTKW8!6jmCY-vijOP!4ER3BXR5RgH}A32Z-<3)iefV9!xh(UcApxnGpoLx~6 zq!%RGZI{*O_?e#{0_W|#Y_LDNLJJfb_eyoIa4>boa zy|+QW%pRmGw3>Mz>@{6h0NfHQNY_z3bPv*-YwVZlD1v&YUfzZDr04^{+rJmmb+R=e zU1wg=L76^CFL$X-7y;zIYkhw^$T^fK7L(IqZb2zYhU#)|7)#UR?tA3jEVx{1$I=IK-wWFNi%R z8wW7nJ>KtwbcKXi7@x_PCO|*MHc~qb>5ZgEWaDEW@ZM-bxDbU1YOgZ_?9uEmwNy}DmEo_0aKs6n7#$8|N>D}G_I19S#4 z;BV~YF$J=5mJ8{%1E779@0o%`ZWLPD?}PM3t9Q%BUnX2N9uwersqTc&eOqKe zy)`;}!S3piT-mtX0_n{=LO&`jx{w8SJtl4jyGQEh0YA3OcG-A^c`^CTl`JUVePBA& zzooGT(jU*wfPQzf+AJH#FkTflho*wv(T3}wTvq8KurJxU4$_68D`ew)CZum_oW!Pk zblkBJ^o%_p37oJK@z5`G!Y9kdJ<~-t{`bH%`5Y2l8T}Jw<8IP704qx$*V5 z7Ux&@J3FA>>6(yE%gu*=G>L-yMyJ6k7Cu(eQsoaij%>*m3IMD4AIbhEN^ zaDCbm(v!X2Hi91C&7w&aNl`I7jo@ux8y41w`~pg z2lWT$x8l6k*TC*sOSnID+L~oT`xM1NdO=+~Sf8n%kRB^O4CxN5jB~*5m^>K&u}g}% z^nOjVxb-PC8|<#R2%3hd>kn?8iAjNR<>8Xdjk8v_AwBt!T_M!VNZTmur`=Fq7<&qI z3||K0Ci%S^q!))q9Ovr$Zk6>Pq&v*>KEm0l3H_}zBn{GaRC{sj#^j^r{mOBK`Y?~` z9F#);c^F-WbPuIwdw}ynJy+J>ke*!c74%zjX*}2NJ8p40$De@l7`vc!hin{hc2|Ca zd9L`X5d16_?dQt(tGfW~+!ybbjSr6BKp=(wxI1GHHx6RC^p=faJ}quDZLe(HaOsb0 zY=Zu-E8^DOb}cx&J{BorEh)?{qFH7hQNK>cY1+r{6V_I$xd)Tdo*7S=>_N7L3;8Qe{S8i*<2tS zm)!U~8p6#Zhu3oHagN;h+=~u!>-XFO*?8sJd)_r}y;lq7=EV$0NcTu_g!xXVudqNi zj-lU@uZD5+V|@ikPxdn4?yo`NT=~gkiEMmBzbdTm!OgQv_d(Yond?d+U#RHS13{V!4l&Js+ehd&2w~*Y*dlMNdb|--`g% zY;68W(zF!an>*yzv2!J;k%RLpeT7nV+QnTJcRM}96r)1+w|y=#=B&MVKa`1q{^X;?i}UqI_6Z2UC-vsx*_XmZ_Y z>ZNE&_T5QMDQA4l=CA{YN>IbWnkW08EkW0oOgq`0=IgmS-*5e~1dYpYa_~t}2|64* zysvhxQZ#B={+aW&OOasm^JYn>N>G^C(d8}mv!7`1g|fmDwC3{cyeYK&?rrH4k0_O* z_a>#c&QYCF-lN@GPdsi4BdAk~(i)S64sS|OAMe|`E3`||2g9B_$I|>pV>C0BX#LHvBS`%lCFsbsuv4}) z|HR-&$TTp*=*WJn+ zKy~lf>{Gap`i-C7a(YVb6j(0y-g>tLiI4Of8h5Y+?QE0rsEqbQV6XAbH_`MbpXXg( zrCf^ov_5AYP5Wzmyv7|T$_e^Zx6?#gzD7i3-&mR+lW(Zs_Ie47`LkgjYb~_PhUwtBai*h-|kh4MpLa* zE}U!>jh5}uD(>JAjZ((P%{Z_miiDSTnIxGPje-LQ*~}XhPI|p7nLMgvH0rte_|0QB z(ZoKpZP&hL(Wsw#?^cn!{fXdp;=DYKXku{P(Ch7>Xr#NLcaw*YqtMCHdGq(sdM%93 z*ml;9Myg?p(#EZdLZ%DsPn;SRO+JjW=yUXI6bk#Am6W?L64lhd{@6<|nq00oe$`wQ zi6#u3IOJ}NXtYaPsnb$g|KXc{*Bcu|qb)iY$3MRki8?)5wlBjmnrw}o)@n}uXw+nm z?F&)6XcX;yzkeT^-hJKR`JK$8QRDCpk4KD+CQ9=Y>N1MaD8$9NT~g<0qWv_*e^AS4 z)bwd5X-17`bYaGh?izKYQD)Rf#rj^+q~;Qh#c{@z8(-FG%fe`~IqvEyZ`yuOJMMij zuUj-J+|@AAdRipfsedNof`2p#m~#4YR*z`nzh>MK1?^}uLN%;NwP!T49;?68l|BUW!CU((8wQJe%j#T%zARX*M{kYj`?E=;0Xf*Vk<-2D}(ZoeMq_~Y^ zG#c_X`HFvW6iVAA5|t)KlSOf}vw9dtlZcsqLp%13CU%Mu<9#fsF75sU&ee`any0p0 z4x-}%rD!*s-+BT;&m2}*y^lg44Vyd(o%E(m85R>HTVz)%?C5y^m+FZQY;|oqvNz z_rLr4P6>*v<8M(+@82W7x=GP=e$w@w|J5R!&ewAS#&_FI=fmrpvW;oI;~VTg-mPDX zwhjw#b%T~at+8mtmscf7U8mXe^R!<7BPE~B={)Z|y06We%O&W%LCoE+PfL*A81>qx z=zP>U?d6bqRL3E$W%&`eO3FX7AUOQskYjyV967-_9 zxaku*KdvoWW*|CWf>tz2Usn971kEJ*?Z?vVb>%sYOQ_z-q0W;A({-hxpWVFvlt1h3 z1COb+J^ChIGFwa6!KKQZtozVwX>76A_OcQ*K&j186Y8Jim$2*O==zi8mQ$QU?WK-L znLL};Uv6D<%8uJ5C?MJB!h1ST-gy1|sVluscA4-lSg#a$Ouy4bMAx0A=J!_*qSs9& zcG|OPyZS#d?6Z{GX+1W*RxzEI?T(Gw@{H!YjJDY3d5Nxj3bQ3_KP2~G+kQLkH}9gv zTL-jgKMqR$5=s5LzHafNo79eT)UK7asE+W&K$BTim%WZ_@J_lOyt_2C*B-hKv_IFo zQ){~JG;c7ZWCG3Vyw#%zQ7bUo1-W-_YB1-i}zTTU57^?0^V z)NrQ#7jn@+(RO0T|CK_+hrc)et^auZKX|@3HTN4vg$=Zs?D|r)R`gZ+TwRKSR-Zla&R9w$ zYmS^}T%}~M{hz7WuKMtE(z+`BKvL&y1&mkP^;dE=2FyW=_~K|MpEP(Kk1n< zk`lD+wEYKtDKhT+`TcRN)n$HDquO60=!^KzJZ?&z zi;k~{vxO7|ze#C(s*@BoOLtmx(L+Lfi)$5RewC1(u}jt(38iS*-Tp887)sHzrx7ks zoTSKhOj_>?dQx=la_1*sy&_1mj`EQa=2G;qc_+PDHzmYv#)q?#wo>v|Q~BcQeo{2D z%(~V-Eh)*=W)_4|J=&WRhwi0UPtT&Y6Fx?e)tj7b%&C6|gL@a&=}OBrucvF-L5f!H z_#)}kjXnnrT6}J}j+AKY?i}6SREp-i`@{yCNYU^UyI)_N12!I(vy;@wtXI|MM}}EVHzRI z)>8B`^-+`}?T?)glX|9hkP>$MKN+u<{Cnfy=8wmJlk=PI&Y}0G=3STdx9R=s(WdJ~ z*5lV}?oxYdPD`$<(EZ@~z1lY}(0Ou=$<@8~bpP&r6kBzq^TyE?$6lGydEeyv<+i8k ze%@`l)~RC3@9kNqWeYn0&iLqe#D>n#;+K|ljz6ID{IQg&&*}WxscU}QIdt7PojAoq zna-P^7D^Ik(0Q)czHKX&X?e36R!*PjIusRucH~Dok4@T8x>b+*q4%auzX!Cw%jGxD zov8h+vYYmo==@pt-R&TC+8+HUG#qV1=S@?S)U?xAO3=Q|7dx@*(|XgR9}=m~C#J*N z&uvhO4&vj<>u9}sMt1R=XgfQkudW+LpL+^oPUv-@_U3JvRKT|9^r1CIt5CmgUz@44 zhhAsZ&9PrVb-p;`q_>rpYrD94pMA6)P2!gx>rA;HYMXUlLiI_O&RQ#^?f2^3*uXH_ zKhwU171`1C;?}s4PcPB-Nj+O)F^$$A>UwiR{Svy4tTjSzv>)PU-gi!->5Ww;$L*o( zK%3J_{L+}FKlA?9eLk({JxMck+%q~}`VR9E($5o*T%K-4?c6xHb^Cmp zZqTUEd)aYr|9?KSg~{)Yf7?GE|2n=?M$`~uQh!o#p1$tOW{05Y@Fv4{XkoH`$)Fdu4KaCj`=}u1JcHsU_AxYUhSC1pb@Hs+ zVH7{0&V9EI7`2MM+Mq^9OnkDmq@lF@%{qha!QP33(eOGNsSssKN~Lc#kW=HXO|}leGDq^k09?=0tCYU6S(Ue@#!l?CD|5uZ!A0GVwf!wZ2m_tT38%e1>YhsTesW%t^EMW60=bs*)vl7_z}4 z@o>>dj7*=zjj6pzOcwU)arq794OHJ!$Fe>q8x$Jq8yQo*`8peBm|#>}!=ZseV@#eK zw^!EBW=MxQDShTF#Yni@cSS=RhL{bj8)6&Fknvx~$DS$CC$WX*`g3eC+U%=2(Q7D1 zb2>c~Cee1Pxy`)qt6H>QpHFR&*A1i7#g{gEG{U6e)^klKD`V2ZGFx>7^|R;N*W*>{ zVbl_@8r+YzgMPlLV<5dN%cTK+#n(x!K{lU7|HctF29M zoVCC({x>LV#Q*E@-$23P$G@tZ^mOdoLi8@q-CH7P|ao20GZ+M3RP;+d}_-_ZT= z1hj4@-FM%A(d1}*IzJT-m~+L5&TEk^?**=+`_7u0U0a0E{hm?pwrxJpdMmDWyi<6s z1a)c9qDRky5;S73-qzdNrD(_1Z8~Ojo_ZiUHMj+xkCqj_3vWs9|0G4RxjUWz(@b@i z^;IcFDQgN&AzE*E^0%$dbUo>J$KZZfntp6&j}(QIbe^Ab_VaAIKIn9qv-)XH3EC@s z78*J27>sW^3wS+CYQ6)^r`vFRi`5G2K5OT=n4QW?BzRRO<#6l_0!k zb~3yE9h0t2nN4*Ec#YCIK-Ve5)Pt6pbY0ngC8w4xo#!3uoz#z{It!P5-LsO~J67`A zG>F!#F|M}lHCq0O`?=>kXu0lBitqKJ>zmoo%^&mWzB=dL+6Do1T{$sm+q#r?rKk(N z*#rmZx|I>FnnKspy{AeqNol>rQ(YUiqxbrgqcs_I9*(#srsFH(#OIG<67=9;vtxV2S>yYB$@CEnQrBQgo+XRP=oHb`_JLwz zoO-eLq%&ePOcK-g=mIf``&=_8c!wCx=x(&f;E5P{()HrK$!HcD0@-b2U z^2$A8)ZS{WCz)ViKXyZOyw$VpOD$gpi<)VAzm)WJ|OxBuBVdFyYEf3rUw|BTw>i1{|T88pXeJ#C%-?jFL6ED zyDu+q+o2#b)B0Ywv~AlE%5E`qnYoyZEQ=F8p0SQx8ITpfX3ut#IqrmB-m*N>uH|6$ z+XDlM+Syfkn`dVt4c$7qmyAUP^jY;82WUffM3ue_6DPj1^lwyE0~PK{cLmhBjk zIL0Q6tj&GpA2w$R`nLFOZs6lBq+Z+THrfNT(7SKL$DSB5me_r{pgOC`1~ft(apvi& zZRq3V`tNHt%tXT$?-(fFypt?88ogk!#$MFKIPl)yy6ed7-kyFguQQ3^-9c?;>cpda zT@>48MWrHLr|lEtO*fG%(~OtJ#U-PaOP#yw#Kj^1nqhMubz_KYqIpAfIu&WgCtZ2! znM+F3drcX1csIInqd?LAiy_mn& zV9cg;;{SD6uRH6Oq8?l8*lXX&CSkWC8|b^_pdPojSk!ULAPx^_?rR~)B1NuE77q;1 zf#=`D_BB<0bNrkC>G)5oe>5@td<1D*d+)=WE^A4M;;dIAla~_HtZ`lJE@YvL2m0IG z>5z@uUD|xSWAZMP6)?_rL+><#v&=o)pWI1S%&}~-AE(lKE>|8cUP>&Bd{5mzv;vJw zs9*HncRd+&|KoXwK`T)kyOf4WmvYdA=jLje$q{7Vxt4R9UD-qeLLc-wo*PHrIn4gH zyHzf-%&fU|=$&kIE6Z+gAIte@PCe(k{X6d>PW2wzCIse@(uH|7n9ftlk%ZD3O|`}$ zgZi_FwD3$L`@eQATb-6edVgxN{>Y06v|j&FfATyX)xCbx`G`**X_<1xIepL$;zODh zrCrV-_q_D3#G7T2Rwm5gqD~>`PDJbn`xm>2YJOuKoy4b?OA;NqYMGt$Z0CMNH$D+C~kv-pLkV>Y@dc^##?W;Zh2VsLW( z*6k$Sw0V}vz&#{tvFXJj-SSX}K`mZMQnN|N!1#R(RFHXdz6cID;-xnn=_gCB<@#? zdcTu=MWd|Hi4BOP)R**cycj8{JQ}!Y&3f{Afc2O3wNuEt-5vecU7Uv=j}M8{N@viW zr1f*in=L3jVDPt7b9SJV1JhTdMZ1w+!>$uAep!vWcd!Tu8rt&4{9^o-U8EB_xWy!43z^!rR>>5t;ppBg-E%8W?L|?& zZfmOL?LddtW-BHXB$3aJH58h6+(|sLT{S|#rjjpRgLc=)S;WN5A$h~*6m-pZy>o4+ z-Dv9p-BxR}#*xYO$7!S-&p>YncL?b%SdX&Xez{*@Jds2^e^D!OaU>dzS~ea~JBjpj zZXBCXmW%wY20u=Cn@OgK+KqP^zJ`>XN_M=}Z6>+2WcID`8avT7b&K)l|i8OW8UzUW5wB8O&&&oj3zDqOiCV7!N%htaS*4lzv z1YdQSRF*}YYL#5n-jYjh_pCi6<$e}P-`*xGX&0Se&lp4>?vROEkDAtLiB&2}kPO_K zAH0URZJA+sZopD9r);IS-i90!-_l3gbSFdVY-;*p+tYYdX7}uNhvb#$Q^O~t$Hc55 zE@Le^6m^S6SMNDZ#`Sa1Ww%9pciqfD4^y8{S=}{{Oc!bny)!6>_t2#0J!r7sev9`BKf(oSZ@u0OJX zT)k-EvHoBV$x2jxb8u%t({60rwr$(i#DM_?61)X5eazey-^sEr#qQ>b=wHWDoCX`=5UtV+JvM2P3-Roks;?Y* zx{OtMrJgpJH1OQZJOW%HYi{#3=o^fvXRgE|<>fjP|Nb^f+B0?EbA8nL%7$55E^g@n zp9G?FZR(8{A{I5oWd2t&0sVMOW`O;(^4Pqn7jNM>Ct8kV*5t^(**gTawZm4Kv$*XN zE@pF`UpILBeA^{IYrDQGE&i_6`c^s0>mBfvm~WTl!M~I22*At>Q|syV9CrD|9_L+k z?(c1zs@$66+{3z+ln)f%EHICLpOtI8(aJQ9XRC*xUmu>3q+m({^uIdtxgai?tscEt zy#!k=X0DZE5{+kb(l5$tC|&%#1N8-ik8RnX;ja%I(4v9vg_kb}%m@o%~LG%;IsJ4#b-M#4&VZE7hG z`Wb|MMis_89Zi56wNr98%FG~C-g)lslM0VYY?m8fb1+AXGb!6aa4W)#FlOJ|5C&u? zj*EU*uRz3AU<&PtZQl=Z)SKfSGfdYk~P zVAfUn>^=>@`vGs>8Gzg$`ftOpH=pl^o448i6TzSSpU?j^0uJ70AzbU9hk4A_7aQA# z01pKLvjN}yKW{IeC;x2#cz=E!y_={%y+6>N2v+-kzhAvx{I|~|_owFkhR~cN;Qb}x zznX8j{*8Za|608J`Hzwx{D0GMevSitzxV!Y0PjqI2KV>R>*w(QqsaO5=k6`y|7zj? zv9kaEuY&Fq!D;&M|5zRH@>$+_^xtVW`9Ey!@BdlL&-O#X@ydVA@Avs$+&cd6(>!Ja zvNyl`oxT@`4h8>p3_bp@y!>DJ`M+}i|J#|LjE4e@^dI>XLHe5S=Kqla0bfq1Ftb4I zglIIPgx_248H9G5pA$bn_XqY~%`Uca6J5QOmkW<>^Cu0Tcby4>C$c`7ot@r26PU6v z(cO6cCO76(@JciEe*xd}wQXx>s{ZZQ4>RsvuhV^}9WrMt=+UNrg9Ds(tqL2Am;G0B zUHI%uP1npn^@9fYv(G-cR^Cr_ehbM%@f-9W8}&%8vRpPleE$x5V>8kD@=uGwgAXx; zjywmy&!;HWENiOqp~)xhC>~&hE zWd+_m1dj*JjcaczVw+;R8i>_zYr?%NkLKHG9-3S-eF4gHwx6$0+uASC=&P7Ez5I1Dp}fnSXNCsH zLKJVdC;7UKU%*aP7N6&4OPC+_oA}}KTE+3thyYw8|_*ZxJPU3iINNo4_T+vkqto!R+C!yQl{d$H+J&9lEzn1kMi^MPp zXIKyt_a8%>Ta_qTMhJ+g7;1lW)084Szg(2i+n+A8i{&6yk=Gm3HZpsYuv<80`1~XR zJcl~Eqv`>7{R_kT&uGD;rJGx@HkNXnGkOA4EF?u!4L2yZN*qAh*f3HaPx;@TPzo-j zWNb*Du4wOu>^vUqY<1h{F1N9`pRnpWmOpa7{!W;2hGCyqY@@}M$C2QYpeGE)8-!C0 z`mGL38V=lC{rl`ae+Zk2d9TtPAU_ag!+@cyr!Nmc^dQ-6D@_V;>M}p=Vh8;D6n~}+ zW5$NEdp~^e;WV2wY<R8}ESm$uHuKFL z!VEO|KN5fY8f0?yejRN6yUp!jE#HVBod{!sll~e2?O+UOa{16-pbmdlv?xIMTsdfD zD!2VgM0XH?$w{YaZ?{|e(owZ-{u1^(UPkoX0Gsl0{kF+MHiE>c7_KPDg*M1V<#ce(uE3r_kqr$z4r?h_&+J-DrnTVDj+aH|8<$ zWeeEbCEm6=Mz%<6UX1dFHe2j8-;mOv&gfmT*_|{Xicr3k#t=eJhK6X1zk0-Zle%i7 z5cCcnv?k*Jgf)hn{R=YF|Ga+93K|5^$^V#sVZR=NWHJASe>*f7R^*Z<^kdWVI~9}k zQ<0?c(k8Q%)MsB1Y=MjA0NmAL0 zv^Cz)Z?%0Q3fM}8#L!{WA^OQ#m=#ZOIO-?B_NV^z0&V&>8-i;d*En2j)X2lFI7NQx zk&HM;49Emf?ux0%tK&bG)^1ndj!uu;EW+xNeP>aK6<6;i+&J}URois01=+;Mg-2&@H z;4j>eluwZIWkdvAl_uyP0enh!`Vp*n<$Ci+Cs(LGlNQJe7`4gN{O-O6MA@ZMRyI|i zu5gIY`p5>y*RK+}hRdV0)o7O{#wyuX>&R8yIIlBo*YnxNzgV;aAID2&jb_!``I-Rf z=A7F#?y$~9S0Cg8H>t{-;gUD4JAiPpqb)!8!=ILy<2|*1*8uyeL!*-ufBof~)wRJU zN%}Q9o+y`Pk%cM~iWH3`u9i)zmr=xG@mEc1lr0kmy9&_v!3Q{@dfvWC0yz0zf4sb! z5Pn#;8=Pog=$>ewxLw=mIS}VM9-1dO??Az7oN(~1_r-zgbn#1yKL$UH-4Gn4y2%A} ze6RR^EATm9!;aW!D@|(xH1VD<|R zVF~fu4JmKVN<>=pTZt`fUKzRr*|!0tU_JE+R8J@RMC&{$SLQJ6AWj5!X#5=GP|(nz z5~>_ciQ{XC#h^!8h=RuZ1^*ZlVB;6rLNoTp@=>w&HR!~nK~>tqo=M3FA@E^T9&odT zJg&K^t<0zf8}?azHpV;-5LLJOSP38kW5{&-epL>NC`w0 zLr_j~gOyt1u6)rXKp-M1hn=#$vI?Y47{~v$ND=DGY9%z`r?R^`?yr7{z;4O2a_gQj z$z9T~lRug3gyOZ>+rQhw1-%R4tv0SQdCr&+*dJmFre+z|$ zvg#bsiPken8n#O)DKlURJk4;_wCBKO!aJXEpTigL>)*|F7;Fo^_)}KE(w-Ek*cm0^ z1=}mNbTyV<;)r3yo$jF6nuD$7>7{G2B@RB-(tb%LAP0OZ?$NKoWC6ZV(z0ZZ99xJj zYf(ZbEg4=$#rx~eMBuBXk@Mb%!r}A!s=kxKsnD$OGxo#r6_I=Otu(qS%`Fw>^i%K# zhvVlS2;hs?b+`W2VfJDBBjS$n8F5^2-)2HkG}KB(4>fiTA#JX*Hnd91#?z8@&`Eeo z)~th}cn&IE^9}j1FI#ePAfI|<1)oXa2P;pv8SYSB;bUOb7D&hS!E(0o+>jXVTMT6J zZ)R}mnR3C$SD^1kU3IPrh&+6sC@!in!Uvrj!z$(Y;I!HPLaadOt=#w#r7fc4>gur5 z`g9|l)^x~S@_oYM!0eUx`U*AfslJUt)yZ!-#9wse<>S-?90y;pMZ9Ty^L6n(mJ;4; zAE#TscEfNyrIwA1b=t7afWaHL4>)FbAJ@!5dl|T7PBvfE!^Yr!IG*v}w1StCEwP!; zGJ{TT1Ikh~2-{uZ;#y-b!k)`{Z0?o7TjmE}xyh43MHEr|(yfbBN40G;k*EAxZ!>mL zt4hK(*+y+`95N{W+)ugGJBsM`{mBg0OQeW53}$|V67ND8Y^8se(6PBt?D|ZZx-d=V zrDR(Vk%I8l^_uf|k!`J(ChCfm(0Tqs_4cdRLcIi(wngz(LrE?Xp87~hnUEEq09t)b zhVlBr44yd9M}vPcmpcA}i5;*1X`brf>rr%WUOnz+k|0YK7lK!3a@XrC@9X%i7FT|l z$72pO=mIc~z|Q>62rYlsZoN0&*c*|QsGAU}_a&C_u;E!7_#w*}G@_IUbF!!56AX1` zYaiP-1jd>;vqS4E>vCjpxgIs8SgFzfOngUP9-+zYhmtVpky|Sc zUzZiU_H$B)5Fl)~RI&8dI^_DR)*ceGJ6-Z*v4>|i&nQ~sQ@!Xg?T`5 zlFexdqOOaBcJ?vK`U_&=nhMfVkX+a)TY5|S1Bda>`MLM5^YB(KYfNKLuWl`5Y+q)i zOADv;Rxx_}XAR_FbSRZn4^t>g z>DAxHpM(k5&*jfeGJj-GP0Szh3ZUolQU@by$e_JRzw)}9IiLbc=!04D0#H*BdqrPT z%B0+|(Z^SKhylRl<9re)&{j594SoLw4;g=$gUd3J&ao?w>#5-F;G zxdZWBcm^^IE)mEObaz`#+#o+ay&jxh5~n%UB;4?eS}1uPS0E91)UGX zL-uxdJcVSZdQDMIppEs1hC!Akfv!ke?|S6WDwgf?s2-)jyrMwNij)O}>ifgmGl%g@ zBE6M!%@;sq3dp2F1ffE#17w0M8I5-R-xQp8D(j`_4D81z{4E^A!R--^*n#p7eo-lfCj@Fs7@&A);b<^z zora^`QXNu%f!(gQGl^C$H>O$*yPmH=T%;29*a2e z;5SKY`nQv1y0X!?SOV3)+@VQ8Bc@5%j9o$gxKZU;u$uN`95L&3dH#w$spXGVwbE3~ z@SJ$!XRl-?!%Fjy=+lvW`WnhXi=a&|#!9c1sxMnHUD@E*KfZup$O}xIyz|}_Mh7f4 z5K&*zw^^**dB2OPA9u~=K{<5ST58AgD7~}S&NlTJ2(@1fb67jM>&CK%+CS)MNTlS7 zkbl^&MjTY;VqWce=l>YReqm8i@p(cOGa;v{HKUE5V_nNaoB2cb7?9>%^|UnpUD}V` zNuaO~Uvmt11$Qp1K*@+rd zj9~r_ye-ov=a2pKCUW1fWKLztBfElc1vj!|`-Hw>3q3EJ5C3=s*_Z_jBid7%p1Cpc z&OtgN%4CxB-dmncp*19?)Ca*A870Uqx#S@U`TRFdLtjBP?9Wi%V6PIm#5o~1{pt+l zupU7`UL^a#JHpCpI=)&6FwIbcyJxmYJEFawKLDI5y<}*AoIhQwdvt}uZ{lmNIPWOD zCIhS|@8iAeN7ZfWYlXPL8P?ynjr8^V%h4!P+oA4@*c2i44#O=a=4=3^Ojxf@#pU(a zvDTubxvUf?xu+Dl8gjlMwt68(0F**FZQ66ggpsSO1zG_n-oSPKSrrS^%0PH=EI>e` zG1z2?7!$H?VZgDBHBW@1={6DQ<@o#{yyGG@aH+>%^dFoZT9GdeY*IyqV(sgUrP{;Q zE6lZswP{!DC-IYWlmGxgVA2Era|)s^rY{1H%-TTeJtSS%^w&cehcE;Rb_l@|@TS@E zmQV)Vsk4mC<2@ZK7JVH&f5eqG{mCnQE}(Cf^^|Wfn&p_*x+w*!7=Zp{vCi=rc?a%S zJ(TUMY7iw`a^U>!1bv<0OeUSfX~~%K;D)6PiVZjMSso4z)D96Pp>GvDT_QpGbeD&M z>&djFV3P+Cp2=5jW|I~QFt?|vQ$)1v#mRJ*p8=xoW>wJ0lLD^|mXOQyhuIbW_L3(! z;y5I*30aDV4;m<#9Mk_E=SK2*tenXZfo5_kJ*C)tlvZ+k)XC#I^4c}E@-@6S$5Fa! z(b?8c?^vA27E<{A?j8<|?MM|@?TQ--siDc)s%#(! zwZRQN4%-W?VXj}lq;#Sk-ti~cV+YK4| z9mY&FES!^my+}TwC&DjUPUMk(`PAS5dGr`%cXKfS z*}zu5@m%K99=XKFuZ=B zz{vU#FeF`1Po-J0B`PozY8|v9n3wWDh$dJBg;Tpnln|yng_B4w$36&z6(3FXisf#g zdRIzo5d`I{=RPBJq5(eD@h2JO`SO8YfwL&+v|2tACe1u*2u%p&jbV2gIGa&`5ol!_;-2{G4 zJt36AJ@1yW&A()Kz{z^?ib{1Bc3}qyR}orXpcrNqvkK%@zDm56W0uhu%kHtv1QXJ3 zz>yY&XwowXh~0JsUxSqFc}$7{^D)^IDIFtkIr?;a?p%8ydO3_=>{5^)pr@PRIJAYY zY7YAfPTM6;H9WXIh;Ot_wVL|%gF1_^rfkaTdvXAA5BEO0Np3t}-@Lj8*{=(Z$VmBO za3mx4fxZ5zpzi_Hg6KQbBZwEIo!o3aM}|@S_dF!pK5gBSroB?_CxwkjvGOUtpB$IT zNJqy#9smxTRtc#MkPJ@Bc!05vaiWUxjGGOMW^?sesy1{ESKaOM@AX8MaBa zEj=KZk4K4C(wgy6+s{%45hlWni=1y$BP^P|%<_L6RWB##qgJHuA`=vP5viLd77T>| z>G(!@i6{0OZb)8^7X*VxNG@WWKccY%MaAPgEDUFePbyN|WyUG0nC(Py7T}i8NMyvs zak63Cai&s(CwC*(lVYJ`gJBv*J|GKv%d^${#({9@hn*;F&_ql0vRc{65c{+lR)JTx zX{(nio<7y~f(!n8lObW-T~Y;!e}@(@SpNjAf^cx35x0Df_mXo8{0X)>xxCOI{bPyp z&cClF!s8hpLBlE(DeJ4 zY?D5|{4J#6gRir z#Sawc<;(qcm}?iP1SMnj!g6$#2L_p}yISmfx}NbIzznXkGZ_r@yrPgmy!m*qz)9TN zYue(v-mX}%djj$l@{J@(Q$-b))0bHkpRTXeJyo)E-I1BvynczmV7~g}twLz+-Wzdk zOu6mkMEuDxLN)t%%z=3j}5wh zaQlAppDf4=pa!=!6qMDt4YD>~JRao)l_h}S`&$h4c@e}jG@lRn_KQI#FLa5EkL2@I?MemUx4C@Iu+<**gNF62tpV>=iCAkm|Lv(dnieDjqi-6I}va5s04QI^z zIKT#Uke<8XS&I({Wec@*buvGs+~4Hg*!IqnmRzxqO#?D{h;|Auc2fyB{RMYN*G{tB zc4qu`zP0FEk|LHApDEF$M&%ICDx4W22<=g+m3dQcn3)_fP4MsT0{E`WJ- zCEk7W5yv#Vv?wU)uKbko2&A%vRxE#{G%&3*I!mVnq}D*rJ)2er1QET?-r9d2A{0)& zT*NvbR9&Z@-h?dgG$!UnySKfi_#9MVg;h+?@U~eoeQund2(9Ai5<4%!TrW*4u{Rg~ z@p|#_Vf_S@=VCYT#7A@xDQh|g*KBrPitzF*_?2{NTsD5Gv=>_a1a+-Waub3qEza42 zY^BhRkfR$?qX#U)D~H876$e-y9vM1MpEDa1C+?lXwTL{54{iAX-}1SwiDwop)7ZeG|4lg)n^0V;L5ICf%NiN~>{+mvN0NXH{nCG`Hl<1RnNZ-xi_Ue%)m z@SBv2Nt-#0*nib1N$wd_`W*Y2h3eLl^jbazC3V3?b43Z!5k zMQ9j##l?dkBBw3WflIdHu;vz%?Y>Z=m&}$Ok0W`Bg3X?OHDu%7(W?>rx<#Tx^E-B~ z=qnRSdvfH**vbnHnfI}}LWq<4?Blm-8SqATw8bZehL4kyY<7{~U!@@Rsx6jG$AOG@ z?pd_k;>?oJ)7Tw#R{<$UQ-CkQmzu?TrH@SzpO+rPa(!4vOOJ6;bV z5Cr^5Y1i<|K4EjVDKw+oW+0eYXSUTU0)#@9T;ArB0_}X;>hjfj zOA6(!yw)w}0IQO2>?lhu2%-GYd(Y3rg(eLhM^G^-EtVSQEuj84SN!;C(FNu$1Cca;4~$+p zw6Qb0apkEp*6+8SbR&;<>OTkL`b`I$J;=o}jXt>C3yF9WA%9@y2BpqwXR2uJsUz2M z6Z_OTusg-ygY^eEkURC()I-klg>leIZJT9%kvp4*G?8L}LQObim^Y{qxSgi2gD%kF zP1W}D^~4;2?i;vkZvJEW_UFGMUjjh`I?)h0rzS0Tg}W=5 zYrZX z3C}|P4&Rt7d-cregD{kx)!UHlLW&2|SEG?H2`7%*zI|J$9^~6Ph=CB*UBud(0v?8n ze7L!E0$8Goy~=J&Elp(^`1`@7TYsxO^t%u>SdFnCct*q0xB^608uHbM z=MyI!3GfF|kY(=IB|bTpsO^LvQF&k*yS6|Ik$U(aqLbG;5#pN7IS#*Y0}xep+=f_2 z^9c4?$Yrel7-M7)N)Z`{+d-9=!vWH*{o4bTC_sAozSb1zm^|pdfpVP7)T;L5i%Ga5 zgf_~(SL5%5O}f1I`PhTN(4A zKNP{V>@{h7qVt;`$Ivf^u>-qF>%chU(}h1tPb}bvYoE2zCc&UO=NA0^l(8_?Q|m6< zC+w&i_m3mn$hTLz5bGg4zXxF@3e~YAI?Tc;>xTuZ#VOiZ4RtFxTj2K zwgv5ou0Wq{^oqMkXd#lLx!f1le|jDh*7dUs{+tf#QfYJg zVeoZ(4GN!&0z-A*sgG+bLrVc-ETiY+A&7&aO)itq2X6h;I8UJX4fhpLm+pAM*kp%T z+%w06iQ|Mr%1^ci&?;e-HShQndCyP3*(VS@yZJUR^8E0mR@TpA1l7Snty@j`Hr0f} z$c{VR5aA7Zn;tH%7{G-jPTW*R1d;J7G@f>1z5f-bp-E|Vh!2S&S?#u8TnpiDwuE-! z@iXM4cO`nJMm=Tn^6S^{hej^7GB!Tnht$DyzZTR(g^rdRIV+$*Aaz_dD6N6EguU%< zPj~^FTWpvx_p|`UVnegamU#@)M*xso!{go#MZ^^6;Rc@&(Dg=OAO^wA57+C|Q3109 z6Nl3{s5(wtTl{kGK^V^`_Hw!EhEAkNNNDS-h2$~7@p)3ZhQyzrxObL$H$-KnW)_)- zW$=s0~<0gfEKm9zp zgvck7gR~VuMJ9mI1&${%+V~V&Hh8hKXeXXiuHx*yc_N7V+uWt#_l=Xhv5$|& z%PiPcGz-L~^Y@eXIT?r)`50C;yK8r0Q2FS>HkL^1%nIz|Lz*n$Y?^gAT=;OVo%K>U zq74LdI?#h)4Qbz#QIYh80$OSFACKc071$q@XxM>tHOCDZ;ZadTl*XoB!(;Cp9MhbW zjX4WGaVm$?ehRc>FkW+BnXXZVVB)cSsbfjw-3L^W=Npb>A|7fdOWH+!B5a*{_x4y6 zq)i9+2lCg*#MA=1NBTu@g9N)xgS0a!_q-|!#HcxlRrUDQKS)@?K)g7Ehf4c|Pd@@* zHg5q~KMOyBpQO#{pTou50AC^-Mvf`oVl|Y)ki0!w%G(8uAmx!L3dKwKT_-20A@OoZ zMF$U5iy%~Ym$lPjW zVll#pfX3xSJ|)b z)Ral~suuL{6a5_DSZd@Q-+tK!fwPxQV*^KhP9K| zBC$%L``++35W4VI9M+*WBig^;myjRdVyr!d-&>WZ0qZvOJy=C4U_T$)9;mo;1j3Wo z{-NYwZzzq6iNyx=|0WkRse-8wE3`iLc*z@GSaTu2NFJC4nR`lCZe9w+a6cOD#fi~W zaXgs`QWrHMTF*|SiBN{HPsR4VVgd1Ezbk>?{NXsD@KclCi76$lQ^`%jwY3Zk?W72Da7uA0# z%08q7Z{HCv&M@1AoPw?2*QEj#n%OKibz30ZMK90a^0`4xA@q~--*Lm4wvx%aUKD`U z*HmLR3A_P|zuZW>s+0p2t;43@fN~HypFl{ztWi1cO8*WCD8jWg+N0q_iUZ>TwXQ8z z^b2(0H##9mk=k8FJ+V&32l3shv~LW!1gF`b380{a1EhILX0qH_?W%24v2f|Q5 zj1>DDf+~mC&QXR32CPtCK2`1(1VL}|>MqYq)W6qf7?{OQl~&`7)$A2fQSkNXg}CiR zBr8VnI=_Xrl>4etDCCIeX!*2dL1F{~98=uvu@4}EKT?0GQ-TKlxy70-Tg%spne{ z3$mxmD2hnkZxic3nsN3*=00-{kny@cUQ$gz8#rv5$#KW*FQFzlErVGGW5j#7 z8_k^)$A09G@fr!(N@bWlaUGF%wlc{E$4p>IaRkbI(kqNP5olisy($B>gHc2mdhD5uIGQ0_a-Ac%`GXD5i*$v

D4^4%>~b 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 A-v9sr literal 0 HcmV?d00001 From 1bf88e34bf3f1550b2f0f761af725380b85da2f3 Mon Sep 17 00:00:00 2001 From: ppegolo Date: Wed, 20 Aug 2025 17:28:14 +0200 Subject: [PATCH 5/8] Restore arch defaults for the loss section --- src/metatrain/experimental/nanopet/default-hypers.yaml | 1 + src/metatrain/pet/default-hypers.yaml | 1 + src/metatrain/soap_bpnn/default-hypers.yaml | 1 + 3 files changed, 3 insertions(+) diff --git a/src/metatrain/experimental/nanopet/default-hypers.yaml b/src/metatrain/experimental/nanopet/default-hypers.yaml index 06d57e83e..ea9202133 100644 --- a/src/metatrain/experimental/nanopet/default-hypers.yaml +++ b/src/metatrain/experimental/nanopet/default-hypers.yaml @@ -31,3 +31,4 @@ architecture: log_mae: false log_separate_blocks: false best_model_metric: rmse_prod + loss: mse diff --git a/src/metatrain/pet/default-hypers.yaml b/src/metatrain/pet/default-hypers.yaml index e04b69d98..efdce89d1 100644 --- a/src/metatrain/pet/default-hypers.yaml +++ b/src/metatrain/pet/default-hypers.yaml @@ -35,3 +35,4 @@ architecture: log_separate_blocks: false best_model_metric: rmse_prod grad_clip_norm: .inf + loss: mse diff --git a/src/metatrain/soap_bpnn/default-hypers.yaml b/src/metatrain/soap_bpnn/default-hypers.yaml index 2bb5b6943..3b6105a00 100644 --- a/src/metatrain/soap_bpnn/default-hypers.yaml +++ b/src/metatrain/soap_bpnn/default-hypers.yaml @@ -37,3 +37,4 @@ architecture: log_mae: false log_separate_blocks: false best_model_metric: rmse_prod + loss: mse From 52b693a82297d9968db1b4784c5381108e55732e Mon Sep 17 00:00:00 2001 From: ppegolo Date: Wed, 20 Aug 2025 17:36:19 +0200 Subject: [PATCH 6/8] Add checkpoint file for nanopet --- .../checkpoints/model-v1_trainer-v2.ckpt.gz | Bin 0 -> 11999 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/metatrain/experimental/nanopet/tests/checkpoints/model-v1_trainer-v2.ckpt.gz diff --git a/src/metatrain/experimental/nanopet/tests/checkpoints/model-v1_trainer-v2.ckpt.gz b/src/metatrain/experimental/nanopet/tests/checkpoints/model-v1_trainer-v2.ckpt.gz new file mode 100644 index 0000000000000000000000000000000000000000..d17bb06d3d21aeffa283b0a08273136ca5b4709b GIT binary patch literal 11999 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 literal 0 HcmV?d00001 From 66c3a903380c44129a84263925d6f62f6e02668e Mon Sep 17 00:00:00 2001 From: ppegolo Date: Thu, 21 Aug 2025 13:03:45 +0200 Subject: [PATCH 7/8] Update the changelog --- docs/src/dev-docs/changelog.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/src/dev-docs/changelog.rst b/docs/src/dev-docs/changelog.rst index 53a2ea93b..335ede281 100644 --- a/docs/src/dev-docs/changelog.rst +++ b/docs/src/dev-docs/changelog.rst @@ -15,8 +15,12 @@ changelog `_ format. This project follows .. Added .. ##### -.. Changed -.. ####### +Changed +####### + +- Refactored the ``loss.py`` module to provide an easier to extend interface for custom + loss functions. +- Updated the trainer checkpoints to account for changes in the loss-related hypers. .. Removed .. ####### From d6fbb29e5e062475e0abb313fd1f23b7e5b1a480 Mon Sep 17 00:00:00 2001 From: ppegolo Date: Thu, 21 Aug 2025 13:33:24 +0200 Subject: [PATCH 8/8] add pet checkpoint file --- .../checkpoints/model-v6_trainer-v4.ckpt.gz | Bin 0 -> 14884 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/metatrain/pet/tests/checkpoints/model-v6_trainer-v4.ckpt.gz diff --git a/src/metatrain/pet/tests/checkpoints/model-v6_trainer-v4.ckpt.gz b/src/metatrain/pet/tests/checkpoints/model-v6_trainer-v4.ckpt.gz new file mode 100644 index 0000000000000000000000000000000000000000..93fe64ca27b6c9b794fdff1c72c4146cda3640f6 GIT binary patch literal 14884 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