Source code for metatrain.utils.testing.input
import copy
from typing import Any
import pytest
from omegaconf import OmegaConf
from metatrain.utils.architectures import check_architecture_options
from metatrain.utils.data import DatasetInfo
from metatrain.utils.pydantic import MetatrainValidationError
from .architectures import ArchitectureTests
[docs]
class InputTests(ArchitectureTests):
"""Test suite to check that the model handles inputs correctly."""
supports_restart: bool = True
"""Whether the architecture supports restarting training."""
[docs]
def test_atomic_baseline(self, default_hypers: dict) -> None:
"""Test that the trainer can accept atomic baselines.
The tests checks that when providing valid atomic baselines,
the architecture options are accepted.
This test is skipped if the architecture's trainer does not use
``atomic_baseline``.
If this test is failing you need to add the correct type hint to
the ``atomic_baseline`` field of the trainer hypers.
I.e., in ``documentation.py`` of your architecture:
.. code-block:: python
from typing_extensions import TypedDict
from metatrain.composition.documentation import FixedCompositionWeights
class TrainerHypers(TypedDict):
... # Rest of hyperparameters
atomic_baseline: FixedCompositionWeights
with the appropiate documentation and default if applicable.
:param default_hypers: The default hyperparameters for the architecture.
"""
if "atomic_baseline" not in default_hypers["training"]:
pytest.skip("The architecture's trainer does not use atomic_baseline")
hypers = copy.deepcopy(default_hypers)
hypers["training"]["atomic_baseline"] = {
"energy": {
1: 1.0,
6: 0.0,
7: 0.0,
8: 0.0,
9: 3000.0,
}
}
hypers = OmegaConf.create(hypers)
check_architecture_options(
name=self.architecture, options=OmegaConf.to_container(hypers)
)
hypers["training"]["atomic_baseline"] = {"energy": 0.0}
hypers = OmegaConf.create(hypers)
check_architecture_options(
name=self.architecture, options=OmegaConf.to_container(hypers)
)
[docs]
def test_atomic_baseline_error(self, default_hypers: dict) -> None:
"""Test that invalid input is not accepted for ``atomic_baseline``.
The tests checks that when providing invalid atomic baselines,
the architecture options raise a validation error.
This test is skipped if the architecture's trainer does not use
``atomic_baseline``.
If this test is failing you need to add the correct type hint to
the ``atomic_baseline`` field of the trainer hypers.
I.e., in ``documentation.py`` of your architecture:
.. code-block:: python
from typing_extensions import TypedDict
from metatrain.composition.documentation import FixedCompositionWeights
class TrainerHypers(TypedDict):
... # Rest of hyperparameters
atomic_baseline: FixedCompositionWeights
with the appropiate documentation and default if applicable.
:param default_hypers: The default hyperparameters for the architecture.
"""
if "atomic_baseline" not in default_hypers["training"]:
pytest.skip("The architecture's trainer does not use atomic_baseline")
hypers = copy.deepcopy(default_hypers)
hypers["training"]["atomic_baseline"] = {"energy": {"H": 300.0}}
hypers = OmegaConf.create(hypers)
with pytest.raises(
MetatrainValidationError, match=r"Input should be a valid integer"
):
check_architecture_options(
name=self.architecture, options=OmegaConf.to_container(hypers)
)
[docs]
def test_restart(
self, minimal_model_hypers: dict, dataset_info: DatasetInfo
) -> None:
"""Test that the model can be restarted with the same hyperparameters
and same dataset information.
If the model doesn't support restarting (which should be indicated by
setting ``supports_restart = False``), a call to ``model.restart()``
is supposed to raise a ``NotImplementedError``.
If your model supports restarting, but this test is failing, you need
to make sure that the model's ``restart`` method is implemented
correctly. Essentially, a call to ``model.restart()`` with the same
dataset information and same model hyperparameters should
keep the model unchanged.
:param minimal_model_hypers: The hyperparameters used to initialize the
model.
:param dataset_info: The dataset information used to initialize the model.
"""
model = self.model_cls(minimal_model_hypers, dataset_info)
if not self.supports_restart:
with pytest.raises(NotImplementedError):
model.restart(dataset_info=dataset_info)
return
# This should work, as we are not changing the hypers
model.restart(dataset_info=dataset_info)
model.restart(dataset_info=dataset_info, model_hypers={})
model.restart(dataset_info=dataset_info, model_hypers=minimal_model_hypers)
[docs]
def test_restart_hypers_mismatch(
self,
default_hypers: dict,
minimal_model_hypers: dict,
dataset_info: DatasetInfo,
) -> None:
"""Test that the model throws an error when there is an attempt to
restart training with different model hyperparameters.
This test is skipped if the architecture does not support restarting
(which should be indicated by setting ``supports_restart = False``)
or if the architecture does not have any hyperparameters.
If this test is failing, you need to make sure that the model's
``restart`` method checks that the provided hyperparameters
match the ones used to initialize the model. An easy way to do this
is by doing the following in your model's ``restart`` method:
.. code-block:: python
from metatrain.utils.hypers import raise_if_hypers_mismatch
def restart(
self,
dataset_info: DatasetInfo,
model_hypers: Optional[dict[str, Any]] = None,
):
if model_hypers is not None:
raise_if_hypers_mismatch(self.hypers, model_hypers)
# Rest of the restart logic...
:param default_hypers: The default hyperparameters for the architecture.
:param minimal_model_hypers: The hyperparameters used to initialize the
model.
:param dataset_info: The dataset information used to initialize the model.
"""
if not self.supports_restart:
pytest.skip("The architecture does not support restart")
if len(default_hypers["model"]) == 0:
pytest.skip("The model does not have any hyperparameters")
model = self.model_cls(minimal_model_hypers, dataset_info)
# Find an input that is a number or string and change it.
new_hypers: dict[str, Any] = {}
for k, v in minimal_model_hypers.items():
if isinstance(v, (int, float)):
new_hypers[k] = v + 1.0
break
elif isinstance(v, str):
new_hypers[k] = "new_value"
break
else:
# No numbers or strings found, just change any value to a string
new_hypers[list(minimal_model_hypers.keys())[0]] = "new_value"
# This shouldn't work, as the hypers have been changed
with pytest.raises(ValueError):
model.restart(dataset_info=dataset_info, model_hypers=new_hypers)