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 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)
@@ -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,4 @@
1
+ Property,Type,Units,Description,URL
2
+ energy#MBE#{model},float,kJ/mol,The MBE-corrected total energy,
3
+ gradients#MBE#{model},json,kJ/mol/Å,The MBE-corrected gradients,
4
+ stress#MBE#{model},json,GPa,"The MBE-corrected stress, Voigt, sigma = -P",
@@ -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