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.
- IMDgroup/__init__.py +82 -0
- matrifflow/__init__.py +39 -0
- matrifflow/common.py +150 -0
- matrifflow/gorun/__init__.py +36 -0
- matrifflow/gorun/adapters/__init__.py +32 -0
- matrifflow/gorun/adapters/_mace_train_masked.py +218 -0
- matrifflow/gorun/adapters/atat_local.py +156 -0
- matrifflow/gorun/adapters/base.py +128 -0
- matrifflow/gorun/adapters/gpu.py +205 -0
- matrifflow/gorun/adapters/mace.py +873 -0
- matrifflow/gorun/adapters/maps_adapter.py +177 -0
- matrifflow/gorun/adapters/vasp.py +386 -0
- matrifflow/gorun/cleanVASP.py +334 -0
- matrifflow/gorun/core/__init__.py +28 -0
- matrifflow/gorun/core/files.py +168 -0
- matrifflow/gorun/core/incar.py +334 -0
- matrifflow/gorun/core/pipeline.py +345 -0
- matrifflow/gorun/data/gorun-config.example.toml +230 -0
- matrifflow/gorun/gorun.py +744 -0
- matrifflow/gorun/gorun_atat_local.py +230 -0
- matrifflow/gorun/gorun_maps.py +42 -0
- matrifflow/gorun/sbatch.py +253 -0
- matrifflow/gorun/slurm.py +226 -0
- matrifflow/gorun/vasp_calculator.py +80 -0
- matrifflow/pymatgen/__init__.py +28 -0
- matrifflow/pymatgen/cli/__init__.py +30 -0
- matrifflow/pymatgen/cli/mrf.py +281 -0
- matrifflow/pymatgen/cli/mrf_analyze.py +392 -0
- matrifflow/pymatgen/cli/mrf_create.py +185 -0
- matrifflow/pymatgen/cli/mrf_derive.py +1609 -0
- matrifflow/pymatgen/cli/mrf_diff.py +419 -0
- matrifflow/pymatgen/cli/mrf_run.py +69 -0
- matrifflow/pymatgen/cli/mrf_status.py +842 -0
- matrifflow/pymatgen/cli/mrf_visualize.py +1808 -0
- matrifflow/pymatgen/core/__init__.py +28 -0
- matrifflow/pymatgen/core/structure.py +870 -0
- matrifflow/pymatgen/diffusion/__init__.py +28 -0
- matrifflow/pymatgen/diffusion/neb.py +832 -0
- matrifflow/pymatgen/io/__init__.py +28 -0
- matrifflow/pymatgen/io/atat.py +131 -0
- matrifflow/pymatgen/io/vasp/__init__.py +30 -0
- matrifflow/pymatgen/io/vasp/diagnostics.py +224 -0
- matrifflow/pymatgen/io/vasp/functionals.yaml +48 -0
- matrifflow/pymatgen/io/vasp/incar_toml.py +250 -0
- matrifflow/pymatgen/io/vasp/inputs.py +228 -0
- matrifflow/pymatgen/io/vasp/outputs.py +803 -0
- matrifflow/pymatgen/io/vasp/sets.py +1092 -0
- matrifflow/pymatgen/io/vasp/vaspdir.py +1514 -0
- matrifflow/pymatgen/transformations/__init__.py +28 -0
- matrifflow/pymatgen/transformations/insert_molecule.py +661 -0
- matrifflow/pymatgen/transformations/symmetry_clone.py +309 -0
- matrifflow/utils/__init__.py +28 -0
- matrifflow/utils/mpl.py +106 -0
- matrifflow-2.0.1.data/scripts/gorun-all-ready.sh +245 -0
- matrifflow-2.0.1.dist-info/METADATA +136 -0
- matrifflow-2.0.1.dist-info/RECORD +60 -0
- matrifflow-2.0.1.dist-info/WHEEL +5 -0
- matrifflow-2.0.1.dist-info/entry_points.txt +8 -0
- matrifflow-2.0.1.dist-info/licenses/LICENSE +9 -0
- 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()
|