orca-step 2026.6.27__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.
orca_step/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+ orca_step
5
+ A SEAMM plug-in for ORCA, with a sub-flowchart of capabilities (Energy,
6
+ Optimization, ...), modeled on the MOPAC and Gaussian steps.
7
+ """
8
+
9
+ # Metadata and the shared base first (others import them).
10
+ from .metadata import metadata # noqa: F401
11
+ from .orca_base import ORCABase # noqa: F401
12
+
13
+ # Sub-step parameters / nodes / Tk / factories.
14
+ from .energy_parameters import EnergyParameters # noqa: F401
15
+ from .energy import Energy # noqa: F401
16
+ from .tk_energy import TkEnergy # noqa: F401
17
+ from .energy_step import EnergyStep # noqa: F401
18
+
19
+ from .optimization_parameters import OptimizationParameters # noqa: F401
20
+ from .optimization import Optimization # noqa: F401
21
+ from .tk_optimization import TkOptimization # noqa: F401
22
+ from .optimization_step import OptimizationStep # noqa: F401
23
+
24
+ # Main node, Tk, and factory.
25
+ from .orca import ORCA # noqa: F401
26
+ from .tk_orca import TkORCA # noqa: F401
27
+ from .orca_step import ORCAStep # noqa: F401
28
+
29
+ # Versioneer
30
+ from ._version import get_versions
31
+
32
+ __author__ = "Paul Saxe"
33
+ __email__ = "psaxe@molssi.org"
34
+ versions = get_versions()
35
+ __version__ = versions["version"]
36
+ __git_revision__ = versions["full-revisionid"]
37
+ del get_versions, versions
orca_step/__main__.py ADDED
@@ -0,0 +1,16 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """Handle the installation of the ORCA step."""
5
+
6
+ from .installer import Installer
7
+
8
+
9
+ def run():
10
+ """Find and/or register the ORCA executable in seamm.ini."""
11
+ installer = Installer()
12
+ installer.run()
13
+
14
+
15
+ if __name__ == "__main__":
16
+ run()
orca_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-06-27T21:40:25-0400",
12
+ "dirty": false,
13
+ "error": null,
14
+ "full-revisionid": "fafca4b1d969cf0c6ca3e0ae5c8e21afc8f78b1c",
15
+ "version": "2026.6.27"
16
+ }
17
+ ''' # END VERSION_JSON
18
+
19
+
20
+ def get_versions():
21
+ return json.loads(version_json)
@@ -0,0 +1,16 @@
1
+ [orca-step]
2
+ # Path to the ORCA executable, if it is not on the PATH. ORCA must be invoked by
3
+ # its full path so it can find its sub-programs.
4
+ # orca-path =
5
+
6
+ # How many cores/processes ORCA may use (via %pal). Default is serial (1).
7
+ # Parallel ORCA requires a matching OpenMPI runtime (see library-path).
8
+ # ncores = 1
9
+
10
+ # Directory holding ORCA's required OpenMPI libraries (e.g. libmpi.40.dylib),
11
+ # prepended to (DY)LD_LIBRARY_PATH for parallel runs. On this machine the
12
+ # seamm-orca conda environment provides them.
13
+ # library-path =
14
+
15
+ # Maximum number of atoms for which to print detailed per-atom results.
16
+ # max_atoms_to_print = 25
@@ -0,0 +1 @@
1
+ Property,Type,Units,Description,URL
@@ -0,0 +1,40 @@
1
+ This is an initial library of BibTex references for the orca_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{orca_step,
32
+ author = {Paul Saxe},
33
+ title = {ORCA plug-in for SEAMM},
34
+ month = {$month},
35
+ year = {$year},
36
+ organization = {molssi-seamm},
37
+ url = {https://github.com/molssi-seamm/orca_step },
38
+ address = {USA},
39
+ version = {$version}
40
+ }
orca_step/energy.py ADDED
@@ -0,0 +1,186 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """An ORCA single-point energy sub-step."""
4
+
5
+ import logging
6
+ import pprint # noqa: F401
7
+
8
+ import orca_step
9
+ import seamm
10
+ from seamm_util import ureg, Q_ # noqa: F401
11
+ import seamm_util.printing as printing
12
+ from seamm_util.printing import FormattedText as __
13
+
14
+ logger = logging.getLogger(__name__)
15
+ job = printing.getPrinter()
16
+ printer = printing.getPrinter("ORCA")
17
+
18
+
19
+ class Energy(orca_step.ORCABase):
20
+ """A single-point energy with ORCA.
21
+
22
+ See Also
23
+ --------
24
+ TkEnergy, EnergyParameters, Optimization
25
+ """
26
+
27
+ def __init__(self, flowchart=None, title="Energy", extension=None, logger=logger):
28
+ """Initialize the Energy sub-step."""
29
+ logger.debug(f"Creating ORCA Energy {self}")
30
+
31
+ super().__init__(
32
+ flowchart=flowchart,
33
+ title=title,
34
+ extension=extension,
35
+ module=__name__,
36
+ logger=logger,
37
+ )
38
+
39
+ self._calculation = "energy"
40
+ self._metadata = orca_step.metadata
41
+ self.parameters = orca_step.EnergyParameters()
42
+
43
+ def description_text(self, P=None):
44
+ """Describe what this sub-step will do."""
45
+ if not P:
46
+ P = self.parameters.values_to_dict()
47
+
48
+ use_mc = P["use model chemistry"]
49
+ if not isinstance(use_mc, bool):
50
+ use_mc = use_mc == "yes"
51
+
52
+ if use_mc:
53
+ text = (
54
+ "Single-point energy with ORCA using the model chemistry from a "
55
+ "preceding Model Chemistry step."
56
+ )
57
+ else:
58
+ text = f"Single-point energy with ORCA at {P['method']}/{P['basis']}."
59
+ return self.header + "\n" + __(text, indent=4 * " ").__str__()
60
+
61
+ def keyword_line(self, P):
62
+ """Build the ORCA '!' keyword line.
63
+
64
+ If ``use model chemistry`` is set, the method (and basis, if given) come
65
+ from the global ``_model_chemistry`` variable defined by a preceding
66
+ Model Chemistry step; ORCA raises a clear error if it cannot provide that
67
+ model chemistry. Otherwise the explicit method/basis parameters are used.
68
+ The auxiliary basis and extra keywords always come from this node.
69
+ """
70
+ use_mc = P["use model chemistry"]
71
+ if not isinstance(use_mc, bool):
72
+ use_mc = use_mc == "yes"
73
+
74
+ if use_mc:
75
+ method, basis = self._method_basis_from_model_chemistry(P)
76
+ else:
77
+ method, basis = P["method"], P["basis"]
78
+
79
+ keywords = [method, basis]
80
+ aux = P["auxiliary basis"]
81
+ if aux and aux.lower() != "none":
82
+ keywords.append(aux)
83
+ extra = P["extra keywords"].strip()
84
+ if extra:
85
+ keywords.append(extra)
86
+ return " ".join(k for k in keywords if k)
87
+
88
+ def _method_basis_from_model_chemistry(self, P):
89
+ """Resolve (method, basis) from the global model chemistry, erroring
90
+ clearly if ORCA cannot provide it.
91
+ """
92
+ if not self.variable_exists("_model_chemistry"):
93
+ raise RuntimeError(
94
+ "'Use the global model chemistry' is selected, but no Model "
95
+ "Chemistry step has defined one (the '_model_chemistry' variable "
96
+ "is not set). Add a Model Chemistry step before this ORCA step, "
97
+ "or turn off 'use model chemistry' and set the method and basis "
98
+ "explicitly."
99
+ )
100
+ mc = self.get_variable("_model_chemistry")
101
+ level = mc.get("level", "") or mc.get("method", "")
102
+ owner = (mc.get("owner") or "").strip()
103
+ mtype = (mc.get("type") or "").strip()
104
+ method = (mc.get("method") or "").strip()
105
+ basis = mc.get("basis") or ""
106
+
107
+ # ORCA can evaluate an ORCA-owned or program-agnostic level of theory,
108
+ # but not another program's (e.g. MOPAC/xTB/LAMMPS) or a non-QC type.
109
+ if owner not in ("", "ORCA"):
110
+ raise RuntimeError(
111
+ f"The model chemistry '{level}' is owned by '{owner}', which "
112
+ "ORCA cannot provide. Choose an ORCA (or program-agnostic) model "
113
+ "chemistry, or set the method explicitly in the ORCA step."
114
+ )
115
+ if mtype.upper() in ("SQM", "FF", "MLFF"):
116
+ raise RuntimeError(
117
+ f"The model chemistry '{level}' is of type '{mtype}', which ORCA "
118
+ "does not provide. ORCA handles HF, DFT, MP2, and coupled-cluster "
119
+ "(QC) methods."
120
+ )
121
+ if method == "":
122
+ raise RuntimeError(
123
+ f"The model chemistry '{level}' does not name a method ORCA can " "use."
124
+ )
125
+ # If the model chemistry omits a basis, fall back to this node's basis.
126
+ if basis == "":
127
+ basis = P["basis"]
128
+ return method, basis
129
+
130
+ def run(self, keywords=None):
131
+ """Run the single-point energy."""
132
+ next_node = super().run(printer)
133
+
134
+ P = self.parameters.current_values_to_dict(
135
+ context=seamm.flowchart_variables._data
136
+ )
137
+
138
+ printer.important(__(self.description_text(P), indent=self.indent))
139
+
140
+ keyword_line = self.keyword_line(P)
141
+ if keywords:
142
+ keyword_line += " " + " ".join(keywords)
143
+
144
+ data = self.run_orca(keyword_line)
145
+
146
+ self._data = data
147
+ self.analyze(P=P, data=data)
148
+
149
+ return next_node
150
+
151
+ def analyze(self, indent="", P=None, data=None, **kwargs):
152
+ """Report the energy."""
153
+ if data is None:
154
+ data = getattr(self, "_data", {})
155
+ energy = data.get("energy")
156
+ if energy is None:
157
+ printer.normal(
158
+ __(
159
+ "ORCA finished but no final single-point energy was found.",
160
+ indent=self.indent + 4 * " ",
161
+ )
162
+ )
163
+ return
164
+
165
+ # Store on the configuration as a property.
166
+ _, configuration = self.get_system_configuration(None)
167
+ try:
168
+ configuration.properties.put(
169
+ "total energy#ORCA#{model}",
170
+ energy,
171
+ _type="float",
172
+ units="E_h",
173
+ )
174
+ except Exception as e: # pragma: no cover - property store is best-effort
175
+ logger.debug(f"Could not store energy property: {e}")
176
+
177
+ printer.normal(
178
+ __(
179
+ f"The total energy is {energy:.8f} E_h.",
180
+ indent=self.indent + 4 * " ",
181
+ )
182
+ )
183
+
184
+ def cleanup(self):
185
+ """Nothing to clean up for a single point."""
186
+ pass
@@ -0,0 +1,98 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Control parameters for an ORCA single-point energy."""
3
+
4
+ import logging
5
+
6
+ import orca_step
7
+ import seamm
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class EnergyParameters(seamm.Parameters):
13
+ """The control parameters for an ORCA energy calculation.
14
+
15
+ The default path takes the method from a preceding Model Chemistry step; the
16
+ user can turn that off and choose the method and basis explicitly (similar to
17
+ the Gaussian step). The explicit choices are driven by the step's metadata.
18
+ """
19
+
20
+ parameters = {
21
+ "use model chemistry": {
22
+ "default": "yes",
23
+ "kind": "boolean",
24
+ "default_units": "",
25
+ "enumeration": ("yes", "no"),
26
+ "format_string": "",
27
+ "description": "Use the global model chemistry:",
28
+ "help_text": (
29
+ "Use the model chemistry defined by a preceding Model Chemistry "
30
+ "step (the '_model_chemistry' variable). If ORCA cannot provide "
31
+ "it, an error is raised. Turn this off to set the method and basis "
32
+ "explicitly below."
33
+ ),
34
+ },
35
+ "method": {
36
+ "default": "DLPNO-CCSD(T)",
37
+ "kind": "enum",
38
+ "default_units": "",
39
+ "enumeration": tuple(orca_step.metadata["methods"].keys()),
40
+ "format_string": "",
41
+ "description": "Method:",
42
+ "help_text": "The ORCA method (an ORCA '!' keyword).",
43
+ },
44
+ "basis": {
45
+ "default": "def2-TZVP",
46
+ "kind": "enum",
47
+ "default_units": "",
48
+ "enumeration": tuple(orca_step.metadata["basis sets"]),
49
+ "format_string": "",
50
+ "description": "Basis set:",
51
+ "help_text": (
52
+ "The orbital basis set. ORCA's built-in name is used; a valid "
53
+ "name may also be typed in. (Basis Set Exchange is a planned "
54
+ "opt-in alternative.)"
55
+ ),
56
+ },
57
+ "auxiliary basis": {
58
+ "default": "AutoAux",
59
+ "kind": "enum",
60
+ "default_units": "",
61
+ "enumeration": tuple(orca_step.metadata["auxiliary basis sets"]),
62
+ "format_string": "",
63
+ "description": "Auxiliary (fitting) basis:",
64
+ "help_text": (
65
+ "The auxiliary/fitting basis. 'AutoAux' generates a fitting basis "
66
+ "automatically and is the robust choice for correlated methods "
67
+ "(DLPNO, MP2). 'none' omits it."
68
+ ),
69
+ },
70
+ "extra keywords": {
71
+ "default": "TightSCF",
72
+ "kind": "string",
73
+ "default_units": "",
74
+ "enumeration": tuple(),
75
+ "format_string": "",
76
+ "description": "Extra keywords:",
77
+ "help_text": (
78
+ "Any additional ORCA '!' keywords to append, e.g. 'TightSCF', "
79
+ "'RIJCOSX', 'Grid5'."
80
+ ),
81
+ },
82
+ "results": {
83
+ "default": {},
84
+ "kind": "dictionary",
85
+ "default_units": "",
86
+ "enumeration": tuple(),
87
+ "format_string": "",
88
+ "description": "results",
89
+ "help_text": "The results to save to variables or in tables.",
90
+ },
91
+ }
92
+
93
+ def __init__(self, defaults={}, data=None):
94
+ """Initialize with the parameters above plus any overrides."""
95
+ logger.debug("EnergyParameters.__init__")
96
+ super().__init__(
97
+ defaults={**EnergyParameters.parameters, **defaults}, data=data
98
+ )
@@ -0,0 +1,25 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """Stevedore helper for the ORCA Energy sub-step."""
4
+
5
+ import orca_step
6
+
7
+
8
+ class EnergyStep(object):
9
+ my_description = {
10
+ "description": "Single-point energy with ORCA",
11
+ "group": "Calculation",
12
+ "name": "Energy",
13
+ }
14
+
15
+ def __init__(self, flowchart=None, gui=None):
16
+ pass
17
+
18
+ def description(self):
19
+ return EnergyStep.my_description
20
+
21
+ def create_node(self, flowchart=None, **kwargs):
22
+ return orca_step.Energy(flowchart=flowchart, **kwargs)
23
+
24
+ def create_tk_node(self, canvas=None, **kwargs):
25
+ return orca_step.TkEnergy(canvas=canvas, **kwargs)
orca_step/installer.py ADDED
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """Installer for the ORCA plug-in.
4
+
5
+ ORCA is licensed software installed manually from https://www.faccts.de / the
6
+ ORCA forum, so this installer locates the existing ``orca`` executable and
7
+ registers its full path in seamm.ini (ORCA needs its full path to launch its
8
+ sub-programs).
9
+ """
10
+
11
+ import importlib
12
+ import logging
13
+ import shutil
14
+
15
+ import seamm_installer
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class Installer(seamm_installer.InstallerBase):
21
+ """Locate the ORCA executable and register it in seamm.ini."""
22
+
23
+ def __init__(self, logger=logger):
24
+ super().__init__(logger=logger)
25
+ logger.debug("Initializing the ORCA installer object.")
26
+
27
+ self.section = "orca-step"
28
+ self.executables = ["orca"]
29
+ self.resource_path = importlib.resources.files("orca_step") / "data"
30
+
31
+ def exe_version(self, config):
32
+ """Return the ORCA name and version.
33
+
34
+ ORCA has no stable ``--version`` flag (the version is in the run banner),
35
+ so this just confirms the executable is reachable.
36
+ """
37
+ code = config.get("code", "orca")
38
+ path = shutil.which(code) or code
39
+ return "ORCA", "unknown" if path is None else "unknown"
orca_step/metadata.py ADDED
@@ -0,0 +1,80 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """Metadata describing ORCA's methods, basis sets, and results.
4
+
5
+ This drives the explicit method/basis GUI (like the Gaussian step) and will also
6
+ back the model-chemistry protocol (``get_model_chemistry_options``) used by the
7
+ Model Chemistry step. It is kept deliberately small for now and will be filled
8
+ out as more of ORCA's capabilities are exposed.
9
+ """
10
+
11
+ metadata = {}
12
+
13
+ # Methods offered in the explicit GUI. Each is an ORCA "!" keyword. "type"
14
+ # groups them for the model-chemistry protocol (QC = wavefunction, DFT, etc.).
15
+ metadata["methods"] = {
16
+ "HF": {"type": "HF", "description": "Hartree-Fock"},
17
+ "MP2": {"type": "MP2", "description": "second-order Moller-Plesset (MP2)"},
18
+ "RI-MP2": {"type": "MP2", "description": "RI-MP2 (resolution of identity)"},
19
+ "CCSD(T)": {"type": "QC", "description": "coupled cluster CCSD(T)"},
20
+ "DLPNO-CCSD(T)": {
21
+ "type": "QC",
22
+ "description": "near-linear-scaling DLPNO-CCSD(T)",
23
+ "needs_aux": True,
24
+ },
25
+ "B3LYP": {"type": "DFT", "description": "B3LYP hybrid functional"},
26
+ "PBE": {"type": "DFT", "description": "PBE GGA functional"},
27
+ "PBE0": {"type": "DFT", "description": "PBE0 hybrid functional"},
28
+ "wB97X-D3": {
29
+ "type": "DFT",
30
+ "description": "range-separated wB97X-D3 functional",
31
+ },
32
+ "M06-2X": {"type": "DFT", "description": "M06-2X meta-hybrid functional"},
33
+ }
34
+
35
+ # Curated basis-set families (ORCA's built-in names). Free text is also allowed
36
+ # in the GUI; these are the guided choices.
37
+ metadata["basis sets"] = [
38
+ # Pople
39
+ "6-31G",
40
+ "6-31G*",
41
+ "6-31G**",
42
+ "6-31+G*",
43
+ "6-311G**",
44
+ "6-311++G**",
45
+ # Dunning correlation-consistent
46
+ "cc-pVDZ",
47
+ "cc-pVTZ",
48
+ "cc-pVQZ",
49
+ "aug-cc-pVDZ",
50
+ "aug-cc-pVTZ",
51
+ # Karlsruhe def2
52
+ "def2-SVP",
53
+ "def2-TZVP",
54
+ "def2-TZVPP",
55
+ "def2-QZVP",
56
+ ]
57
+
58
+ # Auxiliary / fitting basis choices. AutoAux generates a fitting basis for any
59
+ # method/basis and is the robust default for correlated (DLPNO/MP2) methods.
60
+ metadata["auxiliary basis sets"] = [
61
+ "AutoAux",
62
+ "none",
63
+ "def2/J",
64
+ "def2/JK",
65
+ ]
66
+
67
+ """Results ORCA can produce. Same recognized fields as the other QM steps."""
68
+ metadata["results"] = {
69
+ "energy": {
70
+ "description": "The total energy",
71
+ "dimensionality": "scalar",
72
+ "property": "total energy#ORCA#{model}",
73
+ "type": "float",
74
+ "units": "E_h",
75
+ },
76
+ }
77
+
78
+ # Placeholder for the model-chemistry protocol; populated in the Model Chemistry
79
+ # integration phase via get_model_chemistry_options() on ORCAStep.
80
+ metadata["computational models"] = {}
@@ -0,0 +1,114 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """An ORCA geometry-optimization sub-step.
4
+
5
+ Extends the Energy sub-step by adding the ``Opt`` keyword and, after the run,
6
+ updating the configuration with the optimized geometry.
7
+ """
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ import pprint # noqa: F401
12
+
13
+ import orca_step
14
+ from .energy import Energy
15
+ import seamm # noqa: F401
16
+ from seamm_util.printing import FormattedText as __
17
+ import seamm_util.printing as printing
18
+
19
+ logger = logging.getLogger(__name__)
20
+ printer = printing.getPrinter("ORCA")
21
+
22
+
23
+ class Optimization(Energy):
24
+ """A geometry optimization with ORCA.
25
+
26
+ See Also
27
+ --------
28
+ TkOptimization, OptimizationParameters, Energy
29
+ """
30
+
31
+ def __init__(
32
+ self, flowchart=None, title="Optimization", extension=None, logger=logger
33
+ ):
34
+ logger.debug(f"Creating ORCA Optimization {self}")
35
+ super().__init__(
36
+ flowchart=flowchart, title=title, extension=extension, logger=logger
37
+ )
38
+ self._calculation = "optimization"
39
+ self.parameters = orca_step.OptimizationParameters()
40
+
41
+ def description_text(self, P=None):
42
+ if not P:
43
+ P = self.parameters.values_to_dict()
44
+ use_mc = P["use model chemistry"]
45
+ if not isinstance(use_mc, bool):
46
+ use_mc = use_mc == "yes"
47
+ method = "the model chemistry" if use_mc else f"{P['method']}/{P['basis']}"
48
+ conv = P["optimization convergence"]
49
+ text = f"Geometry optimization with ORCA at {method} ({conv})."
50
+ return self.header + "\n" + __(text, indent=4 * " ").__str__()
51
+
52
+ def run(self, keywords=None):
53
+ """Run the optimization by adding the Opt keyword to the energy run."""
54
+ P = self.parameters.current_values_to_dict(
55
+ context=seamm.flowchart_variables._data
56
+ )
57
+ opt_keywords = [P["optimization convergence"]]
58
+ if keywords:
59
+ opt_keywords += list(keywords)
60
+ return super().run(keywords=opt_keywords)
61
+
62
+ def analyze(self, indent="", P=None, data=None, **kwargs):
63
+ """Report the energy and update the configuration with the optimized
64
+ geometry, plus check that the optimization converged.
65
+ """
66
+ super().analyze(indent=indent, P=P, data=data)
67
+
68
+ directory = Path(self.directory)
69
+
70
+ # Convergence check from the output.
71
+ out = directory / "orca.out"
72
+ converged = False
73
+ if out.exists():
74
+ text = out.read_text()
75
+ converged = "THE OPTIMIZATION HAS CONVERGED" in text
76
+ if not converged:
77
+ printer.normal(
78
+ __(
79
+ "Warning: the ORCA geometry optimization did not report "
80
+ "convergence.",
81
+ indent=self.indent + 4 * " ",
82
+ )
83
+ )
84
+
85
+ # Update the configuration with the optimized geometry (ORCA writes it
86
+ # to orca.xyz).
87
+ xyz = directory / "orca.xyz"
88
+ if xyz.exists():
89
+ coords = self._read_xyz_coordinates(xyz)
90
+ _, configuration = self.get_system_configuration(None)
91
+ if len(coords) == configuration.n_atoms:
92
+ configuration.atoms.set_coordinates(coords, fractionals=False)
93
+ printer.normal(
94
+ __(
95
+ "Updated the structure with the optimized geometry.",
96
+ indent=self.indent + 4 * " ",
97
+ )
98
+ )
99
+ else:
100
+ logger.warning(
101
+ f"orca.xyz has {len(coords)} atoms, configuration has "
102
+ f"{configuration.n_atoms}; not updating geometry."
103
+ )
104
+
105
+ @staticmethod
106
+ def _read_xyz_coordinates(path):
107
+ """Read [[x, y, z], ...] in Angstrom from a standard .xyz file."""
108
+ lines = path.read_text().splitlines()
109
+ n = int(lines[0].split()[0])
110
+ coords = []
111
+ for line in lines[2 : 2 + n]:
112
+ fields = line.split()
113
+ coords.append([float(fields[1]), float(fields[2]), float(fields[3])])
114
+ return coords