Source code for xtb_step.optimization

# -*- coding: utf-8 -*-

"""Non-graphical part of the Optimization step in an xTB flowchart."""

import importlib.resources
import logging
from pathlib import Path
import pprint  # noqa: F401

import xtb_step
from .energy import Energy
import molsystem
import seamm
from seamm_util import ureg, Q_  # noqa: F401
import seamm_util.printing as printing
from seamm_util.printing import FormattedText as __

logger = logging.getLogger(__name__)
job = printing.getPrinter()
printer = printing.getPrinter("xTB")

# Add this module's properties to the standard properties
path = importlib.resources.files("xtb_step") / "data"
csv_file = path / "properties.csv"
if path.exists():
    molsystem.add_properties_from_file(csv_file)


[docs] class Optimization(Energy): """Run an xTB geometry optimization (ANCopt). Inherits from :class:`Energy`; reuses the parameter set, the executor invocation, the JSON parsing, and the citation logic. Adds the ``--opt LEVEL`` flag and post-processing of the optimized geometry. See Also -------- Energy, Substep, OptimizationParameters, TkOptimization """ def __init__( self, flowchart=None, title="Optimization", extension=None, logger=logger ): """A substep for an xTB geometry optimization.""" # Skip Energy's __init__ (it sets parameters/calculation labels we # need to override) and call Substep.__init__ directly via the # grandparent. super(Energy, self).__init__( flowchart=flowchart, title=title, extension=extension, module=__name__, logger=logger, ) self._calculation = "Optimization" self._model = None self._metadata = xtb_step.metadata self.parameters = xtb_step.OptimizationParameters() # ------------------------------------------------------------------ # Description # ------------------------------------------------------------------
[docs] def description_text(self, P=None, calculation_type=None): """Override Energy's description with optimization-specific text.""" if not P: P = self.parameters.values_to_dict() level = P.get("optimization level", "normal") ctype = calculation_type or f"geometry optimization (--opt {level})" text = super().description_text(P=P, calculation_type=ctype) handling = P.get("structure handling", "Overwrite the current configuration") text += f"\n Structure handling: {handling}." return text
# ------------------------------------------------------------------ # Run # ------------------------------------------------------------------
[docs] def run(self, extra_args=None): """Run the Optimization step.""" P = self.parameters.current_values_to_dict( context=seamm.flowchart_variables._data ) # Build the --opt flag level = P.get("optimization level", "normal") opt_args = ["--opt", level] # Allow further appending by Frequencies.run(). if extra_args: opt_args = list(extra_args) + opt_args # Delegate the heavy lifting to Energy.run(), which will write # coord.xyz, invoke xtb, parse JSON, store results, and call # self.analyze(). next_node = super().run(extra_args=opt_args) # Post-process: pick up the optimized geometry from xtbopt.xyz. directory = Path(self.directory) self._handle_optimized_structure(directory, P) return next_node
# ------------------------------------------------------------------ # Geometry post-processing # ------------------------------------------------------------------ def _handle_optimized_structure(self, directory, P): """Read xtbopt.xyz and apply the user-selected structure handling. xtb writes the optimized geometry to ``xtbopt.xyz`` (XMOL format) when invoked with ``--opt``. The trajectory is in ``xtbopt.log``. """ opt_path = directory / "xtbopt.xyz" if not opt_path.exists(): logger.warning(f"xtbopt.xyz not found at {opt_path}") return handling = P.get("structure handling", "Overwrite the current configuration") if handling == "Discard the optimized structure": return try: xyz_text = opt_path.read_text() except OSError as exc: logger.warning(f"Could not read {opt_path}: {exc}") return # Choose the destination configuration per the user's request. if handling == "Overwrite the current configuration": _, configuration = self.get_system_configuration(None) elif handling == "Add a new configuration": _, configuration = self.get_system_configuration( P={"structure handling": "Create a new configuration"}, same_as="current", ) elif handling == "Add a new system": _, configuration = self.get_system_configuration( P={"structure handling": "Create a new system and configuration"}, same_as="current", ) else: logger.warning(f"Unknown structure-handling mode: {handling!r}") return # Update the configuration's coordinates from the XYZ text. if hasattr(configuration, "from_xyz_text"): try: configuration.from_xyz_text(xyz_text) except Exception as exc: logger.warning(f"Could not apply optimized geometry: {exc}") return else: # Fallback: parse the XMOL XYZ ourselves (atom count + 1 comment # + N lines of "symbol x y z"). lines = xyz_text.strip().splitlines() try: n = int(lines[0].split()[0]) except (IndexError, ValueError): logger.warning("Malformed xtbopt.xyz") return atoms = lines[2 : 2 + n] new_xyz = [] for line in atoms: parts = line.split() if len(parts) < 4: continue new_xyz.append([float(parts[1]), float(parts[2]), float(parts[3])]) try: configuration.atoms.set_coordinates(new_xyz, fractionals=False) except Exception as exc: logger.warning(f"Could not update coordinates: {exc}") printer.normal( __( f"Updated structure from {opt_path.name}.", indent=4 * " ", wrap=False, dedent=False, ) )