mbe-step 2026.10.3__py2.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.
- mbe_step/__init__.py +26 -0
- mbe_step/_version.py +21 -0
- mbe_step/counterpoise.py +114 -0
- mbe_step/data/properties.csv +4 -0
- mbe_step/data/references.bib +82 -0
- mbe_step/labels.py +207 -0
- mbe_step/levels.py +213 -0
- mbe_step/mbe.py +709 -0
- mbe_step/mbe_parameters.py +430 -0
- mbe_step/mbe_step.py +97 -0
- mbe_step/metadata.py +207 -0
- mbe_step/tk_mbe.py +216 -0
- mbe_step-2026.10.3.dist-info/METADATA +123 -0
- mbe_step-2026.10.3.dist-info/RECORD +20 -0
- mbe_step-2026.10.3.dist-info/WHEEL +6 -0
- mbe_step-2026.10.3.dist-info/entry_points.txt +5 -0
- mbe_step-2026.10.3.dist-info/licenses/AUTHORS.rst +5 -0
- mbe_step-2026.10.3.dist-info/licenses/LICENSE +31 -0
- mbe_step-2026.10.3.dist-info/top_level.txt +1 -0
- mbe_step-2026.10.3.dist-info/zip-safe +1 -0
mbe_step/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
mbe_step
|
|
5
|
+
A SEAMM plug-in for many-body expansion (MBE) corrections of periodic cells and clusters
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Bring up the classes so that they appear to be directly in
|
|
9
|
+
# the mbe_step package.
|
|
10
|
+
|
|
11
|
+
from .mbe import Mbe # noqa: F401, E501
|
|
12
|
+
from .mbe_parameters import MbeParameters # noqa: F401, E501
|
|
13
|
+
from .mbe_step import MbeStep # noqa: F401, E501
|
|
14
|
+
from .tk_mbe import TkMbe # noqa: F401, E501
|
|
15
|
+
|
|
16
|
+
from .metadata import metadata # noqa: F401
|
|
17
|
+
|
|
18
|
+
# Handle versioneer
|
|
19
|
+
from ._version import get_versions
|
|
20
|
+
|
|
21
|
+
__author__ = "Paul Saxe"
|
|
22
|
+
__email__ = "psaxe@molssi.org"
|
|
23
|
+
versions = get_versions()
|
|
24
|
+
__version__ = versions["version"]
|
|
25
|
+
__git_revision__ = versions["full-revisionid"]
|
|
26
|
+
del get_versions, versions
|
mbe_step/_version.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
# This file was generated by 'versioneer.py' (0.18) from
|
|
3
|
+
# revision-control system data, or from the parent directory name of an
|
|
4
|
+
# unpacked source archive. Distribution tarballs contain a pre-generated copy
|
|
5
|
+
# of this file.
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
version_json = '''
|
|
10
|
+
{
|
|
11
|
+
"date": "2026-10-03T17:26:32-0400",
|
|
12
|
+
"dirty": false,
|
|
13
|
+
"error": null,
|
|
14
|
+
"full-revisionid": "75d8c631ab753ad62195bb5914986616eee5eda3",
|
|
15
|
+
"version": "2026.10.3"
|
|
16
|
+
}
|
|
17
|
+
''' # END VERSION_JSON
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_versions():
|
|
21
|
+
return json.loads(version_json)
|
mbe_step/counterpoise.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""Pairwise counterpoise for the MBE step, through seamm_bsse.
|
|
4
|
+
|
|
5
|
+
For a selected pair, the Boys-Bernardi correction needs each monomer in the
|
|
6
|
+
pair's basis (the other monomer's atoms as ghosts): two extra calculations per
|
|
7
|
+
pair at each molecular level. With the pair itself and the two monomers (both
|
|
8
|
+
already computed as fragments), seamm_bsse.combine gives the counterpoise-
|
|
9
|
+
corrected pair energy and gradient, guarded against unphysical ghost gradients.
|
|
10
|
+
|
|
11
|
+
The correction enters the sum only (seamm_mbe's ``corrections``): with
|
|
12
|
+
[high - low] for the pair,
|
|
13
|
+
|
|
14
|
+
correction = (E_high^CP - E_high) - (E_low^CP - E_low)
|
|
15
|
+
|
|
16
|
+
the second term only when the pair is referenced to the molecular low level (a
|
|
17
|
+
plane-wave periodic level has no basis-set superposition error). Triples still
|
|
18
|
+
subtract the uncorrected pairs.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import seamm_bsse
|
|
23
|
+
from seamm_util import Q_
|
|
24
|
+
|
|
25
|
+
from . import levels
|
|
26
|
+
|
|
27
|
+
HARTREE_KJ = Q_(1.0, "E_h").m_as("kJ/mol")
|
|
28
|
+
EV_KJ = Q_(1.0, "eV").m_as("kJ/mol")
|
|
29
|
+
BOHR = Q_(1.0, "bohr").m_as("Å")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def specs(system, pair):
|
|
33
|
+
"""The seamm_bsse job specs for a pair (atom indices within the pair)."""
|
|
34
|
+
types = [system.types[system.molecules[m].type] for m in pair.molecules]
|
|
35
|
+
fragments = [
|
|
36
|
+
seamm_bsse.Fragment(
|
|
37
|
+
label=label,
|
|
38
|
+
atom_indices=[int(i) for i in pair.slot_atoms[slot]],
|
|
39
|
+
charge=types[slot].charge,
|
|
40
|
+
multiplicity=types[slot].multiplicity,
|
|
41
|
+
)
|
|
42
|
+
for slot, label in enumerate(("A", "B"))
|
|
43
|
+
]
|
|
44
|
+
return seamm_bsse.generate_job_specs(fragments)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def ghost_jobs(system, pair):
|
|
48
|
+
"""The extra calculations for a pair: {label: (structure, options)} for
|
|
49
|
+
each monomer in the pair's basis."""
|
|
50
|
+
geometry = levels.geometry(system, pair)
|
|
51
|
+
jobs = {}
|
|
52
|
+
for spec in specs(system, pair):
|
|
53
|
+
if spec.kind != seamm_bsse.FRAGMENT_IN_CLUSTER:
|
|
54
|
+
continue
|
|
55
|
+
jobs[spec.label] = (
|
|
56
|
+
geometry,
|
|
57
|
+
{
|
|
58
|
+
"atom_indices": list(spec.atom_indices),
|
|
59
|
+
"ghost_atoms": sorted(spec.ghost_indices),
|
|
60
|
+
"charge": spec.charge,
|
|
61
|
+
"multiplicity": spec.multiplicity,
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
return jobs
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def key(prefix, pair_name, label):
|
|
68
|
+
"""The task key of a ghost job."""
|
|
69
|
+
return f"{prefix}{pair_name}-cp-{label}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _job_result(result):
|
|
73
|
+
"""An EvaluatorResult (kJ/mol, kJ/mol/Å) as a seamm_bsse.JobResult (E_h,
|
|
74
|
+
E_h/bohr)."""
|
|
75
|
+
gradient = np.asarray(result.gradients, dtype=float) * BOHR / HARTREE_KJ
|
|
76
|
+
return seamm_bsse.JobResult(energy=result.energy / HARTREE_KJ, gradient=gradient)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def correction(system, fragments, pair, results, prefix):
|
|
80
|
+
"""The counterpoise correction of one pair at one level.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
results : {str: EvaluatorResult}
|
|
85
|
+
The level's results by key (the pair, its monomers and its ghost jobs).
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
energy : float
|
|
90
|
+
E^CP - E for the pair, eV.
|
|
91
|
+
forces : numpy.ndarray
|
|
92
|
+
(n_atoms, 3) the change of the pair's forces, eV/Å.
|
|
93
|
+
fallback : bool
|
|
94
|
+
Whether seamm_bsse kept the uncorrected gradient (unphysical ghost
|
|
95
|
+
gradients: the energy is corrected, the forces are not).
|
|
96
|
+
"""
|
|
97
|
+
monomers = {slots[0]: name for name, slots in pair.subfragments if len(slots) == 1}
|
|
98
|
+
by_label = {
|
|
99
|
+
"cluster": results[prefix + pair.name],
|
|
100
|
+
"A-alone": results[prefix + monomers[0]],
|
|
101
|
+
"B-alone": results[prefix + monomers[1]],
|
|
102
|
+
"A-in-cluster": results[key(prefix, pair.name, "A-in-cluster")],
|
|
103
|
+
"B-in-cluster": results[key(prefix, pair.name, "B-in-cluster")],
|
|
104
|
+
}
|
|
105
|
+
for label, result in by_label.items():
|
|
106
|
+
if not result.ok:
|
|
107
|
+
raise KeyError(f"{pair.name} {label}: {result.reason}")
|
|
108
|
+
job_results = {label: _job_result(r) for label, r in by_label.items()}
|
|
109
|
+
cp = seamm_bsse.combine(specs(system, pair), job_results, n_atoms=pair.n_atoms)
|
|
110
|
+
cluster = job_results["cluster"]
|
|
111
|
+
d_energy = (cp.energy - cluster.energy) * HARTREE_KJ / EV_KJ
|
|
112
|
+
d_gradient = np.asarray(cp.gradient) - np.asarray(cluster.gradient)
|
|
113
|
+
d_forces = -d_gradient * HARTREE_KJ / BOHR / EV_KJ
|
|
114
|
+
return d_energy, d_forces, bool(cp.gradient_fallback)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
This is an initial library of BibTex references for the mbe_step plug-in for SEAMM,
|
|
2
|
+
generated automatically by the cookiecutter. You may wish to edit the first entry to correct some
|
|
3
|
+
of the information.
|
|
4
|
+
|
|
5
|
+
Add other references to the end of the file and add code in the plug-in to cite the references as
|
|
6
|
+
appropriate. This is done with code like this:
|
|
7
|
+
|
|
8
|
+
self.references.cite(
|
|
9
|
+
raw=self._bibliography['doi:10.1002/jcc.21224'],
|
|
10
|
+
alias='packmol',
|
|
11
|
+
module='packmol_step',
|
|
12
|
+
level=1,
|
|
13
|
+
note='The principle PACKMOL citation.'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
which is typically in the run() method of the plug-in.
|
|
17
|
+
|
|
18
|
+
The base node class in SEAMM automatically reads this list of references into a dictionary in
|
|
19
|
+
self._bibliography with the key being the key of the BibTex entry -- usually the name on the
|
|
20
|
+
first line of the entry after the 'article' or 'misc' (for software) keyword.
|
|
21
|
+
|
|
22
|
+
See the documentation for the Reference Handler for the other arguments to cite().
|
|
23
|
+
|
|
24
|
+
# End of initial comments #
|
|
25
|
+
|
|
26
|
+
# This is the citation for this plug-in, generated and used automatically.
|
|
27
|
+
# You might need to edit the author, address, etc. The month, year and version fields
|
|
28
|
+
# are automatically filled in with information about current version of the plug-in.
|
|
29
|
+
# This is handled in seamm/nody.py, the run() method.
|
|
30
|
+
|
|
31
|
+
@Misc{mbe_step,
|
|
32
|
+
author = {Paul Saxe},
|
|
33
|
+
title = {MBE plug-in for SEAMM},
|
|
34
|
+
month = {$month},
|
|
35
|
+
year = {$year},
|
|
36
|
+
organization = {molssi-seamm},
|
|
37
|
+
url = {https://github.com/molssi-seamm/mbe_step },
|
|
38
|
+
address = {USA},
|
|
39
|
+
version = {$version}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Misc{seamm_mbe,
|
|
43
|
+
author = {Paul Saxe},
|
|
44
|
+
title = {seamm_mbe: many-body expansion bookkeeping for SEAMM},
|
|
45
|
+
year = {2026},
|
|
46
|
+
organization = {molssi-seamm},
|
|
47
|
+
url = {https://github.com/molssi-seamm/seamm_mbe},
|
|
48
|
+
address = {USA}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@article{Herbert_2019,
|
|
52
|
+
title = {Fantasy versus reality in fragment-based quantum chemistry},
|
|
53
|
+
volume = {151},
|
|
54
|
+
number = {17},
|
|
55
|
+
pages = {170901},
|
|
56
|
+
journal = {The Journal of Chemical Physics},
|
|
57
|
+
author = {Herbert, John M.},
|
|
58
|
+
year = {2019},
|
|
59
|
+
doi = {10.1063/1.5126216}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@article{Richard_2014,
|
|
63
|
+
title = {Aiming for Benchmark Accuracy with the Many-Body Expansion},
|
|
64
|
+
volume = {47},
|
|
65
|
+
number = {9},
|
|
66
|
+
pages = {2828--2836},
|
|
67
|
+
journal = {Accounts of Chemical Research},
|
|
68
|
+
author = {Richard, Ryan M. and Lao, Ka Un and Herbert, John M.},
|
|
69
|
+
year = {2014},
|
|
70
|
+
doi = {10.1021/ar500119q}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@article{Beran_2016,
|
|
74
|
+
title = {Modeling Polymorphic Molecular Crystals with Electronic Structure Theory},
|
|
75
|
+
volume = {116},
|
|
76
|
+
number = {9},
|
|
77
|
+
pages = {5567--5613},
|
|
78
|
+
journal = {Chemical Reviews},
|
|
79
|
+
author = {Beran, Gregory J. O.},
|
|
80
|
+
year = {2016},
|
|
81
|
+
doi = {10.1021/acs.chemrev.5b00648}
|
|
82
|
+
}
|
mbe_step/labels.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""Helpers of the MBE step that need no flowchart: parsing the cutoff and
|
|
4
|
+
offset tables, the stress layout, and the extended XYZ labels file."""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
import seamm_mbe
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_table(text, what="cutoff"):
|
|
12
|
+
"""A table of values by molecule-type pair.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
text : str
|
|
17
|
+
Entries 'type type value' separated by ';' or new lines, e.g.
|
|
18
|
+
'water water 4.5; Li+ * 3.0'. '*' matches any type.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
{(str, str): float}
|
|
23
|
+
"""
|
|
24
|
+
table = {}
|
|
25
|
+
for entry in text.replace("\n", ";").split(";"):
|
|
26
|
+
entry = entry.strip()
|
|
27
|
+
if not entry:
|
|
28
|
+
continue
|
|
29
|
+
fields = entry.split()
|
|
30
|
+
if len(fields) != 3:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"The {what} table entry '{entry}' should be 'type type value', "
|
|
33
|
+
"e.g. 'water water 4.5'."
|
|
34
|
+
)
|
|
35
|
+
try:
|
|
36
|
+
value = float(fields[2])
|
|
37
|
+
except ValueError:
|
|
38
|
+
raise ValueError(f"The {what} in '{entry}' is not a number.") from None
|
|
39
|
+
table[(fields[0], fields[1])] = value
|
|
40
|
+
if not table:
|
|
41
|
+
raise ValueError(f"The {what} table is empty.")
|
|
42
|
+
return table
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_offsets(text):
|
|
46
|
+
"""Energy offsets in eV per molecule by type, from entries 'type value'
|
|
47
|
+
separated by ';' or new lines, or None for 'none' or an empty text."""
|
|
48
|
+
text = text.strip()
|
|
49
|
+
if text.lower() in ("", "none"):
|
|
50
|
+
return None
|
|
51
|
+
offsets = {}
|
|
52
|
+
for entry in text.replace("\n", ";").split(";"):
|
|
53
|
+
entry = entry.strip()
|
|
54
|
+
if not entry:
|
|
55
|
+
continue
|
|
56
|
+
fields = entry.split()
|
|
57
|
+
if len(fields) != 2:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"The energy offset '{entry}' should be 'type value', e.g. "
|
|
60
|
+
"'water 2074.69325'."
|
|
61
|
+
)
|
|
62
|
+
try:
|
|
63
|
+
offsets[fields[0]] = float(fields[1])
|
|
64
|
+
except ValueError:
|
|
65
|
+
raise ValueError(f"The offset in '{entry}' is not a number.") from None
|
|
66
|
+
return offsets
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def voigt(tensor):
|
|
70
|
+
"""A (3, 3) tensor as Voigt [6]: xx yy zz yz xz xy (symmetrized)."""
|
|
71
|
+
t = np.asarray(tensor, dtype=float)
|
|
72
|
+
t = (t + t.T) / 2
|
|
73
|
+
return [t[0, 0], t[1, 1], t[2, 2], t[1, 2], t[0, 2], t[0, 1]]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def write_extxyz(
|
|
77
|
+
path,
|
|
78
|
+
system,
|
|
79
|
+
labels,
|
|
80
|
+
*,
|
|
81
|
+
name,
|
|
82
|
+
model,
|
|
83
|
+
identifier=None,
|
|
84
|
+
counterpoise=False,
|
|
85
|
+
extra="",
|
|
86
|
+
):
|
|
87
|
+
"""Add one configuration's labels to an extended XYZ file.
|
|
88
|
+
|
|
89
|
+
The file collects configurations (several in one step, or across a loop's
|
|
90
|
+
iterations when it is a job-level file). Writing is idempotent: an earlier
|
|
91
|
+
frame with the same ``identifier`` (SEAMM/configuration_id) is replaced, so
|
|
92
|
+
rerunning a job does not duplicate labels.
|
|
93
|
+
|
|
94
|
+
The columns are species, pos and REF_forces (eV/Å); the header carries
|
|
95
|
+
REF_energy (eV, the reference scale), REF_stress (the nine values of
|
|
96
|
+
sigma = -P in eV/ų, for a cell), the Lattice and pbc, the model, whether
|
|
97
|
+
counterpoise was used and the configuration's name. Note that ASE reads the
|
|
98
|
+
nine-value REF_stress as a flat vector: a reader must reshape it.
|
|
99
|
+
"""
|
|
100
|
+
fields = []
|
|
101
|
+
if system.periodic:
|
|
102
|
+
lattice = " ".join(f"{v:.8f}" for v in system.cell.ravel())
|
|
103
|
+
fields.append(f'Lattice="{lattice}"')
|
|
104
|
+
fields.append("Properties=species:S:1:pos:R:3:REF_forces:R:3")
|
|
105
|
+
fields.append(f"REF_energy={labels.reference_energy:.8f}")
|
|
106
|
+
if system.periodic:
|
|
107
|
+
stress = " ".join(f"{v:.10e}" for v in labels.stress.ravel())
|
|
108
|
+
fields.append(f'REF_stress="{stress}"')
|
|
109
|
+
fields.append('pbc="T T T"')
|
|
110
|
+
else:
|
|
111
|
+
fields.append('pbc="F F F"')
|
|
112
|
+
fields.append(f'model="{model}"')
|
|
113
|
+
fields.append(f"counterpoise={'T' if counterpoise else 'F'}")
|
|
114
|
+
fields.append(f'SEAMM/configuration_name="{name}"')
|
|
115
|
+
if identifier is not None:
|
|
116
|
+
fields.append(f"SEAMM/configuration_id={identifier}")
|
|
117
|
+
if extra:
|
|
118
|
+
fields.append(extra)
|
|
119
|
+
lines = [str(len(system.symbols)), " ".join(fields)]
|
|
120
|
+
for symbol, xyz, force in zip(system.symbols, system.coordinates, labels.forces):
|
|
121
|
+
lines.append(
|
|
122
|
+
f"{symbol:2s} {xyz[0]:14.8f} {xyz[1]:14.8f} {xyz[2]:14.8f} "
|
|
123
|
+
f"{force[0]:14.8f} {force[1]:14.8f} {force[2]:14.8f}"
|
|
124
|
+
)
|
|
125
|
+
frames = _read_frames(path)
|
|
126
|
+
if identifier is not None:
|
|
127
|
+
tag = f"SEAMM/configuration_id={identifier}"
|
|
128
|
+
frames = [f for f in frames if tag not in f[1].split()]
|
|
129
|
+
frames.append(lines)
|
|
130
|
+
with open(path, "w") as fd:
|
|
131
|
+
for frame in frames:
|
|
132
|
+
fd.write("\n".join(frame) + "\n")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _read_frames(path):
|
|
136
|
+
"""The frames of an extended XYZ file as lists of lines ([] if none)."""
|
|
137
|
+
try:
|
|
138
|
+
text = open(path).read().splitlines()
|
|
139
|
+
except FileNotFoundError:
|
|
140
|
+
return []
|
|
141
|
+
frames = []
|
|
142
|
+
i = 0
|
|
143
|
+
while i < len(text):
|
|
144
|
+
if not text[i].strip():
|
|
145
|
+
i += 1
|
|
146
|
+
continue
|
|
147
|
+
try:
|
|
148
|
+
n = int(text[i])
|
|
149
|
+
except ValueError:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"{path}, line {i + 1}: expected the number of atoms of a frame, "
|
|
152
|
+
f"found '{text[i][:40]}'. Is it an extended XYZ file?"
|
|
153
|
+
) from None
|
|
154
|
+
frames.append(text[i : i + n + 2])
|
|
155
|
+
i += n + 2
|
|
156
|
+
return frames
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def counts_text(fragments):
|
|
160
|
+
"""'64 monomers, 391 pairs (+268 outer), 559 triples'."""
|
|
161
|
+
names = {1: "monomers", 2: "pairs", 3: "triples", 4: "4-bodies"}
|
|
162
|
+
parts = []
|
|
163
|
+
for order, count in sorted(fragments.counts().items()):
|
|
164
|
+
text = f"{count['selected']} {names.get(order, f'{order}-bodies')}"
|
|
165
|
+
if count["auxiliary"]:
|
|
166
|
+
extra = "outer" if order == 2 else "auxiliary"
|
|
167
|
+
text += f" (+{count['auxiliary']} {extra})"
|
|
168
|
+
parts.append(text)
|
|
169
|
+
return ", ".join(parts)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def to_seamm(labels, volume):
|
|
173
|
+
"""The labels in SEAMM's units for store_results: energies kJ/mol,
|
|
174
|
+
gradients kJ/mol/Å, stress Voigt [6] in GPa as sigma = -P, pressures in
|
|
175
|
+
atm."""
|
|
176
|
+
units = seamm_mbe.units
|
|
177
|
+
data = units.to_seamm(energy=labels.energy, forces=labels.forces)
|
|
178
|
+
data["gradients"] = data["gradients"].tolist()
|
|
179
|
+
data["reference energy"] = float(units.ev_to_kj_per_mol(labels.reference_energy))
|
|
180
|
+
if labels.virial is not None:
|
|
181
|
+
stress = units.tensor_from_virial(
|
|
182
|
+
labels.virial, volume, convention="stress", units="GPa"
|
|
183
|
+
)
|
|
184
|
+
data["stress"] = voigt(stress)
|
|
185
|
+
data["pressure"] = labels.pressure
|
|
186
|
+
data["molecular pressure"] = labels.molecular_pressure
|
|
187
|
+
data["MBE pressure"] = labels.breakdown["MBE"]["pressure"]
|
|
188
|
+
data["MBE energy"] = float(
|
|
189
|
+
units.ev_to_kj_per_mol(labels.breakdown["MBE"]["energy"])
|
|
190
|
+
)
|
|
191
|
+
data["per-body energies"] = {
|
|
192
|
+
str(k): float(units.ev_to_kj_per_mol(v["energy"]))
|
|
193
|
+
for k, v in labels.per_body.items()
|
|
194
|
+
}
|
|
195
|
+
if labels.virial is not None:
|
|
196
|
+
data["per-body pressures"] = {
|
|
197
|
+
str(k): v["pressure"] for k, v in labels.per_body.items()
|
|
198
|
+
}
|
|
199
|
+
data["maximum increment net force"] = float(
|
|
200
|
+
units.ev_to_kj_per_mol(labels.max_increment_net_force)
|
|
201
|
+
)
|
|
202
|
+
forces = np.asarray(labels.forces)
|
|
203
|
+
data["maximum force"] = float(units.ev_to_kj_per_mol(np.abs(forces).max()))
|
|
204
|
+
data["rms force"] = float(
|
|
205
|
+
units.ev_to_kj_per_mol(np.sqrt((forces**2).sum(1).mean()))
|
|
206
|
+
)
|
|
207
|
+
return data
|
mbe_step/levels.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""The MBE step's levels of theory: resolving their model chemistries and
|
|
4
|
+
evaluating structures with each through seamm_exec's Evaluator.
|
|
5
|
+
|
|
6
|
+
The step uses up to four levels:
|
|
7
|
+
|
|
8
|
+
* high: every fragment (by default the flowchart's current Model Chemistry);
|
|
9
|
+
* molecular: the molecular low level, on the fragments referenced to it;
|
|
10
|
+
* periodic: the periodic low level, on the compact fragments referenced to it;
|
|
11
|
+
* cell: the low level of the whole cell (or cluster).
|
|
12
|
+
|
|
13
|
+
Each level is one Evaluator with its own directory, so each keeps its own task
|
|
14
|
+
manifest (restart) and bundles; the Evaluator chooses how the level runs
|
|
15
|
+
(batch tasks or an MDI engine). In this phase the levels run one after the
|
|
16
|
+
other; running them concurrently is a seamm_exec follow-up.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
import seamm_mbe
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
#: The sentinel for the flowchart's current Model Chemistry
|
|
29
|
+
CURRENT = "current model chemistry"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve(text, context, current=None, periodic=False):
|
|
33
|
+
"""The ``_model_chemistry`` wrapper for a level given as text.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
text : str
|
|
38
|
+
A level spec such as ``"ORCA:DFT@r2SCAN-D4/def2-TZVPPD"``, possibly with
|
|
39
|
+
``$variable`` components, or :data:`CURRENT`.
|
|
40
|
+
context : dict
|
|
41
|
+
The flowchart variables, to dereference any ``$variable``.
|
|
42
|
+
current : dict or None
|
|
43
|
+
The flowchart's ``_model_chemistry``, for :data:`CURRENT`.
|
|
44
|
+
periodic : bool
|
|
45
|
+
Whether the level must handle periodic systems.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
dict
|
|
50
|
+
The ``_model_chemistry`` wrapper: level, step, options, ...
|
|
51
|
+
"""
|
|
52
|
+
from model_chemistry_step.model_chemistry import (
|
|
53
|
+
availability_problem,
|
|
54
|
+
discover_model_chemistries,
|
|
55
|
+
match_model_chemistry,
|
|
56
|
+
resolve_level,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if text == CURRENT:
|
|
60
|
+
if current is None:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"The MBE step's high level is the current model chemistry, but "
|
|
63
|
+
"there is none: add a Model Chemistry step before the MBE step, "
|
|
64
|
+
"or give the high level explicitly."
|
|
65
|
+
)
|
|
66
|
+
return current
|
|
67
|
+
text = resolve_level(text, context)
|
|
68
|
+
available = discover_model_chemistries(periodic_only=periodic)
|
|
69
|
+
if text in available:
|
|
70
|
+
return available[text]
|
|
71
|
+
mc = match_model_chemistry(text, available)
|
|
72
|
+
if mc is None:
|
|
73
|
+
raise ValueError(availability_problem(text, available, periodic=periodic))
|
|
74
|
+
return mc
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def stress_convention(mc):
|
|
78
|
+
"""The sign convention of a level's stress, as its provider declares it
|
|
79
|
+
("pressure" or "stress"); refuse a periodic level that does not."""
|
|
80
|
+
convention = (mc.get("options") or {}).get("stress_convention")
|
|
81
|
+
if convention not in seamm_mbe.units.CONVENTIONS:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"The model chemistry '{mc['level']}' does not declare the sign "
|
|
84
|
+
"convention of its stress (options['stress_convention'] = 'pressure' "
|
|
85
|
+
"or 'stress'), so its stress cannot be used for a periodic cell."
|
|
86
|
+
)
|
|
87
|
+
return convention
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Level:
|
|
91
|
+
"""One level of theory and how it runs.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
name : str
|
|
96
|
+
"high", "molecular", "periodic" or "cell"; also the directory name.
|
|
97
|
+
mc : dict
|
|
98
|
+
The ``_model_chemistry`` wrapper.
|
|
99
|
+
ranks : int
|
|
100
|
+
MPI ranks per calculation.
|
|
101
|
+
memory : float
|
|
102
|
+
Memory per rank, in MB.
|
|
103
|
+
bundle : int
|
|
104
|
+
Calculations per batch job on a queue.
|
|
105
|
+
walltime : float
|
|
106
|
+
Seconds per batch job.
|
|
107
|
+
archive : bool
|
|
108
|
+
Tar finished bundles.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, name, mc, *, ranks, memory, bundle, walltime, archive):
|
|
112
|
+
self.name = name
|
|
113
|
+
self.mc = mc
|
|
114
|
+
self.ranks = int(ranks)
|
|
115
|
+
self.memory = float(memory)
|
|
116
|
+
self.bundle = int(bundle)
|
|
117
|
+
self.walltime = float(walltime)
|
|
118
|
+
self.archive = bool(archive)
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def level(self):
|
|
122
|
+
return self.mc["level"]
|
|
123
|
+
|
|
124
|
+
def resources(self):
|
|
125
|
+
from seamm_exec import Resources
|
|
126
|
+
|
|
127
|
+
return Resources(ntasks=self.ranks, mem_per_cpu=int(self.memory * 1e6))
|
|
128
|
+
|
|
129
|
+
def evaluate(self, node, structures, *, stress=False):
|
|
130
|
+
"""Evaluate structures at this level.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
node : seamm.Node
|
|
135
|
+
The MBE step.
|
|
136
|
+
structures : {str: object}
|
|
137
|
+
Configurations or ``seamm_exec.Geometry`` objects by key.
|
|
138
|
+
stress : bool
|
|
139
|
+
Request the stress (for a periodic cell).
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
{str: seamm_exec.EvaluatorResult}
|
|
144
|
+
"""
|
|
145
|
+
import seamm_exec
|
|
146
|
+
|
|
147
|
+
properties = ("energy", "gradients") + (("stress",) if stress else ())
|
|
148
|
+
options = {"archive": self.archive, "bundle_tasks": self.bundle}
|
|
149
|
+
if self.walltime > 0:
|
|
150
|
+
options["bundle_walltime"] = self.walltime
|
|
151
|
+
results = {}
|
|
152
|
+
with seamm_exec.Evaluator(
|
|
153
|
+
node,
|
|
154
|
+
self.mc,
|
|
155
|
+
properties=properties,
|
|
156
|
+
directory=Path(node.directory) / self.name,
|
|
157
|
+
task_set_options=options,
|
|
158
|
+
resources=self.resources(),
|
|
159
|
+
name=f"MBE_{self.name}", # no spaces: it goes into MDI_Init
|
|
160
|
+
) as evaluator:
|
|
161
|
+
for key, structure in structures.items():
|
|
162
|
+
options = None
|
|
163
|
+
if isinstance(structure, tuple):
|
|
164
|
+
structure, options = structure
|
|
165
|
+
evaluator.submit(structure, key=key, options=options)
|
|
166
|
+
for result in evaluator.results():
|
|
167
|
+
results[result.key] = result
|
|
168
|
+
return results
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def geometry(system, fragment):
|
|
172
|
+
"""The isolated fragment as a ``seamm_exec.Geometry``: its atoms in
|
|
173
|
+
fragment order at the fragment's coordinates, with its charge and
|
|
174
|
+
multiplicity."""
|
|
175
|
+
from seamm_exec import Geometry
|
|
176
|
+
|
|
177
|
+
return Geometry(
|
|
178
|
+
system.atomic_numbers[fragment.atoms],
|
|
179
|
+
fragment.coordinates,
|
|
180
|
+
charge=fragment.charge,
|
|
181
|
+
multiplicity=fragment.multiplicity,
|
|
182
|
+
name=fragment.name,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def whole(system):
|
|
187
|
+
"""The whole cell or cluster as a ``seamm_exec.Geometry`` with the charge
|
|
188
|
+
the molecules add up to (their types' charges, from the formal charges or
|
|
189
|
+
the catalog) -- the same charges the fragments use -- and multiplicity 1
|
|
190
|
+
(open-shell molecules are refused before this)."""
|
|
191
|
+
from seamm_exec import Geometry
|
|
192
|
+
|
|
193
|
+
charge = sum(system.types[m.type].charge for m in system.molecules)
|
|
194
|
+
return Geometry(
|
|
195
|
+
system.atomic_numbers,
|
|
196
|
+
system.coordinates,
|
|
197
|
+
charge=charge,
|
|
198
|
+
multiplicity=1,
|
|
199
|
+
cell=system.cell,
|
|
200
|
+
name="whole system",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def to_library(result, *, volume=None, convention=None):
|
|
205
|
+
"""An EvaluatorResult in seamm_mbe's units: (energy eV, forces eV/Å), plus
|
|
206
|
+
the virial (eV) when the result has a stress."""
|
|
207
|
+
data = {"energy": result.energy, "gradients": np.asarray(result.gradients)}
|
|
208
|
+
if result.stress is not None and volume is not None:
|
|
209
|
+
data["stress"] = result.stress
|
|
210
|
+
out = seamm_mbe.units.from_analyze_task(
|
|
211
|
+
data, volume=volume, stress_convention=convention
|
|
212
|
+
)
|
|
213
|
+
return out
|