diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 4cc3b5f031..234f0c39e2 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -37,16 +37,24 @@ jobs: with: environment-name: arc_env environment-file: ARC/environment.yml + condarc: | + channels: + - conda-forge + - danagroup cache-environment: true + cache-environment-key: py314v3-arc-env cache-downloads: true + generate-run-shell: true # ── Complie ARC ────────────────────── - name: Build ARC in arc_env - run: micromamba run -n arc_env make compile -C ARC -j"$(nproc)" - + run: make compile -C ARC -j"$(nproc)" + shell: micromamba-shell {0} + # ── Install pyrdl ────────────────────── - name: Install pyrdl - run: micromamba run -n arc_env make install-pyrdl -C ARC -j"$(nproc)" + run: make install-pyrdl -C ARC -j"$(nproc)" + shell: micromamba-shell {0} # ── minimal TeX for png‑math in Sphinx ────────────────────── - name: System TeX tools @@ -57,6 +65,12 @@ jobs: run: micromamba install -y -n arc_env -c conda-forge libgfortran=3 shell: micromamba-shell {0} + # ── ensure ase is installed (the danagroup channel and/or + # libgfortran install can cause the solver to drop ase) + - name: Ensure ase is installed + run: pip install "ase>=3.22.1" && python -c "import ase; print(f'ase {ase.__version__} OK')" + shell: micromamba-shell {0} + # ── build HTML docs ───────────────────────────────────────── - name: Set env vars & Build docs run: | diff --git a/Dockerfile b/Dockerfile index b99169453c..592cbb8321 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,7 +55,7 @@ RUN micromamba run -n rmg_env bash -c "\ " WORKDIR /home/mambauser/Code/ARC -RUN micromamba create -y -v -n arc_env python=3.12 -f environment.yml && \ +RUN micromamba create -y -v -n arc_env python=3.14 -f environment.yml && \ micromamba install -y -v -n arc_env -c conda-forge pytest && \ micromamba clean --all -f -y diff --git a/README.md b/README.md index cb59e07086..e387b20545 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![codecov](https://codecov.io/gh/ReactionMechanismGenerator/ARC/branch/main/graph/badge.svg)](https://codecov.io/gh/ReactionMechanismGenerator/ARC) [![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT) ![Release](https://img.shields.io/badge/version-1.1.0-blue.svg) +![Python](https://img.shields.io/badge/python-3.14-blue.svg) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3356849.svg)](https://doi.org/10.5281/zenodo.3356849) ARC logo diff --git a/arc/__init__.py b/arc/__init__.py index c2cf415ad1..87a79c3f4c 100644 --- a/arc/__init__.py +++ b/arc/__init__.py @@ -1,3 +1,19 @@ +import glob +import os +import sys + +# Ensure BABEL_LIBDIR and BABEL_DATADIR are set before any openbabel import. +# The danagroup conda build doesn't configure these paths automatically. +_prefix = os.environ.get('CONDA_PREFIX', sys.prefix) +if not os.environ.get('BABEL_LIBDIR'): + _ob_dirs = glob.glob(os.path.join(_prefix, 'lib', 'openbabel', '*')) + if _ob_dirs and os.path.isdir(_ob_dirs[0]): + os.environ['BABEL_LIBDIR'] = _ob_dirs[0] +if not os.environ.get('BABEL_DATADIR'): + _ob_data = glob.glob(os.path.join(_prefix, 'share', 'openbabel', '*')) + if _ob_data and os.path.isdir(_ob_data[0]): + os.environ['BABEL_DATADIR'] = _ob_data[0] + import arc.exceptions import arc.main from arc.main import ARC diff --git a/arc/checks/common.py b/arc/checks/common.py index 4a471d2091..a53f63051a 100644 --- a/arc/checks/common.py +++ b/arc/checks/common.py @@ -5,8 +5,6 @@ import datetime -from typing import List, Optional - CONFORMER_JOB_TYPES = ('conf_opt', 'conf_sp') @@ -23,7 +21,7 @@ def is_conformer_job(job_name: str) -> bool: return job_name.startswith(CONFORMER_JOB_TYPES) -def sum_time_delta(timedelta_list: List[datetime.timedelta]) -> datetime.timedelta: +def sum_time_delta(timedelta_list: list[datetime.timedelta]) -> datetime.timedelta: """ A helper function for summing datetime.timedelta objects. @@ -39,8 +37,7 @@ def sum_time_delta(timedelta_list: List[datetime.timedelta]) -> datetime.timedel result += timedelta return result - -def get_i_from_job_name(job_name: str) -> Optional[int]: +def get_i_from_job_name(job_name: str) -> int | None: """ Get the conformer or tsg index from the job name. @@ -48,7 +45,7 @@ def get_i_from_job_name(job_name: str) -> Optional[int]: job_name (str): The job name, e.g., 'conformer12' or 'tsg5'. Returns: - Optional[int]: The corresponding conformer or tsg index. + int | None: The corresponding conformer or tsg index. """ i = None for prefix in CONFORMER_JOB_TYPES: diff --git a/arc/checks/nmd.py b/arc/checks/nmd.py index 726d07e824..230434bc76 100644 --- a/arc/checks/nmd.py +++ b/arc/checks/nmd.py @@ -5,7 +5,7 @@ import numpy as np from collections import Counter from itertools import product -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union +from typing import TYPE_CHECKING from arc.parser import parser from arc.common import get_element_mass, get_logger @@ -24,12 +24,11 @@ STD_FLOOR = 1e-4 DIRECTIONALITY_MIN_DELTA = 0.005 - def analyze_ts_normal_mode_displacement(reaction: 'ARCReaction', - job: Optional['JobAdapter'], - amplitude: Union[float, list] = 0.25, - weights: Union[bool, np.array] = True, - ) -> Optional[bool]: + job: 'JobAdapter' | None, + amplitude: float | list = 0.25, + weights: bool | np.ndarray = True, + ) -> bool | None: """ Analyze the normal mode displacement by identifying bonds that break and form and comparing them to the expected given reaction. @@ -38,14 +37,14 @@ def analyze_ts_normal_mode_displacement(reaction: 'ARCReaction', Args: reaction (ARCReaction): The reaction for which the TS is checked. job (JobAdapter): The frequency job object instance that points to the respective log file. - amplitude (Union[float, list]): The amplitude of the normal mode displacement motion to check. + amplitude (float | list): The amplitude of the normal mode displacement motion to check. If a list, all possible results are returned. - weights (Union[bool, np.array]): Whether to use weights for the displacement. + weights (bool | np.ndarray): Whether to use weights for the displacement. If ``False``, use ones as weights. If ``True``, use sqrt of atom masses. If an array, use the array values it as individual weights per atom. Returns: - Optional[bool]: Whether the TS normal mode displacement is consistent with the desired reaction. + bool | None: Whether the TS normal mode displacement is consistent with the desired reaction. """ if job is None: return None @@ -84,10 +83,9 @@ def analyze_ts_normal_mode_displacement(reaction: 'ARCReaction', return True return False - -def check_bond_directionality(formed_bonds: List[Tuple[int, int]], - broken_bonds: List[Tuple[int, int]], - xyzs: Tuple[dict, dict], +def check_bond_directionality(formed_bonds: list[tuple[int, int]], + broken_bonds: list[tuple[int, int]], + xyzs: tuple[dict, dict], min_delta: float = DIRECTIONALITY_MIN_DELTA, ) -> bool: """ @@ -98,9 +96,9 @@ def check_bond_directionality(formed_bonds: List[Tuple[int, int]], Only bonds with ``|delta| > min_delta`` are checked to avoid false failures from numerical noise. Args: - formed_bonds (List[Tuple[int, int]]): The bonds that are formed in the reaction. - broken_bonds (List[Tuple[int, int]]): The bonds that are broken in the reaction. - xyzs (Tuple[dict, dict]): The Cartesian coordinates of the TS displaced along the normal mode. + formed_bonds (list[tuple[int, int]]): The bonds that are formed in the reaction. + broken_bonds (list[tuple[int, int]]): The bonds that are broken in the reaction. + xyzs (tuple[dict, dict]): The Cartesian coordinates of the TS displaced along the normal mode. min_delta (float): Minimum absolute signed difference for a bond to participate in the check. Returns: @@ -144,14 +142,13 @@ def _get_signed_deltas(bonds): return True - def is_nmd_correct_for_any_mapping(reaction: 'ARCReaction', - xyzs: Tuple[dict, dict], - formed_bonds: List[Tuple[int, int]], - broken_bonds: List[Tuple[int, int]], - changed_bonds: List[Tuple[int, int]], - r_eq_atoms: List[List[int]], - weights: Optional[np.ndarray], + xyzs: tuple[dict, dict], + formed_bonds: list[tuple[int, int]], + broken_bonds: list[tuple[int, int]], + changed_bonds: list[tuple[int, int]], + r_eq_atoms: list[list[int]], + weights: np.ndarray | None, amplitude: float, ) -> bool: """ @@ -159,12 +156,12 @@ def is_nmd_correct_for_any_mapping(reaction: 'ARCReaction', Args: reaction (ARCReaction): The reaction for which the TS is checked. - xyzs (Tuple[dict, dict]): The Cartesian coordinates of the TS displaced along the normal mode. - formed_bonds (List[Tuple[int, int]]): The bonds that are formed in the reaction. - broken_bonds (List[Tuple[int, int]]): The bonds that are broken in the reaction. - changed_bonds (List[Tuple[int, int]]): The bonds that are changed in the reaction. - r_eq_atoms (List[List[int]]): A list of equivalent atoms in the reactants. - weights (np.array): The weights for the atoms. + xyzs (tuple[dict, dict]): The Cartesian coordinates of the TS displaced along the normal mode. + formed_bonds (list[tuple[int, int]]): The bonds that are formed in the reaction. + broken_bonds (list[tuple[int, int]]): The bonds that are broken in the reaction. + changed_bonds (list[tuple[int, int]]): The bonds that are changed in the reaction. + r_eq_atoms (list[list[int]]): A list of equivalent atoms in the reactants. + weights (np.ndarray): The weights for the atoms. amplitude (float): The motion amplitude. Returns: @@ -233,23 +230,22 @@ def is_nmd_correct_for_any_mapping(reaction: 'ARCReaction', return True return False - -def get_eq_formed_and_broken_bonds(formed_bonds: List[Tuple[int, int]], - broken_bonds: List[Tuple[int, int]], - changed_bonds: List[Tuple[int, int]], - r_eq_atoms: List[List[int]], - ) -> List[Tuple[List[Tuple[int, int]], List[Tuple[int, int]], List[Tuple[int, int]]]]: +def get_eq_formed_and_broken_bonds(formed_bonds: list[tuple[int, int]], + broken_bonds: list[tuple[int, int]], + changed_bonds: list[tuple[int, int]], + r_eq_atoms: list[list[int]], + ) -> list[tuple[list[tuple[int, int]], list[tuple[int, int]], list[tuple[int, int]]]]: """ Get the equivalent formed and broken bonds. Args: - formed_bonds (List[Tuple[int, int]]): The bonds that are formed in the reaction. - broken_bonds (List[Tuple[int, int]]): The bonds that are broken in the reaction. - changed_bonds (List[Tuple[int, int]]): The bonds that are changed in the reaction. - r_eq_atoms (List[List[int]]): A list of equivalent atoms in the reactants. + formed_bonds (list[tuple[int, int]]): The bonds that are formed in the reaction. + broken_bonds (list[tuple[int, int]]): The bonds that are broken in the reaction. + changed_bonds (list[tuple[int, int]]): The bonds that are changed in the reaction. + r_eq_atoms (list[list[int]]): A list of equivalent atoms in the reactants. Returns: - List[Tuple[List[Tuple[int, int]], List[Tuple[int, int]], List[Tuple[int, int]]]]: The equivalent formed and broken bonds. + list[tuple[list[tuple[int, int]], list[tuple[int, int]], list[tuple[int, int]]]]: The equivalent formed and broken bonds. """ all_changing_indices = list() for bond in formed_bonds + broken_bonds: @@ -262,23 +258,22 @@ def get_eq_formed_and_broken_bonds(formed_bonds: List[Tuple[int, int]], equivalences=r_eq_atoms) return modified_bond_grand_list - -def translate_all_tuples_simultaneously(list_1: List[Tuple[int, int]], - list_2: List[Tuple[int, int]], - list_3: List[Tuple[int, int]], - equivalences: List[List[int]], - ) -> List[Tuple[List[Tuple[int, int]], List[Tuple[int, int]], List[Tuple[int, int]]]]: +def translate_all_tuples_simultaneously(list_1: list[tuple[int, int]], + list_2: list[tuple[int, int]], + list_3: list[tuple[int, int]], + equivalences: list[list[int]], + ) -> list[tuple[list[tuple[int, int]], list[tuple[int, int]], list[tuple[int, int]]]]: """ Translate all tuples simultaneously using a mapping. Args: - list_1 (List[Tuple[int, int]]): The first list of tuples. - list_2 (List[Tuple[int, int]]): The second list of tuples. - list_3 (List[Tuple[int, int]]): The third list of tuples. - equivalences (List[List[int]]): A list of equivalent atoms. + list_1 (list[tuple[int, int]]): The first list of tuples. + list_2 (list[tuple[int, int]]): The second list of tuples. + list_3 (list[tuple[int, int]]): The third list of tuples. + equivalences (list[list[int]]): A list of equivalent atoms. Returns: - List[Tuple[List[Tuple[int, int]], List[Tuple[int, int]], List[Tuple[int, int]]]]: The translated tuples. + list[tuple[list[tuple[int, int]], list[tuple[int, int]], list[tuple[int, int]]]]: The translated tuples. """ mapping = create_equivalence_mapping(equivalences) all_indices = {i for tup in list_1 + list_2 + list_3 for i in tup} @@ -300,16 +295,15 @@ def translate_all_tuples_simultaneously(list_1: List[Tuple[int, int]], all_translated_tuples.append((translated_list_1, translated_list_2, translated_list_3)) return all_translated_tuples - -def create_equivalence_mapping(equivalences: List[List[int]]) -> Dict[int, List[int]]: +def create_equivalence_mapping(equivalences: list[list[int]]) -> dict[int, list[int]]: """ Create a mapping of atom indices to equivalence groups. Args: - equivalences (List[List[int]]): A list of equivalent atoms. + equivalences (list[list[int]]): A list of equivalent atoms. Returns: - Dict[int, List[int]]: The mapping of atom indices to equivalence groups + dict[int, list[int]]: The mapping of atom indices to equivalence groups """ mapping = dict() for group in equivalences: @@ -317,21 +311,20 @@ def create_equivalence_mapping(equivalences: List[List[int]]) -> Dict[int, List[ mapping[item] = group return mapping - def get_weights_from_xyz(xyz: dict, - weights: Union[bool, np.array] = True, - ) -> np.array: + weights: bool | np.ndarray = True, + ) -> np.ndarray: """ Get weights for atoms in a molecule. Args: xyz (dict): The Cartesian coordinates. - weights (Union[bool, np.array]): Whether to use weights for the displacement. + weights (bool | np.ndarray): Whether to use weights for the displacement. If ``False``, use ones as weights. If ``True``, use sqrt of atom masses. If an array, use it as weights. Returns: - np.array: The weights for the atoms. + np.ndarray: The weights for the atoms. """ if isinstance(weights, bool): if weights: @@ -347,23 +340,22 @@ def get_weights_from_xyz(xyz: dict, weights = np.ones((len(xyz['symbols']), 1)) return weights - def get_displaced_xyzs(xyz: dict, amplitude: float, - normal_mode_disp: np.array, - weights: np.array, - ) -> Tuple[dict, dict]: + normal_mode_disp: np.ndarray, + weights: np.ndarray, + ) -> tuple[dict, dict]: """ Get the Cartesian coordinates of the TS displaced along a normal mode. Args: xyz (dict): The Cartesian coordinates. amplitude (float): The amplitude of the displacement. - normal_mode_disp (np.array): The normal mode displacement matrix corresponding to the imaginary frequency. - weights (np.array): The weights for the atoms. + normal_mode_disp (np.ndarray): The normal mode displacement matrix corresponding to the imaginary frequency. + weights (np.ndarray): The weights for the atoms. Returns: - Tuple[dict, dict]: The Cartesian coordinates of the TS displaced along the normal mode. + tuple[dict, dict]: The Cartesian coordinates of the TS displaced along the normal mode. """ np_coords = xyz_to_np_array(xyz) xyz_1 = xyz_from_data(coords=np_coords - amplitude * normal_mode_disp * weights, @@ -372,11 +364,10 @@ def get_displaced_xyzs(xyz: dict, symbols=xyz['symbols'], isotopes=xyz['isotopes']) return xyz_1, xyz_2 - -def get_bond_length_changes_baseline_and_std(non_reactive_bonds: List[Tuple[int, int]], - xyzs: Tuple[dict, dict], - weights: Optional[np.array] = None, - ) -> Tuple[Optional[float], Optional[float]]: +def get_bond_length_changes_baseline_and_std(non_reactive_bonds: list[tuple[int, int]], + xyzs: tuple[dict, dict], + weights: np.ndarray | None = None, + ) -> tuple[float | None, float | None]: """ Get the baseline and spread of bond length changes for non-reactive bonds using robust statistics. @@ -388,12 +379,12 @@ def get_bond_length_changes_baseline_and_std(non_reactive_bonds: List[Tuple[int, non_reactive_bonds = set(r_bonds) & set(p_bonds) Args: - non_reactive_bonds (List[Tuple[int, int]]): The non-reactive bonds. - xyzs (Tuple[dict, dict]): The Cartesian coordinates of the TS displaced along the normal mode. - weights (np.array): The weights for the atoms. + non_reactive_bonds (list[tuple[int, int]]): The non-reactive bonds. + xyzs (tuple[dict, dict]): The Cartesian coordinates of the TS displaced along the normal mode. + weights (np.ndarray): The weights for the atoms. Returns: - Tuple[Optional[float], Optional[float]]: + tuple[float | None, float | None]: - The median baseline of bond length differences for non-reactive bonds. - The MAD-based spread estimate of bond length differences for non-reactive bonds. """ @@ -405,22 +396,21 @@ def get_bond_length_changes_baseline_and_std(non_reactive_bonds: List[Tuple[int, std = mad * 1.4826 # scale factor for consistency with normal distribution return baseline, std - -def get_bond_length_changes(bonds: Union[List[Tuple[int, int]], Set[Tuple[int, int]]], - xyzs: Tuple[dict, dict], - weights: Optional[np.array] = None, - amplitude: Optional[float] = None, +def get_bond_length_changes(bonds: list[tuple[int, int]] | set[tuple[int, int]], + xyzs: tuple[dict, dict], + weights: np.ndarray | None = None, + amplitude: float | None = None, return_none_if_change_is_insignificant: bool = False, considered_reactive: bool = False, - ) -> Optional[np.array]: + ) -> np.ndarray | None: """ Get the bond length changes of specific bonds. Args: - bonds (Union[list, tuple]): The bonds to check. - xyzs (Tuple[dict, dict]): The Cartesian coordinates of the TS displaced along the normal mode. - weights (np.array, optional): The weights for the atoms. - amplitude (Optional[float]): The motion amplitude. + bonds (list | tuple): The bonds to check. + xyzs (tuple[dict, dict]): The Cartesian coordinates of the TS displaced along the normal mode. + weights (np.ndarray, optional): The weights for the atoms. + amplitude (float | None): The motion amplitude. return_none_if_change_is_insignificant (bool, optional): Whether to check for significant motion and return None if motion is insignificant. Relevant for bonds that change during a reaction, @@ -428,7 +418,7 @@ def get_bond_length_changes(bonds: Union[List[Tuple[int, int]], Set[Tuple[int, i considered_reactive (bool): Whether the bonds are considered reactive in the reaction. Returns: - Optional[np.array]: The bond length changes of the specified bonds. + np.ndarray | None: The bond length changes of the specified bonds. """ diffs = list() report = None @@ -450,18 +440,17 @@ def get_bond_length_changes(bonds: Union[List[Tuple[int, int]], Set[Tuple[int, i diffs = np.array(diffs) return diffs, report - -def get_bond_length_in_reaction(bond: Union[Tuple[int, int], List[int]], +def get_bond_length_in_reaction(bond: tuple[int, int] | list[int], xyz: dict, - weights: Optional[np.array] = None, - ) -> Optional[float]: + weights: np.ndarray | None = None, + ) -> float | None: """ Get the length of a bond in either the reactants or the products of a reaction. Args: - bond (Tuple[int, int]): The bond to check. + bond (tuple[int, int]): The bond to check. xyz (dict): The Cartesian coordinates mapped to the indices of the reactants. - weights (np.array): The weights for the atoms. + weights (np.ndarray): The weights for the atoms. Returns: float: The bond length. @@ -476,10 +465,9 @@ def get_bond_length_in_reaction(bond: Union[Tuple[int, int], List[int]], return distance.item() return float(distance) - def find_equivalent_atoms(reaction: 'ARCReaction', reactant_only: bool = True, - ) -> Tuple[List[List[int]], List[List[int]]]: + ) -> tuple[list[list[int]], list[list[int]]]: """ Find equivalent atoms in the reactants and products of a reaction. This is a tentative function that should be replaced when atom mapping returns a list. @@ -490,7 +478,7 @@ def find_equivalent_atoms(reaction: 'ARCReaction', reactant_only (bool): Whether to search for equivalent atoms in the reactants only. Returns: - Tuple[List[List[int]], List[List[int]]]: + tuple[list[list[int]], list[list[int]]]: - A list of equivalent atoms in the reactants. - A list of equivalent atoms in the products, indices are atom mapped to the reactants. """ @@ -509,21 +497,20 @@ def find_equivalent_atoms(reaction: 'ARCReaction', )) return r_eq_atoms, p_eq_atoms - def identify_equivalent_atoms_in_molecule(molecule: 'Molecule', inc: int = 0, - atom_map: Optional[List[int]] = None, - ) -> List[List[int]]: + atom_map: list[int] | None = None, + ) -> list[list[int]]: """ Identify equivalent atoms in a molecule. Args: molecule (Molecule): The molecule to check. inc (int): The increment to be added. - atom_map (Optional[List[int]]): The atom map. + atom_map (list[int] | None): The atom map. Returns: - List[List[int]]: A list of equivalent atoms. + list[list[int]]: A list of equivalent atoms. """ element_counts = Counter([atom.element.number for atom in molecule.atoms]) repeated_elements = [element for element, count in element_counts.items() if count > 1] @@ -551,10 +538,9 @@ def identify_equivalent_atoms_in_molecule(molecule: 'Molecule', eq_atoms = [eq for eq in eq_atoms if len(eq) > 1] return eq_atoms - def fingerprint_atom(atom_index: int, molecule: 'Molecule', - excluded_atom_indices: Optional[List[int]] = None, + excluded_atom_indices: list[int] | None = None, depth: int = 3, ) -> list: """ @@ -563,11 +549,11 @@ def fingerprint_atom(atom_index: int, Args: atom_index (int): The index of the atom to map. molecule (Molecule): The molecule to which the atom belongs. - excluded_atom_indices (Optional[List[int]]): Atom indices to exclude from the mapping. + excluded_atom_indices (list[int] | None): Atom indices to exclude from the mapping. depth (int): The depth of the atom map. Returns: - List[int]: The atom map. + list[int]: The atom map. """ atom_0 = molecule.atoms[atom_index] fingerprint = [atom_0.element.number] diff --git a/arc/checks/ts.py b/arc/checks/ts.py index b37aee705b..f6f55417cb 100644 --- a/arc/checks/ts.py +++ b/arc/checks/ts.py @@ -6,7 +6,7 @@ import os import numpy as np -from typing import TYPE_CHECKING, List, Optional, Tuple, Union +from typing import TYPE_CHECKING from arc.parser import parser from arc.checks.nmd import analyze_ts_normal_mode_displacement @@ -35,16 +35,15 @@ MAX_IRC_FRAGMENTS_FOR_CHARGE_SEARCH = 4 LOWEST_MAJOR_TS_FREQ, HIGHEST_MAJOR_TS_FREQ = settings['LOWEST_MAJOR_TS_FREQ'], settings['HIGHEST_MAJOR_TS_FREQ'] - def check_ts(reaction: 'ARCReaction', - job: Optional['JobAdapter'] = None, - checks: Optional[List[str]] = None, - rxn_zone_atom_indices: Optional[List[int]] = None, - species_dict: Optional[dict] = None, - project_directory: Optional[str] = None, - kinetics_adapter: Optional[str] = None, - output: Optional[dict] = None, - sp_level: Optional['Level'] = None, + job: 'JobAdapter' | None = None, + checks: list[str] | None = None, + rxn_zone_atom_indices: list[int] | None = None, + species_dict: dict | None = None, + project_directory: str | None = None, + kinetics_adapter: str | None = None, + output: dict | None = None, + sp_level: 'Level' | None = None, freq_scale_factor: float = 1.0, skip_nmd: bool = False, verbose: bool = True, @@ -60,8 +59,8 @@ def check_ts(reaction: 'ARCReaction', Args: reaction (ARCReaction): The reaction for which the TS is checked. job (JobAdapter, optional): The frequency job object instance. - checks (List[str], optional): Specific checks to run. Optional values: 'energy', 'NMD', 'IRC', 'rotors'. - rxn_zone_atom_indices (List[int], optional): The 0-indices of atoms identified by the normal mode displacement + checks (list[str], optional): Specific checks to run. Optional values: 'energy', 'NMD', 'IRC', 'rotors'. + rxn_zone_atom_indices (list[int], optional): The 0-indices of atoms identified by the normal mode displacement as the reaction zone. Automatically determined if not given. species_dict (dict, optional): The Scheduler species dictionary. project_directory (str, optional): The path to ARC's project directory. @@ -102,9 +101,8 @@ def check_ts(reaction: 'ARCReaction', invalidate_rotors_with_both_pivots_in_a_reactive_zone(reaction, job, rxn_zone_atom_indices=rxn_zone_atom_indices) - def ts_passed_checks(species: 'ARCSpecies', - exemptions: Optional[List[str]] = None, + exemptions: list[str] | None = None, verbose: bool = False, ) -> bool: """ @@ -112,7 +110,7 @@ def ts_passed_checks(species: 'ARCSpecies', Args: species (ARCSpecies): The TS species. - exemptions (List[str], optional): Keys of the TS.ts_checks dict to pass. + exemptions (list[str], optional): Keys of the TS.ts_checks dict to pass. verbose (bool, optional): Whether to log findings. Returns: @@ -126,7 +124,6 @@ def ts_passed_checks(species: 'ARCSpecies', return False return True - def check_rxn_e_elect(reaction: 'ARCReaction', verbose: bool = True, ) -> None: @@ -165,7 +162,6 @@ def check_rxn_e_elect(reaction: 'ARCReaction', if 'Could not determine TS e_elect relative to the wells; ' not in reaction.ts_species.ts_checks['warnings']: reaction.ts_species.ts_checks['warnings'] += 'Could not determine TS e_elect relative to the wells; ' - def compute_rxn_e0(reaction: 'ARCReaction', species_dict: dict, project_directory: str, @@ -173,7 +169,7 @@ def compute_rxn_e0(reaction: 'ARCReaction', output: dict, sp_level: 'Level', freq_scale_factor: float = 1.0, - ) -> Optional['ARCReaction']: + ) -> 'ARCReaction' | None: """ Checking the E0 values between wells and a TS in a ``reaction`` using ZPE from statmech. This function computed E0 values and populates them in a copy of the given reaction instance. @@ -188,7 +184,7 @@ def compute_rxn_e0(reaction: 'ARCReaction', freq_scale_factor (float, optional): The frequency scaling factor. Returns: - Optional['ARCReaction']: A copy of the reaction object with E0 values populated. + 'ARCReaction' | None: A copy of the reaction object with E0 values populated. """ if any(val is None for val in [species_dict, project_directory, kinetics_adapter, output, sp_level, freq_scale_factor]): @@ -217,7 +213,6 @@ def compute_rxn_e0(reaction: 'ARCReaction', statmech_adapter.compute_thermo(e0_only=True, skip_rotors=True) return rxn_copy - def check_rxn_e0(reaction: 'ARCReaction', verbose: bool = True, ): @@ -249,7 +244,6 @@ def check_rxn_e0(reaction: 'ARCReaction', else: reaction.ts_species.ts_checks['E0'] = True - def report_ts_and_wells_energy(r_e: float, p_e: float, ts_e: float, @@ -281,10 +275,9 @@ def report_ts_and_wells_energy(r_e: float, f'TS: {ts_text}\n' f'Products: {p_text}') - def check_normal_mode_displacement(reaction: 'ARCReaction', - job: Optional['JobAdapter'], - amplitude: Optional[Union[float, list]] = None, + job: 'JobAdapter' | None, + amplitude: float | list | None = None, ): """ Check the normal mode displacement by identifying bonds that break and form @@ -293,7 +286,7 @@ def check_normal_mode_displacement(reaction: 'ARCReaction', Args: reaction (ARCReaction): The reaction for which the TS is checked. job (JobAdapter): The frequency job object instance. - amplitude (Union[float, list]): The amplitude of the normal mode displacement motion to check. + amplitude (float | list): The amplitude of the normal mode displacement motion to check. If a list, all possible results are returned. """ amplitude = amplitude or 0.25 @@ -302,21 +295,21 @@ def check_normal_mode_displacement(reaction: 'ARCReaction', amplitude=amplitude, ) -def determine_changing_bond(bond: Tuple[int, ...], - dmat_bonds_1: List[Tuple[int, int]], - dmat_bonds_2: List[Tuple[int, int]], - ) -> Optional[str]: +def determine_changing_bond(bond: tuple[int, ...], + dmat_bonds_1: list[tuple[int, int]], + dmat_bonds_2: list[tuple[int, int]], + ) -> str | None: """ Determine whether a bond breaks or forms in a TS. Note that ``bond`` and all bond entries in `dmat_bonds_1/2`` must be already sorted from small to large indices. Args: - bond (Tuple[int]): The atom indices describing the bond. - dmat_bonds_1 (List[Tuple[int, int]]): The bonds perceived from dmat_1. - dmat_bonds_2 (List[Tuple[int, int]]): The bonds perceived from dmat_2. + bond (tuple[int]): The atom indices describing the bond. + dmat_bonds_1 (list[tuple[int, int]]): The bonds perceived from dmat_1. + dmat_bonds_2 (list[tuple[int, int]]): The bonds perceived from dmat_2. Returns: - Optional[bool]: + bool | None: 'forming' if the bond indeed forms between ``dmat_1`` and ``dmat_2``, 'breaking' if it indeed breaks, ``None`` if it does not change significantly. """ @@ -329,10 +322,9 @@ def determine_changing_bond(bond: Tuple[int, ...], return 'breaking' return None - def invalidate_rotors_with_both_pivots_in_a_reactive_zone(reaction: 'ARCReaction', job: 'JobAdapter', - rxn_zone_atom_indices: Optional[List[int]] = None, + rxn_zone_atom_indices: list[int] | None = None, ): """ Invalidate rotors in which both pivots are included in the reactive zone. @@ -340,7 +332,7 @@ def invalidate_rotors_with_both_pivots_in_a_reactive_zone(reaction: 'ARCReaction Args: reaction (ARCReaction): The respective reaction object instance. job (JobAdapter): The frequency job object instance. - rxn_zone_atom_indices (List[int], optional): The 0-indices of atoms identified by the normal mode displacement + rxn_zone_atom_indices (list[int], optional): The 0-indices of atoms identified by the normal mode displacement as the reaction zone. Automatically determined if not given. """ rxn_zone_atom_indices = rxn_zone_atom_indices or get_rxn_zone_atom_indices(reaction, job) @@ -354,10 +346,9 @@ def invalidate_rotors_with_both_pivots_in_a_reactive_zone(reaction: 'ARCReaction rotor['invalidation_reason'] += 'Pivots participate in the TS reaction zone (code: pivTS). ' logger.info(f"\nNot considering rotor {key} with pivots {rotor['pivots']} in TS {reaction.ts_species.label}\n") - def get_rxn_zone_atom_indices(reaction: 'ARCReaction', job: 'JobAdapter', - ) -> List[int]: + ) -> list[int]: """ Get the reaction zone atom indices by parsing normal mode displacement. @@ -366,7 +357,7 @@ def get_rxn_zone_atom_indices(reaction: 'ARCReaction', job (JobAdapter): The frequency job object instance. Returns: - List[int]: The indices of the atoms participating in the reaction. + list[int]: The indices of the atoms participating in the reaction. The indices are 0-indexed and sorted in an increasing order. """ freqs, normal_mode_disp = parser.parse_normal_mode_displacement(log_file_path=job.local_path_to_output_file, @@ -379,11 +370,10 @@ def get_rxn_zone_atom_indices(reaction: 'ARCReaction', indices = sorted(range(len(normal_mode_disp_rms)), key=lambda i: normal_mode_disp_rms[i], reverse=True)[:num_of_atoms] return indices - def get_rms_from_normal_mode_disp(normal_mode_disp: np.ndarray, freqs: np.ndarray, - reaction: Optional['ARCReaction'] = None, - ) -> List[float]: + reaction: 'ARCReaction' | None = None, + ) -> list[float]: """ Get the root mean squares of the normal mode displacements. Use atom mass weights if ``reaction`` is given. @@ -394,7 +384,7 @@ def get_rms_from_normal_mode_disp(normal_mode_disp: np.ndarray, reaction (ARCReaction): The respective reaction object instance. Returns: - List[float]: The RMS of the normal mode displacements. + list[float]: The RMS of the normal mode displacements. """ mode_index = get_index_of_abs_largest_neg_freq(freqs) nmd = normal_mode_disp[mode_index] @@ -404,8 +394,7 @@ def get_rms_from_normal_mode_disp(normal_mode_disp: np.ndarray, rms.append(((entry[0] ** 2 + entry[1] ** 2 + entry[2] ** 2) ** 0.5) * masses[i] ** 0.55) return rms - -def get_index_of_abs_largest_neg_freq(freqs: np.ndarray) -> Optional[int]: +def get_index_of_abs_largest_neg_freq(freqs: np.ndarray) -> int | None: """ Get the index of the |largest| negative frequency. @@ -413,16 +402,15 @@ def get_index_of_abs_largest_neg_freq(freqs: np.ndarray) -> Optional[int]: freqs (np.ndarray): Entries are frequency values. Returns: - Optional[int]: The 0-index of the largest absolute negative frequency. + int | None: The 0-index of the largest absolute negative frequency. """ if not len(freqs) or all(freq > 0 for freq in freqs): return None return list(freqs).index(min(freqs)) - -def get_expected_num_atoms_with_largest_normal_mode_disp(normal_mode_disp_rms: List[float], - ts_guesses: List['TSGuess'], - reaction: Optional['ARCReaction'] = None, +def get_expected_num_atoms_with_largest_normal_mode_disp(normal_mode_disp_rms: list[float], + ts_guesses: list['TSGuess'], + reaction: 'ARCReaction' | None = None, ) -> int: """ Get the number of atoms that are expected to have the largest normal mode displacement for the TS @@ -430,8 +418,8 @@ def get_expected_num_atoms_with_largest_normal_mode_disp(normal_mode_disp_rms: L It is theoretically possible that TSGuesses of the same species will belong to different families. Args: - normal_mode_disp_rms (List[float]): The RMS of the normal mode displacements. - ts_guesses (List[TSGuess]): The TSGuess objects of a TS species. + normal_mode_disp_rms (list[float]): The RMS of the normal mode displacements. + ts_guesses (list[TSGuess]): The TSGuess objects of a TS species. reaction (ARCReaction): The respective reaction object instance. Returns: @@ -448,10 +436,9 @@ def get_expected_num_atoms_with_largest_normal_mode_disp(normal_mode_disp_rms: L for family in families]) return num_of_atoms - -def get_rxn_normal_mode_disp_atom_number(rxn_family: Optional[str] = None, - reaction: Optional['ARCReaction'] = None, - rms_list: Optional[List[float]] = None, +def get_rxn_normal_mode_disp_atom_number(rxn_family: str | None = None, + reaction: 'ARCReaction' | None = None, + rms_list: list[float] | None = None, ) -> int: """ Get the number of atoms expected to have the largest normal mode displacement per family. @@ -460,7 +447,7 @@ def get_rxn_normal_mode_disp_atom_number(rxn_family: Optional[str] = None, Args: rxn_family (str, optional): The reaction family label. reaction (ARCReaction, optional): The reaction object instance. - rms_list (List[float], optional): The root mean squares of the normal mode displacements. + rms_list (list[float], optional): The root mean squares of the normal mode displacements. Raises: TypeError: If ``rms_list`` is not ``None`` and is either not a list or does not contain floats. @@ -492,10 +479,9 @@ def get_rxn_normal_mode_disp_atom_number(rxn_family: Optional[str] = None, number_by_family += 1 return number_by_family - def check_irc_species_and_rxn(xyz_1: dict, xyz_2: dict, - rxn: Optional['ARCReaction'], + rxn: 'ARCReaction' | None, ): """ Check that the two species that result from optimizing the outputs of two IRC runs @@ -550,10 +536,9 @@ def check_irc_species_and_rxn(xyz_1: dict, or _check_equal_bonds_list(dmat_bonds_2, r_bonds) and _check_equal_bonds_list(dmat_bonds_1, p_bonds): rxn.ts_species.ts_checks['IRC'] = True - def _perceive_irc_fragments(xyz: dict, charge: int = 0, - ) -> Optional[List['Molecule']]: + ) -> list['Molecule'] | None: """ Perceive individual molecular fragments from an IRC endpoint geometry. @@ -567,7 +552,7 @@ def _perceive_irc_fragments(xyz: dict, charge (int): The net charge of the full system. Returns: - Optional[List[Molecule]]: A list of perceived ``Molecule`` objects (one per fragment), + list[Molecule] | None: A list of perceived ``Molecule`` objects (one per fragment), or ``None`` if perception fails for any fragment. """ symbols = xyz['symbols'] @@ -639,9 +624,8 @@ def _perceive_irc_fragments(xyz: dict, break return best_mols - -def _match_fragments_to_species(fragments: List['Molecule'], - expected_mols: List['Molecule'], +def _match_fragments_to_species(fragments: list['Molecule'], + expected_mols: list['Molecule'], ) -> bool: """ Check whether a list of perceived molecular fragments matches a list of expected species @@ -649,8 +633,8 @@ def _match_fragments_to_species(fragments: List['Molecule'], one-to-one matching between fragments and expected species using backtracking with pruning. Args: - fragments (List[Molecule]): Perceived fragment molecules from an IRC endpoint. - expected_mols (List[Molecule]): Expected species molecules from the reaction. + fragments (list[Molecule]): Perceived fragment molecules from an IRC endpoint. + expected_mols (list[Molecule]): Expected species molecules from the reaction. Returns: bool: Whether a valid one-to-one isomorphic matching exists. @@ -682,16 +666,15 @@ def _backtrack(i: int) -> bool: return _backtrack(0) - -def _check_equal_bonds_list(bonds_1: List[Tuple[int, int]], - bonds_2: List[Tuple[int, int]], +def _check_equal_bonds_list(bonds_1: list[tuple[int, int]], + bonds_2: list[tuple[int, int]], ) -> bool: """ Check whether two lists of bonds are equal. Args: - bonds_1 (List[Tuple[int, int]]): List 1 of bonds. - bonds_2 (List[Tuple[int, int]]): List 2 of bonds. + bonds_1 (list[tuple[int, int]]): List 1 of bonds. + bonds_2 (list[tuple[int, int]]): List 2 of bonds. Returns: bool: Whether the two lists of bonds are equal. @@ -702,8 +685,7 @@ def _check_equal_bonds_list(bonds_1: List[Tuple[int, int]], return True return False - -def check_imaginary_frequencies(imaginary_freqs: Optional[List[float]]) -> bool: +def check_imaginary_frequencies(imaginary_freqs: list[float] | None) -> bool: """ Check that the number of imaginary frequencies make sense. Theoretically, a TS should only have one "large" imaginary frequency, @@ -711,7 +693,7 @@ def check_imaginary_frequencies(imaginary_freqs: Optional[List[float]]) -> bool: This method does not consider the normal mode displacement check. Args: - imaginary_freqs (List[float]): The imaginary frequencies of the TS guess after optimization. + imaginary_freqs (list[float]): The imaginary frequencies of the TS guess after optimization. Returns: bool: Whether the imaginary frequencies make sense. diff --git a/arc/common.py b/arc/common.py index 32df575376..031fa51cbe 100644 --- a/arc/common.py +++ b/arc/common.py @@ -20,7 +20,8 @@ import warnings import yaml from collections import deque -from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Any +from collections.abc import Sequence import numpy as np import pandas as pd @@ -29,11 +30,9 @@ from arc.exceptions import InputError, SettingsError from arc.imports import settings - if TYPE_CHECKING: from arc.molecule.molecule import Molecule - logger = logging.getLogger('arc') logging.getLogger('matplotlib.font_manager').disabled = True @@ -48,8 +47,7 @@ default_job_types, servers, supported_ess = settings['default_job_types'], settings['servers'], settings['supported_ess'] - -def initialize_job_types(job_types: Optional[dict] = None, +def initialize_job_types(job_types: dict | None = None, specific_job_type: str = '', ) -> dict: """ @@ -112,8 +110,7 @@ def initialize_job_types(job_types: Optional[dict] = None, logger.info(f'\nConsidering the following job types: {job_types_report}\n') return job_types - -def check_ess_settings(ess_settings: Optional[dict] = None) -> dict: +def check_ess_settings(ess_settings: dict | None = None) -> dict: """ A helper function to convert servers in the ess_settings dict to lists Assists in troubleshooting job and trying a different server @@ -150,10 +147,9 @@ def check_ess_settings(ess_settings: Optional[dict] = None) -> dict: logger.info(f'\nUsing the following ESS settings:\n{pprint.pformat(settings_dict)}\n') return settings_dict - def initialize_log(log_file: str, project: str, - project_directory: Optional[str] = None, + project_directory: str | None = None, verbose: int = logging.INFO, ) -> None: """ @@ -211,14 +207,12 @@ def initialize_log(log_file: str, warnings.filterwarnings(action='ignore', module='.*matplotlib.*') logging.captureWarnings(capture=False) - def get_logger(): """ Get the ARC logger (avoid having multiple entries of the logger). """ return logger - def log_header(project: str, level: int = logging.INFO, ) -> None: @@ -256,7 +250,6 @@ def log_header(project: str, logger.info(f'Starting project {project}\n\n') - def log_footer(execution_time: str, level: int = logging.INFO, ) -> None: @@ -271,8 +264,7 @@ def log_footer(execution_time: str, logger.log(level, f'Total execution time: {execution_time}') logger.log(level, f'ARC execution terminated on {time.asctime()}') - -def get_git_commit(path: Optional[str] = None) -> Tuple[str, str]: +def get_git_commit(path: str | None = None) -> tuple[str, str]: """ Get the recent git commit to be logged. @@ -295,8 +287,7 @@ def get_git_commit(path: Optional[str] = None) -> Tuple[str, str]: return head, date return head, date - -def get_git_branch(path: Optional[str] = None) -> str: +def get_git_branch(path: str | None = None) -> str: """ Get the git branch to be logged. @@ -315,13 +306,13 @@ def get_git_branch(path: Optional[str] = None) -> str: for branch_name in branch_list: if '*' in branch_name.decode(): return branch_name.decode()[2:] + return '' else: return '' - def read_yaml_file(path: str, - project_directory: Optional[str] = None, - ) -> Union[dict, list]: + project_directory: str | None = None, + ) -> dict | list: """ Read a YAML file (usually an input / restart file, but also conformers file) and return the parameters as python variables. @@ -330,7 +321,7 @@ def read_yaml_file(path: str, path (str): The YAML file path to read. project_directory (str, optional): The current project directory to rebase upon. - Returns: Union[dict, list] + Returns: dict | list The content read from the file. """ if project_directory is not None: @@ -343,9 +334,8 @@ def read_yaml_file(path: str, content = yaml.load(stream=f, Loader=yaml.FullLoader) return content - def save_yaml_file(path: str, - content: Union[list, dict], + content: list | dict, ) -> None: """ Save a YAML file (usually an input / restart file, but also conformers file). @@ -362,19 +352,17 @@ def save_yaml_file(path: str, with open(path, 'w') as f: f.write(yaml_str) - -def from_yaml(yaml_str: str) -> Union[dict, list]: +def from_yaml(yaml_str: str) -> dict | list: """ Read a YAML string and decode to the respective Python object. Args: yaml_str (str): The YAML string content. - Returns: Union[dict, list] + Returns: dict | list The respective Python object. """ return yaml.load(stream=yaml_str, Loader=yaml.FullLoader) - -def to_yaml(py_content: Union[list, dict]) -> str: +def to_yaml(py_content: list | dict) -> str: """ Convert a Python list or dictionary to a YAML string format. @@ -388,7 +376,6 @@ def to_yaml(py_content: Union[list, dict]) -> str: yaml_str = yaml.dump(data=py_content) return yaml_str - def globalize_paths(file_path: str, project_directory: str, ) -> str: @@ -425,7 +412,6 @@ def globalize_paths(file_path: str, else: return file_path - def globalize_path(string: str, project_directory: str, ) -> str: @@ -453,7 +439,6 @@ def globalize_path(string: str, string = string.replace(old_dir, project_directory) return string - def delete_check_files(project_directory: str): """ Delete ESS checkfiles. They usually take up lots of space and are not needed after ARC terminates. @@ -472,7 +457,6 @@ def delete_check_files(project_directory: str): logged = True os.remove(os.path.join(root, file_)) - def string_representer(dumper, data): """ Add a custom string representer to use block literals for multiline strings. @@ -481,7 +465,6 @@ def string_representer(dumper, data): return dumper.represent_scalar(tag='tag:yaml.org,2002:str', value=data, style='|') return dumper.represent_scalar(tag='tag:yaml.org,2002:str', value=data) - def get_ordinal_indicator(number: int) -> str: """ Returns the ordinal indicator for an integer. @@ -499,7 +482,6 @@ def get_ordinal_indicator(number: int) -> str: return ordinal_dict[number] return 'th' - def get_number_with_ordinal_indicator(number: int) -> str: """ Returns the number as a string with the ordinal indicator. @@ -512,11 +494,11 @@ def get_number_with_ordinal_indicator(number: int) -> str: """ return f'{number}{get_ordinal_indicator(number)}' -def read_element_dicts() -> Tuple[dict, dict, dict, dict]: +def read_element_dicts() -> tuple[dict, dict, dict, dict]: """ Read the element dictionaries from the elements.yml data file. - Returns: Tuple[dict, dict, dict] + Returns: tuple[dict, dict, dict] - A dictionary of element symbol by name. - A dictionary of element number by symbol. - A dictionary of element mass by symbol, including isotope and occurrence frequency. @@ -531,11 +513,9 @@ def read_element_dicts() -> Tuple[dict, dict, dict, dict]: covalent_radii = {element['symbol']: element['radius'] for element in covalent_radii} return symbol_by_number, number_by_symbol, mass_by_symbol, covalent_radii - SYMBOL_BY_NUMBER, NUMBER_BY_SYMBOL, MASS_BY_SYMBOL, COVALENT_RADII = read_element_dicts() - -def get_atom_radius(symbol: str) -> Optional[float]: +def get_atom_radius(symbol: str) -> float | None: """ Get the atom covalent radius of an atom in Angstroms. @@ -552,10 +532,9 @@ def get_atom_radius(symbol: str) -> Optional[float]: r = COVALENT_RADII.get(symbol, None) return r - -def get_element_mass(input_element: Union[int, str], - isotope: Optional[int] = None, - ) -> Tuple[float, int]: +def get_element_mass(input_element: int | str, + isotope: int | None = None, + ) -> tuple[float, int]: """ Returns the mass and z number of the requested isotope for a given element. Data taken from NIST, https://physics.nist.gov/cgi-bin/Compositions/stand_alone.pl (accessed October 2018) @@ -564,7 +543,7 @@ def get_element_mass(input_element: Union[int, str], input_element (int, str): The atomic number or symbol of the element. isotope (int, optional): The isotope number. - Returns: Tuple[float, int] + Returns: tuple[float, int] - The mass of the element in amu. - The atomic number of the element. """ @@ -611,7 +590,6 @@ def get_element_mass(input_element: Union[int, str], mass = iso_mass[1] return mass, number - # A bond length dictionary of single bonds in Angstrom. # https://sites.google.com/site/chempendix/bond-lengths # https://courses.lumenlearning.com/suny-potsdam-organicchemistry/chapter/1-3-basics-of-bonding/ @@ -633,7 +611,6 @@ def get_element_mass(input_element: Union[int, str], 'I_I': 2.66, } - def get_single_bond_length(symbol_1: str, symbol_2: str, charge_1: int = 0, @@ -661,28 +638,27 @@ def get_single_bond_length(symbol_1: str, return SINGLE_BOND_LENGTH[bond2] return 1.75 - def get_bonds_from_dmat( dmat: np.ndarray, - elements: Union[Tuple[str, ...], List[str]], - charges: Optional[List[int]] = None, + elements: tuple[str, ...] | list[str], + charges: list[int] | None = None, tolerance: float = 1.2, bond_lone_hydrogens: bool = True, n_fragments: int = 1, -) -> List[Tuple[int, int]]: +) -> list[tuple[int, int]]: """ Guess the connectivity of a molecule from its distance matrix. Args: dmat (np.ndarray): An NxN matrix of atom distances in Angstrom. - elements (List[str]): The corresponding element list in the same atomic order. - charges (List[int], optional): A corresponding list of formal atomic charges. + elements (list[str]): The corresponding element list in the same atomic order. + charges (list[int], optional): A corresponding list of formal atomic charges. tolerance (float, optional): Factor by which the single‐bond length threshold is multiplied. bond_lone_hydrogens (bool, optional): Whether to bond unassigned H atoms. n_fragments (int, optional): Desired number of disconnected fragments. Defaults to 1. Returns: - List[Tuple[int, int]]: Sorted list of bonded atom index pairs. + list[tuple[int, int]]: Sorted list of bonded atom index pairs. """ n = dmat.shape[0] if len(elements) != n or dmat.shape != (n, n): @@ -825,8 +801,7 @@ def _fragments(): break return sorted(bonds) - -def determine_top_group_indices(mol: 'Molecule', atom1: 'Atom', atom2: 'Atom', index: int = 1) -> Tuple[list, bool]: +def determine_top_group_indices(mol: 'Molecule', atom1: 'Atom', atom2: 'Atom', index: int = 1) -> tuple[list, bool]: """ Determine the indices of a "top group" in a molecule. The top is defined as all atoms connected to atom2, including atom2, excluding the direction of atom1. @@ -838,7 +813,7 @@ def determine_top_group_indices(mol: 'Molecule', atom1: 'Atom', atom2: 'Atom', i atom2 (Atom): The beginning of the top relative to atom1 in mol. index (int, optional): Whether to return 1-index or 0-index conventions. 1 for 1-index. - Returns: Tuple[list, bool] + Returns: tuple[list, bool] - The indices of the atoms in the top (either 0-index or 1-index, as requested). - Whether the top has heavy atoms (is not just a hydrogen atom). ``True`` if it has heavy atoms. """ @@ -858,10 +833,9 @@ def determine_top_group_indices(mol: 'Molecule', atom1: 'Atom', atom2: 'Atom', i atom_list_to_explore1, atom_list_to_explore2 = atom_list_to_explore2, [] return top, not atom2.is_hydrogen() - def extremum_list(lst: list, return_min: bool = True, - ) -> Optional[Union[int, None]]: + ) -> int | None: """ A helper function for finding the extremum (either minimum or maximum) of a list of numbers (int/float) where some entries could be ``None``. @@ -871,7 +845,7 @@ def extremum_list(lst: list, return_min (bool, optional): Whether to return the minimum or the maximum. ``True`` for minimum, ``False`` for maximum, ``True`` by default. - Returns: Optional[Union[int, None]] + Returns: int | None The entry with the minimal/maximal value. """ if lst is None or len(lst) == 0 or all([entry is None for entry in lst]): @@ -883,11 +857,10 @@ def extremum_list(lst: list, else: return max([entry for entry in lst if entry is not None]) - def get_extremum_index(lst: list, return_min: bool = True, - skip_values: Optional[list] = None - ) -> Optional[Union[int, None]]: + skip_values: list | None = None + ) -> int | None: """ A helper function for finding the extremum (either minimum or maximum) of a list of numbers (int/float) where some entries could be ``None``. @@ -898,7 +871,7 @@ def get_extremum_index(lst: list, ``True`` for minimum, ``False`` for maximum, ``True`` by default. skip_values (list, optional): Values to skip when checking for extermum, e.g., 0. - Returns: Optional[Union[int, None]] + Returns: int | None The index of an entry with the minimal/maximal value. """ if len(lst) == 0: @@ -920,10 +893,9 @@ def get_extremum_index(lst: list, extremum_index = i return extremum_index - -def sum_list_entries(lst: List[Union[int, float]], - multipliers: Optional[List[Union[int, float]]] = None, - ) -> Optional[float]: +def sum_list_entries(lst: list[int | float], + multipliers: list[int | float] | None = None, + ) -> float | None: """ Sum all entries in a list. If any entry is ``None``, return ``None``. If ``multipliers`` is given, multiply each entry in ``lst`` by the respective multiplier entry. @@ -933,7 +905,7 @@ def sum_list_entries(lst: List[Union[int, float]], multipliers (list, optional): A list of multipliers. Returns: - Optional[float]: The result. + float | None: The result. """ if any(entry is None or not isinstance(entry, (int, float)) for entry in lst): return None @@ -941,10 +913,9 @@ def sum_list_entries(lst: List[Union[int, float]], return sum(lst) return float(np.dot(lst, multipliers + [1] * (len(lst) - len(multipliers)))) - -def sort_two_lists_by_the_first(list1: List[Union[float, int, None]], - list2: List[Union[float, int, None]], - ) -> Tuple[List[Union[float, int]], List[Union[float, int]]]: +def sort_two_lists_by_the_first(list1: list[float | int | None], + list2: list[float | int | None], + ) -> tuple[list[float | int], list[float | int]]: """ Sort two lists in increasing order by the values of the first list. Ignoring None entries from list1 and their respective entries in list2. @@ -959,7 +930,7 @@ def sort_two_lists_by_the_first(list1: List[Union[float, int, None]], Raises: InputError: If types are wrong, or lists are not the same length. - Returns: Tuple[list, list] + Returns: tuple[list, list] - Sorted values from list1, ignoring None entries. - Respective entries from list2. """ @@ -988,8 +959,7 @@ def sort_two_lists_by_the_first(list1: List[Union[float, int, None]], sorted_list2[counter] = new_list2[index] return sorted_list1, sorted_list2 - -def check_that_all_entries_are_in_list(list_1: Union[list, tuple], list_2: Union[list, tuple]) -> bool: +def check_that_all_entries_are_in_list(list_1: list | tuple, list_2: list | tuple) -> bool: """ Check that all entries from ``list_2`` are in ``list_1``, and that the lists are the same length. Useful for testing that two lists are equal regardless of entry order. @@ -1008,7 +978,6 @@ def check_that_all_entries_are_in_list(list_1: Union[list, tuple], list_2: Union return False return True - def key_by_val(dictionary: dict, value: Any, ) -> Any: @@ -1031,9 +1000,8 @@ def key_by_val(dictionary: dict, return key raise ValueError(f'Could not find value {value} in the dictionary\n{dictionary}') - -def almost_equal_lists(iter1: Union[list, tuple, np.ndarray], - iter2: Union[list, tuple, np.ndarray], +def almost_equal_lists(iter1: list | tuple | np.ndarray, + iter2: list | tuple | np.ndarray, rtol: float = 1e-05, atol: float = 1e-08, ) -> bool: @@ -1041,8 +1009,8 @@ def almost_equal_lists(iter1: Union[list, tuple, np.ndarray], A helper function for checking whether two iterables are almost equal. Args: - iter1 (list, tuple, np.array): An iterable. - iter2 (list, tuple, np.array): An iterable. + iter1 (list, tuple, np.ndarray): An iterable. + iter2 (list, tuple, np.ndarray): An iterable. rtol (float, optional): The relative tolerance parameter. atol (float, optional): The absolute tolerance parameter. @@ -1065,7 +1033,6 @@ def almost_equal_lists(iter1: Union[list, tuple, np.ndarray], return False return True - def almost_equal_coords(xyz1: dict, xyz2: dict, rtol: float = 1e-03, @@ -1095,9 +1062,8 @@ def almost_equal_coords(xyz1: dict, return False return True - -def almost_equal_coords_lists(xyz1: Union[List[dict], dict], - xyz2: Union[List[dict], dict], +def almost_equal_coords_lists(xyz1: list[dict] | dict, + xyz2: list[dict] | dict, rtol: float = 1e-03, atol: float = 1e-04, ) -> bool: @@ -1106,8 +1072,8 @@ def almost_equal_coords_lists(xyz1: Union[List[dict], dict], Useful for comparing xyzs in unit tests. Args: - xyz1 (Union[List[dict], dict]): Either a dict-format xyz, or a list of them. - xyz2 (Union[List[dict], dict]): Either a dict-format xyz, or a list of them. + xyz1 (list[dict] | dict): Either a dict-format xyz, or a list of them. + xyz2 (list[dict] | dict): Either a dict-format xyz, or a list of them. rtol (float, optional): The relative tolerance parameter. atol (float, optional): The absolute tolerance parameter. @@ -1126,9 +1092,8 @@ def almost_equal_coords_lists(xyz1: Union[List[dict], dict], return True return False - -def is_equal_family_product_dicts(dicts1: List[dict], - dicts2: List[dict], +def is_equal_family_product_dicts(dicts1: list[dict], + dicts2: list[dict], ) -> bool: """ Compare two lists of family‐product dictionaries for equality. @@ -1174,7 +1139,6 @@ def is_equal_family_product_dicts(dicts1: List[dict], return True - def is_notebook() -> bool: """ Check whether ARC was called from an IPython notebook. @@ -1193,8 +1157,7 @@ def is_notebook() -> bool: except NameError: return False # Probably standard Python interpreter. - -def is_str_float(value: Optional[str]) -> bool: +def is_str_float(value: str | None) -> bool: """ Check whether a string can be converted to a floating number. @@ -1210,8 +1173,7 @@ def is_str_float(value: Optional[str]) -> bool: except (ValueError, TypeError): return False - -def is_str_int(value: Optional[str]) -> bool: +def is_str_int(value: str | None) -> bool: """ Check whether a string can be converted to an integer. @@ -1227,7 +1189,6 @@ def is_str_int(value: Optional[str]) -> bool: except (ValueError, TypeError): return False - def clean_text(text: str) -> str: """ Clean a text string from leading and trailing whitespaces, newline characters, and double quotes. @@ -1245,7 +1206,6 @@ def clean_text(text: str) -> str: text = text.lstrip('\n').rstrip('\n') return text - def time_lapse(t0) -> str: """ A helper function returning the elapsed time since t0. @@ -1266,11 +1226,10 @@ def time_lapse(t0) -> str: d = '' return f'{d}{h:02.0f}:{m:02.0f}:{s:02.0f}' - def estimate_orca_mem_cpu_requirement(num_heavy_atoms: int, server: str = '', consider_server_limits: bool = False, - ) -> Tuple[int, float]: + ) -> tuple[int, float]: """ Estimates memory and cpu requirements for an Orca job. @@ -1279,7 +1238,7 @@ def estimate_orca_mem_cpu_requirement(num_heavy_atoms: int, server (str): The name of the server where Orca runs. consider_server_limits (bool): Try to give realistic estimations. - Returns: Tuple[int, float]: + Returns: tuple[int, float]: - The amount of total memory (MB) - The number of cpu cores required for the Orca job for a given species. """ @@ -1307,12 +1266,11 @@ def estimate_orca_mem_cpu_requirement(num_heavy_atoms: int, return est_cpu, est_memory - def check_torsion_change(torsions: pd.DataFrame, - index_1: Union[int, str], - index_2: Union[int, str], - threshold: Union[float, int] = 20.0, - delta: Union[float, int] = 0.0, + index_1: int | str, + index_2: int | str, + threshold: float | int = 20.0, + delta: float | int = 0.0, ) -> pd.DataFrame: """ Compare two sets of torsions (in DataFrame) and check if any entry has a @@ -1321,10 +1279,10 @@ def check_torsion_change(torsions: pd.DataFrame, Args: torsions (pd.DataFrame): A DataFrame consisting of multiple sets of torsions. - index_1 (Union[int, str]): The index of the first conformer. - index_2 (Union[int, str]): The index of the second conformer. - threshold (Union[float, int]): The threshold used to determine the difference significance. - delta (Union[float, int]): A known difference between torsion pairs, + index_1 (int | str): The index of the first conformer. + index_2 (int | str): The index of the second conformer. + threshold (float | int): The threshold used to determine the difference significance. + delta (float | int): A known difference between torsion pairs, delta = tor[index_1] - tor[index_2]. E.g.,for the torsions to be scanned, the differences are equal to the scan resolution. @@ -1347,18 +1305,17 @@ def check_torsion_change(torsions: pd.DataFrame, change[label] = False return change - -def is_same_pivot(torsion1: Union[list, str], - torsion2: Union[list, str], - ) -> Optional[bool]: +def is_same_pivot(torsion1: list | str, + torsion2: list | str, + ) -> bool | None: """ Check if two torsions have the same pivots. Args: - torsion1 (Union[list, str]): The four atom indices representing the first torsion. + torsion1 (list | str): The four atom indices representing the first torsion. torsion2 (Union: [list, str]): The four atom indices representing the second torsion. - Returns: Optional[bool] + Returns: bool | None ``True`` if two torsions share the same pivots. """ torsion1 = ast.literal_eval(torsion1) if isinstance(torsion1, str) else torsion1 @@ -1367,7 +1324,7 @@ def is_same_pivot(torsion1: Union[list, str], return False if torsion1[1:3] == torsion2[1:3] or torsion1[1:3] == torsion2[1:3][::-1]: return True - + return False def is_same_sequence_sublist(child_list: list, parent_list: list) -> bool: """ @@ -1392,7 +1349,6 @@ def is_same_sequence_sublist(child_list: list, parent_list: list) -> bool: return True return False - def distance_matrix(a: np.ndarray, b: np.ndarray) -> np.ndarray: """ Compute the Euclidean distance matrix between rows of two arrays. @@ -1414,11 +1370,10 @@ def distance_matrix(a: np.ndarray, b: np.ndarray) -> np.ndarray: sq_diff = diff ** 2 return np.sqrt(np.sum(sq_diff, axis=-1)) - def get_ordered_intersection_of_two_lists(l1: list, l2: list, - order_by_first_list: Optional[bool] = True, - return_unique: Optional[bool] = True, + order_by_first_list: bool | None = True, + return_unique: bool | None = True, ) -> list: """ Find the intersection of two lists by order. @@ -1456,7 +1411,6 @@ def get_ordered_intersection_of_two_lists(l1: list, return l3 - def is_angle_linear(angle: float, tolerance: float = 0.9, ) -> bool: @@ -1472,8 +1426,7 @@ def is_angle_linear(angle: float, """ return (180 - tolerance < angle <= 180) or (0 <= angle < tolerance) - -def is_xyz_linear(xyz: Optional[dict]) -> Optional[bool]: +def is_xyz_linear(xyz: dict | None) -> bool | None: """ Determine whether the xyz coords represents a linear molecule. @@ -1506,12 +1459,11 @@ def is_xyz_linear(xyz: Optional[dict]) -> Optional[bool]: return False return True - FULL_CIRCLE = 360.0 HALF_CIRCLE = 180.0 def get_angle_in_180_range(angle: float, - round_to: Optional[int] = 2, + round_to: int | None = 2, ) -> float: """ Get the corresponding angle in the -180 to +180 degree range. @@ -1526,7 +1478,6 @@ def get_angle_in_180_range(angle: float, """ return (angle + HALF_CIRCLE) % FULL_CIRCLE - HALF_CIRCLE - def signed_angular_diff(phi_1: float, phi_2: float) -> float: r""" Compute the signed minimal angular difference between two angles (in degrees). @@ -1559,19 +1510,18 @@ def signed_angular_diff(phi_1: float, phi_2: float) -> float: """ return get_angle_in_180_range(phi_1 - phi_2) - -def get_close_tuple(key_1: Tuple[Union[float, str], ...], - keys: List[Tuple[Union[float, str], ...]], +def get_close_tuple(key_1: tuple[float | str, ...], + keys: list[tuple[float | str, ...]], tolerance: float = 0.05, raise_error: bool = False, - ) -> Optional[Tuple[Union[float, str], ...]]: + ) -> tuple[float | str, ...] | None: """ Get a key from a list of keys close in value to the given key. Even if just one of the items in the key has a close match, use the close value. Args: - key_1 (Tuple[Union[float, str], Union[float, str]]): The key used for the search. - keys (List[Tuple[Union[float, str], Union[float, str]]]): The list of keys to search within. + key_1 (tuple[float | str, float | str]): The key used for the search. + keys (list[tuple[float | str, float | str]]): The list of keys to search within. tolerance (float, optional): The tolerance within which keys are determined to be close. raise_error (bool, optional): Whether to raise a ValueError if a close key wasn't found. @@ -1580,7 +1530,7 @@ def get_close_tuple(key_1: Tuple[Union[float, str], ...], ValueError: If a close key was not found and ``raise_error`` is ``True``. Returns: - Optional[Tuple[Union[float, str], ...]]: A key from the keys list close in value to the given key. + tuple[float | str, ...] | None: A key from the keys list close in value to the given key. """ key_1_floats = tuple(float(item) for item in key_1) for key_2 in keys: @@ -1605,7 +1555,6 @@ def get_close_tuple(key_1: Tuple[Union[float, str], ...], return None raise ValueError(f'Could not locate a key close to {key_1} within the tolerance {tolerance} in the given keys list.') - def timedelta_from_str(time_str: str): """ Get a datetime.timedelta object from its str() representation @@ -1628,10 +1577,9 @@ def timedelta_from_str(time_str: str): time_params[name] = int(param) return datetime.timedelta(**time_params) - -def torsions_to_scans(descriptor: Optional[List[List[int]]], +def torsions_to_scans(descriptor: list[list[int]] | None, direction: int = 1, - ) -> Optional[List[List[int]]]: + ) -> list[list[int]] | None: """ Convert torsions to scans or vice versa. In ARC we define a torsion as a list of four atoms with 0-indices. @@ -1643,7 +1591,7 @@ def torsions_to_scans(descriptor: Optional[List[List[int]]], direction (int, optional): 1: Convert torsions to scans; -1: Convert scans to torsions. Returns: - Optional[List[List[int]]]: The converted indices. + list[list[int]] | None: The converted indices. """ if descriptor is None: return None @@ -1657,8 +1605,7 @@ def torsions_to_scans(descriptor: Optional[List[List[int]]], raise ValueError(f'Got an illegal value when converting:\n{descriptor}\ninto:\n{new_descriptor}') return new_descriptor - -def convert_list_index_0_to_1(_list: Union[list, tuple], direction: int = 1) -> Union[list, tuple]: +def convert_list_index_0_to_1(_list: list | tuple, direction: int = 1) -> list | tuple: """ Convert a list from 0-indexed to 1-indexed, or vice versa. Ensures positive values in the resulting list. @@ -1671,7 +1618,7 @@ def convert_list_index_0_to_1(_list: Union[list, tuple], direction: int = 1) -> ValueError: If the new list contains negative values. Returns: - Union[list, tuple]: The converted indices. + list | tuple: The converted indices. """ new_list = [item + direction for item in _list] if any(val < 0 for val in new_list): @@ -1680,16 +1627,15 @@ def convert_list_index_0_to_1(_list: Union[list, tuple], direction: int = 1) -> new_list = tuple(new_list) return new_list - -def calc_rmsd(x: Union[list, np.array], - y: Union[list, np.array], +def calc_rmsd(x: list | np.ndarray, + y: list | np.ndarray, ) -> float: """ Compute the root-mean-square deviation between two matrices. Args: - x (np.array): Matrix 1. - y (np.array): Matrix 2. + x (np.ndarray): Matrix 1. + y (np.ndarray): Matrix 2. Returns: float: The RMSD score of two matrices. @@ -1702,7 +1648,6 @@ def calc_rmsd(x: Union[list, np.array], rmsd = np.sqrt(sqr_sum / n) return float(rmsd) - def safe_copy_file(source: str, destination: str, wait: int = 10, @@ -1735,11 +1680,10 @@ def safe_copy_file(source: str, if i >= max_cycles: break - def dfs(mol: 'Molecule', start: int, sort_result: bool = True, - ) -> List[int]: + ) -> list[int]: """ A depth-first search algorithm for graph traversal of a Molecule object instance. @@ -1749,7 +1693,7 @@ def dfs(mol: 'Molecule', sort_result (bool, optional): Whether to sort the returned visited indices. Returns: - List[int]: Indices of all atoms connected to the starting atom. + list[int]: Indices of all atoms connected to the starting atom. """ if start >= len(mol.atoms): raise ValueError(f'Got a wrong start number {start} for a molecule with only {len(mol.atoms)} atoms.') @@ -1766,7 +1710,6 @@ def dfs(mol: 'Molecule', visited = sorted(visited) if sort_result else visited return visited - def sort_atoms_in_descending_label_order(mol: 'Molecule') -> None: """ If all atoms in the molecule object have a label, this function reassign the @@ -1785,7 +1728,6 @@ def sort_atoms_in_descending_label_order(mol: 'Molecule') -> None: logger.warning(f"Some atom(s) in molecule.atoms are not integers.\nGot {[atom.label for atom in mol.atoms]}") return None - def is_xyz_mol_match(mol: 'Molecule', xyz: dict) -> bool: """ @@ -1814,7 +1756,6 @@ def is_xyz_mol_match(mol: 'Molecule', return False return True - def convert_to_hours(time_str:str) -> float: """Convert walltime string in format HH:MM:SS to hours. @@ -1827,11 +1768,10 @@ def convert_to_hours(time_str:str) -> float: h, m, s = map(int, time_str.split(':')) return h + m / 60 + s / 3600 - -def calculate_arrhenius_rate_coefficient(A: Union[int, float, Sequence[float], np.ndarray], - n: Union[int, float, Sequence[float], np.ndarray], - Ea: Union[int, float, Sequence[float], np.ndarray], - T: Union[int, float, Sequence[float], np.ndarray], +def calculate_arrhenius_rate_coefficient(A: int | float | Sequence[float] | np.ndarray, + n: int | float | Sequence[float] | np.ndarray, + Ea: int | float | Sequence[float] | np.ndarray, + T: int | float | Sequence[float] | np.ndarray, Ea_units: str = 'kJ/mol', ) -> np.ndarray: """ diff --git a/arc/common_test.py b/arc/common_test.py index 9c1fa9be9a..8c0b74ecee 100644 --- a/arc/common_test.py +++ b/arc/common_test.py @@ -875,13 +875,13 @@ def test_globalize_paths(self): globalized_restart_path = os.path.join(project_directory, 'restart_paths_globalized.yml') content = common.read_yaml_file(globalized_restart_path) self.assertEqual(content['output']['restart'], 'Restarted ARC at 2020-02-28 12:51:14.446086; ') - self.assertIn('ARC/arc/testing/restart/4_globalized_paths/calcs/Species/HCN/freq_a38229/output.out', + self.assertIn('arc/testing/restart/4_globalized_paths/calcs/Species/HCN/freq_a38229/output.out', content['output']['spc']['paths']['freq']) self.assertNotIn('gpfs/workspace/users/user', content['output']['spc']['paths']['freq']) path = '/home/user/runs/ARC/ARC_Project/calcs/Species/H/sp_a4339/output.out' new_path = common.globalize_path(path, project_directory) - self.assertIn('/ARC/arc/testing/restart/4_globalized_paths/calcs/Species/H/sp_a4339/output.out', new_path) + self.assertIn('arc/testing/restart/4_globalized_paths/calcs/Species/H/sp_a4339/output.out', new_path) def test_globalize_path(self): """Test rebasing a single path to the current ARC project""" diff --git a/arc/family/family.py b/arc/family/family.py index 27de1ab2d9..99514c7db6 100644 --- a/arc/family/family.py +++ b/arc/family/family.py @@ -2,7 +2,7 @@ A module for working with RMG reaction families. """ -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union +from typing import TYPE_CHECKING import ast import os import re @@ -20,10 +20,8 @@ RMG_DB_PATH = settings['RMG_DB_PATH'] ARC_FAMILIES_PATH = settings['ARC_FAMILIES_PATH'] - logger = get_logger() - def get_rmg_db_subpath(*parts: str, must_exist: bool = False) -> str: """Return a path under the RMG database, handling both source and packaged layouts.""" if RMG_DB_PATH is None: @@ -40,7 +38,6 @@ def get_rmg_db_subpath(*parts: str, must_exist: bool = False) -> str: return candidate return candidates[0] - class ReactionFamily(object): """ A class for representing a reaction family. @@ -79,7 +76,7 @@ def __str__(self): """ return f'ReactionFamily(label={self.label})' - def get_groups_file_as_lines(self, consider_arc_families: bool = True) -> List[str]: + def get_groups_file_as_lines(self, consider_arc_families: bool = True) -> list[str]: """ Get the groups file as a list of lines. Precedence is given to RMG families (ARC families should therefore have distinct names than RMG's) @@ -88,7 +85,7 @@ def get_groups_file_as_lines(self, consider_arc_families: bool = True) -> List[s consider_arc_families (bool, optional): Whether to consider ARC's custom families. Returns: - List[str]: The groups file as a list of lines. + list[str]: The groups file as a list of lines. """ groups_path = get_rmg_db_subpath('kinetics', 'families', self.label, 'groups.py', must_exist=True) if not os.path.isfile(groups_path): @@ -101,8 +98,8 @@ def get_groups_file_as_lines(self, consider_arc_families: bool = True) -> List[s return groups_as_lines def generate_products(self, - reactants: List['ARCSpecies'], - ) -> Dict[Union[str, Tuple[str, str]], List[Tuple[List['Molecule'], Dict[int, str]]]]: + reactants: list['ARCSpecies'], + ) -> dict[str | tuple[str, str], list[tuple[list['Molecule'], dict[int, str]]]]: """ Generate a list of all the possible reaction products of this family starting from the list of ``reactants``. @@ -112,10 +109,10 @@ def generate_products(self, 1: [{'group': 0, 'subgroup':