MatRiFFlow 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. IMDgroup/__init__.py +82 -0
  2. matrifflow/__init__.py +39 -0
  3. matrifflow/common.py +150 -0
  4. matrifflow/gorun/__init__.py +36 -0
  5. matrifflow/gorun/adapters/__init__.py +32 -0
  6. matrifflow/gorun/adapters/_mace_train_masked.py +218 -0
  7. matrifflow/gorun/adapters/atat_local.py +156 -0
  8. matrifflow/gorun/adapters/base.py +128 -0
  9. matrifflow/gorun/adapters/gpu.py +205 -0
  10. matrifflow/gorun/adapters/mace.py +873 -0
  11. matrifflow/gorun/adapters/maps_adapter.py +177 -0
  12. matrifflow/gorun/adapters/vasp.py +386 -0
  13. matrifflow/gorun/cleanVASP.py +334 -0
  14. matrifflow/gorun/core/__init__.py +28 -0
  15. matrifflow/gorun/core/files.py +168 -0
  16. matrifflow/gorun/core/incar.py +334 -0
  17. matrifflow/gorun/core/pipeline.py +345 -0
  18. matrifflow/gorun/data/gorun-config.example.toml +230 -0
  19. matrifflow/gorun/gorun.py +744 -0
  20. matrifflow/gorun/gorun_atat_local.py +230 -0
  21. matrifflow/gorun/gorun_maps.py +42 -0
  22. matrifflow/gorun/sbatch.py +253 -0
  23. matrifflow/gorun/slurm.py +226 -0
  24. matrifflow/gorun/vasp_calculator.py +80 -0
  25. matrifflow/pymatgen/__init__.py +28 -0
  26. matrifflow/pymatgen/cli/__init__.py +30 -0
  27. matrifflow/pymatgen/cli/mrf.py +281 -0
  28. matrifflow/pymatgen/cli/mrf_analyze.py +392 -0
  29. matrifflow/pymatgen/cli/mrf_create.py +185 -0
  30. matrifflow/pymatgen/cli/mrf_derive.py +1609 -0
  31. matrifflow/pymatgen/cli/mrf_diff.py +419 -0
  32. matrifflow/pymatgen/cli/mrf_run.py +69 -0
  33. matrifflow/pymatgen/cli/mrf_status.py +842 -0
  34. matrifflow/pymatgen/cli/mrf_visualize.py +1808 -0
  35. matrifflow/pymatgen/core/__init__.py +28 -0
  36. matrifflow/pymatgen/core/structure.py +870 -0
  37. matrifflow/pymatgen/diffusion/__init__.py +28 -0
  38. matrifflow/pymatgen/diffusion/neb.py +832 -0
  39. matrifflow/pymatgen/io/__init__.py +28 -0
  40. matrifflow/pymatgen/io/atat.py +131 -0
  41. matrifflow/pymatgen/io/vasp/__init__.py +30 -0
  42. matrifflow/pymatgen/io/vasp/diagnostics.py +224 -0
  43. matrifflow/pymatgen/io/vasp/functionals.yaml +48 -0
  44. matrifflow/pymatgen/io/vasp/incar_toml.py +250 -0
  45. matrifflow/pymatgen/io/vasp/inputs.py +228 -0
  46. matrifflow/pymatgen/io/vasp/outputs.py +803 -0
  47. matrifflow/pymatgen/io/vasp/sets.py +1092 -0
  48. matrifflow/pymatgen/io/vasp/vaspdir.py +1514 -0
  49. matrifflow/pymatgen/transformations/__init__.py +28 -0
  50. matrifflow/pymatgen/transformations/insert_molecule.py +661 -0
  51. matrifflow/pymatgen/transformations/symmetry_clone.py +309 -0
  52. matrifflow/utils/__init__.py +28 -0
  53. matrifflow/utils/mpl.py +106 -0
  54. matrifflow-2.0.1.data/scripts/gorun-all-ready.sh +245 -0
  55. matrifflow-2.0.1.dist-info/METADATA +136 -0
  56. matrifflow-2.0.1.dist-info/RECORD +60 -0
  57. matrifflow-2.0.1.dist-info/WHEEL +5 -0
  58. matrifflow-2.0.1.dist-info/entry_points.txt +8 -0
  59. matrifflow-2.0.1.dist-info/licenses/LICENSE +9 -0
  60. matrifflow-2.0.1.dist-info/top_level.txt +2 -0
IMDgroup/__init__.py ADDED
@@ -0,0 +1,82 @@
1
+ """Deprecated compatibility package for the MatRiFFlow rename.
2
+
3
+ ``IMDgroup`` was the import package name before the project was renamed to
4
+ MatRiFFlow (import package ``matrifflow``). Importing anything under
5
+ ``IMDgroup`` emits a :class:`DeprecationWarning` and resolves to the
6
+ corresponding ``matrifflow`` module, so existing scripts keep working
7
+ during the transition.
8
+ """
9
+
10
+ import importlib
11
+ import importlib.abc
12
+ import importlib.util
13
+ import sys
14
+ import warnings
15
+
16
+ _LEGACY = "IMDgroup"
17
+ _TARGET = "matrifflow"
18
+
19
+ # The 8 CLI subcommand modules were renamed ``imdg*`` -> ``mrf*`` during the
20
+ # MatRiFFlow rename. The prefix-only mapping above cannot recover those, so
21
+ # map their basenames explicitly. Everything else shares its basename.
22
+ _CLI_REMAP = {
23
+ "imdg": "mrf",
24
+ "imdg_create": "mrf_create",
25
+ "imdg_derive": "mrf_derive",
26
+ "imdg_diff": "mrf_diff",
27
+ "imdg_analyze": "mrf_analyze",
28
+ "imdg_status": "mrf_status",
29
+ "imdg_visualize": "mrf_visualize",
30
+ "imdg_run": "mrf_run",
31
+ }
32
+
33
+ warnings.warn(
34
+ "IMDgroup is deprecated; use matrifflow instead.",
35
+ DeprecationWarning,
36
+ stacklevel=2,
37
+ )
38
+
39
+
40
+ class _AliasLoader(importlib.abc.Loader):
41
+ """Loader that returns the already-imported real module.
42
+
43
+ Reusing the real module object (rather than re-executing its source)
44
+ means ``IMDgroup.X`` *is* ``matrifflow.X``, so ``isinstance`` checks,
45
+ pickling, and pymatgen's MSONable registry keep working across the two
46
+ import paths.
47
+ """
48
+
49
+ def __init__(self, real_name: str):
50
+ self._real_name = real_name
51
+
52
+ def create_module(self, spec):
53
+ _ = spec # unused
54
+ return importlib.import_module(self._real_name)
55
+
56
+ def exec_module(self, module):
57
+ _ = module # unused
58
+ # Already executed by create_module()'s import_module() call.
59
+ pass
60
+
61
+
62
+ class _AliasFinder(importlib.abc.MetaPathFinder):
63
+ def find_spec(self, fullname, path=None, target=None):
64
+ _, _ = path, target # unused
65
+ if fullname == _LEGACY or not fullname.startswith(_LEGACY + "."):
66
+ return None
67
+ real_name = _TARGET + fullname[len(_LEGACY):]
68
+ pkg, _, leaf = real_name.rpartition(".")
69
+ if leaf in _CLI_REMAP:
70
+ real_name = f"{pkg}.{_CLI_REMAP[leaf]}"
71
+ real_spec = importlib.util.find_spec(real_name)
72
+ if real_spec is None:
73
+ return None
74
+ spec = importlib.util.spec_from_loader(fullname, _AliasLoader(real_name))
75
+ assert spec is not None
76
+ if real_spec.submodule_search_locations is not None:
77
+ spec.submodule_search_locations = list(real_spec.submodule_search_locations)
78
+ return spec
79
+
80
+
81
+ if not any(isinstance(f, _AliasFinder) for f in sys.meta_path):
82
+ sys.meta_path.insert(0, _AliasFinder())
matrifflow/__init__.py ADDED
@@ -0,0 +1,39 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2024-2026 Inverse Materials Design Group
4
+ #
5
+ # Author: Ihor Radchenko <yantar92@posteo.net>
6
+ #
7
+ # This file is a part of MatRiFFlow package
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ # of this software and associated documentation files (the "Software"), to deal
11
+ # in the Software without restriction, including without limitation the rights
12
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the Software is
14
+ # furnished to do so, subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in all
17
+ # copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ # SOFTWARE.
26
+
27
+
28
+ """Pymatgen add-ons and cluster job-submission tools.
29
+
30
+ Top-level namespace for the Inverse Materials Design group's tooling.
31
+ Subpackages:
32
+
33
+ - ``matrifflow.pymatgen``: pymatgen add-ons (structure, I/O, diffusion,
34
+ transformations, CLI).
35
+ - ``matrifflow.gorun``: Slurm job-submission helpers (VASP, MACE, ATAT).
36
+ - ``matrifflow.utils``: plotting helpers.
37
+
38
+ See https://www.oimalyi.org/ and https://yantar92.github.io/MatRiFFlow/
39
+ """
matrifflow/common.py ADDED
@@ -0,0 +1,150 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2024-2026 Inverse Materials Design Group
4
+ #
5
+ # Author: Ihor Radchenko <yantar92@posteo.net>
6
+ #
7
+ # This file is a part of MatRiFFlow package
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ # of this software and associated documentation files (the "Software"), to deal
11
+ # in the Software without restriction, including without limitation the rights
12
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the Software is
14
+ # furnished to do so, subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in all
17
+ # copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ # SOFTWARE.
26
+
27
+
28
+ """Common routines used across the sub-libraries."""
29
+ import logging
30
+ from pathlib import Path
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ def short_path(path: str) -> str:
36
+ """Return the final path component for compact log messages.
37
+
38
+ gorun working directories live under long cluster mount points
39
+ (e.g. ``/net/storage/.../generations.Li/.../relax.2``). Repeating
40
+ the full path on every line drowns out the useful information, so
41
+ per-step messages only show the final component. The working
42
+ directory is announced once at the start of a run.
43
+ """
44
+ name = Path(path).name
45
+ return name if name else str(path)
46
+
47
+
48
+ def configure_progress_output():
49
+ """Send ``alive_progress`` output to stderr.
50
+
51
+ ``alive_progress`` writes progress bars and their final receipt to
52
+ stdout by default. For CLI tools, stdout is reserved for actual
53
+ results, so redirect progress to stderr. This is a process-wide
54
+ setting; call it once at each CLI entry point.
55
+ """
56
+ import sys
57
+ from alive_progress import config_handler
58
+ # alive_progress 3.1.5 monkey-patches ``set_global`` onto the
59
+ # ``config_handler`` callable. Pyright only sees the function type
60
+ # and cannot resolve the attached attribute.
61
+ config_handler.set_global(file=sys.stderr) # pyright: ignore[reportFunctionMemberAccess]
62
+
63
+
64
+ def suppress_spglib_warnings():
65
+ """Silence spglib's raw C-level warnings.
66
+
67
+ spglib prints internal retry diagnostics directly to stderr when
68
+ symmetry detection needs several attempts (e.g. ``"spglib:
69
+ ssm_get_exact_positions failed."``, ``"spglib: No point group was
70
+ found"``). They are harmless and duplicate the structured warnings
71
+ surfaced by matrifflow's own diagnostics, but they interleave with
72
+ rich tables and corrupt the display.
73
+
74
+ spglib reads ``SPGLIB_WARNING`` on every call (see its
75
+ ``warning_enabled``), so setting it at runtime is enough. We use
76
+ ``setdefault`` so a user who has already chosen a value keeps
77
+ control; only the absent-key case is forced to ``OFF``.
78
+ """
79
+ import os
80
+ os.environ.setdefault("SPGLIB_WARNING", "OFF")
81
+
82
+
83
+ def groupby_cmp(lst, cmp_eq, title_function=None):
84
+ """Group elements of a list by pairwise equality.
85
+
86
+ Each item is compared only against the first member of every existing
87
+ group, so the number of ``cmp_eq`` calls is O(n * g), where ``g`` is
88
+ the number of groups produced, instead of O(n^2) over group members.
89
+
90
+ This is exact when ``cmp_eq`` is an equivalence relation (reflexive,
91
+ symmetric, and transitive). A non-transitive ``cmp_eq`` still works,
92
+ but it is only compared against the first member of each group, so
93
+ items that would match a later member but not the representative may
94
+ be split into separate groups.
95
+
96
+ Args:
97
+ lst: List of elements to group.
98
+ cmp_eq: Function that takes two elements and returns True if they
99
+ should belong to the same group. Ideally an equivalence
100
+ relation; see above for the behavior when it is not
101
+ transitive.
102
+ title_function: Optional function that takes an element and returns
103
+ a string used in log messages. When None, "??" is logged.
104
+
105
+ Returns:
106
+ list[list]: List of grouped lists. Each sublist contains elements
107
+ that are pairwise equivalent according to cmp_eq.
108
+ """
109
+ groups = []
110
+ for item in lst:
111
+ title = title_function(item) if title_function is not None else "??"
112
+ for group in groups:
113
+ if cmp_eq(item, group[0]):
114
+ logger.debug('Appending %s to existing group', title)
115
+ group.append(item)
116
+ break
117
+ else:
118
+ logger.debug('Creating a new group for %s', title)
119
+ groups.append([item])
120
+ return groups
121
+
122
+
123
+ def deprecated_aliases(module_globals, **aliases):
124
+ """Return a module-level ``__getattr__`` resolving deprecated names.
125
+
126
+ ``aliases`` maps each legacy name to the current name present in
127
+ ``module_globals``. Accessing a legacy name emits a
128
+ :class:`DeprecationWarning` and returns the current object, so old
129
+ ``IMD*`` / ``IMDG*`` class names keep working after the MatRiFFlow
130
+ rename.
131
+
132
+ Usage (at the bottom of a module)::
133
+
134
+ __getattr__ = deprecated_aliases(globals(), IMDGVaspDir="MRFVaspDir")
135
+ """
136
+ import warnings
137
+
138
+ def __getattr__(name):
139
+ if name in aliases:
140
+ warnings.warn(
141
+ f"{name} is deprecated; use {aliases[name]} instead.",
142
+ DeprecationWarning,
143
+ stacklevel=2,
144
+ )
145
+ return module_globals[aliases[name]]
146
+ raise AttributeError(
147
+ f"module {module_globals['__name__']!r} has no attribute {name!r}"
148
+ )
149
+
150
+ return __getattr__
@@ -0,0 +1,36 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2024-2026 Inverse Materials Design Group
4
+ #
5
+ # Author: Ihor Radchenko <yantar92@posteo.net>
6
+ #
7
+ # This file is a part of MatRiFFlow package
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ # of this software and associated documentation files (the "Software"), to deal
11
+ # in the Software without restriction, including without limitation the rights
12
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the Software is
14
+ # furnished to do so, subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in all
17
+ # copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ # SOFTWARE.
26
+
27
+
28
+ """Slurm job submission helpers tailored for Inverse Materials Design group.
29
+
30
+ Supports:
31
+ - VASP: ``gorun vasp`` (or plain ``gorun`` for backward compat)
32
+ - MACE fine-tuning: ``gorun mace`` (or ``gorun-mace``)
33
+ - ATAT cluster expansion: ``gorun-maps``, ``gorun-atat-local``
34
+
35
+ See https://www.oimalyi.org/ and https://yantar92.github.io/MatRiFFlow/
36
+ """
@@ -0,0 +1,32 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2024-2026 Inverse Materials Design Group
4
+ #
5
+ # Author: Ihor Radchenko <yantar92@posteo.net>
6
+ #
7
+ # This file is a part of MatRiFFlow package
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ # of this software and associated documentation files (the "Software"), to deal
11
+ # in the Software without restriction, including without limitation the rights
12
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the Software is
14
+ # furnished to do so, subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in all
17
+ # copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ # SOFTWARE.
26
+
27
+
28
+ """Software adapters for gorun.
29
+
30
+ Each module in this package implements the `SoftwareAdapter` protocol
31
+ defined in `adapters.base`.
32
+ """
@@ -0,0 +1,218 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2024-2026 Inverse Materials Design Group
4
+ #
5
+ # Author: Ihor Radchenko <yantar92@posteo.net>
6
+ #
7
+ # This file is a part of MatRiFFlow package
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ # of this software and associated documentation files (the "Software"), to deal
11
+ # in the Software without restriction, including without limitation the rights
12
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the Software is
14
+ # furnished to do so, subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in all
17
+ # copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ # SOFTWARE.
26
+
27
+ """Wrapper around ``mace.cli.run_train`` that replaces the
28
+ energy/forces/stress loss with a mask-aware variant.
29
+
30
+ When ``ref.atom_mask`` is present on a batch (per-atom float tensor,
31
+ 1 = supervised, 0 = masked), masked atoms' site energies and atomic
32
+ stresses are detached from autograd, preventing direct energy/stress
33
+ training on those atoms while preserving indirect training through
34
+ neighbour message passing. When ``ref.atom_mask`` is absent (e.g.
35
+ pt_head replay data), the standard loss is used unchanged.
36
+
37
+ Usage: python -u -m matrifflow.gorun.adapters._mace_train_masked [ARGS...]
38
+
39
+ Does *not* require a custom ``--loss`` value -- keep using
40
+ ``--loss stress``. The ``configure_model`` stress check and SWA
41
+ loss selection both work because the original class is replaced
42
+ in-place.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import logging
48
+
49
+ import torch
50
+
51
+ import mace.modules.loss as mace_loss
52
+ from mace.tools.scatter import scatter_sum
53
+
54
+ logger = logging.getLogger(__name__)
55
+
56
+ #: Threshold above which a mask entry is considered "supervised"
57
+ #: (contributes to the loss). Mask values are 0 (masked) or 1
58
+ #: (supervised), but a float comparison avoids strict equality.
59
+ MASK_THRESHOLD: float = 0.5
60
+
61
+
62
+ ## Masked loss class (replaces WeightedEnergyForcesStressLoss)
63
+
64
+ class MaskedEnergyForcesStressLoss(
65
+ mace_loss.WeightedEnergyForcesStressLoss,
66
+ ):
67
+ """Standard E+F+S loss with per-atom masking when ``ref.atom_mask``
68
+ is present on the batch."""
69
+
70
+ # Pyright infers torch.Tensor | Module which should never happen
71
+ # in runtime.
72
+ energy_weight: torch.Tensor
73
+ forces_weight: torch.Tensor
74
+ stress_weight: torch.Tensor
75
+
76
+ def forward(
77
+ self,
78
+ ref,
79
+ pred,
80
+ ddp: bool | None = None,
81
+ ) -> torch.Tensor:
82
+ mask = getattr(ref, 'atom_mask', None)
83
+ if mask is None:
84
+ return super().forward(ref, pred, ddp)
85
+
86
+ return (
87
+ self.energy_weight * _masked_energy(ref, pred, mask, ddp)
88
+ + self.forces_weight * _masked_forces(ref, pred, mask, ddp)
89
+ + self.stress_weight * _masked_stress(ref, pred, mask, ddp)
90
+ )
91
+
92
+
93
+ ## Replace the class in-place so that get_loss_fn and get_swa
94
+ ## pick up the masked variant automatically.
95
+
96
+ mace_loss.WeightedEnergyForcesStressLoss = MaskedEnergyForcesStressLoss
97
+
98
+
99
+ ## Masked helpers
100
+
101
+ def _masked_energy(
102
+ ref,
103
+ pred,
104
+ mask: torch.Tensor,
105
+ ddp: bool | None,
106
+ ) -> torch.Tensor:
107
+ """Compute masked energy loss.
108
+
109
+ Node energies of masked atoms are detached from autograd
110
+ so they contribute to the per-structure total (and thus
111
+ indirectly to training via message passing) but their
112
+ site-energy gradient is zero.
113
+ """
114
+ supervised_float = (
115
+ (mask > MASK_THRESHOLD)
116
+ .to(dtype=pred['node_energy'].dtype)
117
+ )
118
+ node_energy = pred['node_energy']
119
+ hybrid_node_energy = (
120
+ node_energy * supervised_float
121
+ + node_energy.detach() * (1.0 - supervised_float)
122
+ )
123
+
124
+ predicted_energy = scatter_sum(
125
+ hybrid_node_energy,
126
+ ref['batch'],
127
+ dim=0,
128
+ dim_size=ref.num_graphs,
129
+ )
130
+ natoms_per_config = ref.ptr[1:] - ref.ptr[:-1]
131
+ raw_loss = (
132
+ ref.weight
133
+ * ref.energy_weight
134
+ * torch.square(
135
+ (ref['energy'] - predicted_energy) / natoms_per_config,
136
+ )
137
+ )
138
+ return mace_loss.reduce_loss(raw_loss, ddp)
139
+
140
+
141
+ def _masked_forces(
142
+ ref,
143
+ pred,
144
+ mask: torch.Tensor,
145
+ ddp: bool | None,
146
+ ) -> torch.Tensor:
147
+ """Compute masked forces loss.
148
+
149
+ Masked atoms get zero gradient for the force residual --
150
+ only supervised atoms drive the force loss.
151
+ """
152
+ mask_2d = mask.view(-1, 1)
153
+ natoms_per_config = ref.ptr[1:] - ref.ptr[:-1]
154
+ config_weight = torch.repeat_interleave(
155
+ ref.weight, natoms_per_config,
156
+ ).unsqueeze(-1)
157
+ config_forces_weight = torch.repeat_interleave(
158
+ ref.forces_weight, natoms_per_config,
159
+ ).unsqueeze(-1)
160
+ raw_loss = (
161
+ config_weight
162
+ * config_forces_weight
163
+ * mask_2d
164
+ * torch.square(ref['forces'] - pred['forces'])
165
+ )
166
+ return mace_loss.reduce_loss(raw_loss, ddp)
167
+
168
+
169
+ def _masked_stress(
170
+ ref,
171
+ pred,
172
+ mask: torch.Tensor,
173
+ ddp: bool | None,
174
+ ) -> torch.Tensor:
175
+ """Compute masked stress loss.
176
+
177
+ When atomic stresses are available, masked atoms' stress
178
+ contributions are detached. Falls back to the standard
179
+ per-structure stress loss when atomic stresses are absent.
180
+ """
181
+ if (
182
+ 'atomic_stresses' not in pred
183
+ or pred['atomic_stresses'] is None
184
+ ):
185
+ return mace_loss.weighted_mean_squared_stress(ref, pred, ddp)
186
+
187
+ supervised_float = (
188
+ (mask > MASK_THRESHOLD)
189
+ .to(dtype=pred['atomic_stresses'].dtype)
190
+ .view(-1, 1, 1)
191
+ )
192
+ atomic_stresses = pred['atomic_stresses']
193
+ hybrid_atomic_stresses = (
194
+ atomic_stresses * supervised_float
195
+ + atomic_stresses.detach() * (1.0 - supervised_float)
196
+ )
197
+
198
+ predicted_stress = scatter_sum(
199
+ hybrid_atomic_stresses,
200
+ ref['batch'],
201
+ dim=0,
202
+ dim_size=ref.num_graphs,
203
+ )
204
+ config_weight = ref.weight.view(-1, 1, 1)
205
+ config_stress_weight = ref.stress_weight.view(-1, 1, 1)
206
+ raw_loss = (
207
+ config_weight
208
+ * config_stress_weight
209
+ * torch.square(ref['stress'] - predicted_stress)
210
+ )
211
+ return mace_loss.reduce_loss(raw_loss, ddp)
212
+
213
+
214
+ ## Delegate to MACE
215
+
216
+ if __name__ == '__main__':
217
+ from mace.cli.run_train import main
218
+ main()