From e5a4da984968a0e5399ef256490680fc508c7f84 Mon Sep 17 00:00:00 2001 From: benshi97 Date: Sun, 9 Jun 2024 10:45:06 +0100 Subject: [PATCH 01/29] Class for MRCC inputs --- src/quacc/atoms/skzcam.py | 619 ++++++++++++++------------------ tests/core/atoms/test_skzcam.py | 264 ++++++-------- 2 files changed, 397 insertions(+), 486 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index 9ffad083f6..2d08d69e6e 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -158,392 +158,329 @@ class MultiplicityDict(TypedDict): has_chemshell = find_spec("chemsh") is not None +class MRCCInputGenerator(): + def __init__(self, + embedded_adsorbed_cluster: Atoms, + quantum_cluster_indices: list[int], + ecp_region_indices: list[int], + element_info: dict[ElementStr, ElementInfo] | None = None, + include_cp: bool = True, + multiplicities: MultiplicityDict | None = None) -> None: + """ + Parameters + ---------- + embedded_adsorbed_cluster + The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This object is created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + quantum_cluster_indices + A list containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + ecp_region_indices + A list containing the indices of the atoms in each ECP region. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + element_info + A dictionary with elements as keys which gives the (1) number of core electrons as 'core', (2) basis set as 'basis', (3) effective core potential as 'ecp', (4) resolution-of-identity/density-fitting auxiliary basis set for DFT/HF calculations as 'ri_scf_basis' and (5) resolution-of-identity/density-fitting for correlated wave-function methods as 'ri_cwft_basis'. + include_cp + If True, the coords strings will include the counterpoise correction (i.e., ghost atoms) for the adsorbate and slab. + multiplicities + The multiplicity of the adsorbate-slab complex, adsorbate and slab respectively, with the keys 'adsorbate_slab', 'adsorbate', and 'slab'. + + Returns + ------- + None + """ + + # Check that none of the indices in quantum_cluster_indices are in ecp_region_indices + if not np.all([x not in ecp_region_indices for x in quantum_cluster_indices]): + raise ValueError("An atom in the quantum cluster is also in the ECP region.") + + self.embedded_adsorbed_cluster = embedded_adsorbed_cluster + self.quantum_cluster_indices = quantum_cluster_indices + self.ecp_region_indices = ecp_region_indices + self.element_info = element_info + + # Set multiplicities + if multiplicities is None: + self.multiplicities = {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} + else: + self.multiplicities = multiplicities -def create_mrcc_eint_blocks( - embedded_adsorbed_cluster: Atoms, - quantum_cluster_indices: list[int], - ecp_region_indices: list[int], - element_info: dict[ElementStr, ElementInfo] | None = None, - include_cp: bool = True, - multiplicities: MultiplicityDict | None = None, -) -> BlockInfo: - """ - Creates the orcablocks input for the MRCC ASE calculator. + # Create the adsorbate-slab complex quantum cluster and ECP region cluster + self.adsorbate_slab_cluster = self.embedded_adsorbed_cluster[self.quantum_cluster_indices] + self.ecp_region = self.embedded_adsorbed_cluster[self.ecp_region_indices] - Parameters - ---------- - embedded_adsorbed_cluster - The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This object is created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - quantum_cluster_indices - A list containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - ecp_region_indices - A list containing the indices of the atoms in each ECP region. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - element_info - A dictionary with elements as keys which gives the (1) number of core electrons as 'core', (2) basis set as 'basis', (3) effective core potential as 'ecp', (4) resolution-of-identity/density-fitting auxiliary basis set for DFT/HF calculations as 'ri_scf_basis' and (5) resolution-of-identity/density-fitting for correlated wave-function methods as 'ri_cwft_basis'. - include_cp - If True, the coords strings will include the counterpoise correction for the adsorbate and slab. - multiplicities - The multiplicity of the adsorbate-slab complex, adsorbate and slab respectively, with the keys 'adsorbate_slab', 'adsorbate', and 'slab'. + # Get the indices of the adsorbates from the quantum cluster + self.adsorbate_indices = [ + i + for i in range(len(self.adsorbate_slab_cluster)) + if self.adsorbate_slab_cluster.get_array("atom_type")[i] == "adsorbate" + ] + # Get the indices of the slab from the quantum cluster + self.slab_indices = [ + i + for i in range(len(self.adsorbate_slab_cluster)) + if self.adsorbate_slab_cluster.get_array("atom_type")[i] != "adsorbate" + ] - Returns - ------- - BlockInfo - The ORCA input block (to be put in 'orcablocks' parameter) as a string for the adsorbate-slab complex, the adsorbate, and the slab in a dictionary with the keys 'adsorbate_slab', 'adsorbate', and 'slab' respectively. - """ + # Create the adsorbate and slab quantum clusters + self.adsorbate_cluster = self.adsorbate_slab_cluster[self.adsorbate_indices] + self.slab_cluster = self.adsorbate_slab_cluster[self.slab_indices] - # Create the blocks for the basis sets (basis, basis_sm, dfbasis_scf, dfbasis_cor, ecp) - basis_ecp_block = generate_mrcc_basis_ecp_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=quantum_cluster_indices, - ecp_region_indices=ecp_region_indices, - element_info=element_info, - include_cp=include_cp, - ) + self.include_cp = include_cp - # Create the blocks for the coordinates - coords_block = generate_mrcc_coords_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=quantum_cluster_indices, - ecp_region_indices=ecp_region_indices, - element_info=element_info, - include_cp=include_cp, - multiplicities=multiplicities, - ) + # Initialize the mrccblocks input strings for the adsorbate-slab complex, adsorbate, and slab + self.mrccblocks = { + "adsorbate_slab": "", + "adsorbate": "", + "slab": "", + } - # Create the point charge block - point_charge_block = generate_mrcc_point_charge_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=quantum_cluster_indices, - ecp_region_indices=ecp_region_indices, - ) - # Combine the blocks - return { - "adsorbate_slab": basis_ecp_block["adsorbate_slab"] - + coords_block["adsorbate_slab"] - + point_charge_block, - "adsorbate": basis_ecp_block["adsorbate"] + coords_block["adsorbate"], - "slab": basis_ecp_block["slab"] + coords_block["slab"] + point_charge_block, - } + def generate_input(self) -> BlockInfo: + """ + Creates the mrccblocks input for the MRCC ASE calculator. + Parameters + ---------- -def generate_mrcc_basis_ecp_block( - embedded_adsorbed_cluster: Atoms, - quantum_cluster_indices: list[int], - ecp_region_indices: list[int], - element_info: dict[ElementStr, ElementInfo] | None = None, - include_cp: bool = True, -) -> BlockInfo: - """ - Generates the basis and ECP block for the MRCC input file. + Returns + ------- + BlockInfo + The MRCC input block (to be put in 'mrccblocks' parameter) as a string for the adsorbate-slab complex, the adsorbate, and the slab in a dictionary with the keys 'adsorbate_slab', 'adsorbate', and 'slab' respectively. + """ - Parameters - ---------- - embedded_adsorbed_cluster - The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file. This object is created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function and should contain the atom_types for the adsorbate and slab. - quantum_cluster_indices - A list of lists containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - ecp_region_indices - A list of lists containing the indices of the atoms in the ECP region for each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - element_info - A dictionary with elements as keys which gives the (1) number of core electrons as 'core', (2) basis set as 'basis', (3) effective core potential as 'ecp', (4) resolution-of-identity/density-fitting auxiliary basis set for DFT/HF calculations as 'ri_scf_basis' and (5) resolution-of-identity/density-fitting for correlated wave-function methods as 'ri_cwft_basis'. - include_cp - If True, the coords strings will include the counterpoise correction (i.e., ghost atoms) for the adsorbate and slab. + # Create the blocks for the basis sets (basis, basis_sm, dfbasis_scf, dfbasis_cor, ecp) + self.generate_basis_ecp_block() - Returns - ------- - BlockInfo - The basis and ECP block for the MRCC input file for the adsorbate-slab complex, the adsorbate, and the slab in a dictionary with the keys 'adsorbate_slab', 'adsorbate', and 'slab' respectively. - """ + # Create the blocks for the coordinates + self.generate_coords_block() - # Create the quantum cluster and ECP region cluster - adsorbate_slab_cluster = embedded_adsorbed_cluster[quantum_cluster_indices] - ecp_region = embedded_adsorbed_cluster[ecp_region_indices] + # Create the point charge block and add it to the adsorbate-slab complex and slab blocks + point_charge_block = self.generate_point_charge_block() + self.mrccblocks["adsorbate_slab"] += point_charge_block + self.mrccblocks["slab"] += point_charge_block - # Get the indices of the adsorbates from the quantum cluster - adsorbate_indices = [ - i - for i in range(len(adsorbate_slab_cluster)) - if adsorbate_slab_cluster.get_array("atom_type")[i] == "adsorbate" - ] + # Combine the blocks + return self.mrccblocks - # Get the indices of the slab from the quantum cluster - slab_indices = [ - i - for i in range(len(adsorbate_slab_cluster)) - if adsorbate_slab_cluster.get_array("atom_type")[i] != "adsorbate" - ] - adsorbate_cluster = adsorbate_slab_cluster[adsorbate_indices] - slab_cluster = adsorbate_slab_cluster[slab_indices] + def generate_basis_ecp_block(self) -> None: + """ + Generates the basis and ECP block for the MRCC input file. + + Parameters + ---------- - # Helper to generate basis strings for MRCC - def _create_basis_block(quantum_region, ecp_region=None): - return f""" + Returns + ------- + None + """ + + # Helper to generate basis strings for MRCC + def _create_basis_block(quantum_region, ecp_region=None): + return f""" basis_sm=atomtype -{create_mrcc_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: 'def2-SVP' for element in element_info})} +{self.create_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: 'def2-SVP' for element in self.element_info})} basis=atomtype -{create_mrcc_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: element_info[element]['basis'] for element in element_info})} +{self.create_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: self.element_info[element]['basis'] for element in self.element_info})} dfbasis_scf=atomtype -{create_mrcc_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: element_info[element]['ri_scf_basis'] for element in element_info})} +{self.create_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: self.element_info[element]['ri_scf_basis'] for element in self.element_info})} dfbasis_cor=atomtype -{create_mrcc_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: element_info[element]['ri_cwft_basis'] for element in element_info})} +{self.create_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: self.element_info[element]['ri_cwft_basis'] for element in self.element_info})} """ - if include_cp: - return { - "adsorbate_slab": _create_basis_block( - quantum_region=adsorbate_slab_cluster, ecp_region=ecp_region - ), - "slab": _create_basis_block( - quantum_region=adsorbate_slab_cluster, ecp_region=ecp_region - ), - "adsorbate": _create_basis_block( - quantum_region=adsorbate_slab_cluster, ecp_region=None - ), - } - else: - return { - "adsorbate_slab": _create_basis_block( - quantum_region=adsorbate_slab_cluster, ecp_region=ecp_region + if self.include_cp: + self.mrccblocks['adsorbate_slab'] += _create_basis_block( + quantum_region=self.adsorbate_slab_cluster, ecp_region=self.ecp_region + ) + self.mrccblocks['slab'] += _create_basis_block( + quantum_region=self.adsorbate_slab_cluster, ecp_region=self.ecp_region + ) + self.mrccblocks['adsorbate'] += _create_basis_block( + quantum_region=self.adsorbate_slab_cluster, ecp_region=None + ) + else: + self.mrccblocks['adsorbate_slab'] += _create_basis_block( + quantum_region=self.adsorbate_slab_cluster, ecp_region=self.ecp_region + ) + self.mrccblocks['slab'] += _create_basis_block( + quantum_region=self.slab_cluster, ecp_region=self.ecp_region + ) + self.mrccblocks['adsorbate'] += _create_basis_block( + quantum_region=self.adsorbate_cluster, ecp_region=None + ) + + def create_atomtype_basis( + self, + quantum_region: Atoms, + element_basis_info: dict[ElementStr, str], + ecp_region: Atoms | None = None, + ) -> str: + """ + Creates a column for the basis set for each atom in the Atoms object, given by element_info. + + Parameters + ---------- + quantum_region + The ASE Atoms object containing the atomic coordinates of the quantum cluster region (could be the adsorbate-slab complex, slab or adsorbate by itself). + element_basis_info + A dictionary with elements as keys which gives the basis set for each element. + ecp_region + The ASE atoms object containing the atomic coordinates of the capped ECP region. + + Returns + ------- + str + The basis set for each atom in the Atoms object given as a column (of size N, where N is the number of atoms). + """ + + basis_str = "" + for atom in quantum_region: + basis_str += f"{element_basis_info[atom.symbol]}\n" + if ecp_region is not None: + basis_str += "no-basis-set\n" * len(ecp_region) + + return basis_str + + + def generate_coords_block( + self + ) -> None: + """ + Generates the coordinates block for the MRCC input file. This includes the coordinates of the quantum cluster, the ECP region, and the point charges. It will return three strings for the adsorbate-slab complex, adsorbate and slab. + + Parameters + ---------- + + Returns + ------- + None + """ + + # Get the charge of the quantum cluster + charge = int(sum(self.adsorbate_slab_cluster.get_array("oxi_states"))) + + # Get the total number of core electrons for the quantum cluster + core = { + "adsorbate_slab": sum( + [self.element_info[atom.symbol]["core"] for atom in self.adsorbate_slab_cluster] ), - "slab": _create_basis_block( - quantum_region=slab_cluster, ecp_region=ecp_region + "adsorbate": sum( + [ + self.element_info[atom.symbol]["core"] + for atom in self.adsorbate_cluster + ] ), - "adsorbate": _create_basis_block( - quantum_region=adsorbate_cluster, ecp_region=None + "slab": sum( + [ + self.element_info[atom.symbol]["core"] + for atom in self.slab_cluster + ] ), } - -def create_mrcc_atomtype_basis( - quantum_region: Atoms, - element_basis_info: dict[ElementStr, str], - ecp_region: Atoms | None = None, -) -> str: - """ - Creates a column for the basis set for each atom in the Atoms object, given by element_info. - - Parameters - ---------- - quantum_region - The ASE Atoms object containing the atomic coordinates of the quantum cluster region (could be the adsorbate-slab complex, slab or adsorbate by itself). - element_basis_info - A dictionary with elements as keys which gives the basis set for each element. - ecp_region - The ASE atoms object containing the atomic coordinates of the capped ECP region. - - Returns - ------- - str - The basis set for each atom in the Atoms object given as a column (of size N, where N is the number of atoms). - """ - - basis_str = "" - for atom in quantum_region: - basis_str += f"{element_basis_info[atom.symbol]}\n" - if ecp_region is not None: - basis_str += "no-basis-set\n" * len(ecp_region) - - return basis_str - - -def generate_mrcc_coords_block( - embedded_adsorbed_cluster: Atoms, - quantum_cluster_indices: list[int], - ecp_region_indices: list[int], - element_info: ElementInfo, - include_cp: bool = True, - multiplicities: MultiplicityDict | None = None, -) -> BlockInfo: - """ - Generates the coordinates block for the MRCC input file. This includes the coordinates of the quantum cluster, the ECP region, and the point charges. It will return three strings for the adsorbate-slab complex, adsorbate and slab. - - Parameters - ---------- - embedded_adsorbed_cluster - The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This Atoms object is typically produced from [quacc.atoms.skzcam.create_skzcam_clusters][]. - quantum_cluster_indices - A list containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - ecp_region_indices - A list containing the indices of the atoms in each ECP region. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - element_info - A dictionary with elements as keys which gives the (1) number of core electrons as 'core', (2) basis set as 'basis', (3) effective core potential as 'ecp', (4) resolution-of-identity/density-fitting auxiliary basis set for DFT/HF calculations as 'ri_scf_basis' and (5) resolution-of-identity/density-fitting for correlated wave-function methods as 'ri_cwft_basis'. - include_cp - If True, the coords strings will include the counterpoise correction for the adsorbate and slab. - multiplicities - The multiplicity of the adsorbate-slab complex, adsorbate and slab respectively, with the keys 'adsorbate_slab', 'adsorbate', and 'slab'. - - Returns - ------- - BlockInfo - The coordinates block for the ORCA input block (to be put in 'orcablocks' parameter) as a string for the adsorbate-slab complex, the adsorbate, and the slab in a dictionary with the keys 'adsorbate_slab', 'adsorbate', and 'slab' respectively. - """ - - # Create the quantum cluster and ECP region cluster - if multiplicities is None: - multiplicities = {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} - quantum_cluster = embedded_adsorbed_cluster[quantum_cluster_indices] - ecp_region = embedded_adsorbed_cluster[ecp_region_indices] - - # Get the indices of the adsorbates from the quantum cluster - adsorbate_indices = [ - i - for i in range(len(quantum_cluster)) - if quantum_cluster.get_array("atom_type")[i] == "adsorbate" - ] - - # Get the indices of the slab from the quantum cluster - slab_indices = [ - i - for i in range(len(quantum_cluster)) - if quantum_cluster.get_array("atom_type")[i] != "adsorbate" - ] - - # Get the charge of the quantum cluster - charge = int(sum(quantum_cluster.get_array("oxi_states"))) - - # Get the total number of core electrons for the quantum cluster - core = { - "adsorbate_slab": sum( - [element_info[atom.symbol]["core"] for atom in quantum_cluster] - ), - "adsorbate": sum( - [ - element_info[atom.symbol]["core"] - for atom in quantum_cluster - if atom.index in adsorbate_indices - ] - ), - "slab": sum( - [ - element_info[atom.symbol]["core"] - for atom in quantum_cluster - if atom.index in slab_indices - ] - ), - } - - # Create the coords strings for the adsorbate-slab complex, adsorbate, and slab - coords_block = { - "adsorbate_slab": f"""charge={charge} -mult={multiplicities['adsorbate_slab']} + # Add the charge and core electron information to mrccblocks + self.mrccblocks["adsorbate_slab"] += f"""charge={charge} +mult={self.multiplicities['adsorbate_slab']} core={int(core['adsorbate_slab']/2)} unit=angs geom=xyz -""", - "adsorbate": f"""charge=0 -mult={multiplicities['adsorbate']} +""" + self.mrccblocks["adsorbate"] += f"""charge=0 +mult={self.multiplicities['adsorbate']} core={int(core['adsorbate']/2)} unit=angs geom=xyz -""", - "slab": f"""charge={charge} -mult={multiplicities['slab']} +""" + self.mrccblocks["slab"] += f"""charge={charge} +mult={self.multiplicities['slab']} core={int(core['slab']/2)} unit=angs geom=xyz -""", - } - - # Set the number of atoms for each system - if include_cp: - coords_block["adsorbate_slab"] += ( - f"{len(quantum_cluster) + len(ecp_region)}\n\n" - ) - coords_block["adsorbate"] += f"{len(quantum_cluster)}\n\n" - coords_block["slab"] += f"{len(quantum_cluster) + len(ecp_region)}\n\n" - else: - coords_block["adsorbate_slab"] += ( - f"{len(quantum_cluster) + len(ecp_region)}\n\n" - ) - coords_block["adsorbate"] += f"{len(adsorbate_indices)}\n\n" - coords_block["slab"] += f"{len(slab_indices) + len(ecp_region)}\n\n" - - for i, atom in enumerate(quantum_cluster): - # Create the coords section for the adsorbate-slab complex - coords_block["adsorbate_slab"] += create_atom_coord_string(atom=atom) - - # Create the coords section for the adsorbate and slab - if i in adsorbate_indices: - coords_block["adsorbate"] += create_atom_coord_string(atom=atom) - if include_cp: - coords_block["slab"] += create_atom_coord_string(atom=atom) - elif i in slab_indices: - coords_block["slab"] += create_atom_coord_string(atom=atom) - if include_cp: - coords_block["adsorbate"] += create_atom_coord_string(atom=atom) - - # Create the coords section for the ECP region - for atom in ecp_region: - coords_block["adsorbate_slab"] += create_atom_coord_string(atom=atom) - coords_block["slab"] += create_atom_coord_string(atom=atom) - - # Adding the ghost atoms for the counterpoise correction - for system in ["adsorbate_slab", "adsorbate", "slab"]: - coords_block[system] += "\nghost=serialno\n" - if include_cp and system in ["adsorbate"]: - coords_block[system] += ",".join( - [str(atom_idx + 1) for atom_idx in slab_indices] - ) - elif include_cp and system in ["slab"]: - coords_block[system] += ",".join( - [str(atom_idx + 1) for atom_idx in adsorbate_indices] +""" + # Create the atom coordinates block for the adsorbate-slab cluster, ECP region + adsorbate_slab_coords_block = "" + for atom in self.adsorbate_slab_cluster: + adsorbate_slab_coords_block += create_atom_coord_string(atom=atom) + + ecp_region_block = "" + for ecp_atom in self.ecp_region: + ecp_region_block += create_atom_coord_string(atom=ecp_atom) + + + # Set the number of atoms for each system. This would be the number of atoms in the quantum cluster plus the number of atoms in the ECP region. If include_cp is True, then the number of atoms in the quantum cluster is the number of atoms in the adsorbate-slab complex for both the adsorbate and slab. + if self.include_cp: + self.mrccblocks["adsorbate_slab"] += ( + f"{len(self.adsorbate_slab_cluster) + len(self.ecp_region)}\n\n" ) - coords_block[system] += "\n\n" + self.mrccblocks["adsorbate_slab"] += adsorbate_slab_coords_block + ecp_region_block - return coords_block + self.mrccblocks["adsorbate"] += f"{len(self.adsorbate_slab_cluster)}\n\n" + self.mrccblocks["adsorbate"] += adsorbate_slab_coords_block + self.mrccblocks["slab"] += f"{len(self.adsorbate_slab_cluster) + len(self.ecp_region)}\n\n" + self.mrccblocks["slab"] += adsorbate_slab_coords_block + ecp_region_block -def generate_mrcc_point_charge_block( - embedded_adsorbed_cluster: Atoms, - quantum_cluster_indices: list[int], - ecp_region_indices: list[int], -) -> str: - """ - Create the point charge block for the MRCC input file. This requires the embedded_cluster Atoms object containing both atom_type and oxi_states arrays, as well as the indices of the quantum cluster and ECP region. Such arrays are created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - - Parameters - ---------- - embedded_adsorbed_cluster - The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file. This object can be created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - quantum_cluster_indices - A list of lists containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - ecp_region_indices - A list of lists containing the indices of the atoms in the ECP region for each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - - Returns - ------- - str - The point charge block for the MRCC input file. - """ - - # Get the oxi_states arrays from the embedded_cluster - oxi_states = embedded_adsorbed_cluster.get_array("oxi_states") - - # Check that none of the indices in quantum_cluster_indices are in ecp_region_indices - if not np.all([x not in ecp_region_indices for x in quantum_cluster_indices]): - raise ValueError("An atom in the quantum cluster is also in the ECP region.") - # Get the number of point charges for this system. There is a point charge associated with each capped ECP as well. - pc_region_indices = ecp_region_indices + [ - atom.index - for atom in embedded_adsorbed_cluster - if atom not in quantum_cluster_indices + ecp_region_indices - ] + for system in ["adsorbate_slab", "adsorbate", "slab"]: + self.mrccblocks[system] += "\nghost=serialno\n" + # Add the ghost atoms for the counterpoise correction in the adsorbate and slab + if system == "adsorbate": + self.mrccblocks[system] += ",".join( + [str(atom_idx + 1) for atom_idx in self.slab_indices] + ) + elif system == "slab": + self.mrccblocks[system] += ",".join( + [str(atom_idx + 1) for atom_idx in self.adsorbate_indices] + ) + self.mrccblocks[system] += "\n\n" + else: + self.mrccblocks["adsorbate_slab"] += ( + f"{len(self.adsorbate_slab_cluster) + len(self.ecp_region)}\n\n" + ) + self.mrccblocks["adsorbate_slab"] += adsorbate_slab_coords_block + ecp_region_block + + self.mrccblocks["adsorbate"] += f"{len(self.adsorbate_cluster)}\n\n" + for atom in self.adsorbate_cluster: + self.mrccblocks["adsorbate"] += create_atom_coord_string(atom=atom) + + self.mrccblocks["slab"] += f"{len(self.slab_cluster) + len(self.ecp_region)}\n\n" + for atom in self.slab_cluster: + self.mrccblocks["slab"] += create_atom_coord_string(atom=atom) + self.mrccblocks["slab"] += ecp_region_block + + def generate_point_charge_block(self) -> str: + """ + Create the point charge block for the MRCC input file. This requires the embedded_cluster Atoms object containing both atom_type and oxi_states arrays, as well as the indices of the quantum cluster and ECP region. Such arrays are created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + + Parameters + ---------- + + Returns + ------- + str + The point charge block for the MRCC input file. + """ + + # Get the oxi_states arrays from the embedded_cluster + oxi_states = self.embedded_adsorbed_cluster.get_array("oxi_states") + + # Get the number of point charges for this system. There is a point charge associated with each capped ECP as well. + pc_region_indices = [ + atom.index + for atom in self.embedded_adsorbed_cluster + if atom.index not in self.quantum_cluster_indices + ] - num_pc = len(pc_region_indices) - pc_block = f"qmmm=Amber\npointcharges\n{num_pc}\n" + num_pc = len(pc_region_indices) + pc_block = f"qmmm=Amber\npointcharges\n{num_pc}\n" - # Add the ecp_region indices - for i in pc_region_indices: - position = embedded_adsorbed_cluster[i].position - pc_block += f" {position[0]:-16.11f} {position[1]:-16.11f} {position[2]:-16.11f} {oxi_states[i]:-16.11f}\n" + # Add the ecp_region indices + for i in pc_region_indices: + position = self.embedded_adsorbed_cluster[i].position + pc_block += f" {position[0]:-16.11f} {position[1]:-16.11f} {position[2]:-16.11f} {oxi_states[i]:-16.11f}\n" - return pc_block + return pc_block def create_orca_eint_blocks( @@ -1378,7 +1315,7 @@ def insert_adsorbate_to_embedded_cluster( adsorbate.set_array("atom_type", np.array(["adsorbate"] * len(adsorbate))) # Add the adsorbate to the embedded cluster - embedded_adsorbate_cluster = adsorbate + embedded_cluster + embedded_adsorbed_cluster = adsorbate + embedded_cluster # Update the quantum cluster and ECP region indices if quantum_cluster_indices is not None: @@ -1391,7 +1328,7 @@ def insert_adsorbate_to_embedded_cluster( [idx + len(adsorbate) for idx in cluster] for cluster in ecp_region_indices ] - return embedded_adsorbate_cluster, quantum_cluster_indices, ecp_region_indices + return embedded_adsorbed_cluster, quantum_cluster_indices, ecp_region_indices def _get_atom_distances(embedded_cluster: Atoms, center_position: NDArray) -> NDArray: diff --git a/tests/core/atoms/test_skzcam.py b/tests/core/atoms/test_skzcam.py index 319e9d5be0..6d70dfc365 100644 --- a/tests/core/atoms/test_skzcam.py +++ b/tests/core/atoms/test_skzcam.py @@ -6,25 +6,25 @@ import pytest from ase import Atoms from ase.io import read +from ase.calculators.calculator import compare_atoms + +from copy import deepcopy + from numpy.testing import assert_allclose, assert_equal from quacc.atoms.skzcam import ( + MRCCInputGenerator, _find_cation_shells, _get_anion_coordination, _get_atom_distances, _get_ecp_region, convert_pun_to_atoms, create_atom_coord_string, - create_mrcc_atomtype_basis, - create_mrcc_eint_blocks, create_orca_eint_blocks, create_orca_point_charge_file, create_skzcam_clusters, format_ecp_info, generate_coords_block, - generate_mrcc_basis_ecp_block, - generate_mrcc_coords_block, - generate_mrcc_point_charge_block, generate_orca_input_preamble, get_cluster_info_from_slab, insert_adsorbate_to_embedded_cluster, @@ -40,6 +40,16 @@ def embedded_cluster(): {"Mg": 2.0, "O": -2.0}, ) +@pytest.fixture() +def mrcc_input_generator(embedded_adsorbed_cluster, element_info): + return MRCCInputGenerator( + embedded_adsorbed_cluster=embedded_adsorbed_cluster, + quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], + ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], + element_info=element_info, + include_cp=True, + multiplicities={"adsorbate_slab": 3, "adsorbate": 1, "slab": 2}, + ) @pytest.fixture() def embedded_adsorbed_cluster(): @@ -97,54 +107,77 @@ def element_info(): def distance_matrix(embedded_cluster): return embedded_cluster.get_all_distances() +def test_MRCCInputGenerator_init(embedded_adsorbed_cluster, element_info): -def test_create_mrcc_eint_blocks(embedded_adsorbed_cluster, element_info): - mrcc_blocks = create_mrcc_eint_blocks( + # Check what happens if multiplicities is not provided + mrcc_input_generator = MRCCInputGenerator( embedded_adsorbed_cluster=embedded_adsorbed_cluster, quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], element_info=element_info, - include_cp=True, + include_cp=True ) - mrcc_blocks_nocp = create_mrcc_eint_blocks( + assert mrcc_input_generator.multiplicities == {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} + + mrcc_input_generator = MRCCInputGenerator( embedded_adsorbed_cluster=embedded_adsorbed_cluster, quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], element_info=element_info, - include_cp=False, + include_cp=True, + multiplicities={"adsorbate_slab": 3, "adsorbate": 1, "slab": 2}, ) - reference_block_collated = { - "adsorbate_slab": { - "float": [21.0, 2.0, -2.0, -2.0, 2.0, -8.9536039173], - "string": ["basis_sm=atomtype", "def2/JK", "O"], - }, - "adsorbate": {"float": [8.0], "string": ["basis_sm=atomtype", "3,4,5,6,7,8"]}, - "slab": { - "float": [21.0, 2.0, -2.0, -2.0, 2.0, -8.9536039173], - "string": ["basis_sm=atomtype", "def2/JK", "O"], - }, - } + assert not compare_atoms(mrcc_input_generator.embedded_adsorbed_cluster, embedded_adsorbed_cluster) + assert_equal(mrcc_input_generator.quantum_cluster_indices,[0, 1, 2, 3, 4, 5, 6, 7]) + assert_equal(mrcc_input_generator.adsorbate_indices,[0, 1]) + assert_equal(mrcc_input_generator.slab_indices,[2, 3, 4, 5, 6, 7]) + assert_equal(mrcc_input_generator.ecp_region_indices,[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24]) + assert mrcc_input_generator.element_info == element_info + assert mrcc_input_generator.include_cp == True + assert mrcc_input_generator.multiplicities == {"adsorbate_slab": 3, "adsorbate": 1, "slab": 2} - reference_block_nocp_collated = { - "adsorbate_slab": { - "float": [21.0, 2.0, -2.0, -2.0, 2.0, -8.9536039173], - "string": ["basis_sm=atomtype", "def2/JK", "O"], - }, - "adsorbate": {"float": [2.0], "string": ["basis_sm=atomtype"]}, - "slab": { - "float": [ - 19.0, - -4.22049352791, - 0.0, - -6.33028254133, - 10.55242967836, - 30.92617453977, - ], - "string": ["basis_sm=atomtype", "no-basis-set", "Mg"], - }, - } + # Check if error raise if quantum_cluster_indices and ecp_region_indices overlap + + with pytest.raises( + ValueError, match="An atom in the quantum cluster is also in the ECP region." + ): + mrcc_input_generator = MRCCInputGenerator( + embedded_adsorbed_cluster=embedded_adsorbed_cluster, + quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], + ecp_region_indices=[7, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], + element_info=element_info, + include_cp=True + ) + + +def test_MRCCInputGenerator_create_eint_blocks(mrcc_input_generator): + + mrcc_input_generator_nocp = deepcopy(mrcc_input_generator) + + mrcc_input_generator_nocp.include_cp = False + mrcc_input_generator_nocp.generate_input() + + mrcc_input_generator.generate_input() + + + reference_block_collated = {'adsorbate_slab': {'float': [21.0, -2.0, 2.0, 2.0, 2.0, 0.1474277671], + 'string': ['basis_sm=atomtype', 'def2/JK', 'O']}, + 'adsorbate': {'float': [8.0], 'string': ['basis_sm=atomtype', '3,4,5,6,7,8']}, + 'slab': {'float': [21.0, -2.0, 2.0, 2.0, 2.0, 0.1474277671], + 'string': ['basis_sm=atomtype', 'def2/JK', 'O']}} + + reference_block_nocp_collated = {'adsorbate_slab': {'float': [21.0, -2.0, 2.0, 2.0, 2.0, 0.1474277671], + 'string': ['basis_sm=atomtype', 'def2/JK', 'O']}, + 'adsorbate': {'float': [2.0], 'string': ['basis_sm=atomtype']}, + 'slab': {'float': [19.0, + -4.22049352791, + 4.22049352791, + 4.22049352791, + 2.11024676395, + -0.0], + 'string': ['basis_sm=atomtype', 'no-basis-set', 'Mg']}} generated_block_collated = { system: {"float": [], "string": []} @@ -158,23 +191,23 @@ def test_create_mrcc_eint_blocks(embedded_adsorbed_cluster, element_info): for system in ["adsorbate_slab", "adsorbate", "slab"]: generated_block_collated[system]["float"] = [ float(x) - for x in mrcc_blocks[system].split() + for x in mrcc_input_generator.mrccblocks[system].split() if x.replace(".", "", 1).replace("-", "", 1).isdigit() ][::300] generated_block_collated[system]["string"] = [ x - for x in mrcc_blocks[system].split() + for x in mrcc_input_generator.mrccblocks[system].split() if not x.replace(".", "", 1).replace("-", "", 1).isdigit() ][::50] generated_block_nocp_collated[system]["float"] = [ float(x) - for x in mrcc_blocks_nocp[system].split() + for x in mrcc_input_generator_nocp.mrccblocks[system].split() if x.replace(".", "", 1).replace("-", "", 1).isdigit() ][::300] generated_block_nocp_collated[system]["string"] = [ x - for x in mrcc_blocks_nocp[system].split() + for x in mrcc_input_generator_nocp.mrccblocks[system].split() if not x.replace(".", "", 1).replace("-", "", 1).isdigit() ][::50] @@ -201,22 +234,16 @@ def test_create_mrcc_eint_blocks(embedded_adsorbed_cluster, element_info): ) -def test_generate_mrcc_basis_ecp_block(embedded_adsorbed_cluster, element_info): - generated_mrcc_blocks = generate_mrcc_basis_ecp_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], - element_info=element_info, - include_cp=True, - ) +def test_MRCCInputGenerator_generate_basis_ecp_block(mrcc_input_generator): + + mrcc_input_generator_nocp = deepcopy(mrcc_input_generator) + + + mrcc_input_generator_nocp.include_cp = False + mrcc_input_generator_nocp.generate_basis_ecp_block() + + mrcc_input_generator.generate_basis_ecp_block() - generated_mrcc_blocks_nocp = generate_mrcc_basis_ecp_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], - element_info=element_info, - include_cp=False, - ) reference_mrcc_blocks_collated = { "adsorbate_slab": [ @@ -276,10 +303,10 @@ def test_generate_mrcc_basis_ecp_block(embedded_adsorbed_cluster, element_info): system: [] for system in ["adsorbate_slab", "slab", "adsorbate"] } for system in ["adsorbate_slab", "adsorbate", "slab"]: - generated_mrcc_blocks_collated[system] = generated_mrcc_blocks[system].split()[ + generated_mrcc_blocks_collated[system] = mrcc_input_generator.mrccblocks[system].split()[ ::10 ] - generated_mrcc_blocks_nocp_collated[system] = generated_mrcc_blocks_nocp[ + generated_mrcc_blocks_nocp_collated[system] = mrcc_input_generator_nocp.mrccblocks[ system ].split()[::10] @@ -293,24 +320,21 @@ def test_generate_mrcc_basis_ecp_block(embedded_adsorbed_cluster, element_info): ) -def test_create_mrcc_atomtype_basis(embedded_adsorbed_cluster, element_info): - quantum_region = embedded_adsorbed_cluster[[0, 1, 2, 3, 4, 5, 6, 7]] - ecp_region = embedded_adsorbed_cluster[ - [8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24] - ] +def test_MRCCInputGenerator_create_atomtype_basis(mrcc_input_generator): + - generated_basis_block_without_ecp = create_mrcc_atomtype_basis( - quantum_region=quantum_region, + generated_basis_block_without_ecp = mrcc_input_generator.create_atomtype_basis( + quantum_region=mrcc_input_generator.adsorbate_slab_cluster, element_basis_info={ - element: element_info[element]["ri_cwft_basis"] for element in element_info + element: mrcc_input_generator.element_info[element]["ri_cwft_basis"] for element in mrcc_input_generator.element_info }, ) - generated_basis_block_with_ecp = create_mrcc_atomtype_basis( - quantum_region=quantum_region, + generated_basis_block_with_ecp = mrcc_input_generator.create_atomtype_basis( + quantum_region=mrcc_input_generator.adsorbate_slab_cluster, element_basis_info={ - element: element_info[element]["ri_cwft_basis"] for element in element_info + element: mrcc_input_generator.element_info[element]["ri_cwft_basis"] for element in mrcc_input_generator.element_info }, - ecp_region=ecp_region, + ecp_region=mrcc_input_generator.ecp_region, ) reference_basis_block_without_ecp = "aug-cc-pVDZ/C\naug-cc-pVDZ/C\ncc-pVDZ/C\naug-cc-pVDZ/C\naug-cc-pVDZ/C\naug-cc-pVDZ/C\naug-cc-pVDZ/C\naug-cc-pVDZ/C\n" @@ -320,37 +344,16 @@ def test_create_mrcc_atomtype_basis(embedded_adsorbed_cluster, element_info): assert generated_basis_block_with_ecp == reference_basis_block_with_ecp -def test_generate_mrcc_coords_block(embedded_adsorbed_cluster, element_info): - # Check if multiplicity is read +def test_MRCCInputGenerator_generate_coords_block(mrcc_input_generator): - mrcc_blocks = generate_mrcc_coords_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], - element_info=element_info, - include_cp=True, - multiplicities={"adsorbate_slab": 3, "adsorbate": 1, "slab": 2}, - ) + mrcc_input_generator_nocp = deepcopy(mrcc_input_generator) - assert mrcc_blocks["adsorbate"].split()[1][-1] == "1" - assert mrcc_blocks["adsorbate_slab"].split()[1][-1] == "3" - assert mrcc_blocks["slab"].split()[1][-1] == "2" + mrcc_input_generator_nocp.include_cp = False + mrcc_input_generator_nocp.generate_coords_block() + + mrcc_input_generator.generate_coords_block() - mrcc_blocks = generate_mrcc_coords_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], - element_info=element_info, - include_cp=True, - ) - mrcc_blocks_nocp = generate_mrcc_coords_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], - element_info=element_info, - include_cp=False, - ) reference_block_collated = { "adsorbate_slab": { @@ -415,23 +418,23 @@ def test_generate_mrcc_coords_block(embedded_adsorbed_cluster, element_info): for system in ["adsorbate_slab", "adsorbate", "slab"]: generated_block_collated[system]["float"] = [ float(x) - for x in mrcc_blocks[system].split() + for x in mrcc_input_generator.mrccblocks[system].split() if x.replace(".", "", 1).replace("-", "", 1).isdigit() ][::10] generated_block_collated[system]["string"] = [ x - for x in mrcc_blocks[system].split() + for x in mrcc_input_generator.mrccblocks[system].split() if not x.replace(".", "", 1).replace("-", "", 1).isdigit() ][::5] generated_block_nocp_collated[system]["float"] = [ float(x) - for x in mrcc_blocks_nocp[system].split() + for x in mrcc_input_generator_nocp.mrccblocks[system].split() if x.replace(".", "", 1).replace("-", "", 1).isdigit() ][::10] generated_block_nocp_collated[system]["string"] = [ x - for x in mrcc_blocks_nocp[system].split() + for x in mrcc_input_generator_nocp.mrccblocks[system].split() if not x.replace(".", "", 1).replace("-", "", 1).isdigit() ][::5] @@ -458,52 +461,23 @@ def test_generate_mrcc_coords_block(embedded_adsorbed_cluster, element_info): ) -def test_generate_mrcc_point_charge_block(embedded_adsorbed_cluster): - with pytest.raises( - ValueError, match="An atom in the quantum cluster is also in the ECP region." - ): - generate_mrcc_point_charge_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ecp_region_indices=[7, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], - ) - - generated_point_charge_block = generate_mrcc_point_charge_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], - ).split() +def test_MRCCInputGenerator_generate_point_charge_block(mrcc_input_generator): + + generated_point_charge_block = mrcc_input_generator.generate_point_charge_block() generated_point_charge_block_shortened = [ - float(x) for x in generated_point_charge_block[5::70] + float(x) for x in generated_point_charge_block.split()[5::180] ] - reference_point_charge_block_shortened = [ - -0.04367284424, - 2.12018425659, - -0.04269731856, - 0.0, - -4.26789528527, - -2.11024676395, - -6.37814204923, - -6.32889565859, - -2.14445912302, - 4.22049352791, - -2.14129966123, - 6.32954443328, - -2.14923989662, - 0.0, - -4.26789528527, - 8.44098705582, - -6.37814204923, - 8.44098705582, - -4.26789528527, - -54.86641586281, - -57.14411935233, - 49.48825903601, - 0.0, - 43.73621546646, - ] + reference_point_charge_block_shortened = [-0.04367284424, + -0.03992370948, + -2.14923989662, + -6.37814204923, + -2.1415520695, + -4.26789528527, + -2.1415520695, + -0.03992370948, + 0.0] assert_allclose( generated_point_charge_block_shortened, From 8dd8cc5c83279837293d435f97f47f270d7473a8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 9 Jun 2024 09:46:11 +0000 Subject: [PATCH 02/29] pre-commit auto-fixes --- src/quacc/atoms/skzcam.py | 118 ++++++++++++++------------- tests/core/atoms/test_skzcam.py | 138 ++++++++++++++++++-------------- 2 files changed, 140 insertions(+), 116 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index 2d08d69e6e..17df623339 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -158,14 +158,17 @@ class MultiplicityDict(TypedDict): has_chemshell = find_spec("chemsh") is not None -class MRCCInputGenerator(): - def __init__(self, - embedded_adsorbed_cluster: Atoms, - quantum_cluster_indices: list[int], - ecp_region_indices: list[int], - element_info: dict[ElementStr, ElementInfo] | None = None, - include_cp: bool = True, - multiplicities: MultiplicityDict | None = None) -> None: + +class MRCCInputGenerator: + def __init__( + self, + embedded_adsorbed_cluster: Atoms, + quantum_cluster_indices: list[int], + ecp_region_indices: list[int], + element_info: dict[ElementStr, ElementInfo] | None = None, + include_cp: bool = True, + multiplicities: MultiplicityDict | None = None, + ) -> None: """ Parameters ---------- @@ -189,7 +192,9 @@ def __init__(self, # Check that none of the indices in quantum_cluster_indices are in ecp_region_indices if not np.all([x not in ecp_region_indices for x in quantum_cluster_indices]): - raise ValueError("An atom in the quantum cluster is also in the ECP region.") + raise ValueError( + "An atom in the quantum cluster is also in the ECP region." + ) self.embedded_adsorbed_cluster = embedded_adsorbed_cluster self.quantum_cluster_indices = quantum_cluster_indices @@ -203,15 +208,17 @@ def __init__(self, self.multiplicities = multiplicities # Create the adsorbate-slab complex quantum cluster and ECP region cluster - self.adsorbate_slab_cluster = self.embedded_adsorbed_cluster[self.quantum_cluster_indices] + self.adsorbate_slab_cluster = self.embedded_adsorbed_cluster[ + self.quantum_cluster_indices + ] self.ecp_region = self.embedded_adsorbed_cluster[self.ecp_region_indices] # Get the indices of the adsorbates from the quantum cluster - self.adsorbate_indices = [ - i - for i in range(len(self.adsorbate_slab_cluster)) - if self.adsorbate_slab_cluster.get_array("atom_type")[i] == "adsorbate" - ] + self.adsorbate_indices = [ + i + for i in range(len(self.adsorbate_slab_cluster)) + if self.adsorbate_slab_cluster.get_array("atom_type")[i] == "adsorbate" + ] # Get the indices of the slab from the quantum cluster self.slab_indices = [ i @@ -226,12 +233,7 @@ def __init__(self, self.include_cp = include_cp # Initialize the mrccblocks input strings for the adsorbate-slab complex, adsorbate, and slab - self.mrccblocks = { - "adsorbate_slab": "", - "adsorbate": "", - "slab": "", - } - + self.mrccblocks = {"adsorbate_slab": "", "adsorbate": "", "slab": ""} def generate_input(self) -> BlockInfo: """ @@ -260,7 +262,6 @@ def generate_input(self) -> BlockInfo: # Combine the blocks return self.mrccblocks - def generate_basis_ecp_block(self) -> None: """ Generates the basis and ECP block for the MRCC input file. @@ -290,25 +291,25 @@ def _create_basis_block(quantum_region, ecp_region=None): """ if self.include_cp: - self.mrccblocks['adsorbate_slab'] += _create_basis_block( - quantum_region=self.adsorbate_slab_cluster, ecp_region=self.ecp_region - ) - self.mrccblocks['slab'] += _create_basis_block( - quantum_region=self.adsorbate_slab_cluster, ecp_region=self.ecp_region - ) - self.mrccblocks['adsorbate'] += _create_basis_block( - quantum_region=self.adsorbate_slab_cluster, ecp_region=None - ) + self.mrccblocks["adsorbate_slab"] += _create_basis_block( + quantum_region=self.adsorbate_slab_cluster, ecp_region=self.ecp_region + ) + self.mrccblocks["slab"] += _create_basis_block( + quantum_region=self.adsorbate_slab_cluster, ecp_region=self.ecp_region + ) + self.mrccblocks["adsorbate"] += _create_basis_block( + quantum_region=self.adsorbate_slab_cluster, ecp_region=None + ) else: - self.mrccblocks['adsorbate_slab'] += _create_basis_block( - quantum_region=self.adsorbate_slab_cluster, ecp_region=self.ecp_region - ) - self.mrccblocks['slab'] += _create_basis_block( - quantum_region=self.slab_cluster, ecp_region=self.ecp_region - ) - self.mrccblocks['adsorbate'] += _create_basis_block( - quantum_region=self.adsorbate_cluster, ecp_region=None - ) + self.mrccblocks["adsorbate_slab"] += _create_basis_block( + quantum_region=self.adsorbate_slab_cluster, ecp_region=self.ecp_region + ) + self.mrccblocks["slab"] += _create_basis_block( + quantum_region=self.slab_cluster, ecp_region=self.ecp_region + ) + self.mrccblocks["adsorbate"] += _create_basis_block( + quantum_region=self.adsorbate_cluster, ecp_region=None + ) def create_atomtype_basis( self, @@ -342,10 +343,7 @@ def create_atomtype_basis( return basis_str - - def generate_coords_block( - self - ) -> None: + def generate_coords_block(self) -> None: """ Generates the coordinates block for the MRCC input file. This includes the coordinates of the quantum cluster, the ECP region, and the point charges. It will return three strings for the adsorbate-slab complex, adsorbate and slab. @@ -363,7 +361,10 @@ def generate_coords_block( # Get the total number of core electrons for the quantum cluster core = { "adsorbate_slab": sum( - [self.element_info[atom.symbol]["core"] for atom in self.adsorbate_slab_cluster] + [ + self.element_info[atom.symbol]["core"] + for atom in self.adsorbate_slab_cluster + ] ), "adsorbate": sum( [ @@ -372,10 +373,7 @@ def generate_coords_block( ] ), "slab": sum( - [ - self.element_info[atom.symbol]["core"] - for atom in self.slab_cluster - ] + [self.element_info[atom.symbol]["core"] for atom in self.slab_cluster] ), } @@ -402,26 +400,28 @@ def generate_coords_block( adsorbate_slab_coords_block = "" for atom in self.adsorbate_slab_cluster: adsorbate_slab_coords_block += create_atom_coord_string(atom=atom) - + ecp_region_block = "" for ecp_atom in self.ecp_region: ecp_region_block += create_atom_coord_string(atom=ecp_atom) - # Set the number of atoms for each system. This would be the number of atoms in the quantum cluster plus the number of atoms in the ECP region. If include_cp is True, then the number of atoms in the quantum cluster is the number of atoms in the adsorbate-slab complex for both the adsorbate and slab. if self.include_cp: self.mrccblocks["adsorbate_slab"] += ( f"{len(self.adsorbate_slab_cluster) + len(self.ecp_region)}\n\n" ) - self.mrccblocks["adsorbate_slab"] += adsorbate_slab_coords_block + ecp_region_block + self.mrccblocks["adsorbate_slab"] += ( + adsorbate_slab_coords_block + ecp_region_block + ) self.mrccblocks["adsorbate"] += f"{len(self.adsorbate_slab_cluster)}\n\n" self.mrccblocks["adsorbate"] += adsorbate_slab_coords_block - self.mrccblocks["slab"] += f"{len(self.adsorbate_slab_cluster) + len(self.ecp_region)}\n\n" + self.mrccblocks["slab"] += ( + f"{len(self.adsorbate_slab_cluster) + len(self.ecp_region)}\n\n" + ) self.mrccblocks["slab"] += adsorbate_slab_coords_block + ecp_region_block - for system in ["adsorbate_slab", "adsorbate", "slab"]: self.mrccblocks[system] += "\nghost=serialno\n" # Add the ghost atoms for the counterpoise correction in the adsorbate and slab @@ -438,13 +438,17 @@ def generate_coords_block( self.mrccblocks["adsorbate_slab"] += ( f"{len(self.adsorbate_slab_cluster) + len(self.ecp_region)}\n\n" ) - self.mrccblocks["adsorbate_slab"] += adsorbate_slab_coords_block + ecp_region_block - + self.mrccblocks["adsorbate_slab"] += ( + adsorbate_slab_coords_block + ecp_region_block + ) + self.mrccblocks["adsorbate"] += f"{len(self.adsorbate_cluster)}\n\n" for atom in self.adsorbate_cluster: self.mrccblocks["adsorbate"] += create_atom_coord_string(atom=atom) - self.mrccblocks["slab"] += f"{len(self.slab_cluster) + len(self.ecp_region)}\n\n" + self.mrccblocks["slab"] += ( + f"{len(self.slab_cluster) + len(self.ecp_region)}\n\n" + ) for atom in self.slab_cluster: self.mrccblocks["slab"] += create_atom_coord_string(atom=atom) self.mrccblocks["slab"] += ecp_region_block diff --git a/tests/core/atoms/test_skzcam.py b/tests/core/atoms/test_skzcam.py index 6d70dfc365..21601dab16 100644 --- a/tests/core/atoms/test_skzcam.py +++ b/tests/core/atoms/test_skzcam.py @@ -1,15 +1,13 @@ from __future__ import annotations +from copy import deepcopy from pathlib import Path import numpy as np import pytest from ase import Atoms -from ase.io import read from ase.calculators.calculator import compare_atoms - -from copy import deepcopy - +from ase.io import read from numpy.testing import assert_allclose, assert_equal from quacc.atoms.skzcam import ( @@ -40,6 +38,7 @@ def embedded_cluster(): {"Mg": 2.0, "O": -2.0}, ) + @pytest.fixture() def mrcc_input_generator(embedded_adsorbed_cluster, element_info): return MRCCInputGenerator( @@ -51,6 +50,7 @@ def mrcc_input_generator(embedded_adsorbed_cluster, element_info): multiplicities={"adsorbate_slab": 3, "adsorbate": 1, "slab": 2}, ) + @pytest.fixture() def embedded_adsorbed_cluster(): embedded_cluster, quantum_cluster_indices, ecp_region_indices = ( @@ -107,18 +107,22 @@ def element_info(): def distance_matrix(embedded_cluster): return embedded_cluster.get_all_distances() -def test_MRCCInputGenerator_init(embedded_adsorbed_cluster, element_info): +def test_MRCCInputGenerator_init(embedded_adsorbed_cluster, element_info): # Check what happens if multiplicities is not provided mrcc_input_generator = MRCCInputGenerator( embedded_adsorbed_cluster=embedded_adsorbed_cluster, quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], element_info=element_info, - include_cp=True + include_cp=True, ) - assert mrcc_input_generator.multiplicities == {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} + assert mrcc_input_generator.multiplicities == { + "adsorbate_slab": 1, + "adsorbate": 1, + "slab": 1, + } mrcc_input_generator = MRCCInputGenerator( embedded_adsorbed_cluster=embedded_adsorbed_cluster, @@ -129,14 +133,23 @@ def test_MRCCInputGenerator_init(embedded_adsorbed_cluster, element_info): multiplicities={"adsorbate_slab": 3, "adsorbate": 1, "slab": 2}, ) - assert not compare_atoms(mrcc_input_generator.embedded_adsorbed_cluster, embedded_adsorbed_cluster) - assert_equal(mrcc_input_generator.quantum_cluster_indices,[0, 1, 2, 3, 4, 5, 6, 7]) - assert_equal(mrcc_input_generator.adsorbate_indices,[0, 1]) - assert_equal(mrcc_input_generator.slab_indices,[2, 3, 4, 5, 6, 7]) - assert_equal(mrcc_input_generator.ecp_region_indices,[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24]) + assert not compare_atoms( + mrcc_input_generator.embedded_adsorbed_cluster, embedded_adsorbed_cluster + ) + assert_equal(mrcc_input_generator.quantum_cluster_indices, [0, 1, 2, 3, 4, 5, 6, 7]) + assert_equal(mrcc_input_generator.adsorbate_indices, [0, 1]) + assert_equal(mrcc_input_generator.slab_indices, [2, 3, 4, 5, 6, 7]) + assert_equal( + mrcc_input_generator.ecp_region_indices, + [8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], + ) assert mrcc_input_generator.element_info == element_info - assert mrcc_input_generator.include_cp == True - assert mrcc_input_generator.multiplicities == {"adsorbate_slab": 3, "adsorbate": 1, "slab": 2} + assert mrcc_input_generator.include_cp is True + assert mrcc_input_generator.multiplicities == { + "adsorbate_slab": 3, + "adsorbate": 1, + "slab": 2, + } # Check if error raise if quantum_cluster_indices and ecp_region_indices overlap @@ -148,36 +161,48 @@ def test_MRCCInputGenerator_init(embedded_adsorbed_cluster, element_info): quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], ecp_region_indices=[7, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], element_info=element_info, - include_cp=True + include_cp=True, ) def test_MRCCInputGenerator_create_eint_blocks(mrcc_input_generator): - mrcc_input_generator_nocp = deepcopy(mrcc_input_generator) mrcc_input_generator_nocp.include_cp = False mrcc_input_generator_nocp.generate_input() - - mrcc_input_generator.generate_input() + mrcc_input_generator.generate_input() - reference_block_collated = {'adsorbate_slab': {'float': [21.0, -2.0, 2.0, 2.0, 2.0, 0.1474277671], - 'string': ['basis_sm=atomtype', 'def2/JK', 'O']}, - 'adsorbate': {'float': [8.0], 'string': ['basis_sm=atomtype', '3,4,5,6,7,8']}, - 'slab': {'float': [21.0, -2.0, 2.0, 2.0, 2.0, 0.1474277671], - 'string': ['basis_sm=atomtype', 'def2/JK', 'O']}} + reference_block_collated = { + "adsorbate_slab": { + "float": [21.0, -2.0, 2.0, 2.0, 2.0, 0.1474277671], + "string": ["basis_sm=atomtype", "def2/JK", "O"], + }, + "adsorbate": {"float": [8.0], "string": ["basis_sm=atomtype", "3,4,5,6,7,8"]}, + "slab": { + "float": [21.0, -2.0, 2.0, 2.0, 2.0, 0.1474277671], + "string": ["basis_sm=atomtype", "def2/JK", "O"], + }, + } - reference_block_nocp_collated = {'adsorbate_slab': {'float': [21.0, -2.0, 2.0, 2.0, 2.0, 0.1474277671], - 'string': ['basis_sm=atomtype', 'def2/JK', 'O']}, - 'adsorbate': {'float': [2.0], 'string': ['basis_sm=atomtype']}, - 'slab': {'float': [19.0, - -4.22049352791, - 4.22049352791, - 4.22049352791, - 2.11024676395, - -0.0], - 'string': ['basis_sm=atomtype', 'no-basis-set', 'Mg']}} + reference_block_nocp_collated = { + "adsorbate_slab": { + "float": [21.0, -2.0, 2.0, 2.0, 2.0, 0.1474277671], + "string": ["basis_sm=atomtype", "def2/JK", "O"], + }, + "adsorbate": {"float": [2.0], "string": ["basis_sm=atomtype"]}, + "slab": { + "float": [ + 19.0, + -4.22049352791, + 4.22049352791, + 4.22049352791, + 2.11024676395, + -0.0, + ], + "string": ["basis_sm=atomtype", "no-basis-set", "Mg"], + }, + } generated_block_collated = { system: {"float": [], "string": []} @@ -235,15 +260,12 @@ def test_MRCCInputGenerator_create_eint_blocks(mrcc_input_generator): def test_MRCCInputGenerator_generate_basis_ecp_block(mrcc_input_generator): - mrcc_input_generator_nocp = deepcopy(mrcc_input_generator) - mrcc_input_generator_nocp.include_cp = False mrcc_input_generator_nocp.generate_basis_ecp_block() - - mrcc_input_generator.generate_basis_ecp_block() + mrcc_input_generator.generate_basis_ecp_block() reference_mrcc_blocks_collated = { "adsorbate_slab": [ @@ -303,12 +325,12 @@ def test_MRCCInputGenerator_generate_basis_ecp_block(mrcc_input_generator): system: [] for system in ["adsorbate_slab", "slab", "adsorbate"] } for system in ["adsorbate_slab", "adsorbate", "slab"]: - generated_mrcc_blocks_collated[system] = mrcc_input_generator.mrccblocks[system].split()[ - ::10 - ] - generated_mrcc_blocks_nocp_collated[system] = mrcc_input_generator_nocp.mrccblocks[ + generated_mrcc_blocks_collated[system] = mrcc_input_generator.mrccblocks[ system ].split()[::10] + generated_mrcc_blocks_nocp_collated[system] = ( + mrcc_input_generator_nocp.mrccblocks[system].split()[::10] + ) assert_equal( generated_mrcc_blocks_collated[system], @@ -321,18 +343,18 @@ def test_MRCCInputGenerator_generate_basis_ecp_block(mrcc_input_generator): def test_MRCCInputGenerator_create_atomtype_basis(mrcc_input_generator): - - generated_basis_block_without_ecp = mrcc_input_generator.create_atomtype_basis( quantum_region=mrcc_input_generator.adsorbate_slab_cluster, element_basis_info={ - element: mrcc_input_generator.element_info[element]["ri_cwft_basis"] for element in mrcc_input_generator.element_info + element: mrcc_input_generator.element_info[element]["ri_cwft_basis"] + for element in mrcc_input_generator.element_info }, ) generated_basis_block_with_ecp = mrcc_input_generator.create_atomtype_basis( quantum_region=mrcc_input_generator.adsorbate_slab_cluster, element_basis_info={ - element: mrcc_input_generator.element_info[element]["ri_cwft_basis"] for element in mrcc_input_generator.element_info + element: mrcc_input_generator.element_info[element]["ri_cwft_basis"] + for element in mrcc_input_generator.element_info }, ecp_region=mrcc_input_generator.ecp_region, ) @@ -345,15 +367,12 @@ def test_MRCCInputGenerator_create_atomtype_basis(mrcc_input_generator): def test_MRCCInputGenerator_generate_coords_block(mrcc_input_generator): - mrcc_input_generator_nocp = deepcopy(mrcc_input_generator) mrcc_input_generator_nocp.include_cp = False mrcc_input_generator_nocp.generate_coords_block() - - mrcc_input_generator.generate_coords_block() - + mrcc_input_generator.generate_coords_block() reference_block_collated = { "adsorbate_slab": { @@ -462,22 +481,23 @@ def test_MRCCInputGenerator_generate_coords_block(mrcc_input_generator): def test_MRCCInputGenerator_generate_point_charge_block(mrcc_input_generator): - generated_point_charge_block = mrcc_input_generator.generate_point_charge_block() generated_point_charge_block_shortened = [ float(x) for x in generated_point_charge_block.split()[5::180] ] - reference_point_charge_block_shortened = [-0.04367284424, - -0.03992370948, - -2.14923989662, - -6.37814204923, - -2.1415520695, - -4.26789528527, - -2.1415520695, - -0.03992370948, - 0.0] + reference_point_charge_block_shortened = [ + -0.04367284424, + -0.03992370948, + -2.14923989662, + -6.37814204923, + -2.1415520695, + -4.26789528527, + -2.1415520695, + -0.03992370948, + 0.0, + ] assert_allclose( generated_point_charge_block_shortened, From 3de98c0e82292204ec3b50112de8dc968cf5f077 Mon Sep 17 00:00:00 2001 From: benshi97 Date: Sun, 9 Jun 2024 12:08:24 +0100 Subject: [PATCH 03/29] Make a class for generating orca inputs --- src/quacc/atoms/skzcam.py | 716 +++++++++---------- tests/core/atoms/test_skzcam.py | 1189 +++++++++++-------------------- 2 files changed, 758 insertions(+), 1147 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index 17df623339..4509e5f07c 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -486,449 +486,421 @@ def generate_point_charge_block(self) -> str: return pc_block +class ORCAInputGenerator: + def __init__( + self, + embedded_adsorbed_cluster: Atoms, + quantum_cluster_indices: list[int], + ecp_region_indices: list[int], + element_info: dict[ElementStr, ElementInfo] | None = None, + include_cp: bool = True, + multiplicities: MultiplicityDict | None = None, + pal_nprocs_block: dict[str, int] | None = None, + method_block: dict[str, str] | None = None, + scf_block: dict[str, str] | None = None, + ecp_info: dict[ElementStr, str] | None = None, + ) -> None: + """ + Parameters + ---------- + embedded_adsorbed_cluster + The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This object is created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + quantum_cluster_indices + A list containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + ecp_region_indices + A list containing the indices of the atoms in each ECP region. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + element_info + A dictionary with elements as keys which gives the (1) number of core electrons as 'core', (2) basis set as 'basis', (3) effective core potential as 'ecp', (4) resolution-of-identity/density-fitting auxiliary basis set for DFT/HF calculations as 'ri_scf_basis' and (5) resolution-of-identity/density-fitting for correlated wave-function methods as 'ri_cwft_basis'. + include_cp + If True, the coords strings will include the counterpoise correction (i.e., ghost atoms) for the adsorbate and slab. + multiplicities + The multiplicity of the adsorbate-slab complex, adsorbate and slab respectively, with the keys 'adsorbate_slab', 'adsorbate', and 'slab'. + pal_nprocs_block + A dictionary with the number of processors for the PAL block as 'nprocs' and the maximum memory-per-core in megabytes blocks as 'maxcore'. + method_block + A dictionary that contains the method block for the ORCA input file. The key is the ORCA setting and the value is that setting's value. + scf_block + A dictionary that contains the SCF block for the ORCA input file. The key is the ORCA setting and the value is that setting's value. + ecp_info + A dictionary with the ECP data (in ORCA format) for the cations in the ECP region. -def create_orca_eint_blocks( - embedded_adsorbed_cluster: Atoms, - quantum_cluster_indices: list[int], - ecp_region_indices: list[int], - element_info: dict[ElementStr, ElementInfo] | None = None, - pal_nprocs_block: dict[str, int] | None = None, - method_block: dict[str, str] | None = None, - scf_block: dict[str, str] | None = None, - ecp_info: dict[ElementStr, str] | None = None, - include_cp: bool = True, - multiplicities: MultiplicityDict | None = None, -) -> BlockInfo: - """ - Creates the orcablocks input for the ORCA ASE calculator. - - Parameters - ---------- - embedded_adsorbed_cluster - The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This object is created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - quantum_cluster_indices - A list containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - ecp_region_indices - A list containing the indices of the atoms in each ECP region. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - element_info - A dictionary with elements as keys which gives the (1) number of core electrons as 'core', (2) basis set as 'basis', (3) effective core potential as 'ecp', (4) resolution-of-identity/density-fitting auxiliary basis set for DFT/HF calculations as 'ri_scf_basis' and (5) resolution-of-identity/density-fitting for correlated wave-function methods as 'ri_cwft_basis'. - pal_nprocs_block - A dictionary with the number of processors for the PAL block as 'nprocs' and the maximum memory-per-core in megabytes blocks as 'maxcore'. - method_block - A dictionary that contains the method block for the ORCA input file. The key is the ORCA setting and the value is that setting's value. - scf_block - A dictionary that contains the SCF block for the ORCA input file. The key is the ORCA setting and the value is that setting's value. - ecp_info - A dictionary with the ECP data (in ORCA format) for the cations in the ECP region. - include_cp - If True, the coords strings will include the counterpoise correction for the adsorbate and slab. - multiplicities - The multiplicity of the adsorbate-slab complex, adsorbate and slab respectively, with the keys 'adsorbate_slab', 'adsorbate', and 'slab'. + Returns + ------- + None + """ - Returns - ------- - BlockInfo - The ORCA input block (to be put in 'orcablocks' parameter) as a string for the adsorbate-slab complex, the adsorbate, and the slab in a dictionary with the keys 'adsorbate_slab', 'adsorbate', and 'slab' respectively. - """ + # Check that none of the indices in quantum_cluster_indices are in ecp_region_indices + if not np.all([x not in ecp_region_indices for x in quantum_cluster_indices]): + raise ValueError( + "An atom in the quantum cluster is also in the ECP region." + ) - # First generate the preamble block - preamble_block = generate_orca_input_preamble( - embedded_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=quantum_cluster_indices, - element_info=element_info, - pal_nprocs_block=pal_nprocs_block, - method_block=method_block, - scf_block=scf_block, - ) + self.embedded_adsorbed_cluster = embedded_adsorbed_cluster + self.quantum_cluster_indices = quantum_cluster_indices + self.ecp_region_indices = ecp_region_indices + self.element_info = element_info - # Generate the coords block - coords_block = generate_coords_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=quantum_cluster_indices, - ecp_region_indices=ecp_region_indices, - ecp_info=ecp_info, - include_cp=include_cp, - multiplicities=multiplicities, - ) + # Set multiplicities + if multiplicities is None: + self.multiplicities = {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} + else: + self.multiplicities = multiplicities - # Combine the blocks - return { - "adsorbate_slab": preamble_block + coords_block["adsorbate_slab"], - "adsorbate": preamble_block + coords_block["adsorbate"], - "slab": preamble_block + coords_block["slab"], - } + # Create the adsorbate-slab complex quantum cluster and ECP region cluster + self.adsorbate_slab_cluster = self.embedded_adsorbed_cluster[ + self.quantum_cluster_indices + ] + self.ecp_region = self.embedded_adsorbed_cluster[self.ecp_region_indices] + # Get the indices of the adsorbates from the quantum cluster + self.adsorbate_indices = [ + i + for i in range(len(self.adsorbate_slab_cluster)) + if self.adsorbate_slab_cluster.get_array("atom_type")[i] == "adsorbate" + ] + # Get the indices of the slab from the quantum cluster + self.slab_indices = [ + i + for i in range(len(self.adsorbate_slab_cluster)) + if self.adsorbate_slab_cluster.get_array("atom_type")[i] != "adsorbate" + ] -def create_atom_coord_string( - atom: Atom, - is_ghost_atom: bool = False, - atom_ecp_info: str | None = None, - pc_charge: float | None = None, -) -> str: - """ - Creates a string containing the Atom symbol and coordinates in the ORCA input file format, with additional information for atoms in the ECP region as well as ghost atoms. + # Create the adsorbate and slab quantum clusters + self.adsorbate_cluster = self.adsorbate_slab_cluster[self.adsorbate_indices] + self.slab_cluster = self.adsorbate_slab_cluster[self.slab_indices] - Parameters - ---------- - atom - The ASE Atom (not Atoms) object containing the atomic coordinates. - is_ghost_atom - If True, then the atom is a ghost atom. - atom_ecp_info - If not None, then assume this is an atom in the ECP region and adds the ECP info. - pc_charge - The point charge value for the ECP region atom. + self.include_cp = include_cp - Returns - ------- - str - The atom symbol and coordinates in the ORCA input file format. - """ + self.pal_nprocs_block = pal_nprocs_block + self.method_block = method_block + self.scf_block = scf_block + self.ecp_info = ecp_info - # If ecp_info is not None and ghost_atom is True, raise an error - if atom_ecp_info and is_ghost_atom: - raise ValueError("ECP info cannot be provided for ghost atoms.") + # Initialize the orcablocks input strings for the adsorbate-slab complex, adsorbate, and slab + self.orcablocks = {"adsorbate_slab": "", "adsorbate": "", "slab": ""} - # Check that pc_charge is a float if atom_ecp_info is not None - if atom_ecp_info and pc_charge is None: - raise ValueError("Point charge value must be given for atoms with ECP info.") + def generate_input( + self + ) -> None: + """ + Creates the orcablocks input for the ORCA ASE calculator. - if is_ghost_atom: - atom_coord_str = f"{(atom.symbol + ':').ljust(3)} {' '*16} {atom.position[0]:-16.11f} {atom.position[1]:-16.11f} {atom.position[2]:-16.11f}\n" - elif atom_ecp_info is not None: - atom_coord_str = f"{(atom.symbol + '>').ljust(3)} {pc_charge:-16.11f} {atom.position[0]:-16.11f} {atom.position[1]:-16.11f} {atom.position[2]:-16.11f}\n{atom_ecp_info}" - else: - atom_coord_str = f"{atom.symbol.ljust(3)} {' '*16} {atom.position[0]:-16.11f} {atom.position[1]:-16.11f} {atom.position[2]:-16.11f}\n" + Parameters + ---------- - return atom_coord_str + Returns + ------- + None + """ + # First generate the preamble block + self.generate_preamble_block() -def generate_coords_block( - embedded_adsorbed_cluster: Atoms, - quantum_cluster_indices: list[int], - ecp_region_indices: list[int], - ecp_info: dict[ElementStr, str] | None = None, - include_cp: bool = True, - multiplicities: MultiplicityDict | None = None, -) -> BlockInfo: - """ - Generates the coordinates block for the ORCA input file. This includes the coordinates of the quantum cluster, the ECP region, and the point charges. It will return three strings for the adsorbate-slab complex, adsorbate and slab. + # Create the blocks for the coordinates + self.generate_coords_block() - Parameters - ---------- - embedded_adsorbed_cluster - The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This Atoms object is typically produced from [quacc.atoms.skzcam.create_skzcam_clusters][]. - quantum_cluster_indices - A list containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - ecp_region_indices - A list containing the indices of the atoms in each ECP region. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - ecp_info - A dictionary with the ECP data (in ORCA format) for the cations in the ECP region. - include_cp - If True, the coords strings will include the counterpoise correction for the adsorbate and slab. - multiplicities - The multiplicity of the adsorbate-slab complex, adsorbate and slab respectively, with the keys 'adsorbate_slab', 'adsorbate', and 'slab'. + # Combine the blocks + return self.orcablocks - Returns - ------- - BlockInfo - The coordinates block for the ORCA input block (to be put in 'orcablocks' parameter) as a string for the adsorbate-slab complex, the adsorbate, and the slab in a dictionary with the keys 'adsorbate_slab', 'adsorbate', and 'slab' respectively. - """ + def generate_coords_block( + self + ) -> None: + """ + Generates the coordinates block for the ORCA input file. This includes the coordinates of the quantum cluster, the ECP region, and the point charges. It will return three strings for the adsorbate-slab complex, adsorbate and slab. - # Create the quantum cluster and ECP region cluster - if multiplicities is None: - multiplicities = {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} - quantum_cluster = embedded_adsorbed_cluster[quantum_cluster_indices] - ecp_region = embedded_adsorbed_cluster[ecp_region_indices] - - # Get the indices of the adsorbates from the quantum cluster - adsorbate_indices = [ - i - for i in range(len(quantum_cluster)) - if quantum_cluster.get_array("atom_type")[i] == "adsorbate" - ] + Parameters + ---------- + - # Get the indices of the slab from the quantum cluster - slab_indices = [ - i - for i in range(len(quantum_cluster)) - if quantum_cluster.get_array("atom_type")[i] != "adsorbate" - ] + Returns + ------- + None + """ - # Get the charge of the quantum cluster - charge = int(sum(quantum_cluster.get_array("oxi_states"))) + # Get the charge of the adsorbate_slab cluster + charge = int(sum(self.adsorbate_slab_cluster.get_array("oxi_states"))) - # Create the coords strings for the adsorbate-slab complex, adsorbate, and slab - coords_block = { - "adsorbate_slab": f"""%coords + # Add the coords strings for the adsorbate-slab complex, adsorbate, and slab + self.orcablocks["adsorbate_slab"] += f"""%coords CTyp xyz -Mult {multiplicities['adsorbate_slab']} +Mult {self.multiplicities['adsorbate_slab']} Units angs Charge {charge} coords -""", - "adsorbate": f"""%coords +""" + self.orcablocks["adsorbate"] += f"""%coords CTyp xyz -Mult {multiplicities['adsorbate']} +Mult {self.multiplicities['adsorbate']} Units angs Charge 0 coords -""", - "slab": f"""%coords +""" + self.orcablocks["slab"] += f"""%coords CTyp xyz -Mult {multiplicities['slab']} +Mult {self.multiplicities['slab']} Units angs Charge {charge} coords -""", - } +""" - for i, atom in enumerate(quantum_cluster): - # Create the coords section for the adsorbate-slab complex - coords_block["adsorbate_slab"] += create_atom_coord_string(atom=atom) + for i, atom in enumerate(self.adsorbate_slab_cluster): + # Create the coords section for the adsorbate-slab complex + self.orcablocks["adsorbate_slab"] += create_atom_coord_string(atom=atom) - # Create the coords section for the adsorbate and slab - if i in adsorbate_indices: - coords_block["adsorbate"] += create_atom_coord_string(atom=atom) - if include_cp: - coords_block["slab"] += create_atom_coord_string( - atom=atom, is_ghost_atom=True - ) - elif i in slab_indices: - coords_block["slab"] += create_atom_coord_string(atom=atom) - if include_cp: - coords_block["adsorbate"] += create_atom_coord_string( - atom=atom, is_ghost_atom=True - ) + # Create the coords section for the adsorbate and slab + if i in self.adsorbate_indices: + self.orcablocks["adsorbate"] += create_atom_coord_string(atom=atom) + if self.include_cp: + self.orcablocks["slab"] += create_atom_coord_string( + atom=atom, is_ghost_atom=True + ) + elif i in self.slab_indices: + self.orcablocks["slab"] += create_atom_coord_string(atom=atom) + if self.include_cp: + self.orcablocks["adsorbate"] += create_atom_coord_string( + atom=atom, is_ghost_atom=True + ) - # Create the coords section for the ECP region - ecp_region_coords_section = "" - for i, atom in enumerate(ecp_region): - atom_ecp_info = format_ecp_info(atom_ecp_info=ecp_info[atom.symbol]) - ecp_region_coords_section += create_atom_coord_string( - atom=atom, - atom_ecp_info=atom_ecp_info, - pc_charge=ecp_region.get_array("oxi_states")[i], - ) + # Create the coords section for the ECP region + ecp_region_coords_section = "" + for i, atom in enumerate(self.ecp_region): + atom_ecp_info = self.format_ecp_info(atom_ecp_info=self.ecp_info[atom.symbol]) + ecp_region_coords_section += create_atom_coord_string( + atom=atom, + atom_ecp_info=atom_ecp_info, + pc_charge=self.ecp_region.get_array("oxi_states")[i], + ) - # Add the ECP region coords section to the ads_slab_coords string - coords_block["adsorbate_slab"] += f"{ecp_region_coords_section}end\nend\n" - coords_block["slab"] += f"{ecp_region_coords_section}end\nend\n" - coords_block["adsorbate"] += "end\nend\n" + # Add the ECP region coords section to the ads_slab_coords string + self.orcablocks["adsorbate_slab"] += f"{ecp_region_coords_section}end\nend\n" + self.orcablocks["slab"] += f"{ecp_region_coords_section}end\nend\n" + self.orcablocks["adsorbate"] += "end\nend\n" - return coords_block -def format_ecp_info(atom_ecp_info: str) -> str: - """ - Formats the ECP info so that it can be inputted to ORCA without problems. + def format_ecp_info(self,atom_ecp_info: str) -> str: + """ + Formats the ECP info so that it can be inputted to ORCA without problems. - Parameters - ---------- - atom_ecp_info - The ECP info for a single atom. + Parameters + ---------- + atom_ecp_info + The ECP info for a single atom. - Returns - ------- - str - The formatted ECP info. - """ - # Find the starting position of "NewECP" and "end" - start_pos = atom_ecp_info.lower().find("newecp") - end_pos = atom_ecp_info.lower().find("end", start_pos) + Returns + ------- + str + The formatted ECP info. + """ + # Find the starting position of "NewECP" and "end" + start_pos = atom_ecp_info.lower().find("newecp") + end_pos = atom_ecp_info.lower().find("end", start_pos) - start_pos += len("NewECP") + start_pos += len("NewECP") - # If "NewECP" or "end" is not found, then we assume that ecp_info has been given without these lines but in the correct format - if start_pos == -1 or end_pos == -1: - raise ValueError("ECP info does not contain 'NewECP' or 'end' keyword.") + # If "NewECP" or "end" is not found, then we assume that ecp_info has been given without these lines but in the correct format + if start_pos == -1 or end_pos == -1: + raise ValueError("ECP info does not contain 'NewECP' or 'end' keyword.") - # Extract content between "NewECP" and "end", exclusive of "end", then add correctly formatted "NewECP" and "end" - return f"NewECP\n{atom_ecp_info[start_pos:end_pos].strip()}\nend\n" + # Extract content between "NewECP" and "end", exclusive of "end", then add correctly formatted "NewECP" and "end" + return f"NewECP\n{atom_ecp_info[start_pos:end_pos].strip()}\nend\n" -def generate_orca_input_preamble( - embedded_cluster: Atoms, - quantum_cluster_indices: list[int], - element_info: dict[ElementStr, ElementInfo] | None = None, - pal_nprocs_block: dict[str, int] | None = None, - method_block: dict[str, str] | None = None, - scf_block: dict[str, str] | None = None, -) -> str: - """ - From the quantum cluster Atoms object, generate the ORCA input preamble for the basis, method, pal, and scf blocks. + def generate_preamble_block( + self + ) -> str: + """ + From the quantum cluster Atoms object, generate the ORCA input preamble for the basis, method, pal, and scf blocks. - Parameters - ---------- - embedded_cluster - The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun ChemShell file. This object is created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - quantum_cluster_indices - A list containing the indices of the atoms of embedded_cluster that form a quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - element_info - A dictionary with elements as keys which gives the (1) number of core electrons as 'core', (2) basis set as 'basis', (3) effective core potential as 'ecp', (4) resolution-of-identity/density-fitting auxiliary basis set for DFT/HF calculations as 'ri_scf_basis' and (5) resolution-of-identity/density-fitting for correlated wave-function methods as 'ri_cwft_basis'. - pal_nprocs_block - A dictionary with the number of processors for the PAL block as 'nprocs' and the maximum memory-per-core blocks as 'maxcore'. - method_block - A dictionary that contains the method block for the ORCA input file. The key is the ORCA setting and the value is that setting's value. - scf_block - A dictionary that contains the SCF block for the ORCA input file. The key is the ORCA setting and the value is that setting's value. + Parameters + ---------- - Returns - ------- - str - The ORCA input preamble. - """ + Returns + ------- + None + """ - # Create the quantum cluster - quantum_cluster = embedded_cluster[quantum_cluster_indices] - # Get the set of element symbols from the quantum cluster - element_symbols = list(set(quantum_cluster.get_chemical_symbols())) - element_symbols.sort() + # Get the set of element symbols from the quantum cluster + element_symbols = list(set(self.adsorbate_slab_cluster.get_chemical_symbols())) + element_symbols.sort() - # Check all element symbols are provided in element_info keys - if element_info is not None and not all( - element in element_info for element in element_symbols - ): - raise ValueError( - "Not all element symbols are provided in the element_info dictionary." - ) + # Check all element symbols are provided in element_info keys + if self.element_info is not None and not all( + element in self.element_info for element in element_symbols + ): + raise ValueError( + "Not all element symbols are provided in the element_info dictionary." + ) - # Initialize preamble_info - preamble_info = """""" - - # Add the pal_nprocs_block - if pal_nprocs_block is not None: - preamble_info += f"%pal nprocs {pal_nprocs_block['nprocs']} end\n" - preamble_info += f"%maxcore {pal_nprocs_block['maxcore']} end\n" - - # Add pointcharge file to read. It will be assumed that it is in the same folder as the input file - preamble_info += '%pointcharges "orca.pc"\n' - - # Make the method block - if method_block is not None and element_info is not None: - preamble_info += "%method\n" - # Iterate through the keys of method_block and add key value - if method_block is not None: - for key in method_block: - preamble_info += f"{key} {method_block[key]}\n" - # Iterate over the core value for each element (if it has been given) - if element_info is not None: - for element in element_symbols: - if "core" in element_info[element]: - preamble_info += ( - f"NewNCore {element} {element_info[element]['core']} end\n" - ) - if method_block is not None and element_info is not None: - preamble_info += "end\n" + # Initialize preamble_input + preamble_input = "" + + # Add the pal_nprocs_block + if self.pal_nprocs_block is not None: + preamble_input += f"%pal nprocs {self.pal_nprocs_block['nprocs']} end\n" + preamble_input += f"%maxcore {self.pal_nprocs_block['maxcore']} end\n" + + # Add pointcharge file to read. It will be assumed that it is in the same folder as the input file + preamble_input += '%pointcharges "orca.pc"\n' + + # Make the method block + if self.method_block is not None and self.element_info is not None: + preamble_input += "%method\n" + # Iterate through the keys of method_block and add key value + if self.method_block is not None: + for key in self.method_block: + preamble_input += f"{key} {self.method_block[key]}\n" + # Iterate over the core value for each element (if it has been given) + if self.element_info is not None: + for element in element_symbols: + if "core" in self.element_info[element]: + preamble_input += ( + f"NewNCore {element} {self.element_info[element]['core']} end\n" + ) + if self.method_block is not None and self.element_info is not None: + preamble_input += "end\n" - # Make the basis block + # Make the basis block - # First check if the basis key is the same for all elements. We use """ here because an option for these keys is "AutoAux" - if element_info is not None: - preamble_info += "%basis\n" - if len({element_info[element]["basis"] for element in element_symbols}) == 1: - preamble_info += f"""Basis {element_info[element_symbols[0]]['basis']}\n""" - else: - for element in element_symbols: - element_basis = element_info[element]["basis"] - preamble_info += f"""NewGTO {element} "{element_basis}" end\n""" + # First check if the basis key is the same for all elements. We use """ here because an option for these keys is "AutoAux" + if self.element_info is not None: + preamble_input += "%basis\n" + if len({self.element_info[element]["basis"] for element in element_symbols}) == 1: + preamble_input += f"""Basis {self.element_info[element_symbols[0]]['basis']}\n""" + else: + for element in element_symbols: + element_basis = self.element_info[element]["basis"] + preamble_input += f"""NewGTO {element} "{element_basis}" end\n""" - # Do the same for ri_scf_basis and ri_cwft_basis. - if ( - len({element_info[element]["ri_scf_basis"] for element in element_symbols}) - == 1 - ): - preamble_info += ( - f"""Aux {element_info[element_symbols[0]]['ri_scf_basis']}\n""" - ) - else: - for element in element_symbols: - element_basis = element_info[element]["ri_scf_basis"] - preamble_info += f'NewAuxJGTO {element} "{element_basis}" end\n' - - if ( - len( - list( - { - element_info[element]["ri_cwft_basis"] - for element in element_symbols - } + # Do the same for ri_scf_basis and ri_cwft_basis. + if ( + len({self.element_info[element]["ri_scf_basis"] for element in element_symbols}) + == 1 + ): + preamble_input += ( + f"""Aux {self.element_info[element_symbols[0]]['ri_scf_basis']}\n""" ) - ) - == 1 - ): - preamble_info += ( - f"""AuxC {element_info[element_symbols[0]]['ri_cwft_basis']}\n""" - ) - else: - for element in element_symbols: - element_basis = element_info[element]["ri_cwft_basis"] - preamble_info += f"""NewAuxCGTO {element} "{element_basis}" end\n""" + else: + for element in element_symbols: + element_basis = self.element_info[element]["ri_scf_basis"] + preamble_input += f'NewAuxJGTO {element} "{element_basis}" end\n' - preamble_info += "end\n" + if ( + len( + list( + { + self.element_info[element]["ri_cwft_basis"] + for element in element_symbols + } + ) + ) + == 1 + ): + preamble_input += ( + f"""AuxC {self.element_info[element_symbols[0]]['ri_cwft_basis']}\n""" + ) + else: + for element in element_symbols: + element_basis = self.element_info[element]["ri_cwft_basis"] + preamble_input += f"""NewAuxCGTO {element} "{element_basis}" end\n""" - # Write the scf block - if scf_block is not None: - preamble_info += "%scf\n" - for key in scf_block: - preamble_info += f"""{key} {scf_block[key]}\n""" - preamble_info += "end\n" + preamble_input += "end\n" - return preamble_info + # Write the scf block + if self.scf_block is not None: + preamble_input += "%scf\n" + for key in self.scf_block: + preamble_input += f"""{key} {self.scf_block[key]}\n""" + preamble_input += "end\n" + # Add preamble_input to the orcablocks for the adsorbate-slab complex, adsorbate, and slab + self.orcablocks["adsorbate_slab"] += preamble_input + self.orcablocks["adsorbate"] += preamble_input + self.orcablocks["slab"] += preamble_input -def create_orca_point_charge_file( - embedded_cluster: Atoms, - quantum_cluster_indices: list[int], - ecp_region_indices: list[int], - pc_file: str | Path, -) -> None: + def create_point_charge_file( + self, + pc_file: str | Path, + ) -> None: + """ + Create a point charge file that can be read by ORCA. This requires the embedded_cluster Atoms object containing both atom_type and oxi_states arrays, as well as the indices of the quantum cluster and ECP region. + + Parameters + ---------- + pc_file + A file containing the point charges to be written by ORCA. + + Returns + ------- + None + """ + + # Get the oxi_states arrays from the embedded_cluster + oxi_states = self.embedded_adsorbed_cluster.get_array("oxi_states") + + # Get the number of point charges for this system + total_indices = self.quantum_cluster_indices + self.ecp_region_indices + num_pc = len(self.embedded_adsorbed_cluster) - len(total_indices) + counter = 0 + with Path.open(pc_file, "w") as f: + # Write the number of point charges first + f.write(f"{num_pc}\n") + for i in range(len(self.embedded_adsorbed_cluster)): + if i not in total_indices: + counter += 1 + position = self.embedded_adsorbed_cluster[i].position + if counter != num_pc: + f.write( + f"{oxi_states[i]:-16.11f} {position[0]:-16.11f} {position[1]:-16.11f} {position[2]:-16.11f}\n" + ) + else: + f.write( + f"{oxi_states[i]:-16.11f} {position[0]:-16.11f} {position[1]:-16.11f} {position[2]:-16.11f}" + ) + +def create_atom_coord_string( + atom: Atom, + is_ghost_atom: bool = False, + atom_ecp_info: str | None = None, + pc_charge: float | None = None, +) -> str: """ - Create a point charge file that can be read by ORCA. This requires the embedded_cluster Atoms object containing both atom_type and oxi_states arrays, as well as the indices of the quantum cluster and ECP region. + Creates a string containing the Atom symbol and coordinates for both MRCC and ORCA, with additional information for atoms in the ECP region as well as ghost atoms. Parameters ---------- - embedded_cluster - The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file. This object is created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - quantum_cluster_indices - A list of lists containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - ecp_region_indices - A list of lists containing the indices of the atoms in the ECP region for each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - pc_file - A file containing the point charges to be written by ORCA. + atom + The ASE Atom (not Atoms) object containing the atomic coordinates. + is_ghost_atom + If True, then the atom is a ghost atom. + atom_ecp_info + If not None, then assume this is an atom in the ECP region and adds the ECP info. + pc_charge + The point charge value for the ECP region atom. Returns ------- - None + str + The atom symbol and coordinates in the ORCA input file format. """ - # Get the oxi_states arrays from the embedded_cluster - oxi_states = embedded_cluster.get_array("oxi_states") - - # Check that none of the indices in quantum_cluster_indices are in ecp_region_indices - if not np.all([x not in ecp_region_indices for x in quantum_cluster_indices]): - raise ValueError("An atom in the quantum cluster is also in the ECP region.") - - # Get the number of point charges for this system - total_indices = quantum_cluster_indices + ecp_region_indices - num_pc = len(embedded_cluster) - len(total_indices) - counter = 0 - with Path.open(pc_file, "w") as f: - # Write the number of point charges first - f.write(f"{num_pc}\n") - for i in range(len(embedded_cluster)): - if i not in total_indices: - counter += 1 - position = embedded_cluster[i].position - if counter != num_pc: - f.write( - f"{oxi_states[i]:-16.11f} {position[0]:-16.11f} {position[1]:-16.11f} {position[2]:-16.11f}\n" - ) - else: - f.write( - f"{oxi_states[i]:-16.11f} {position[0]:-16.11f} {position[1]:-16.11f} {position[2]:-16.11f}" - ) + # If ecp_info is not None and ghost_atom is True, raise an error + if atom_ecp_info and is_ghost_atom: + raise ValueError("ECP info cannot be provided for ghost atoms.") + + # Check that pc_charge is a float if atom_ecp_info is not None + if atom_ecp_info and pc_charge is None: + raise ValueError("Point charge value must be given for atoms with ECP info.") + + if is_ghost_atom: + atom_coord_str = f"{(atom.symbol + ':').ljust(3)} {' '*16} {atom.position[0]:-16.11f} {atom.position[1]:-16.11f} {atom.position[2]:-16.11f}\n" + elif atom_ecp_info is not None: + atom_coord_str = f"{(atom.symbol + '>').ljust(3)} {pc_charge:-16.11f} {atom.position[0]:-16.11f} {atom.position[1]:-16.11f} {atom.position[2]:-16.11f}\n{atom_ecp_info}" + else: + atom_coord_str = f"{atom.symbol.ljust(3)} {' '*16} {atom.position[0]:-16.11f} {atom.position[1]:-16.11f} {atom.position[2]:-16.11f}\n" + + return atom_coord_str def get_cluster_info_from_slab( diff --git a/tests/core/atoms/test_skzcam.py b/tests/core/atoms/test_skzcam.py index 21601dab16..54e51e169b 100644 --- a/tests/core/atoms/test_skzcam.py +++ b/tests/core/atoms/test_skzcam.py @@ -12,18 +12,15 @@ from quacc.atoms.skzcam import ( MRCCInputGenerator, + ORCAInputGenerator, + create_atom_coord_string, _find_cation_shells, _get_anion_coordination, _get_atom_distances, _get_ecp_region, convert_pun_to_atoms, - create_atom_coord_string, - create_orca_eint_blocks, - create_orca_point_charge_file, + create_skzcam_clusters, - format_ecp_info, - generate_coords_block, - generate_orca_input_preamble, get_cluster_info_from_slab, insert_adsorbate_to_embedded_cluster, ) @@ -50,6 +47,49 @@ def mrcc_input_generator(embedded_adsorbed_cluster, element_info): multiplicities={"adsorbate_slab": 3, "adsorbate": 1, "slab": 2}, ) +@pytest.fixture() +def orca_input_generator(embedded_adsorbed_cluster, element_info): + pal_nprocs_block = {"nprocs": 1, "maxcore": 5000} + + method_block = {"Method": "hf", "RI": "on", "RunTyp": "Energy"} + + scf_block = { + "HFTyp": "rhf", + "Guess": "MORead", + "MOInp": '"orca_svp_start.gbw"', + "SCFMode": "Direct", + "sthresh": "1e-6", + "AutoTRAHIter": 60, + "MaxIter": 1000, + } + + ecp_info = { + "Mg": """NewECP +N_core 0 +lmax f +s 1 +1 1.732000000 14.676000000 2 +p 1 +1 1.115000000 5.175700000 2 +d 1 +1 1.203000000 -1.816000000 2 +f 1 +1 1.000000000 0.000000000 2 +end""" + } + return ORCAInputGenerator( + embedded_adsorbed_cluster=embedded_adsorbed_cluster, + quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], + ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], + element_info=element_info, + include_cp=True, + multiplicities={"adsorbate_slab": 3, "adsorbate": 1, "slab": 2}, + pal_nprocs_block=pal_nprocs_block, + method_block=method_block, + scf_block=scf_block, + ecp_info=ecp_info + ) + @pytest.fixture() def embedded_adsorbed_cluster(): @@ -506,8 +546,7 @@ def test_MRCCInputGenerator_generate_point_charge_block(mrcc_input_generator): atol=1e-07, ) - -def test_create_orca_eint_blocks(embedded_adsorbed_cluster, element_info): +def test_ORCAInputGenerator_init(embedded_adsorbed_cluster, element_info): pal_nprocs_block = {"nprocs": 1, "maxcore": 5000} method_block = {"Method": "hf", "RI": "on", "RunTyp": "Energy"} @@ -536,335 +575,212 @@ def test_create_orca_eint_blocks(embedded_adsorbed_cluster, element_info): 1 1.000000000 0.000000000 2 end""" } - - orca_blocks = create_orca_eint_blocks( + orca_input_generator = ORCAInputGenerator( embedded_adsorbed_cluster=embedded_adsorbed_cluster, quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], element_info=element_info, + include_cp=True, pal_nprocs_block=pal_nprocs_block, method_block=method_block, scf_block=scf_block, - ecp_info=ecp_info, - include_cp=True, - multiplicities={"adsorbate_slab": 1, "slab": 2, "adsorbate": 3}, + ecp_info=ecp_info ) - adsorbate_slab_block_float = [ - float(x) - for x in orca_blocks["adsorbate_slab"].split()[::10] - if x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - adsorbate_slab_block_string = [ - x - for x in orca_blocks["adsorbate_slab"].split()[::5] - if not x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - adsorbate_block_float = [ - float(x) - for x in orca_blocks["adsorbate"].split()[::2] - if x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - adsorbate_block_string = [ - x - for x in orca_blocks["adsorbate"].split()[::2] - if not x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - slab_block_float = [ - float(x) - for x in orca_blocks["slab"].split()[::10] - if x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - slab_block_string = [ - x - for x in orca_blocks["slab"].split()[::5] - if not x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] + # Check when multiplicities is not provided + assert orca_input_generator.multiplicities == { + "adsorbate_slab": 1, + "adsorbate": 1, + "slab": 1, + } - # Check that the strings and floats in adsorbate_slab_coords matches reference - assert_allclose( - adsorbate_slab_block_float, - [ - 3.128, - 0.0, - 0.00567209089, - 2.0, - 1.203, - 5.1757, - 1.0, - 2.0, - 1.203, - 5.1757, - 1.0, - 2.0, - 1.203, - 5.1757, - 1.0, - 2.0, - 1.203, - 5.1757, - 1.0, - 2.0, - 1.203, - 5.1757, - 1.0, - 2.0, - 1.203, - 5.1757, - 1.0, - 2.0, - 1.203, - ], - rtol=1e-05, - atol=1e-07, + orca_input_generator = ORCAInputGenerator( + embedded_adsorbed_cluster=embedded_adsorbed_cluster, + quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], + ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], + element_info=element_info, + include_cp=True, + multiplicities={"adsorbate_slab": 3, "adsorbate": 1, "slab": 2}, + pal_nprocs_block=pal_nprocs_block, + method_block=method_block, + scf_block=scf_block, + ecp_info=ecp_info ) + assert not compare_atoms( + orca_input_generator.embedded_adsorbed_cluster, embedded_adsorbed_cluster + ) + assert_equal(orca_input_generator.quantum_cluster_indices, [0, 1, 2, 3, 4, 5, 6, 7]) + assert_equal(orca_input_generator.adsorbate_indices, [0, 1]) + assert_equal(orca_input_generator.slab_indices, [2, 3, 4, 5, 6, 7]) assert_equal( - adsorbate_slab_block_string, - [ - "%pal", - "Method", - "Energy", - "NewNCore", - "O", - "NewGTO", - "Mg", - '"aug-cc-pVDZ"', - "end", - "NewAuxJGTO", - "C", - '"cc-pVDZ/C"', - "end", - "Guess", - "Direct", - "MaxIter", - "xyz", - "Charge", - "O", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "end", - ], + orca_input_generator.ecp_region_indices, + [8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], ) + assert orca_input_generator.element_info == element_info + assert orca_input_generator.include_cp is True + assert orca_input_generator.multiplicities == { + "adsorbate_slab": 3, + "adsorbate": 1, + "slab": 2, + } - # Check that the strings and floats in ad_coords matches reference - assert_allclose( - adsorbate_block_float, - [ - 1.0, - 2.0, - 2.0, - 2.0, - 0.0, - 2.0, - 0.0, - 3.128, - 0.0, - 0.0, - -2.12018425659, - 0.00567209089, - 0.0, - 0.00567209089, - 2.12018425659, - 0.00567209089, - 0.0, - 0.00567209089, - 0.0, - -2.14129966123, - ], - rtol=1e-05, - atol=1e-07, - ) + assert orca_input_generator.pal_nprocs_block == pal_nprocs_block + assert orca_input_generator.method_block == method_block + assert orca_input_generator.scf_block == scf_block + assert orca_input_generator.ecp_info == ecp_info - assert_equal( - adsorbate_block_string, - [ - "%pal", - "%maxcore", - "end", - '"orca.pc"', - "Method", - "RI", - "RunTyp", - "NewNCore", - "NewNCore", - "NewNCore", - "end", - "NewGTO", - '"aug-cc-pVDZ"', - "NewGTO", - '"cc-pVDZ"', - "NewGTO", - '"aug-cc-pVDZ"', - "NewAuxJGTO", - '"def2/J"', - "NewAuxJGTO", - '"def2/J"', - "NewAuxJGTO", - '"def2/JK"', - "NewAuxCGTO", - '"aug-cc-pVDZ/C"', - "NewAuxCGTO", - '"cc-pVDZ/C"', - "NewAuxCGTO", - '"aug-cc-pVDZ/C"', - "end", - "HFTyp", - "Guess", - "MOInp", - "SCFMode", - "sthresh", - "AutoTRAHIter", - "MaxIter", - "end", - "CTyp", - "Mult", - "Units", - "Charge", - "coords", - "end", - ], - ) + # Check if error raise if quantum_cluster_indices and ecp_region_indices overlap - # Check that the strings and floats in slab_coords matches reference - assert_allclose( - slab_block_float, - [ - 3.128, - 0.0, - 0.00567209089, - 2.0, - 1.203, - 5.1757, - 1.0, - 2.0, - 1.203, - 5.1757, - 1.0, - 2.0, - 1.203, - 5.1757, - 1.0, - 2.0, - 1.203, - 5.1757, - 1.0, - 2.0, - 1.203, - 5.1757, - 1.0, - 2.0, - 1.203, - 5.1757, - 1.0, - 2.0, - 1.203, - ], - rtol=1e-05, - atol=1e-07, - ) + with pytest.raises( + ValueError, match="An atom in the quantum cluster is also in the ECP region." + ): + orca_input_generator = ORCAInputGenerator( + embedded_adsorbed_cluster=embedded_adsorbed_cluster, + quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], + ecp_region_indices=[7, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], + element_info=element_info, + include_cp=True, + multiplicities={"adsorbate_slab": 3, "adsorbate": 1, "slab": 2}, + pal_nprocs_block=pal_nprocs_block, + method_block=method_block, + scf_block=scf_block, + ecp_info=ecp_info + ) + + +def test_ORCAInputGenerator_generate_input(orca_input_generator): + orca_input_generator_nocp = deepcopy(orca_input_generator) + + orca_input_generator_nocp.include_cp = False + orca_input_generator_nocp.generate_input() + + orca_input_generator.generate_input() + + reference_block_collated = {'adsorbate_slab': {'float': [1.0, 2.0, 1.0, 0.0, 2.0], + 'string': ['%pal', + 'NewNCore', + 'O', + '"aug-cc-pVDZ/C"', + '"orca_svp_start.gbw"', + 'O', + 'Mg>', + 'd', + 'f', + 'NewECP', + 'f', + 's', + 'N_core', + 'end']}, + 'adsorbate': {'float': [1.0], + 'string': ['%pal', + 'NewNCore', + 'O', + '"aug-cc-pVDZ/C"', + '"orca_svp_start.gbw"', + 'O']}, + 'slab': {'float': [1.0, 2.0, 1.0, 0.0, 2.0], + 'string': ['%pal', + 'NewNCore', + 'O', + '"aug-cc-pVDZ/C"', + '"orca_svp_start.gbw"', + 'O:', + 'Mg>', + 'd', + 'f', + 'NewECP', + 'f', + 's', + 'N_core', + 'end']}} + + reference_block_nocp_collated = {'adsorbate_slab': {'float': [1.0, 2.0, 1.0, 0.0, 2.0], + 'string': ['%pal', + 'NewNCore', + 'O', + '"aug-cc-pVDZ/C"', + '"orca_svp_start.gbw"', + 'O', + 'Mg>', + 'd', + 'f', + 'NewECP', + 'f', + 's', + 'N_core', + 'end']}, + 'adsorbate': {'float': [1.0], + 'string': ['%pal', + 'NewNCore', + 'O', + '"aug-cc-pVDZ/C"', + '"orca_svp_start.gbw"', + 'O']}, + 'slab': {'float': [1.0, 2.0, 2.10705287155, 0.0, 1.0], + 'string': ['%pal', + 'NewNCore', + 'O', + '"aug-cc-pVDZ/C"', + '"orca_svp_start.gbw"', + 'O', + 'N_core', + 'end', + 'p', + 'lmax', + 'Mg>', + 'd', + 'f', + 'end']}} + + generated_block_collated = { + system: {"float": [], "string": []} + for system in ["adsorbate_slab", "adsorbate", "slab"] + } + generated_block_nocp_collated = { + system: {"float": [], "string": []} + for system in ["adsorbate_slab", "adsorbate", "slab"] + } + + for system in ["adsorbate_slab", "adsorbate", "slab"]: + generated_block_collated[system]["float"] = [ + float(x) + for x in orca_input_generator.orcablocks[system].split() + if x.replace(".", "", 1).replace("-", "", 1).isdigit() + ][::77] + generated_block_collated[system]["string"] = [ + x + for x in orca_input_generator.orcablocks[system].split() + if not x.replace(".", "", 1).replace("-", "", 1).isdigit() + ][::17] + + assert_equal(reference_block_collated[system]["string"], generated_block_collated[system]["string"]) + assert_allclose( + generated_block_collated[system]["float"], + reference_block_collated[system]["float"], + rtol=1e-05, + atol=1e-07, + ) + + generated_block_nocp_collated[system]["float"] = [ + float(x) + for x in orca_input_generator_nocp.orcablocks[system].split() + if x.replace(".", "", 1).replace("-", "", 1).isdigit() + ][::77] + generated_block_nocp_collated[system]["string"] = [ + x + for x in orca_input_generator_nocp.orcablocks[system].split() + if not x.replace(".", "", 1).replace("-", "", 1).isdigit() + ][::17] + + assert_equal(reference_block_nocp_collated[system]["string"], generated_block_nocp_collated[system]["string"]) + assert_allclose( + generated_block_nocp_collated[system]["float"], + reference_block_nocp_collated[system]["float"], + rtol=1e-05, + atol=1e-07, + ) - assert_equal( - slab_block_string, - [ - "%pal", - "Method", - "Energy", - "NewNCore", - "O", - "NewGTO", - "Mg", - '"aug-cc-pVDZ"', - "end", - "NewAuxJGTO", - "C", - '"cc-pVDZ/C"', - "end", - "Guess", - "Direct", - "MaxIter", - "xyz", - "Charge", - "O", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "Mg>", - "NewECP", - "s", - "end", - ], - ) def test_create_atom_coord_string(embedded_adsorbed_cluster): @@ -907,381 +823,161 @@ def test_create_atom_coord_string(embedded_adsorbed_cluster): ) -def test_generate_coords_block(embedded_adsorbed_cluster): - ecp_info = { - "Mg": """NewECP -N_core 0 -lmax f -s 1 -1 1.732000000 14.676000000 2 -p 1 -1 1.115000000 5.175700000 2 -d 1 -1 1.203000000 -1.816000000 2 -f 1 -1 1.000000000 0.000000000 2 -end""" - } - - # Confirm that multiplicity given as 1 if not provided - coords_block = generate_coords_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], - ecp_info=ecp_info, - ) - - assert coords_block["adsorbate_slab"].split()[4] == "1" - assert coords_block["slab"].split()[4] == "1" - assert coords_block["adsorbate"].split()[4] == "1" +def test_ORCAInputGenerator_generate_coords_block(orca_input_generator): + orca_input_generator_nocp = deepcopy(orca_input_generator) + + orca_input_generator_nocp.include_cp = False + orca_input_generator_nocp.generate_coords_block() + + orca_input_generator.generate_coords_block() + + reference_block_collated = {'adsorbate_slab': {'float': [3.0, 1.0, 5.1757, 1.0, 0.0, 2.0, 1.0], + 'string': ['%coords', + 'coords', + 'O', + 's', + 'N_core', + 'end', + 'p', + 'lmax', + 'Mg>', + 'd', + 'f', + 'NewECP', + 'f', + 's', + 'N_core', + 'end', + 'p', + 'lmax', + 'Mg>', + 'd', + 'f', + 'end']}, + 'adsorbate': {'float': [1.0], 'string': ['%coords', 'coords', 'O:']}, + 'slab': {'float': [2.0, 1.0, 5.1757, 1.0, 0.0, 2.0, 1.0], + 'string': ['%coords', + 'coords', + 'O', + 's', + 'N_core', + 'end', + 'p', + 'lmax', + 'Mg>', + 'd', + 'f', + 'NewECP', + 'f', + 's', + 'N_core', + 'end', + 'p', + 'lmax', + 'Mg>', + 'd', + 'f', + 'end']}} + + reference_block_nocp_collated = {'adsorbate_slab': {'float': [3.0, 1.0, 5.1757, 1.0, 0.0, 2.0, 1.0], + 'string': ['%coords', + 'coords', + 'O', + 's', + 'N_core', + 'end', + 'p', + 'lmax', + 'Mg>', + 'd', + 'f', + 'NewECP', + 'f', + 's', + 'N_core', + 'end', + 'p', + 'lmax', + 'Mg>', + 'd', + 'f', + 'end']}, + 'adsorbate': {'float': [1.0], 'string': ['%coords', 'coords']}, + 'slab': {'float': [2.0, 1.115, 2.0, 2.10705287155, 14.676, 1.0, 1.0], + 'string': ['%coords', + 'coords', + 'Mg>', + 'd', + 'f', + 'NewECP', + 'f', + 's', + 'N_core', + 'end', + 'p', + 'lmax', + 'Mg>', + 'd', + 'f', + 'NewECP', + 'f', + 's', + 'N_core', + 'end', + 'p']}} - coords_block = generate_coords_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], - ecp_info=ecp_info, - include_cp=True, - multiplicities={"adsorbate_slab": 1, "slab": 2, "adsorbate": 3}, - ) - - # Check that the strings and floats in adsorbate_slab_coords matches reference - adsorbate_slab_coords_shortened_list_floats = [ - float(x) - for x in coords_block["adsorbate_slab"].split()[::10] - if x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - adsorbate_slab_coords_shortened_list_str = [ - x - for x in coords_block["adsorbate_slab"].split()[::5] - if not x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - assert_allclose( - adsorbate_slab_coords_shortened_list_floats, - [ - 0.0, - 0.0, - 1.0, - 2.11144262254, - 1.732, - 1.0, - 2.0, - 1.0, - -2.11144262254, - 1.732, - 1.0, - 2.0, - 1.0, - 2.10705287155, - 1.732, - 1.0, - 2.0, - 1.0, - -2.10705287155, - 1.732, - 1.0, - 2.0, - 1.0, - 4.22049352791, - 1.732, - 1.0, - 2.0, - 1.0, - -4.22049352791, - 1.732, - 1.0, - 2.0, - 1.0, - ], - rtol=1e-05, - atol=1e-07, - ) - - assert_equal( - adsorbate_slab_coords_shortened_list_str, - [ - "%coords", - "Units", - "C", - "O", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - ], - ) - - # Check that the strings and floats in ad_coords matches reference - - adsorbate_coords_shortened_list_floats = [ - float(x) - for x in coords_block["adsorbate"].split()[::2] - if x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - adsorbate_coords_shortened_list_str = [ - x - for x in coords_block["adsorbate"].split()[::2] - if not x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - - assert_allclose( - adsorbate_coords_shortened_list_floats, - [3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.12018425659, 0.0, -2.12018425659, 0.0], - rtol=1e-05, - atol=1e-07, - ) - - assert_equal( - adsorbate_coords_shortened_list_str, - [ - "%coords", - "xyz", - "angs", - "C", - "O", - "Mg:", - "O:", - "O:", - "O:", - "O:", - "O:", - "end", - ], - ) - - # Check that the strings and floats in slab_coords matches reference - slab_coords_shortened_list_floats = [ - float(x) - for x in coords_block["slab"].split()[::10] - if x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - slab_coords_shortened_list_str = [ - x - for x in coords_block["slab"].split()[::5] - if not x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - assert_allclose( - slab_coords_shortened_list_floats, - [ - 0.0, - 0.0, - 1.0, - 2.11144262254, - 1.732, - 1.0, - 2.0, - 1.0, - -2.11144262254, - 1.732, - 1.0, - 2.0, - 1.0, - 2.10705287155, - 1.732, - 1.0, - 2.0, - 1.0, - -2.10705287155, - 1.732, - 1.0, - 2.0, - 1.0, - 4.22049352791, - 1.732, - 1.0, - 2.0, - 1.0, - -4.22049352791, - 1.732, - 1.0, - 2.0, - 1.0, - ], - rtol=1e-05, - atol=1e-07, - ) - - assert_equal( - slab_coords_shortened_list_str, - [ - "%coords", - "Units", - "C:", - "O", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - "lmax", - "f", - ], - ) - - # Also check the case where include_cp is False - coords_block = generate_coords_block( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], - ecp_info=ecp_info, - include_cp=False, - multiplicities={"adsorbate_slab": 1, "slab": 2, "adsorbate": 3}, - ) + generated_block_collated = { + system: {"float": [], "string": []} + for system in ["adsorbate_slab", "adsorbate", "slab"] + } + generated_block_nocp_collated = { + system: {"float": [], "string": []} + for system in ["adsorbate_slab", "adsorbate", "slab"] + } - # Check that the strings and floats in ad_coords matches reference - adsorbate_coords_shortened_list_floats = [ - float(x) - for x in coords_block["adsorbate"].split()[::2] - if x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - adsorbate_coords_shortened_list_str = [ - x - for x in coords_block["adsorbate"].split()[::2] - if not x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - assert_allclose( - adsorbate_coords_shortened_list_floats, - [3.0, 0.0, 0.0, 0.0], - rtol=1e-05, - atol=1e-07, - ) + for system in ["adsorbate_slab", "adsorbate", "slab"]: + generated_block_collated[system]["float"] = [ + float(x) + for x in orca_input_generator.orcablocks[system].split() + if x.replace(".", "", 1).replace("-", "", 1).isdigit() + ][::57] + generated_block_collated[system]["string"] = [ + x + for x in orca_input_generator.orcablocks[system].split() + if not x.replace(".", "", 1).replace("-", "", 1).isdigit() + ][::7] - assert_equal( - adsorbate_coords_shortened_list_str, ["%coords", "xyz", "angs", "C", "O", "end"] - ) + assert_equal(reference_block_collated[system]["string"], generated_block_collated[system]["string"]) + assert_allclose( + generated_block_collated[system]["float"], + reference_block_collated[system]["float"], + rtol=1e-05, + atol=1e-07, + ) - # Check that the strings and float in slab_coords matches reference - slab_coords_shortened_list_floats = [ - float(x) - for x in coords_block["slab"].split()[::10] - if x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - slab_coords_shortened_list_str = [ - x - for x in coords_block["slab"].split()[::5] - if not x.replace(".", "", 1).replace("-", "", 1).isdigit() - ] - assert_allclose( - slab_coords_shortened_list_floats, - [ - 2.12018425659, - -1.816, - 2.0, - 1.0, - 2.0, - 1.0, - -1.816, - 2.0, - 1.0, - 2.0, - 1.0, - -1.816, - 2.0, - 1.0, - 2.0, - 1.0, - -1.816, - 2.0, - 1.0, - 2.0, - 1.0, - -1.816, - 2.0, - 1.0, - 2.0, - 1.0, - -1.816, - 2.0, - 1.0, - 2.0, - 1.0, - -1.816, - ], - rtol=1e-05, - atol=1e-07, - ) + generated_block_nocp_collated[system]["float"] = [ + float(x) + for x in orca_input_generator_nocp.orcablocks[system].split() + if x.replace(".", "", 1).replace("-", "", 1).isdigit() + ][::57] + generated_block_nocp_collated[system]["string"] = [ + x + for x in orca_input_generator_nocp.orcablocks[system].split() + if not x.replace(".", "", 1).replace("-", "", 1).isdigit() + ][::7] - assert_equal( - slab_coords_shortened_list_str, - [ - "%coords", - "Units", - "Mg", - "O", - "N_core", - "p", - "N_core", - "p", - "N_core", - "p", - "N_core", - "p", - "N_core", - "p", - "N_core", - "p", - "N_core", - "p", - "N_core", - "p", - "N_core", - "p", - "N_core", - "p", - "N_core", - "p", - "N_core", - "p", - "N_core", - "p", - "end", - ], - ) + assert_equal(reference_block_nocp_collated[system]["string"], generated_block_nocp_collated[system]["string"]) + assert_allclose( + generated_block_nocp_collated[system]["float"], + reference_block_nocp_collated[system]["float"], + rtol=1e-05, + atol=1e-07, + ) -def test_format_ecp_info(): +def test_ORCAInputGenerator_format_ecp_info(orca_input_generator): with pytest.raises(ValueError): - format_ecp_info(atom_ecp_info="dummy_info\nN_core0\nend") + orca_input_generator.format_ecp_info(atom_ecp_info="dummy_info\nN_core0\nend") atom_ecp_info = """ NewECP @@ -1291,77 +987,33 @@ def test_format_ecp_info(): 1 1.732000000 14.676000000 2 end """ - formatted_atom_ecp_info = format_ecp_info(atom_ecp_info=atom_ecp_info) + formatted_atom_ecp_info = orca_input_generator.format_ecp_info(atom_ecp_info=atom_ecp_info) assert ( formatted_atom_ecp_info == "NewECP\nN_core 0\nlmax s\ns 1\n1 1.732000000 14.676000000 2\nend\n" ) -def test_generate_orca_input_preamble(embedded_adsorbed_cluster): - # Set-up some information needed for generating orca input - element_info = { - "C": { - "basis": "aug-cc-pVDZ", - "core": 2, - "ri_scf_basis": "def2/J", - "ri_cwft_basis": "aug-cc-pVDZ/C", - }, - "O": { - "basis": "aug-cc-pVDZ", - "core": 2, - "ri_scf_basis": "def2/JK", - "ri_cwft_basis": "aug-cc-pVDZ/C", - }, - "Mg": { - "basis": "cc-pVDZ", - "core": 2, - "ri_scf_basis": "def2/J", - "ri_cwft_basis": "cc-pVDZ/C", - }, - } +def test_ORCAInputGenerator_generate_preamble_block(orca_input_generator): - pal_nprocs_block = {"nprocs": 1, "maxcore": 5000} + # Make copy of orca_input_generator for further tests + orca_input_generator_1 = deepcopy(orca_input_generator) + orca_input_generator_2 = deepcopy(orca_input_generator) + orca_input_generator_3 = deepcopy(orca_input_generator) - method_block = {"Method": "hf", "RI": "on", "RunTyp": "Energy"} - - scf_block = { - "HFTyp": "rhf", - "Guess": "MORead", - "MOInp": '"orca_svp_start.gbw"', - "SCFMode": "Direct", - "sthresh": "1e-6", - "AutoTRAHIter": 60, - "MaxIter": 1000, - } - - # Check whether error raised if not all element_info is not provided - with pytest.raises(ValueError): - element_info_error = {"C": element_info["C"]} - generate_orca_input_preamble( - embedded_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - element_info=element_info_error, - pal_nprocs_block=pal_nprocs_block, - method_block=method_block, - scf_block=scf_block, - ) # Generate the orca input preamble - preamble_input = generate_orca_input_preamble( - embedded_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - element_info=element_info, - pal_nprocs_block=pal_nprocs_block, - method_block=method_block, - scf_block=scf_block, - ) + orca_input_generator_1.generate_preamble_block() assert ( - preamble_input + orca_input_generator_1.orcablocks['adsorbate_slab'] == '%pal nprocs 1 end\n%maxcore 5000 end\n%pointcharges "orca.pc"\n%method\nMethod hf\nRI on\nRunTyp Energy\nNewNCore C 2 end\nNewNCore Mg 2 end\nNewNCore O 2 end\nend\n%basis\nNewGTO C "aug-cc-pVDZ" end\nNewGTO Mg "cc-pVDZ" end\nNewGTO O "aug-cc-pVDZ" end\nNewAuxJGTO C "def2/J" end\nNewAuxJGTO Mg "def2/J" end\nNewAuxJGTO O "def2/JK" end\nNewAuxCGTO C "aug-cc-pVDZ/C" end\nNewAuxCGTO Mg "cc-pVDZ/C" end\nNewAuxCGTO O "aug-cc-pVDZ/C" end\nend\n%scf\nHFTyp rhf\nGuess MORead\nMOInp "orca_svp_start.gbw"\nSCFMode Direct\nsthresh 1e-6\nAutoTRAHIter 60\nMaxIter 1000\nend\n' ) + assert orca_input_generator_1.orcablocks['adsorbate_slab'] == orca_input_generator_1.orcablocks['adsorbate'] + assert orca_input_generator_1.orcablocks['adsorbate_slab'] == orca_input_generator_1.orcablocks['slab'] + + # Check the case if the element_info has all of the same values element_info = { "C": { @@ -1383,52 +1035,39 @@ def test_generate_orca_input_preamble(embedded_adsorbed_cluster): "ri_cwft_basis": "def2-SVP/C", }, } - - preamble_input = generate_orca_input_preamble( - embedded_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - element_info=element_info, - pal_nprocs_block=pal_nprocs_block, - method_block=method_block, - scf_block=scf_block, - ) + orca_input_generator_2.element_info = element_info + orca_input_generator_2.generate_preamble_block() assert ( - preamble_input + orca_input_generator_2.orcablocks['adsorbate_slab'] == '%pal nprocs 1 end\n%maxcore 5000 end\n%pointcharges "orca.pc"\n%method\nMethod hf\nRI on\nRunTyp Energy\nNewNCore C 2 end\nNewNCore Mg 2 end\nNewNCore O 2 end\nend\n%basis\nBasis def2-SVP\nAux def2/J\nAuxC def2-SVP/C\nend\n%scf\nHFTyp rhf\nGuess MORead\nMOInp "orca_svp_start.gbw"\nSCFMode Direct\nsthresh 1e-6\nAutoTRAHIter 60\nMaxIter 1000\nend\n' ) # Testing the case if we provide no blocks - preamble_input = generate_orca_input_preamble( - embedded_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ) + orca_input_generator_3.scf_block = None + orca_input_generator_3.method_block = None + orca_input_generator_3.pal_nprocs_block = None + orca_input_generator_3.element_info = None + orca_input_generator_3.generate_preamble_block() - assert preamble_input == '%pointcharges "orca.pc"\n' + assert orca_input_generator_3.orcablocks['adsorbate_slab'] == '%pointcharges "orca.pc"\n' + + # Check whether error raised if not all element_info is provided + with pytest.raises(ValueError): + element_info_error = {"C": element_info["C"]} + orca_input_generator_3.element_info = element_info_error + orca_input_generator_3.generate_preamble_block() -def test_create_orca_point_charge_file(embedded_adsorbed_cluster, tmpdir): - # Test whether exception is raised if indices shared between quantum region and ecp region - with pytest.raises( - ValueError, match="An atom in the quantum cluster is also in the ECP region." - ): - create_orca_point_charge_file( - embedded_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ecp_region_indices=[7, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], - pc_file=Path(tmpdir, "orca.pc"), - ) + + +def test_ORCAInputGenerator_create_point_charge_file(orca_input_generator, tmp_path): # Create the point charge file - create_orca_point_charge_file( - embedded_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], - pc_file=Path(tmpdir, "orca.pc"), - ) + orca_input_generator.create_point_charge_file(pc_file=tmp_path / "orca.pc" ) # Read the written file - orca_pc_file = np.loadtxt(Path(tmpdir, "orca.pc"), skiprows=1) + orca_pc_file = np.loadtxt(tmp_path / "orca.pc", skiprows=1) # Check that the contents of the file match the reference assert len(orca_pc_file) == 371 From 449abe33f6f788342d36264c113e8c4942ad33d5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 9 Jun 2024 11:08:36 +0000 Subject: [PATCH 04/29] pre-commit auto-fixes --- src/quacc/atoms/skzcam.py | 57 ++-- tests/core/atoms/test_skzcam.py | 471 ++++++++++++++++++-------------- 2 files changed, 298 insertions(+), 230 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index 4509e5f07c..b658ceeb30 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -486,6 +486,7 @@ def generate_point_charge_block(self) -> str: return pc_block + class ORCAInputGenerator: def __init__( self, @@ -579,9 +580,7 @@ def __init__( # Initialize the orcablocks input strings for the adsorbate-slab complex, adsorbate, and slab self.orcablocks = {"adsorbate_slab": "", "adsorbate": "", "slab": ""} - def generate_input( - self - ) -> None: + def generate_input(self) -> None: """ Creates the orcablocks input for the ORCA ASE calculator. @@ -602,15 +601,13 @@ def generate_input( # Combine the blocks return self.orcablocks - def generate_coords_block( - self - ) -> None: + def generate_coords_block(self) -> None: """ Generates the coordinates block for the ORCA input file. This includes the coordinates of the quantum cluster, the ECP region, and the point charges. It will return three strings for the adsorbate-slab complex, adsorbate and slab. Parameters ---------- - + Returns ------- @@ -664,7 +661,9 @@ def generate_coords_block( # Create the coords section for the ECP region ecp_region_coords_section = "" for i, atom in enumerate(self.ecp_region): - atom_ecp_info = self.format_ecp_info(atom_ecp_info=self.ecp_info[atom.symbol]) + atom_ecp_info = self.format_ecp_info( + atom_ecp_info=self.ecp_info[atom.symbol] + ) ecp_region_coords_section += create_atom_coord_string( atom=atom, atom_ecp_info=atom_ecp_info, @@ -676,9 +675,7 @@ def generate_coords_block( self.orcablocks["slab"] += f"{ecp_region_coords_section}end\nend\n" self.orcablocks["adsorbate"] += "end\nend\n" - - - def format_ecp_info(self,atom_ecp_info: str) -> str: + def format_ecp_info(self, atom_ecp_info: str) -> str: """ Formats the ECP info so that it can be inputted to ORCA without problems. @@ -705,10 +702,7 @@ def format_ecp_info(self,atom_ecp_info: str) -> str: # Extract content between "NewECP" and "end", exclusive of "end", then add correctly formatted "NewECP" and "end" return f"NewECP\n{atom_ecp_info[start_pos:end_pos].strip()}\nend\n" - - def generate_preamble_block( - self - ) -> str: + def generate_preamble_block(self) -> str: """ From the quantum cluster Atoms object, generate the ORCA input preamble for the basis, method, pal, and scf blocks. @@ -720,7 +714,6 @@ def generate_preamble_block( None """ - # Get the set of element symbols from the quantum cluster element_symbols = list(set(self.adsorbate_slab_cluster.get_chemical_symbols())) element_symbols.sort() @@ -766,8 +759,15 @@ def generate_preamble_block( # First check if the basis key is the same for all elements. We use """ here because an option for these keys is "AutoAux" if self.element_info is not None: preamble_input += "%basis\n" - if len({self.element_info[element]["basis"] for element in element_symbols}) == 1: - preamble_input += f"""Basis {self.element_info[element_symbols[0]]['basis']}\n""" + if ( + len( + {self.element_info[element]["basis"] for element in element_symbols} + ) + == 1 + ): + preamble_input += ( + f"""Basis {self.element_info[element_symbols[0]]['basis']}\n""" + ) else: for element in element_symbols: element_basis = self.element_info[element]["basis"] @@ -775,7 +775,12 @@ def generate_preamble_block( # Do the same for ri_scf_basis and ri_cwft_basis. if ( - len({self.element_info[element]["ri_scf_basis"] for element in element_symbols}) + len( + { + self.element_info[element]["ri_scf_basis"] + for element in element_symbols + } + ) == 1 ): preamble_input += ( @@ -797,13 +802,13 @@ def generate_preamble_block( ) == 1 ): - preamble_input += ( - f"""AuxC {self.element_info[element_symbols[0]]['ri_cwft_basis']}\n""" - ) + preamble_input += f"""AuxC {self.element_info[element_symbols[0]]['ri_cwft_basis']}\n""" else: for element in element_symbols: element_basis = self.element_info[element]["ri_cwft_basis"] - preamble_input += f"""NewAuxCGTO {element} "{element_basis}" end\n""" + preamble_input += ( + f"""NewAuxCGTO {element} "{element_basis}" end\n""" + ) preamble_input += "end\n" @@ -819,10 +824,7 @@ def generate_preamble_block( self.orcablocks["adsorbate"] += preamble_input self.orcablocks["slab"] += preamble_input - def create_point_charge_file( - self, - pc_file: str | Path, - ) -> None: + def create_point_charge_file(self, pc_file: str | Path) -> None: """ Create a point charge file that can be read by ORCA. This requires the embedded_cluster Atoms object containing both atom_type and oxi_states arrays, as well as the indices of the quantum cluster and ECP region. @@ -859,6 +861,7 @@ def create_point_charge_file( f"{oxi_states[i]:-16.11f} {position[0]:-16.11f} {position[1]:-16.11f} {position[2]:-16.11f}" ) + def create_atom_coord_string( atom: Atom, is_ghost_atom: bool = False, diff --git a/tests/core/atoms/test_skzcam.py b/tests/core/atoms/test_skzcam.py index 54e51e169b..fe406a7430 100644 --- a/tests/core/atoms/test_skzcam.py +++ b/tests/core/atoms/test_skzcam.py @@ -13,13 +13,12 @@ from quacc.atoms.skzcam import ( MRCCInputGenerator, ORCAInputGenerator, - create_atom_coord_string, _find_cation_shells, _get_anion_coordination, _get_atom_distances, _get_ecp_region, convert_pun_to_atoms, - + create_atom_coord_string, create_skzcam_clusters, get_cluster_info_from_slab, insert_adsorbate_to_embedded_cluster, @@ -47,6 +46,7 @@ def mrcc_input_generator(embedded_adsorbed_cluster, element_info): multiplicities={"adsorbate_slab": 3, "adsorbate": 1, "slab": 2}, ) + @pytest.fixture() def orca_input_generator(embedded_adsorbed_cluster, element_info): pal_nprocs_block = {"nprocs": 1, "maxcore": 5000} @@ -87,7 +87,7 @@ def orca_input_generator(embedded_adsorbed_cluster, element_info): pal_nprocs_block=pal_nprocs_block, method_block=method_block, scf_block=scf_block, - ecp_info=ecp_info + ecp_info=ecp_info, ) @@ -546,6 +546,7 @@ def test_MRCCInputGenerator_generate_point_charge_block(mrcc_input_generator): atol=1e-07, ) + def test_ORCAInputGenerator_init(embedded_adsorbed_cluster, element_info): pal_nprocs_block = {"nprocs": 1, "maxcore": 5000} @@ -584,7 +585,7 @@ def test_ORCAInputGenerator_init(embedded_adsorbed_cluster, element_info): pal_nprocs_block=pal_nprocs_block, method_block=method_block, scf_block=scf_block, - ecp_info=ecp_info + ecp_info=ecp_info, ) # Check when multiplicities is not provided @@ -604,7 +605,7 @@ def test_ORCAInputGenerator_init(embedded_adsorbed_cluster, element_info): pal_nprocs_block=pal_nprocs_block, method_block=method_block, scf_block=scf_block, - ecp_info=ecp_info + ecp_info=ecp_info, ) assert not compare_atoms( @@ -636,17 +637,17 @@ def test_ORCAInputGenerator_init(embedded_adsorbed_cluster, element_info): ValueError, match="An atom in the quantum cluster is also in the ECP region." ): orca_input_generator = ORCAInputGenerator( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, - quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], - ecp_region_indices=[7, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], - element_info=element_info, - include_cp=True, - multiplicities={"adsorbate_slab": 3, "adsorbate": 1, "slab": 2}, - pal_nprocs_block=pal_nprocs_block, - method_block=method_block, - scf_block=scf_block, - ecp_info=ecp_info - ) + embedded_adsorbed_cluster=embedded_adsorbed_cluster, + quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], + ecp_region_indices=[7, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], + element_info=element_info, + include_cp=True, + multiplicities={"adsorbate_slab": 3, "adsorbate": 1, "slab": 2}, + pal_nprocs_block=pal_nprocs_block, + method_block=method_block, + scf_block=scf_block, + ecp_info=ecp_info, + ) def test_ORCAInputGenerator_generate_input(orca_input_generator): @@ -657,81 +658,109 @@ def test_ORCAInputGenerator_generate_input(orca_input_generator): orca_input_generator.generate_input() - reference_block_collated = {'adsorbate_slab': {'float': [1.0, 2.0, 1.0, 0.0, 2.0], - 'string': ['%pal', - 'NewNCore', - 'O', - '"aug-cc-pVDZ/C"', - '"orca_svp_start.gbw"', - 'O', - 'Mg>', - 'd', - 'f', - 'NewECP', - 'f', - 's', - 'N_core', - 'end']}, - 'adsorbate': {'float': [1.0], - 'string': ['%pal', - 'NewNCore', - 'O', - '"aug-cc-pVDZ/C"', - '"orca_svp_start.gbw"', - 'O']}, - 'slab': {'float': [1.0, 2.0, 1.0, 0.0, 2.0], - 'string': ['%pal', - 'NewNCore', - 'O', - '"aug-cc-pVDZ/C"', - '"orca_svp_start.gbw"', - 'O:', - 'Mg>', - 'd', - 'f', - 'NewECP', - 'f', - 's', - 'N_core', - 'end']}} - - reference_block_nocp_collated = {'adsorbate_slab': {'float': [1.0, 2.0, 1.0, 0.0, 2.0], - 'string': ['%pal', - 'NewNCore', - 'O', - '"aug-cc-pVDZ/C"', - '"orca_svp_start.gbw"', - 'O', - 'Mg>', - 'd', - 'f', - 'NewECP', - 'f', - 's', - 'N_core', - 'end']}, - 'adsorbate': {'float': [1.0], - 'string': ['%pal', - 'NewNCore', - 'O', - '"aug-cc-pVDZ/C"', - '"orca_svp_start.gbw"', - 'O']}, - 'slab': {'float': [1.0, 2.0, 2.10705287155, 0.0, 1.0], - 'string': ['%pal', - 'NewNCore', - 'O', - '"aug-cc-pVDZ/C"', - '"orca_svp_start.gbw"', - 'O', - 'N_core', - 'end', - 'p', - 'lmax', - 'Mg>', - 'd', - 'f', - 'end']}} + reference_block_collated = { + "adsorbate_slab": { + "float": [1.0, 2.0, 1.0, 0.0, 2.0], + "string": [ + "%pal", + "NewNCore", + "O", + '"aug-cc-pVDZ/C"', + '"orca_svp_start.gbw"', + "O", + "Mg>", + "d", + "f", + "NewECP", + "f", + "s", + "N_core", + "end", + ], + }, + "adsorbate": { + "float": [1.0], + "string": [ + "%pal", + "NewNCore", + "O", + '"aug-cc-pVDZ/C"', + '"orca_svp_start.gbw"', + "O", + ], + }, + "slab": { + "float": [1.0, 2.0, 1.0, 0.0, 2.0], + "string": [ + "%pal", + "NewNCore", + "O", + '"aug-cc-pVDZ/C"', + '"orca_svp_start.gbw"', + "O:", + "Mg>", + "d", + "f", + "NewECP", + "f", + "s", + "N_core", + "end", + ], + }, + } + + reference_block_nocp_collated = { + "adsorbate_slab": { + "float": [1.0, 2.0, 1.0, 0.0, 2.0], + "string": [ + "%pal", + "NewNCore", + "O", + '"aug-cc-pVDZ/C"', + '"orca_svp_start.gbw"', + "O", + "Mg>", + "d", + "f", + "NewECP", + "f", + "s", + "N_core", + "end", + ], + }, + "adsorbate": { + "float": [1.0], + "string": [ + "%pal", + "NewNCore", + "O", + '"aug-cc-pVDZ/C"', + '"orca_svp_start.gbw"', + "O", + ], + }, + "slab": { + "float": [1.0, 2.0, 2.10705287155, 0.0, 1.0], + "string": [ + "%pal", + "NewNCore", + "O", + '"aug-cc-pVDZ/C"', + '"orca_svp_start.gbw"', + "O", + "N_core", + "end", + "p", + "lmax", + "Mg>", + "d", + "f", + "end", + ], + }, + } generated_block_collated = { system: {"float": [], "string": []} @@ -754,7 +783,10 @@ def test_ORCAInputGenerator_generate_input(orca_input_generator): if not x.replace(".", "", 1).replace("-", "", 1).isdigit() ][::17] - assert_equal(reference_block_collated[system]["string"], generated_block_collated[system]["string"]) + assert_equal( + reference_block_collated[system]["string"], + generated_block_collated[system]["string"], + ) assert_allclose( generated_block_collated[system]["float"], reference_block_collated[system]["float"], @@ -773,7 +805,10 @@ def test_ORCAInputGenerator_generate_input(orca_input_generator): if not x.replace(".", "", 1).replace("-", "", 1).isdigit() ][::17] - assert_equal(reference_block_nocp_collated[system]["string"], generated_block_nocp_collated[system]["string"]) + assert_equal( + reference_block_nocp_collated[system]["string"], + generated_block_nocp_collated[system]["string"], + ) assert_allclose( generated_block_nocp_collated[system]["float"], reference_block_nocp_collated[system]["float"], @@ -782,7 +817,6 @@ def test_ORCAInputGenerator_generate_input(orca_input_generator): ) - def test_create_atom_coord_string(embedded_adsorbed_cluster): atom = embedded_adsorbed_cluster[0] @@ -831,100 +865,120 @@ def test_ORCAInputGenerator_generate_coords_block(orca_input_generator): orca_input_generator.generate_coords_block() - reference_block_collated = {'adsorbate_slab': {'float': [3.0, 1.0, 5.1757, 1.0, 0.0, 2.0, 1.0], - 'string': ['%coords', - 'coords', - 'O', - 's', - 'N_core', - 'end', - 'p', - 'lmax', - 'Mg>', - 'd', - 'f', - 'NewECP', - 'f', - 's', - 'N_core', - 'end', - 'p', - 'lmax', - 'Mg>', - 'd', - 'f', - 'end']}, - 'adsorbate': {'float': [1.0], 'string': ['%coords', 'coords', 'O:']}, - 'slab': {'float': [2.0, 1.0, 5.1757, 1.0, 0.0, 2.0, 1.0], - 'string': ['%coords', - 'coords', - 'O', - 's', - 'N_core', - 'end', - 'p', - 'lmax', - 'Mg>', - 'd', - 'f', - 'NewECP', - 'f', - 's', - 'N_core', - 'end', - 'p', - 'lmax', - 'Mg>', - 'd', - 'f', - 'end']}} - - reference_block_nocp_collated = {'adsorbate_slab': {'float': [3.0, 1.0, 5.1757, 1.0, 0.0, 2.0, 1.0], - 'string': ['%coords', - 'coords', - 'O', - 's', - 'N_core', - 'end', - 'p', - 'lmax', - 'Mg>', - 'd', - 'f', - 'NewECP', - 'f', - 's', - 'N_core', - 'end', - 'p', - 'lmax', - 'Mg>', - 'd', - 'f', - 'end']}, - 'adsorbate': {'float': [1.0], 'string': ['%coords', 'coords']}, - 'slab': {'float': [2.0, 1.115, 2.0, 2.10705287155, 14.676, 1.0, 1.0], - 'string': ['%coords', - 'coords', - 'Mg>', - 'd', - 'f', - 'NewECP', - 'f', - 's', - 'N_core', - 'end', - 'p', - 'lmax', - 'Mg>', - 'd', - 'f', - 'NewECP', - 'f', - 's', - 'N_core', - 'end', - 'p']}} + reference_block_collated = { + "adsorbate_slab": { + "float": [3.0, 1.0, 5.1757, 1.0, 0.0, 2.0, 1.0], + "string": [ + "%coords", + "coords", + "O", + "s", + "N_core", + "end", + "p", + "lmax", + "Mg>", + "d", + "f", + "NewECP", + "f", + "s", + "N_core", + "end", + "p", + "lmax", + "Mg>", + "d", + "f", + "end", + ], + }, + "adsorbate": {"float": [1.0], "string": ["%coords", "coords", "O:"]}, + "slab": { + "float": [2.0, 1.0, 5.1757, 1.0, 0.0, 2.0, 1.0], + "string": [ + "%coords", + "coords", + "O", + "s", + "N_core", + "end", + "p", + "lmax", + "Mg>", + "d", + "f", + "NewECP", + "f", + "s", + "N_core", + "end", + "p", + "lmax", + "Mg>", + "d", + "f", + "end", + ], + }, + } + + reference_block_nocp_collated = { + "adsorbate_slab": { + "float": [3.0, 1.0, 5.1757, 1.0, 0.0, 2.0, 1.0], + "string": [ + "%coords", + "coords", + "O", + "s", + "N_core", + "end", + "p", + "lmax", + "Mg>", + "d", + "f", + "NewECP", + "f", + "s", + "N_core", + "end", + "p", + "lmax", + "Mg>", + "d", + "f", + "end", + ], + }, + "adsorbate": {"float": [1.0], "string": ["%coords", "coords"]}, + "slab": { + "float": [2.0, 1.115, 2.0, 2.10705287155, 14.676, 1.0, 1.0], + "string": [ + "%coords", + "coords", + "Mg>", + "d", + "f", + "NewECP", + "f", + "s", + "N_core", + "end", + "p", + "lmax", + "Mg>", + "d", + "f", + "NewECP", + "f", + "s", + "N_core", + "end", + "p", + ], + }, + } generated_block_collated = { system: {"float": [], "string": []} @@ -947,7 +1001,10 @@ def test_ORCAInputGenerator_generate_coords_block(orca_input_generator): if not x.replace(".", "", 1).replace("-", "", 1).isdigit() ][::7] - assert_equal(reference_block_collated[system]["string"], generated_block_collated[system]["string"]) + assert_equal( + reference_block_collated[system]["string"], + generated_block_collated[system]["string"], + ) assert_allclose( generated_block_collated[system]["float"], reference_block_collated[system]["float"], @@ -966,7 +1023,10 @@ def test_ORCAInputGenerator_generate_coords_block(orca_input_generator): if not x.replace(".", "", 1).replace("-", "", 1).isdigit() ][::7] - assert_equal(reference_block_nocp_collated[system]["string"], generated_block_nocp_collated[system]["string"]) + assert_equal( + reference_block_nocp_collated[system]["string"], + generated_block_nocp_collated[system]["string"], + ) assert_allclose( generated_block_nocp_collated[system]["float"], reference_block_nocp_collated[system]["float"], @@ -987,7 +1047,9 @@ def test_ORCAInputGenerator_format_ecp_info(orca_input_generator): 1 1.732000000 14.676000000 2 end """ - formatted_atom_ecp_info = orca_input_generator.format_ecp_info(atom_ecp_info=atom_ecp_info) + formatted_atom_ecp_info = orca_input_generator.format_ecp_info( + atom_ecp_info=atom_ecp_info + ) assert ( formatted_atom_ecp_info == "NewECP\nN_core 0\nlmax s\ns 1\n1 1.732000000 14.676000000 2\nend\n" @@ -995,24 +1057,27 @@ def test_ORCAInputGenerator_format_ecp_info(orca_input_generator): def test_ORCAInputGenerator_generate_preamble_block(orca_input_generator): - # Make copy of orca_input_generator for further tests orca_input_generator_1 = deepcopy(orca_input_generator) orca_input_generator_2 = deepcopy(orca_input_generator) orca_input_generator_3 = deepcopy(orca_input_generator) - # Generate the orca input preamble orca_input_generator_1.generate_preamble_block() assert ( - orca_input_generator_1.orcablocks['adsorbate_slab'] + orca_input_generator_1.orcablocks["adsorbate_slab"] == '%pal nprocs 1 end\n%maxcore 5000 end\n%pointcharges "orca.pc"\n%method\nMethod hf\nRI on\nRunTyp Energy\nNewNCore C 2 end\nNewNCore Mg 2 end\nNewNCore O 2 end\nend\n%basis\nNewGTO C "aug-cc-pVDZ" end\nNewGTO Mg "cc-pVDZ" end\nNewGTO O "aug-cc-pVDZ" end\nNewAuxJGTO C "def2/J" end\nNewAuxJGTO Mg "def2/J" end\nNewAuxJGTO O "def2/JK" end\nNewAuxCGTO C "aug-cc-pVDZ/C" end\nNewAuxCGTO Mg "cc-pVDZ/C" end\nNewAuxCGTO O "aug-cc-pVDZ/C" end\nend\n%scf\nHFTyp rhf\nGuess MORead\nMOInp "orca_svp_start.gbw"\nSCFMode Direct\nsthresh 1e-6\nAutoTRAHIter 60\nMaxIter 1000\nend\n' ) - assert orca_input_generator_1.orcablocks['adsorbate_slab'] == orca_input_generator_1.orcablocks['adsorbate'] - assert orca_input_generator_1.orcablocks['adsorbate_slab'] == orca_input_generator_1.orcablocks['slab'] - + assert ( + orca_input_generator_1.orcablocks["adsorbate_slab"] + == orca_input_generator_1.orcablocks["adsorbate"] + ) + assert ( + orca_input_generator_1.orcablocks["adsorbate_slab"] + == orca_input_generator_1.orcablocks["slab"] + ) # Check the case if the element_info has all of the same values element_info = { @@ -1039,7 +1104,7 @@ def test_ORCAInputGenerator_generate_preamble_block(orca_input_generator): orca_input_generator_2.generate_preamble_block() assert ( - orca_input_generator_2.orcablocks['adsorbate_slab'] + orca_input_generator_2.orcablocks["adsorbate_slab"] == '%pal nprocs 1 end\n%maxcore 5000 end\n%pointcharges "orca.pc"\n%method\nMethod hf\nRI on\nRunTyp Energy\nNewNCore C 2 end\nNewNCore Mg 2 end\nNewNCore O 2 end\nend\n%basis\nBasis def2-SVP\nAux def2/J\nAuxC def2-SVP/C\nend\n%scf\nHFTyp rhf\nGuess MORead\nMOInp "orca_svp_start.gbw"\nSCFMode Direct\nsthresh 1e-6\nAutoTRAHIter 60\nMaxIter 1000\nend\n' ) @@ -1050,8 +1115,10 @@ def test_ORCAInputGenerator_generate_preamble_block(orca_input_generator): orca_input_generator_3.element_info = None orca_input_generator_3.generate_preamble_block() - - assert orca_input_generator_3.orcablocks['adsorbate_slab'] == '%pointcharges "orca.pc"\n' + assert ( + orca_input_generator_3.orcablocks["adsorbate_slab"] + == '%pointcharges "orca.pc"\n' + ) # Check whether error raised if not all element_info is provided with pytest.raises(ValueError): @@ -1060,11 +1127,9 @@ def test_ORCAInputGenerator_generate_preamble_block(orca_input_generator): orca_input_generator_3.generate_preamble_block() - def test_ORCAInputGenerator_create_point_charge_file(orca_input_generator, tmp_path): - # Create the point charge file - orca_input_generator.create_point_charge_file(pc_file=tmp_path / "orca.pc" ) + orca_input_generator.create_point_charge_file(pc_file=tmp_path / "orca.pc") # Read the written file orca_pc_file = np.loadtxt(tmp_path / "orca.pc", skiprows=1) From 41dd0290f617ceb167a4fe12f5e529c993a58ef2 Mon Sep 17 00:00:00 2001 From: benshi97 Date: Sun, 9 Jun 2024 15:58:03 +0100 Subject: [PATCH 05/29] Class for CreateSKZCAMClusters --- src/quacc/atoms/skzcam.py | 985 +++++++++--------- .../{conftest.py => conftest_noworking.py} | 10 +- .../core/atoms/skzcam_files/CO_MgO.poscar.gz | Bin 0 -> 763 bytes ...luster.pun.gz => ChemShell_Cluster.pun.gz} | Bin .../skzcam_files/REF_ChemShell_Cluster.xyz.gz | Bin 0 -> 1750 bytes .../skzcam_files/REF_ChemShell_cluster.xyz.gz | Bin 13569 -> 0 bytes .../adsorbate_slab_embedded_cluster.npy.gz | Bin 0 -> 3228 bytes tests/core/atoms/test_skzcam.py | 706 +++++-------- 8 files changed, 771 insertions(+), 930 deletions(-) rename tests/core/atoms/{conftest.py => conftest_noworking.py} (59%) create mode 100644 tests/core/atoms/skzcam_files/CO_MgO.poscar.gz rename tests/core/atoms/skzcam_files/{mgo_shells_cluster.pun.gz => ChemShell_Cluster.pun.gz} (100%) create mode 100644 tests/core/atoms/skzcam_files/REF_ChemShell_Cluster.xyz.gz delete mode 100644 tests/core/atoms/skzcam_files/REF_ChemShell_cluster.xyz.gz create mode 100644 tests/core/atoms/skzcam_files/adsorbate_slab_embedded_cluster.npy.gz diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index b658ceeb30..f2ee08d254 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -905,573 +905,588 @@ def create_atom_coord_string( return atom_coord_str +class CreateSKZCAMClusters: + def __init__( + self, + adsorbate_indices: list[int], + slab_center_indices: list[int], + atom_oxi_states: dict[str, int], + adsorbate_slab_file: str | Path | None = None, + pun_file: str | Path | None = None, + ): + """ + Parameters + ---------- + adsorbate_indices + The indices of the atoms that make up the adsorbate molecule. + slab_center_indices + The indices of the atoms that make up the 'center' of the slab right beneath the adsorbate. + atom_oxi_states + A dictionary with the element symbol as the key and its oxidation state as the value. + adsorbate_slab_file + The path to the file containing the adsorbate molecule on the surface slab. It can be in any format that ASE can read. + pun_file + The path to the .pun file containing the atomic coordinates and charges of the adsorbate-slab complex. This file should be generated by ChemShell. If it is None, then ChemShell wil be used to create this file. + """ -def get_cluster_info_from_slab( - adsorbate_slab_file: str | Path, - slab_center_indices: list[int], - adsorbate_indices: list[int], -) -> tuple[Atoms, Atoms, int, NDArray, NDArray]: - """ - Read the file containing the periodic slab and adsorbate (geometry optimized) and return the key information needed to create an embedded cluster in ChemShell. - - Parameters - ---------- - adsorbate_slab_file - The path to the file containing the adsorbate molecule on the surface slab. It can be in any format that ASE can read. - adsorbate_indices - The indices of the atoms that make up the adsorbate molecule. - slab_center_indices - The indices of the atoms that are at the 'center' of the slab right beneath the adsorbate. - - Returns - ------- - Atoms - The Atoms object of the adsorbate molecule. - Atoms - The Atoms object of the surface slab. - int - The index of the first atom of the slab as listed in slab_center_indices. - NDArray - The position of the center of the cluster. - NDArray - The vector from the center of the slab to the center of mass of the adsorbate. - """ + self.adsorbate_indices = adsorbate_indices + self.slab_center_indices = slab_center_indices + self.slab_indices = None # This will be set later + self.atom_oxi_states = atom_oxi_states - # Get the necessary information for the cluster from a provided slab file (in any format that ASE can read) - adsorbate_slab = read(adsorbate_slab_file) + # Check that the adsorbate_indices and slab_center_indices are not the same + if any([x in adsorbate_indices for x in slab_center_indices]): + raise ValueError("The adsorbate and slab center indices cannot be the same.") + + # Check that the adsorbate_slab_file and pun_file are not both None + if adsorbate_slab_file is None and pun_file is None: + raise ValueError("Either the adsorbate_slab_file or pun_file must be provided.") - # Find indices (within adsorbate_slab) of the slab - slab_indices = [ - i for i, _ in enumerate(adsorbate_slab) if i not in adsorbate_indices - ] + self.adsorbate_slab_file = adsorbate_slab_file + self.pun_file = pun_file - # Create slab from adsorbate_slab - slab = adsorbate_slab[slab_indices] + # Initialize the adsorbate, slab and adsorbate_slab Atoms object which contains the adsorbate, slab and adsorbate-slab complex respectively + self.adsorbate = None + self.slab = None + self.adsorbate_slab = None - # Find index of the first center atom of the slab as listed in slab_center_indices - slab_center_idx = next( - index for index, x in enumerate(slab_indices) if x == slab_center_indices[0] - ) + # Initialize the embedded_adsorbate_slab_cluster, and embedded_slab_cluster Atoms object which are the embedded cluster for the adsorbate-slab complex and slab respectively + self.adsorbate_slab_embedded_cluster = None + self.slab_embedded_cluster = None - # Get the center of the cluster from the atom indices - slab_center_position = adsorbate_slab[slab_center_indices].get_positions().sum( - axis=0 - ) / len(slab_center_indices) + # Initialize the quantum cluster indices and ECP region indices + self.quantum_cluster_indices = None + self.ecp_region_indices = None - adsorbate = adsorbate_slab[adsorbate_indices] + def run_skzcam( + self, + shell_max: int = 10, + shell_width: float = 0.1, + bond_dist: float = 2.5, + ecp_dist: float = 6.0, + write_clusters: bool = False, + write_clusters_path: str | Path = ".", + write_include_ecp: bool = False, + ) -> None: + """ + From a provided .pun file (generated by ChemShell), this function creates quantum clusters using the SKZCAM protocol. It will return the embedded cluster Atoms object and the indices of the atoms in the quantum clusters and the ECP region. The number of clusters created is controlled by the rdf_max parameter. - # Get the relative distance of the adsorbate from the first center atom of the slab as defined in the slab_center_indices - adsorbate_com = adsorbate.get_center_of_mass() - adsorbate_vector_from_slab = ( - adsorbate[0].position - adsorbate_slab[slab_center_indices[0]].position - ) + Parameters + ---------- + shell_max + The maximum number of quantum clusters to be created. + shell_width + Defines the distance between atoms within shells; this is the maximum distance between any two atoms within the shell. + bond_dist + The distance within which an anion is considered to be coordinating a cation. + ecp_dist + The distance from edges of the quantum cluster to define the ECP region. + write_clusters + If True, the quantum clusters will be written to a file. + write_clusters_path + The path to the file where the quantum clusters will be written. + write_include_ecp + If True, the ECP region will be included in the quantum clusters. - # Add the height of the adsorbate from the slab along the z-direction relative to the first center atom of the slab as defined in the slab_center_indices - adsorbate_com_z_disp = ( - adsorbate_com[2] - adsorbate_slab[slab_center_indices[0]].position[2] - ) - center_position = ( - np.array([0.0, 0.0, adsorbate_com_z_disp]) - + slab_center_position - - adsorbate_slab[slab_center_indices[0]].position - ) + Returns + ------- + None + """ - return ( - adsorbate, - slab, - slab_center_idx, - center_position, - adsorbate_vector_from_slab, - ) + # Read the .pun file and create the embedded_cluster Atoms object + self.slab_embedded_cluster = self.convert_pun_to_atoms( + pun_file=self.pun_file + ) + # Get distances of all atoms from the cluster center + atom_center_distances = _get_atom_distances( + atoms=self.slab_embedded_cluster, center_position=self.center_position + ) -@requires(has_chemshell, "ChemShell is not installed") -def generate_chemshell_cluster( - slab: Atoms, - slab_center_idx: int, - atom_oxi_states: dict[str, float], - filepath: str | Path, - chemsh_radius_active: float = 40.0, - chemsh_radius_cluster: float = 60.0, - chemsh_bq_layer: float = 6.0, - write_xyz_file: bool = False, -) -> None: - """ - Run ChemShell to create an embedded cluster from a slab. + # Determine the cation shells from the center of the embedded cluster + _, cation_shells_idx = self._find_cation_shells( + slab_embedded_cluster=self.slab_embedded_cluster, + distances=atom_center_distances, + shell_width=shell_width, + ) - Parameters - ---------- - slab - The Atoms object of the slab. - slab_center_idx - The index of the (first) atom at the center of the slab, this index corresponds to the atom in the slab_center_idx list but adjusted for the slab (which does not contain the adsorbate atoms) - atom_oxi_states - The oxidation states of the atoms in the slab as a dictionary - filepath - The location where the ChemShell output files will be written. - chemsh_radius_active - The radius of the active region in Angstroms. This 'active' region is simply region where the charge fitting is performed to ensure correct Madelung potential; it can be a relatively large value. - chemsh_radius_cluster - The radius of the total embedded cluster in Angstroms. - chemsh_bq_layer - The height above the surface to place some additional fitting point charges in Angstroms; simply for better reproduction of the electrostatic potential close to the adsorbate. - write_xyz_file - Whether to write an XYZ file of the cluster for visualisation. + # Create the distance matrix for the embedded cluster + slab_embedded_cluster_all_dist = self.slab_embedded_cluster.get_all_distances() + + # Create the anion coordination list for each cation shell + anion_coord_idx = [] + for shell_idx in range(shell_max): + shell_indices = cation_shells_idx[shell_idx] + anion_coord_idx += [ + self._get_anion_coordination( + slab_embedded_cluster=self.slab_embedded_cluster, cation_shell_indices = shell_indices, + dist_matrix = slab_embedded_cluster_all_dist, bond_dist = bond_dist + ) + ] - Returns - ------- - None - """ - from chemsh.io.tools import convert_atoms_to_frag + # Create the quantum clusters by summing up the indices of the cations and their coordinating anions + slab_quantum_cluster_indices = [] + dummy_cation_indices = [] + dummy_anion_indices = [] + for shell_idx in range(shell_max): + dummy_cation_indices += cation_shells_idx[shell_idx] + dummy_anion_indices += anion_coord_idx[shell_idx] + slab_quantum_cluster_indices += [ + list(set(dummy_cation_indices + dummy_anion_indices)) + ] - # Translate slab such that first Mg atom is at 0,0,0 - slab.translate(-slab.get_positions()[slab_center_idx]) + # Get the ECP region for each quantum cluster + slab_ecp_region_indices = self._get_ecp_region( + slab_embedded_cluster=self.slab_embedded_cluster, + quantum_cluster_indices=slab_quantum_cluster_indices, + dist_matrix=slab_embedded_cluster_all_dist, + ecp_dist=ecp_dist, + ) + # print(slab_quantum_cluster_indices) + # Create the adsorbate_slab_embedded_cluster from slab_embedded_cluster and adsorbate atoms objects. This also sets the final quantum_cluster_indices and ecp_region_indices for the adsorbate_slab_embedded_cluster + self.create_adsorbate_slab_embedded_cluster(quantum_cluster_indices=slab_quantum_cluster_indices, ecp_region_indices=slab_ecp_region_indices) + + # Write the quantum clusters to files + if write_clusters: + for idx in range(len(self.quantum_cluster_indices)): + quantum_atoms = self.adsorbate_slab_embedded_cluster[self.quantum_cluster_indices[idx]] + if write_include_ecp: + ecp_atoms = self.adsorbate_slab_embedded_cluster[self.ecp_region_indices[idx]] + ecp_atoms.set_chemical_symbols(np.array(["U"] * len(ecp_atoms))) + cluster_atoms = quantum_atoms + ecp_atoms + else: + cluster_atoms = quantum_atoms + write(Path(write_clusters_path, f"SKZCAM_cluster_{idx}.xyz"), cluster_atoms) + + + @requires(has_chemshell, "ChemShell is not installed") + def run_chemshell( + self, + filepath: str | Path, + chemsh_radius_active: float = 40.0, + chemsh_radius_cluster: float = 60.0, + chemsh_bq_layer: float = 6.0, + write_xyz_file: bool = False, + ) -> None: + """ + Run ChemShell to create an embedded cluster from a slab. - # Convert ASE Atoms to ChemShell Fragment object - slab_frag = convert_atoms_to_frag(slab, connect_mode="ionic", dim="2D") + Parameters + ---------- + filepath + The location where the ChemShell output files will be written. + chemsh_radius_active + The radius of the active region in Angstroms. This 'active' region is simply region where the charge fitting is performed to ensure correct Madelung potential; it can be a relatively large value. + chemsh_radius_cluster + The radius of the total embedded cluster in Angstroms. + chemsh_bq_layer + The height above the surface to place some additional fitting point charges in Angstroms; simply for better reproduction of the electrostatic potential close to the adsorbate. + write_xyz_file + Whether to write an XYZ file of the cluster for visualisation. - # Add the atomic charges to the fragment - slab_frag.addCharges(atom_oxi_states) + Returns + ------- + None + """ + from chemsh.io.tools import convert_atoms_to_frag - # Create the chemshell cluster (i.e., add electrostatic fitting charges) from the fragment - chemsh_embedded_cluster = slab_frag.construct_cluster( - origin=slab_center_idx, - radius_cluster=chemsh_radius_cluster / Bohr, - radius_active=chemsh_radius_active / Bohr, - bq_layer=chemsh_bq_layer / Bohr, - adjust_charge="coordination_scaled", - ) + # Convert ASE Atoms to ChemShell Fragment object + slab_frag = convert_atoms_to_frag(self.slab, connect_mode="ionic", dim="2D") - # Save the final cluster to a .pun file - chemsh_embedded_cluster.save(filename=Path(filepath).with_suffix(".pun"), fmt="pun") + # Add the atomic charges to the fragment + slab_frag.addCharges(self.atom_oxi_states) - if write_xyz_file: - # XYZ for visualisation - chemsh_embedded_cluster.save( - filename=Path(filepath).with_suffix(".xyz"), fmt="xyz" + # Create the chemshell cluster (i.e., add electrostatic fitting charges) from the fragment + chemsh_slab_embedded_cluster = slab_frag.construct_cluster( + origin=0, + radius_cluster=chemsh_radius_cluster / Bohr, + radius_active=chemsh_radius_active / Bohr, + bq_layer=chemsh_bq_layer / Bohr, + adjust_charge="coordination_scaled", ) + # Save the final cluster to a .pun file + chemsh_slab_embedded_cluster.save(filename=Path(filepath).with_suffix(".pun"), fmt="pun") + self.pun_file = Path(filepath).with_suffix(".pun") -def create_skzcam_clusters( - pun_file: str | Path, - center_position: NDArray, - atom_oxi_states: dict[str, float], - shell_max: int = 10, - shell_width: float = 0.1, - bond_dist: float = 2.5, - ecp_dist: float = 6.0, - write_clusters: bool = False, - write_clusters_path: str | Path = ".", - write_include_ecp: bool = False, -) -> tuple[Atoms, list[list[int]], list[list[int]]]: - """ - From a provided .pun file (generated by ChemShell), this function creates quantum clusters using the SKZCAM protocol. It will return the embedded cluster Atoms object and the indices of the atoms in the quantum clusters and the ECP region. The number of clusters created is controlled by the rdf_max parameter. + if write_xyz_file: + # XYZ for visualisation + chemsh_slab_embedded_cluster.save( + filename=Path(filepath).with_suffix(".xyz"), fmt="xyz" + ) - Parameters - ---------- - pun_file - The path to the .pun file created by ChemShell to be read. - center_position - The position of the center of the embedded cluster (i.e., position of the adsorbate). - atom_oxi_states - A dictionary containing the atomic symbols as keys and the oxidation states as values. - shell_max - The maximum number of quantum clusters to be created. - shell_width - Defines the distance between atoms within shells; this is the maximum distance between any two atoms within the shell. - bond_dist - The distance within which an anion is considered to be coordinating a cation. - ecp_dist - The distance from edges of the quantum cluster to define the ECP region. - write_clusters - If True, the quantum clusters will be written to a file. - write_clusters_path - The path to the file where the quantum clusters will be written. - write_include_ecp - If True, the ECP region will be included in the quantum clusters. - Returns - ------- - Atoms - The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file. - list[list[int]] - A list of lists containing the indices of the atoms in each quantum cluster. - list[list[int]] - A list of lists containing the indices of the atoms in the ECP region for each quantum cluster. - """ - # Read the .pun file and create the embedded_cluster Atoms object - embedded_cluster = convert_pun_to_atoms( - pun_file=pun_file, atom_oxi_states=atom_oxi_states - ) + def convert_slab_to_atoms( + self + ) -> None: + """ + Read the file containing the periodic slab and adsorbate (geometry optimized) and format the resulting Atoms object to be used to create a .pun file in ChemShell. - # Get distances of all atoms from the cluster center - atom_center_distances = _get_atom_distances( - embedded_cluster=embedded_cluster, center_position=center_position - ) + Parameters + ---------- - # Determine the cation shells from the center of the embedded cluster - _, cation_shells_idx = _find_cation_shells( - embedded_cluster=embedded_cluster, - distances=atom_center_distances, - shell_width=shell_width, - ) + Returns + ------- + Atoms + The Atoms object of the adsorbate molecule. + Atoms + The Atoms object of the surface slab. + int + The index of the first atom of the slab as listed in slab_center_indices. + NDArray + The position of the center of the cluster. + NDArray + The vector from the center of the slab to the center of mass of the adsorbate. + """ - # Create the distance matrix for the embedded cluster - embedded_cluster_all_dist = embedded_cluster.get_all_distances() + # Get the necessary information for the cluster from a provided slab file (in any format that ASE can read) + adsorbate_slab = read(self.adsorbate_slab_file) - # Create the anion coordination list for each cation shell - anion_coord_idx = [] - for shell_idx in range(shell_max): - cation_shell = cation_shells_idx[shell_idx] - anion_coord_idx += [ - _get_anion_coordination( - embedded_cluster, cation_shell, embedded_cluster_all_dist, bond_dist - ) + # Find indices (within adsorbate_slab) of the slab + slab_indices = self.slab_center_indices + [ + i for i, _ in enumerate(adsorbate_slab) if i not in (self.adsorbate_indices + self.slab_center_indices) ] - # Create the quantum clusters by summing up the indices of the cations and their coordinating anions - quantum_cluster_indices = [] - dummy_cation_indices = [] - dummy_anion_indices = [] - for shell_idx in range(shell_max): - dummy_cation_indices += cation_shells_idx[shell_idx] - dummy_anion_indices += anion_coord_idx[shell_idx] - quantum_cluster_indices += [ - list(set(dummy_cation_indices + dummy_anion_indices)) - ] + # Create adsorbate and slab from adsorbate_slab + slab = adsorbate_slab[slab_indices] + adsorbate = adsorbate_slab[self.adsorbate_indices] - # Get the ECP region for each quantum cluster - ecp_region_indices = _get_ecp_region( - embedded_cluster=embedded_cluster, - quantum_cluster_indices=quantum_cluster_indices, - dist_matrix=embedded_cluster_all_dist, - ecp_dist=ecp_dist, - ) + adsorbate.translate(-slab[0].position) + slab.translate(-slab[0].position) - # Write the quantum clusters to files - if write_clusters: - for idx in range(len(quantum_cluster_indices)): - quantum_atoms = embedded_cluster[quantum_cluster_indices[idx]] - if write_include_ecp: - ecp_atoms = embedded_cluster[ecp_region_indices[idx]] - ecp_atoms.set_chemical_symbols(np.array(["U"] * len(ecp_atoms))) - cluster_atoms = quantum_atoms + ecp_atoms - else: - cluster_atoms = quantum_atoms - write(Path(write_clusters_path, f"SKZCAM_cluster_{idx}.xyz"), cluster_atoms) - return embedded_cluster, quantum_cluster_indices, ecp_region_indices + # Get the relative distance of the adsorbate from the first center atom of the slab as defined in the slab_center_indices + adsorbate_vector_from_slab = ( + adsorbate[0].position - slab[0].position + ) + # Get the center of the cluster from the slab_center_indices + slab_center_position = slab[:len(self.slab_center_indices)].get_positions().sum( + axis=0 + ) / len(self.slab_center_indices) -def convert_pun_to_atoms( - pun_file: str | Path, atom_oxi_states: dict[str, float] -) -> Atoms: - """ - Reads a .pun file and returns an ASE Atoms object containing the atomic coordinates, - point charges/oxidation states, and atom types. - Parameters - ---------- - pun_file - The path to the .pun file created by ChemShell to be read. - atom_oxi_states - A dictionary containing the atomic symbols as keys and the oxidation states as values. + # Add the height of the adsorbate from the slab along the z-direction relative to the slab_center + adsorbate_com_z_disp = ( + adsorbate.get_center_of_mass()[2] - slab_center_position[2] + ) - Returns - ------- - Atoms - The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file. - The `oxi_states` array contains the atomic charges, and the `atom_type` array contains the - atom types (cation, anion, neutral). - """ + center_position = ( + np.array([0.0, 0.0, adsorbate_com_z_disp]) + + slab_center_position + ) - # Create a dictionary containing the atom types and whether they are cations or anions - atom_type_dict = { - atom: "cation" if oxi_state > 0 else "anion" if oxi_state < 0 else "neutral" - for atom, oxi_state in atom_oxi_states.items() - } - - # Load the pun file as a list of strings - with zopen(zpath(Path(pun_file))) as f: - raw_pun_file = [ - line.rstrip().decode("utf-8") if isinstance(line, bytes) else line.rstrip() - for line in f - ] + self.adsorbate = adsorbate + self.slab = slab + self.adsorbate_slab = adsorbate_slab + self.adsorbate_vector_from_slab = adsorbate_vector_from_slab + self.center_position = center_position - # Get the number of atoms and number of atomic charges in the .pun file - n_atoms = int(raw_pun_file[3].split()[-1]) - n_charges = int(raw_pun_file[4 + n_atoms - 1 + 3].split()[-1]) - # Check if number of atom charges same as number of atom positions - if n_atoms != n_charges: - raise ValueError( - "Number of atomic positions and atomic charges in the .pun file are not the same." - ) + def convert_pun_to_atoms( + self, + pun_file: str | Path + ) -> Atoms: + """ + Reads a .pun file and returns an ASE Atoms object containing the atomic coordinates, + point charges/oxidation states, and atom types. - raw_atom_positions = raw_pun_file[4 : 4 + n_atoms] - raw_charges = raw_pun_file[7 + n_atoms : 7 + 2 * n_atoms] - charges = [float(charge) for charge in raw_charges] - - # Add the atomic positions the embedded_cluster Atoms object (converting from Bohr to Angstrom) - atom_types = [] - atom_numbers = [] - atom_positions = [] - for _, line in enumerate(raw_atom_positions): - line_info = line.split() - - # Add the atom type to the atom_type_list - if line_info[0] in atom_type_dict: - atom_types.append(atom_type_dict[line_info[0]]) - elif line_info[0] == "F": - atom_types.append("pc") - else: - atom_types.append("unknown") - - # Add the atom number to the atom_number_list and position to the atom_position_list - atom_numbers += [atomic_numbers[line_info[0]]] - atom_positions += [ - [ - float(line_info[1]) * Bohr, - float(line_info[2]) * Bohr, - float(line_info[3]) * Bohr, + Parameters + ---------- + pun_file + The path to the .pun file created by ChemShell to be read. + atom_oxi_states + A dictionary containing the atomic symbols as keys and the oxidation states as values. + + Returns + ------- + Atoms + The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file. + The `oxi_states` array contains the atomic charges, and the `atom_type` array contains the + atom types (cation, anion, neutral). + """ + + # Create a dictionary containing the atom types and whether they are cations or anions + atom_type_dict = { + atom: "cation" if oxi_state > 0 else "anion" if oxi_state < 0 else "neutral" + for atom, oxi_state in self.atom_oxi_states.items() + } + + # Load the pun file as a list of strings + with zopen(zpath(Path(pun_file))) as f: + raw_pun_file = [ + line.rstrip().decode("utf-8") if isinstance(line, bytes) else line.rstrip() + for line in f ] - ] - embedded_cluster = Atoms(numbers=atom_numbers, positions=atom_positions) + # Get the number of atoms and number of atomic charges in the .pun file + n_atoms = int(raw_pun_file[3].split()[-1]) + n_charges = int(raw_pun_file[4 + n_atoms - 1 + 3].split()[-1]) + + # Check if number of atom charges same as number of atom positions + if n_atoms != n_charges: + raise ValueError( + "Number of atomic positions and atomic charges in the .pun file are not the same." + ) + + raw_atom_positions = raw_pun_file[4 : 4 + n_atoms] + raw_charges = raw_pun_file[7 + n_atoms : 7 + 2 * n_atoms] + charges = [float(charge) for charge in raw_charges] + + # Add the atomic positions the embedded_cluster Atoms object (converting from Bohr to Angstrom) + atom_types = [] + atom_numbers = [] + atom_positions = [] + for _, line in enumerate(raw_atom_positions): + line_info = line.split() + + # Add the atom type to the atom_type_list + if line_info[0] in atom_type_dict: + atom_types.append(atom_type_dict[line_info[0]]) + elif line_info[0] == "F": + atom_types.append("pc") + else: + atom_types.append("unknown") - # Center the embedded cluster so that atom index 0 is at the [0, 0, 0] position - embedded_cluster.translate(-embedded_cluster[0].position) + # Add the atom number to the atom_number_list and position to the atom_position_list + atom_numbers += [atomic_numbers[line_info[0]]] + atom_positions += [ + [ + float(line_info[1]) * Bohr, + float(line_info[2]) * Bohr, + float(line_info[3]) * Bohr, + ] + ] - # Add the `oxi_states` and `atom_type` arrays to the Atoms object - embedded_cluster.set_array("oxi_states", np.array(charges)) - embedded_cluster.set_array("atom_type", np.array(atom_types)) + slab_embedded_cluster = Atoms(numbers=atom_numbers, positions=atom_positions) - return embedded_cluster + # Center the embedded cluster so that atom index 0 is at the [0, 0, 0] position + slab_embedded_cluster.translate(-slab_embedded_cluster[0].position) + # Add the `oxi_states` and `atom_type` arrays to the Atoms object + slab_embedded_cluster.set_array("oxi_states", np.array(charges)) + slab_embedded_cluster.set_array("atom_type", np.array(atom_types)) -def insert_adsorbate_to_embedded_cluster( - embedded_cluster: Atoms, - adsorbate: Atoms, - adsorbate_vector_from_slab: NDArray, - quantum_cluster_indices: list[list[int]] | None = None, - ecp_region_indices: list[list[int]] | None = None, -) -> tuple[Atoms, list[list[int]], list[list[int]]]: - """ - Insert the adsorbate into the embedded cluster and update the quantum cluster and ECP region indices. + return slab_embedded_cluster - Parameters - ---------- - embedded_cluster - The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file. - adsorbate - The ASE Atoms object of the adsorbate molecule. - adsorbate_vector_from_slab - The vector from the first atom of the embedded cluster to the center of mass of the adsorbate. - quantum_cluster_indices - A list of lists containing the indices of the atoms in each quantum cluster. - ecp_region_indices - A list of lists containing the indices of the atoms in the ECP region for each quantum cluster. - Returns - ------- - Atoms - The ASE Atoms object containing the adsorbate and embedded cluster - list[list[int]] - A list of lists containing the indices of the atoms in each quantum cluster. - list[list[int]] - A list of lists containing the indices of the atoms in the ECP region for each quantum cluster. - """ + def create_adsorbate_slab_embedded_cluster( + self, + quantum_cluster_indices: list[list[int]] | None = None, + ecp_region_indices: list[list[int]] | None = None, + ) -> None: + """ + Insert the adsorbate into the embedded cluster and update the quantum cluster and ECP region indices. - # Remove PBC from the adsorbate - adsorbate.set_pbc(False) + Parameters + ---------- + adsorbate_vector_from_slab + The vector from the first atom of the embedded cluster to the center of mass of the adsorbate. + quantum_cluster_indices + A list of lists containing the indices of the atoms in each quantum cluster. + ecp_region_indices + A list of lists containing the indices of the atoms in the ECP region for each quantum cluster. - # Translate the adsorbate to the correct position relative to the slab - adsorbate.translate(-adsorbate[0].position + adsorbate_vector_from_slab) + Returns + ------- + None + """ - # Set oxi_state and atom_type arrays for the adsorbate - adsorbate.set_array("oxi_states", np.array([0.0] * len(adsorbate))) - adsorbate.set_array("atom_type", np.array(["adsorbate"] * len(adsorbate))) + # Remove PBC from the adsorbate + self.adsorbate.set_pbc(False) - # Add the adsorbate to the embedded cluster - embedded_adsorbed_cluster = adsorbate + embedded_cluster + # Translate the adsorbate to the correct position relative to the slab + self.adsorbate.translate(self.slab_embedded_cluster[0].position - self.adsorbate[0].position + self.adsorbate_vector_from_slab) - # Update the quantum cluster and ECP region indices - if quantum_cluster_indices is not None: - quantum_cluster_indices = [ - list(range(len(adsorbate))) + [idx + len(adsorbate) for idx in cluster] - for cluster in quantum_cluster_indices - ] - if ecp_region_indices is not None: - ecp_region_indices = [ - [idx + len(adsorbate) for idx in cluster] for cluster in ecp_region_indices - ] + # Set oxi_state and atom_type arrays for the adsorbate + self.adsorbate.set_array("oxi_states", np.array([0.0] * len(self.adsorbate))) + self.adsorbate.set_array("atom_type", np.array(["adsorbate"] * len(self.adsorbate))) - return embedded_adsorbed_cluster, quantum_cluster_indices, ecp_region_indices + # Add the adsorbate to the embedded cluster + self.adsorbate_slab_embedded_cluster = self.adsorbate + self.slab_embedded_cluster + # Update the quantum cluster and ECP region indices + if quantum_cluster_indices is not None: + quantum_cluster_indices = [ + list(range(len(self.adsorbate))) + [idx + len(self.adsorbate) for idx in cluster] + for cluster in quantum_cluster_indices + ] + if ecp_region_indices is not None: + ecp_region_indices = [ + [idx + len(self.adsorbate) for idx in cluster] for cluster in ecp_region_indices + ] -def _get_atom_distances(embedded_cluster: Atoms, center_position: NDArray) -> NDArray: - """ - Returns the distance of all atoms from the center position of the embedded cluster + self.quantum_cluster_indices = quantum_cluster_indices + self.ecp_region_indices = ecp_region_indices - Parameters - ---------- - embedded_cluster - The ASE Atoms object containing the atomic coordinates of the embedded cluster. - center_position - The position of the center of the embedded cluster (i.e., position of the adsorbate). - Returns - ------- - NDArray - An array containing the distances of each atom in the Atoms object from the cluster center. - """ - return np.array( - [np.linalg.norm(atom.position - center_position) for atom in embedded_cluster] - ) -def _find_cation_shells( - embedded_cluster: Atoms, distances: NDArray, shell_width: float = 0.1 -) -> list[list[int]]: - """ - Returns a list of lists containing the indices of the cations in each shell, based on distance from the embedded cluster center. - This is achieved by clustering the data based on the DBSCAN clustering algorithm. + def _find_cation_shells(self, + slab_embedded_cluster: Atoms, distances: NDArray, shell_width: float = 0.1 + ) -> list[list[int]]: + """ + Returns a list of lists containing the indices of the cations in each shell, based on distance from the embedded cluster center. + This is achieved by clustering the data based on the DBSCAN clustering algorithm. - Parameters - ---------- - embedded_cluster - The ASE Atoms object containing the atomic coordinates AND the atom types (i.e. cation or anion). - distances - The distance of atoms from the cluster center. - shell_width - Defines the distance between atoms within shells; this is the maximum distance between any two atoms within the shell + Parameters + ---------- + slab_embedded_cluster + The ASE Atoms object containing the atomic coordinates AND the atom types (i.e. cation or anion). + distances + The distance of atoms from the cluster center. + shell_width + Defines the distance between atoms within shells; this is the maximum distance between any two atoms within the shell - Returns - ------- - list[list[int]] - A list of lists containing the indices of the cations in each shell. - """ + Returns + ------- + list[list[int]] + A list of lists containing the distance of the cation in each shell from the adsorbate. + list[list[int]] + A list of lists containing the indices of the cations in each shell. + """ - # Define the empty list to store the cation shells - shells = [] - shells_indices = [] - - # Sort the points by distance from the cluster center for the cations only - distances_sorted = [] - distances_sorted_indices = [] - for i in np.argsort(distances): - if embedded_cluster.get_array("atom_type")[i] == "cation": - distances_sorted.append(distances[i]) - distances_sorted_indices.append(i) - - current_point = distances_sorted[0] - current_shell = [current_point] - current_shell_idx = [distances_sorted_indices[0]] - - for idx, point in enumerate(distances_sorted[1:]): - if point <= current_point + shell_width: - current_shell.append(point) - current_shell_idx.append(distances_sorted_indices[idx + 1]) - else: - shells.append(current_shell) - shells_indices.append(current_shell_idx) - current_shell = [point] - current_shell_idx = [distances_sorted_indices[idx + 1]] - current_point = point - shells.append(current_shell) - shells_indices.append(current_shell_idx) - - return shells, shells_indices - - -def _get_anion_coordination( - embedded_cluster: Atoms, - cation_shell_indices: list[int], - dist_matrix: NDArray, - bond_dist: float = 2.5, -) -> list[int]: - """ - Returns a list of lists containing the indices of the anions coordinating the cation indices provided. + # Define the empty list to store the cation shells + shells_distances = [] + shells_indices = [] + + # Sort the points by distance from the cluster center for the cations only + distances_sorted = [] + distances_sorted_indices = [] + for i in np.argsort(distances): + if slab_embedded_cluster.get_array("atom_type")[i] == "cation": + distances_sorted.append(distances[i]) + distances_sorted_indices.append(i) + + current_point = distances_sorted[0] + current_shell = [current_point] + current_shell_idx = [distances_sorted_indices[0]] + + for idx, point in enumerate(distances_sorted[1:]): + if point <= current_point + shell_width: + current_shell.append(point) + current_shell_idx.append(distances_sorted_indices[idx + 1]) + else: + shells_distances.append(current_shell) + shells_indices.append(current_shell_idx) + current_shell = [point] + current_shell_idx = [distances_sorted_indices[idx + 1]] + current_point = point + shells_distances.append(current_shell) + shells_indices.append(current_shell_idx) + + return shells_distances, shells_indices + + + def _get_anion_coordination(self, + slab_embedded_cluster: Atoms, + cation_shell_indices: list[int], + dist_matrix: NDArray, + bond_dist: float = 2.5, + ) -> list[int]: + """ + Returns a list of lists containing the indices of the anions coordinating the cation indices provided. - Parameters - ---------- - embedded_cluster - The ASE Atoms object containing the atomic coordinates AND the atom types (i.e. cation or anion). - cation_shell_indices - A list of the indices of the cations in the cluster. - dist_matrix - A matrix containing the distances between each pair of atoms in the embedded cluster. - bond_dist - The distance within which an anion is considered to be coordinating a cation. + Parameters + ---------- + slab_embedded_cluster + The ASE Atoms object containing the atomic coordinates AND the atom types (i.e. cation or anion). + cation_shell_indices + A list of the indices of the cations in the cluster. + dist_matrix + A matrix containing the distances between each pair of atoms in the embedded cluster. + bond_dist + The distance within which an anion is considered to be coordinating a cation. - Returns - ------- - list[int] - A list containing the indices of the anions coordinating the cation indices. - """ + Returns + ------- + list[int] + A list containing the indices of the anions coordinating the cation indices. + """ - # Define the empty list to store the anion coordination - anion_coord_indices = [] + # Define the empty list to store the anion coordination + anion_coord_indices = [] - # Iterate over the cation shell indices and find the atoms within the bond distance of each cation - for atom_idx in cation_shell_indices: - anion_coord_indices += [ - idx - for idx, dist in enumerate(dist_matrix[atom_idx]) - if ( - dist < bond_dist - and embedded_cluster.get_array("atom_type")[idx] == "anion" - ) - ] + # Iterate over the cation shell indices and find the atoms within the bond distance of each cation + for atom_idx in cation_shell_indices: + anion_coord_indices += [ + idx + for idx, dist in enumerate(dist_matrix[atom_idx]) + if ( + dist < bond_dist + and slab_embedded_cluster.get_array("atom_type")[idx] == "anion" + ) + ] - return list(set(anion_coord_indices)) + return list(set(anion_coord_indices)) -def _get_ecp_region( - embedded_cluster: Atoms, - quantum_cluster_indices: list[int], - dist_matrix: NDArray, - ecp_dist: float = 6.0, -) -> list[list[int]]: + def _get_ecp_region(self, + slab_embedded_cluster: Atoms, + quantum_cluster_indices: list[int], + dist_matrix: NDArray, + ecp_dist: float = 6.0, + ) -> list[list[int]]: + """ + Returns a list of lists containing the indices of the atoms in the ECP region of the embedded cluster for each quantum cluster + + Parameters + ---------- + slab_embedded_cluster + The ASE Atoms object containing the atomic coordinates AND the atom types (i.e. cation or anion). + quantum_cluster_indices + A list of lists containing the indices of the atoms in each quantum cluster. + dist_matrix + A matrix containing the distances between each pair of atoms in the embedded cluster. + ecp_dist + The distance from edges of the quantum cluster to define the ECP region. + + Returns + ------- + list[list[int]] + A list of lists containing the indices of the atoms in the ECP region for each quantum cluster. + """ + + ecp_region_indices = [] + dummy_cation_indices = [] + + # Iterate over the quantum clusters and find the atoms within the ECP distance of each quantum cluster + for cluster in quantum_cluster_indices: + dummy_cation_indices += cluster + cluster_ecp_region_idx = [] + for atom_idx in dummy_cation_indices: + for idx, dist in enumerate(dist_matrix[atom_idx]): + # Check if the atom is within the ecp_dist region and is not in the quantum cluster and is a cation + if ( + dist < ecp_dist + and idx not in dummy_cation_indices + and slab_embedded_cluster.get_array("atom_type")[idx] == "cation" + ): + cluster_ecp_region_idx += [idx] + + ecp_region_indices += [list(set(cluster_ecp_region_idx))] + + return ecp_region_indices + +def _get_atom_distances(atoms: Atoms, center_position: NDArray) -> NDArray: """ - Returns a list of lists containing the indices of the atoms in the ECP region of the embedded cluster for each quantum cluster + Returns the distance of all atoms from the center position of the embedded cluster Parameters ---------- embedded_cluster - The ASE Atoms object containing the atomic coordinates AND the atom types (i.e. cation or anion). - quantum_cluster_indices - A list of lists containing the indices of the atoms in each quantum cluster. - dist_matrix - A matrix containing the distances between each pair of atoms in the embedded cluster. - ecp_dist - The distance from edges of the quantum cluster to define the ECP region. + The ASE Atoms object containing the atomic coordinates of the embedded cluster. + center_position + The position of the center of the embedded cluster (i.e., position of the adsorbate). Returns ------- - list[list[int]] - A list of lists containing the indices of the atoms in the ECP region for each quantum cluster. + NDArray + An array containing the distances of each atom in the Atoms object from the cluster center. """ - ecp_region_indices = [] - dummy_cation_indices = [] - - # Iterate over the quantum clusters and find the atoms within the ECP distance of each quantum cluster - for cluster in quantum_cluster_indices: - dummy_cation_indices += cluster - cluster_ecp_region_idx = [] - for atom_idx in dummy_cation_indices: - for idx, dist in enumerate(dist_matrix[atom_idx]): - # Check if the atom is within the ecp_dist region and is not in the quantum cluster and is a cation - if ( - dist < ecp_dist - and idx not in dummy_cation_indices - and embedded_cluster.get_array("atom_type")[idx] == "cation" - ): - cluster_ecp_region_idx += [idx] - - ecp_region_indices += [list(set(cluster_ecp_region_idx))] - - return ecp_region_indices + return np.array( + [np.linalg.norm(atom.position - center_position) for atom in atoms] + ) diff --git a/tests/core/atoms/conftest.py b/tests/core/atoms/conftest_noworking.py similarity index 59% rename from tests/core/atoms/conftest.py rename to tests/core/atoms/conftest_noworking.py index aec15d8d58..774a1f32b1 100644 --- a/tests/core/atoms/conftest.py +++ b/tests/core/atoms/conftest_noworking.py @@ -9,22 +9,22 @@ FILE_DIR = Path(__file__).parent -def mock_generate_chemshell_cluster( +def mock_run_chemshell( slab, slab_center_idx, atom_oxi_states, filepath, **kwargs ): with ( gzip.open( - Path(FILE_DIR, "skzcam_files", "REF_ChemShell_cluster.xyz.gz"), "rb" + Path(FILE_DIR, "skzcam_files", "REF_ChemShell_Cluster.xyz.gz"), "rb" ) as f_in, - Path(filepath, "ChemShell_cluster.xyz").open(mode="wb") as f_out, + Path(filepath, "ChemShell_Cluster.xyz").open(mode="wb") as f_out, ): shutil.copyfileobj(f_in, f_out) @pytest.fixture(autouse=True) -def patch_generate_chemshell_cluster(monkeypatch): +def patch_run_chemshell(monkeypatch): from quacc.atoms import skzcam monkeypatch.setattr( - skzcam, "generate_chemshell_cluster", mock_generate_chemshell_cluster + skzcam, "CreateSKZCAMClusters.run_chemshell", mock_run_chemshell ) diff --git a/tests/core/atoms/skzcam_files/CO_MgO.poscar.gz b/tests/core/atoms/skzcam_files/CO_MgO.poscar.gz new file mode 100644 index 0000000000000000000000000000000000000000..09b71f8989a93f547b8f9dc77d9ab13d51dc0f2d GIT binary patch literal 763 zcmVLqSqsLr(zhS50pdK@7d;SL_|B^2gX?p9|a&2lxZ1 zRHzbg0EEQ9$I~RBuugYsiQ7inbnC3&c%EOLC(G@!yj|XW8t+fN4sqcPT)h5#uq<}d z8n`u;hJx%yBMSc{98=cOBS!x8Eb>zrX_WAW89G~YP-JOF1LJliJaREeiMt~({(WaU zz8~J*f4;x_@$mD0`S|PWhc6Fz-w!VzzD4LTmXy_!d915|umK5?stoY==j&NOhGy7> zpgW8|E1)$AvAV2dET|w+j5!6`sOa_idIX{*DFfdG%965J8x#qcxi(PkQx#=f+XPBE zYVPc$!E4-fer!O18xuQX>*&5BdsL($F9MGn>es+Gfo3_~ZqTieJiN6~&oA%pN;2o+4}D2r|CD0Rb%jkqBl7sF#P5W5 z29mHx#?TUQ{d`o?QOQP9K9X2TMd~*Jid~cMs2G7nTcgv+Z5z1 zN1Hmzyr<+xv*)9dj!J$sJNrvID)~{$QDQ9?oFJ)EnBT}OH)J4k6&wqaY`}XqZ->ihkVoNJXNdCdH2{o~N3DL$pyL`IdW;^m4zAC5FK@o+Lrkkp|% zE~%{N{*$rlOugot|8Vd;GMe!7r6Yq7{#WYk*=6OjJ|Yba?P;yAK8nWRxWk_hM;e)U zYqE#ny887H-B6a4of*yae>+l@R;14%CEub4%ms`l{Cw%iAO>$xj@FfS<(kKx_kpbK ze4Uua>N2N6dAE2Eb6pO(#m@K9G1ZZi2{@SQ3q;#-6h5fFH)(d#>5)@=($MQSyfbMm z`1#T;hd}&wf6?*WODluU8hl_pGMe!7r6Yq7-rO+vfwM*_P3m0Z<~_-C7cfQE|I3ZX z$(HvKJ_rwLZ-{c^(OgD-D2yVAT7ZIuiJ$Y~T=<3wfCG_F>oI!EBhNNRnom#sARI2K zg*PV02%P&5Q!rWZ^QBuJ!4*$GilmvcqP3EKa`w3IYJky%pD!I5Wa7QCzxNKM9MW^S z_ulz%bMHfVckD9+%+ZZD8YcRJ@Syfq(HJOPxZDd9L@jV$mo%kx*X&bTUG2WpvU5#G zq!%fD{aS!Xs{>Fntri9z)ENCAM zK*=;K8F)~8Br4dcMGoaUofJXT0+cGzUZpeptdf_32en6{qMcqweXBh26GScWwu{XD z!`bg4t~?ApsJ*w@LTJzJzF~r>1($i$n%1nn)!e9OP1c z8z>~7c$LM#gW6+Jp*LmF`;7NEf~W=F=$EhW(yZ#sPzl1%mu`6k;?dy|?Nu|CcMu-b z-Vhb}QW3s)a0F2cy!{U1>Jo%|&pz%`8S||p_^}}fkN5g10(^eBpM|1FoaX-HdsfQ8 zgW4OSB9CJ2-Ph?ojv#7*x9>xIZ$Mj;1wUUp+!2|t9l_5HL3q5|&lCLq?##5?8UF|2 zLG8WwX69oB6@6iXfCc9Eflg2BT{b#u(T+qsD`((A?U5)3kLEJ!Ltzv_)B^9_hj^xD zs088XOSe3NYhP#li1onQ8NL=i2oGxSjU7UJs~T~8k0Xd$VAQ^$XL3yZ1>r&M4OfSr z$x+!CCJ0zy*1o|H4?%dm?;jsb45 zFV(^wmBDFMi=)Oy527SrzW23Lt`rKne!t-pp!_~Pv zH#h0J%&oMZ=GaEruLM>6YNKoN>(qLOmt%~f&@y_j>+O;|&1ajm|#wnBAfn>yfFH`*&be zP0X3+lxc1~8dJ5D(#I^RtvmcW>XfBr;Y_{vPxseRSJ#rF=K4DVG#Qba1pOFk|B6la zYm9cwmItd$f~W#f4%ky7_u1?C#?fsU`(*<*39M{?95V^5?fy|KlVBXuMv-0p=u!5WKwsQq5=0eA|3++pH3++sI6CShN sneI>Eo^)rVBY!Z)qcO(!NGB8TOm`yg81X^tzwiA20D;uh_i#`E05->lssI20 literal 0 HcmV?d00001 diff --git a/tests/core/atoms/skzcam_files/REF_ChemShell_cluster.xyz.gz b/tests/core/atoms/skzcam_files/REF_ChemShell_cluster.xyz.gz deleted file mode 100644 index eb83aef23ab9ff4a74282186d82bcb81918443c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13569 zcmV+cHU7#UiwFqL0Ul-m14C$KZBuAvY;0d+Y;|*VWpXZfd3s-TWpi`@tzFBm>^O4V z_gD1Zr~{FrDC!4hH}F?93MI{LgC` z|9(wB(>$hOit|*;Y=2wn!_R{8^03TP&INyuHe?8Sig}EATIM?BYy25vn9DS$jFIhk zD?R3lX(`h<4#PZEdLu6wbXnpwO)Gi$nG^n3rVxiRUCF}~rYSFJob&pn^Uu8G1#=Bc zE@fT)_%q=f7myBPT+Y?UpK+MRkjE5aPTTA`7JT}WQ^ZD9a+@WE6c;RY&UN$Z*4yS4 z{IraD4nta(YRezvu#7`o_OG9Q#<_k|2y+ z2}f|Fw{Hrm3}O9#Y!wb9O=-ah>HL8D=~IgHa()2*9&?!HdBVYlWBqZ&ro=g>ahpGZ zd&**KN^vUZ{C4{I))W@(SUOIBm{OjGA!4z|m=ivx%=3^o?%U|GeWeUBrLvA|OR`TY z0mqfol|0Wwn1(pKEq@%wX$b*rk@npiOU5EG-p0t%_ zZ}HQF-(sRMhk&1^I{S=W$AYl-c)L1jT;>weu$*5y%DTTb|28S=D#(q(`+ zk8_;H?H}st5A%SN3-fq>ejc$Td=NPOq_=Mk(~=_gCY`?>a7fsLX`aS&`xab9u=$wd zoxb3@fZ>kmhcYJIf85h@PCw)j%Y;ptF5I7fj8SoX^SB)g=)rtTO5hzxkCpk-Pr(%{ z5u7rwmux@$3{i&51~y6S)yHpR03jH78uGLsUoLjNW-M|Zy7VBAX$Wa5cGvsl_$~GX z`%-XUf3tcBqaos;<{Vbm4L?&ZxOd>6G#uoyKo)^{vDoQy_V|Y(EX59Gx*XIn<*^Ll z(=zW@Z^U0pDmZ`~PtjEez^ZV7W!&zWF%RV90G^DO-NQw~9ba%G%Z0uQen?8pn8#(G zEQVsjAV41HUG(DkhcM&6Wgbt_%LcIh$A8#D`-DEdabK*07o&{l{CU83O_0}Nr-#&C z3ZyRtP`b=-8<26ZQ68G%~MJG@nQ1=SSsO}8Pj}R9+^v zJfusS`{cIzgqt5mJm*E;`;~WQh~&}EaC^7>w})K5y7?u~_ygX!WAokgw)x<>G~?mh zC0~o&9u8a+@ClATDXZTFC(a2MElu&d(@=o0{(`$RoPrx~FdixJSh@{>M-u};_T`g# zAo8ad~=1@Ki8u=%lZg2jT=G^vdb(4J7m1^6^`|5+#n~$LVp% z_MOYHq~C4-jr+aq+Z@2jzvt(_^RF~Np;x9i#R0l`GBq`o%ke{to~r77sLjyJpkoa7 zwaZ2AB?DC14cP-x4OPESZ|4Vn-qeJ=Dn#1+gkG6`fBRu1Owd4N`N5=Ly}e5W|3V#| z`uk7l>*a+RS0Mk#59D_F7Srx0XoR}#er5c_VoU~2b@`3W_OKW+SgET*( zSEeVoS3dq5y>fa^vk6ST;Fdsd#^t}fyu{+S-(o=6u~0n2A-TE9&j6PyJ#eR>jYTYC zAE1G_;K#ur0!aEYpV!9?nJ~=A7J#FNrVzq(JRS|m$QCeU>_1GBbm)E(=y8L5Y(#wL z2u;VP>M!4ZTwwFyx5M`KIYE;a;$uTX{?2&{(_yu@ARmG_897)HmVC(R1K@<;PO$DW zZqSfUCDjes8GQA4UXR&C$alqI z9PHUlW4I!VZoF$B0GtE;{kS?Fz^jSZRp!NF@gT<(E`T2c>=ukm_jK8RFWzbN#jIA4m(yzX!kuoj zsmVZ2v#CYzHJb!_<@!*~P0{<=0ADKp()@&8(Ol~FEji(x!gal9o^HgD44^=M405f? zL&49Wf0hg6%rRF2`qtOW?p20dI?M?-W+=^1FiU0nYI)#df)uwU`4aBeKtnQsg4z+#Q`$g+*-GEfUVrEHJ#_B@M#fU0K<1N!B+UD^0O$R0%`Zdd zH|0b5d^heRXz$ABdtslZ1n}ZE$^cFYwx8c!{z~_c(0_~BE1&O#jauYh*hrw4PQUy> zMSxD0VSZ0alv684$~E9h{m19_ea)se;I|>eI4V%((#bd82q~kFxOe858kh5taz|*@ z18-qwg6Xv*tjPc(V13^|TYs^c?cMsYee~f` z^lnTBx?=1K4*UfJlLaV>JLbn!u-FFhYLG9Wkwcncw@mu#*T-x1H&G9#)h}r7((w^` zW%t>$t8hP||JGfi?7kE8=-NFuv|n?05PyY)_pi?ft{NEztZvfXOdx2`n*O!O*8CkiXo5QNXj~ zx&PGRLgs^XP4$2lG%o244PY4&Y>u-dXkpDBTIRdaxp z#atZ`vaa^#Hkkz&r{E z6MBV#$?}!2C-+z4K0+^rNi*$7g8az<5@hd1o<4Z(J|a(2#Dk)~d!Zi%^A~+icY5Uj zywHmPx_|ri%JDm)mmcQ-kp5D<3mHI(ccJX3=nrfFMS)=Fugp*Aztmr({U`Kdyw4+1 zKmzWW>qJTwV7`;AupgRNJvk>Xd z@(k9?pDgzX;%D1cr8dQS`&V|auHew#K~@{W=u=J!Uy$>8s{8};Gt)pqzS#f@^i5I~ z>iWUfIO;Km>-uSNibrh?qsPhNPQ|>*^QPye{uVMmCId>2x%`PHUgKHOzZCQ?Ka(9K z#|?2Eh^Zo$j1Nr#_qAU4AZ4nM<=`GNEvJlc!c+ylIl4?^Q9}5~*f6#?Y4Q>8)1LkOc z+f7rEeGhl#L5=#krK%|3&*oR6hUf*BkaIGCf*scji1p%S9LIEWusc`D3w|~4kQ4k; z%+FPX8Vlx<#^a;B8<2!tE1D-hm@2(g@MG%F?uPD^s5!-aW)f84KSKYl>q3sXe5Y5g zpBK37J%4$b@tM%wcjMbHy_~pbYj!X0DbP!}ca`0iBm)6n^5ymOW~z_@lvERzs$YjAytn^ABp=2z2x^L*&*L$t9(c8%vNcxUCws7 zC7Vde7Hj}1+k%N6Dcgb!AbGu*=AtNGJkQv#I+1(@=>!P9asW(qP(Gi~D__szs@)K7WS6%6-dH_0$h@4ea&m)G`yvt2PNi%q$?<18?WK5 zg#1Xzt=I5YBL12D5a^lwP-143&v)8=3UIA(kJIjJZ=TC;>!GHP3QrPjOn`GKVS!~_ zNWd{c-j$38k}x6R?rzKJ1N8dQa_Zq8w~P@5w|84UpMdiV@U3OcI4vguxX(hujn{Az z@;%hmdn_M;Uds0fKXz{RENvk>;p>h9Hl)GJSillb)Y;onz{qo27Knp!xB8W24J}@f z4DfBho-#k7SEk2a%|k5#k9Vdhr?-oqn4z^<5!W)GlHT7Ep$-7Ck|Wf|!{_dAiBRch z6V>yodW2t8wZFAgsQkCgrc=&oyHZL^h01`IyoznyrN7crVdZ~l#0%cFai`~?JE5=B zLz0GyD1IV;_9UqEH~}co^LTkYojnOw@;SlooWjs2x8KfIp2>>RPdfW-^f2?{ib-hP zQ_1n>fiIw0E6j7sK5RXXdRQ(Gue+%H%KVmSGt8H*@;4P=O#xQ!9ji7KuF6*vcE0Hz zQoaHiKna(Rq^m&v0GP4?5AZNwls8tQi|oL9{lG$D2^aq58^CNI3AQBzD7dE^yz09i zK7&`?u$%W2s2iB=S%XnkJ{*7-`)b=Cc7^esrq*3VfYeySft~f{FLniCXm25B${l9L zqryLgUWd)!wLj(ldZ9bPdi*k5Mmswn@+Z)XUO(6SPZlL3@$ZT1D5 z{x%IzO!oJu^#O@=Hjy@fgI6vQ&{}}9emDNm4P3cZT9a6af@dxn*8Bh^aEGI3{{1H@ zU&dS|X#+ixQ!95}HPC1FAGJb1AS zC16&W-)Z|Qo)2aJowjS&`n&5+Qmq3rz}K}76!V2cCw}{dg9rZksr9d!PxseL*?&SW zMST(1Mdd$tYZ2l+$wF_B>iE7lS5bTiYyic1Kqa7*u|WnGP|N6cA zzEYLZDZs_uB3Py_oNwW~>qtz{;ys`fpo8+mIIXRd9djv_F0Bme7@aN@o3q zeMg$G7rVSlwjCSb>ugsA_;Q;cofCAKADZ>3U}tHzVDMb;>&AdzHpN!72e0*`S-<`R zq}H#0`@F{22i@;Yekd2fX}k&3E7!+s`w7#FK@*Rj*2d2;rRr0Yj*$IE%7y)SGCeOW zyYi!ynqes;YR&@|U^SZUuudn1zf=*)dQp-VNU5c|B(QE7EOBK&(_TyNTT}4*03-|M zn&+YP=V!IHDm{)L+ck}YX9E}flRcN~_kY_|+lGX!s|mxqLevI7AEmk~d7hxm!JHnh zy7~T_uN3 zgE2ALcK!Ud?nusyoH>VZ{vOx=5?CRUELSEI$J@re2KYtcQxMzD4`-H`{Db$ znv;~3U*w#;C*8<7ANRKERsIAE{>^!s9Ru8|RaN@QQh;$Ga{mKJv8YXaX1rb4txMps zv#ok1$IMe@gj0O>YBgPJJt;{~B;`d(S+b=Tv&V6TF0#j6E3DE(Q;Nnhr|famDr@F^ zXgxm20Oi{rA4=8;8$d}LDWpe-GC!eLrYBh&(*6^A=k}lC3!*++lzq4cu6ZjZkpa@T zUEQnphSe90IGh{kP8-qMWRL-h+9u=PC`3uTU;`+b7wZ!=!g_$Va3~N$^j~_GTU!;- z(X^LKJR`^c)3ex^bb{$wZB3b;yBo zD3x~D07|VJR;p>gJFx~*oU?oYpkChASKb%t`@QJ5 zs4{uX|=9+kWaE$_i;{q-V0?k(08=SO~43{!j`ZriOL{P2MPrHX6B z{g9Ow@lM-MOBwWkzSHzl(oWQJXAaw^cX%G-L!cKg-?QS=n)KyvbMZ)spg*vIw)l(IJs z@KgXloff>Z01f?o_BxPKKfDM+A3}RCS|wcqCjub=n9>1~4OCKJNalueg(>{%KYcI! z((%1`;1>n`RjxmwS8l%-{SlTgUET1LOR<8N<4`uF;@KIZ*=&OR)HUF~_CGwg?`zr_ zFIsPNGJsO=qAqr-QA6PIGiApxmSq7uA7K>j?$(dq>PpT{T>wZMh?f-O0mQLC*^A@_nwQr1tkJc}i`hBS^R3(N~nrpD6l zswYo6v4ugVK}z8ETv!O!+-yQ+SKgXONET38Fj*Cww4dsSl262XUMbiX%%B?#C56m7ztk;0Dm_lkn%I2dkwskP5jF312i=6?uQzIP~} z=+loS$3fV^hKg6+d$C&mc{~&L8c8E$fK~UxWsP{i;|Kcn0mD%bbKc@J#ahQEx3a}8 zjK5ev_nVvaPgX)<8BQ?3JHPVz_S?nWW)oh^pr1c=+uPeSna^{c+R9gJdWCyd;camb zmUE_@-No_++d|7+b|cN^AL?ZukB+mQbc@FTl^&J@H2I@tZ=e31`P4$aj{%%Phi&;U z)%r5X(|$P_a2T)rpI}izFT9|Jjg*_m3Z%F-vVhI@woshq;^$9GTG&^z{LB&0 zaQVdsP`{TyZmbbE;F2J%50EyH`u$xZpyT)VFQ5AT{nK;5pa1x&-%pA9%hu=wD(p1p zUq~QAuLvNferVOo9`stZvWNT@D-}QJ#e@|f?xqAyD;e)@+)Wuki+T5fCj$Mq)voj| zGau29?(y9_eXKDT&PE$#fHmmA-DuOH)1WQPl7ufARa>5V7 z{EG9*jb3yEpKb-)q>_nF_(ID#SJwaVKZ!E%k@lq&Y$X0GMFKj(7olLB#QaY1MTz;- z3eLIEkN(krMn434G3Vo1?@lRqwhwSknp5hXy-2ybmy&VyA_eo5lq-_)^it~Wk$P`0 zC1;OhY$@gXNVz&^#$ONp5)$qy`t@DLyD~pTe}o=)O!;tz{5A)0#{9My;0*e04&aRX z?K$85*gJQ=`@wfN{M7scC;X&&|NR?4z5o92_o7#=Us|~#lJhUdKHjnfYUJTkpw6T) z_45v1vDaWqr-A0K@V^ZJ{RC>pa8!ABm-t`l=fOhU z^0+_RN{#xi%PzQ=Z*O~KaKeJnOzFz zE5*E$CL^o#FfeIcro-jjI6$w})PoFfO;TP}q?!|UvOv~dap@Gga#&q$+z4|OyL@@P zKrHSiF_qrD6yV@-y{%f06bu=yP;OYJBj3l}B*$yA2}#x>-h1y6WFG_-9QViAcV`~H zdaubPV181#ebs%^Y(Q2hr|@$02)NK^6CD<%d+?6CN5PA9h*-r}DLIvKeSJEZTYR>7 zSn07LRx563qh99%ZapsQ?T4tU4)3eamhXViYV6c0y7#zP#U~B`95bAo+9@>l+34(W zfY0bu2GBAdOrqfF!&|>xDjj*dtZeiX1oJQ)m5$s^R=hT^ROjitd=IV6l*{H--cGg3 z>tldFP~}w!IHUebf5;VgIr-%GT^Qe|p0o);J!IW6a2xmz5t z0g}+;fPH~d=jlZXm90@PQj?MQQfX_#p|3~94q0DYY-|dAhU&M(GYzX*YRz`gbSvEM zT?C0eK5lJR>I;+x^P^AG+~0uo?9m~uKRtVdDASWNW77HYm@j*Pp=~TNo-sSUkoV!O z$A?m=js|@d`ydGGc+|NPrdP_`kt%Tt^VY}LtV~!h7yp{OMG&D+LWiJAKA6vQ7!T7~ z>hXb_W3tLTMwe_iZ+(3>KoSb<@5@l*6FN;>fre1cSUF7T5r)8C2VO>G#pFn!H*wE(Z5mPP!wlCn` zq(oUi514WZuui~~fBB?JnREka$dZ5gq(+>=|5be7I(#T)4hg+d3x4eJMu(--kLyhWld$WgkEa z_kRqa#OqV>>&!T54j`tSF!;n@uBYKU652#i07VKLBF-ZrU)O7bD^WjEpIaVuim98Spa3aHXftSA0+&LbmXiLv{%1B4hw5 z-@^%Bb!$xDn*2!4)BNnxBYM9d-b-7PyUP)AzGk7xQFT9zRvc{!r||S=Y!P}TVJkm- zBq^?Ql5HZfzZbg`l5M2hlb=1ZH0Q4i`s?}a9q5h8>iz3Z;o1aaR zMBk|oeCluEXYi>PZhmVKBsstHvqh2QyY@my&F^<&KONt<3;Pl1mFbD=c+ItQHu)m1 z>n!y6@}eJt@+RhcJq=H>$~37p@O71GA>D+A9{AHOxbNRTKlS}9(?7Lexm55y-(QM% ze@Z`Rx<_*WXTC>!0nUVv<^X>n<3lL7#6$o5;I$jQQa(YcxWr=z{qVOJ{c6td1)r1w z6eWs>5Bg7#h7bDp&kG&3mhXg)+A5?E`Vmx6NrVu3MS^&tBjNME%oo?Ho%z5QfnLh? zBzZ4Vp8z(1(kOs+Dv{DQNS~0Gl6OM7OsMqtHWAADN#=soX@<}{8_isvj$4Cfv0Toh z+>KbS=8^dz6+34GIIEqXv4gi#{VyqZf;0>GI`=^=IO$TsI|VcD#qzZc@T&0LiXHFO z{>2S+SpZ!LeruAgl)NIv8N3_Jl1;Q_41CHp)W~T&8TQjM+)r#ER=H6BfWPl*IYPdD4ScBK` z(nABt~$SwBx#|OIe_gS^$qsUz4wDx|OuN&ADOBxmK-Jxqs5?Us!_IjyIW$v(IX$R$q zJ!^#B*8!4g4tTGQyhYRTar@?Y>6FpR8+bOGsQZs0CLG`;cF)~xVx^x`fevs99pv=& z1I$BOL$$XLfHGu9f)m{RN3q?hA!cwyfua<~d}RQ4tB2?NCpDWO14!8~)qSy8q$7;> z)7S{mej>ghVwebD{b^R%r3crNZ zUw#_B7J$`Pg=2-l zp5jjK7e6%eNer61HQG?5FQHe2FVp?BMjH=$Debb#{s;+YK;h}Q#s}5f)#Z}-(g^yu z$1hBv50_gCt1-`Z0A)A_Xykn`*UNX!>6KzrRdib7hb2B;A{8L}VbEbCleQTM?X)v5%L?f1Wu*7O={0t2ddZi|F*uzh>?ysQ;Ef)SrI6sWWV>VYy7@ zK0pq=`_He=ZwjEXer(NX|s_UhG^t0qXknr+$<9mG5V#(&RtW8g9B%v09`vjD8O9+%tT-B_ zJUxg0-LIG3;wy_KpXwKo&@1Q~74-d=A@62=2` zZoUPvVw+07q5c7t83Oe-~cf#INQ6Qs&p1^|{mp!OLpx^*2qqge&sM zzJLs{z1}cn%Aq^A=(ns-+>{|+cH2Kc3*B>G|JDMO^|Se%;-eY)r!1e)i_=%_$KocZ z!%gYG0EAxQe{z2%_9ygC?r)|oyrd=xBymFNGVmguVx*}17wJ_a)ok-ZPfF<7AL#QT z#63BokFM|Tz157AH^GX=DXnh2;6vAO{Mr2P#?HK_Z77{`yykmv_ivqww8R0I_22g! z-m11KjSg7qfYPbpMH)3of$uNUD&h}RZIh~=x!{Lh%IU3FgOvI}T1_aK53J~#(&~dm z&MTchyx@V9b14L!GWFA$_ShW2nfKU^dR2@!Hh^Niv9MERenPKI&-5Q<{|UWRbbz?- zs^=YY8$vX%{M+;05$B<~-lrN#f-#S{)rRlT5|<$9#?c}TPhHdRI3fcmo(nH}B<7Cz zi$2%T3EcL{;L^`AC{nzi^_p_co1VXgnyGzwFuk*(aABrc$4-Vjy?`Mq~ zvoL9J)fIX5x=xt&CU$BeS?5CPlo!8JG!SONcg#-~cPk_wkuy--M?CP>pZ8*kp;w$|ZuqQofG+&2MeoJGgwR{*{GIrj6!|OFe7idz6#+tghmrte*1Ohd zh$P&>+p^T{p*^pcx6Hyt;NLodP+^&?``61*!yUq{7@E-L7R2c9xG6a-n>ZN zwK~5=T`$=44|zLOd^^+DngclV*4hhjrhYXCkaE9R2SI_Bu0NqyZoe1( z5SA}yy+3M3ZFw)LRy~d<_}a-@d#_rx7LA^ujG6P`N>7u<5{$H9SVwqE8uPZ9Cj%%| zi%8iYX#?uzg+MYn6EAU}-lAEg?2)p6WO`@Gt#;V8(sROVJ$2}P#)~q5mU!b+o(1}E z^M16RA1?J%$@^LJzGJb9(QLg!sk$gkG6GJ~LOO{U`L!?Y~~%_m&+>#{xEh(sN+PKCc}!UQ)-4_{?0{ zDi_#Wi&0Ndsgt_$-t=6D1sfnL znHSuFD%EZx0KQZ+9+4 zn7zH-k7nyOWQ5`~MT}AoeWG3V>eem3lH>BtRyAPTkxtJP246@WLkN8FUBeG4Zy|2KujgL31Djus|2d?*o?i<)=D`z6!#ff@p(Nfi^h0U)K+q2`l$xn2lN z9gDdnkI3;x(KGuo^lRMyX%BvgLc8jf9vhe=j_-VE+}*b0vs-u_(pn&!UU+DKjiPI$ z+xdEZrvyU>YdB($8=>>{$5vblpTAunrQP?7^qa3kF0(m5mc0jOzh{EOcJAQcKGRhz zx~7zkG{0pTVge79Q-FqK$$&5mE&y%=e|MR_J?+QDb+E$e=@7t{xeontS*GjsPx!ft zAKPdA-$m6Ac7)?#Y_;D2et?x8mTSUY%V*~AY~ggKl%}1$Q*zB=ylGX({OnVgKf~-O zc3#`&&z8kHrKJ|VKgt$x?B8YkNj1;>Vdty#fr?q7zDvz-x|hO`aD?O$t9m3)#(@i)^2^fe9%bnN%P6k^aJ9v-+U#v z2DZa;G&^eTpZ(Ux1pk-INyLL)i zf8=r90rNggc|HVykbFxP5S7T`^laC>a_|7tZ?bY`A9lcN|0%&IziBvk?RdRDK=o!` z(!t#gqooa~xiherp_`54>FRX{-=}B8Yi0Rv^H+=g#scsL*Z8+~{8@7WO85i~-Zlwp z_FFnqm)i>bWBa3!x)4d#BSnD3XXLvgKUM*36{7LpT#I%)``r`zB(%HtLKm&Gb3zyG z&=WH~@wNE*xNDw%+2=(*gvb{u>2?*}c)DVl%W0>&+ugFr_m%nZq(}4MUMhYynl(RS z?^o`FRcg@;7y1Tr{rG|;G~8v67mYpbV1JJRSnz4b`A{7+u-)UlH=T+?;5Qo}Dv{sm z*{*wkJqDcOyv!etz>7aAspC@1j`ZwUUO!+OtsG<-=MOyS#jjcmNY94h+79yId)EWj z`~o@m-Hcxg8NXz2c=J!vvt_$<05o^N^?-JA2mISd=#}Zo?!_+)k0~NFK7;=i30hw; z=n0d>o}55E-oBy6{!N#{W4!FzgMO|2QOjPem%l^q1z-D)zmMQ+7rc0Xza{Zc=oRVj z#eVyKyN}p!7q}}{FBPiNc**;F|I_d@T>iVx{)egAdRpY{w@&&?(nhg(^_0p5^_-Ux ztUs2EB{V@XwhCm+ocjZ;A?wz;*be&xJySiLN#D%@^sMij-7EFGBLjTh@6PFWY!2Xb zJod641j~`84fstz9{2B`pT_-bmghb0SK0pxzD|SL&SAdxiFaNhGQVrd#Vk{kKitM_ZiUoFGn?ezR&-Nuz>GcQuMOX)VlT#s4kHC?yx)@xgI zJ-U!np=Q%d=~tDX&8jueC+T`wpQ>Pe{>N4%CbXlqPX>>a{3@3+50|L!$4a!5Y>zu>4NY@p*b{S3T)^7~!gcCRe7 zI%bZ;Ec7~lkzPqJsn<8@)};6PzI8iSdJVlu(KMymkQ;jIpW$cdt(WtA@0KpPKKB8x z{n{1ZXMT1omt4Q3+tW+!mVWEDu6T~!uvM>W*{$2vx82&E0&v&gT(FTquTB5(e47AC z*8F6waTA9Cayq`N0K-fH`dA7l@X}zOXRmjq*&C^tr}TRB0xyE&#d-I8K~w&6-8|;$ z+k)Bt`DxsXe|jo>ohjeV0h~GCJ+Fb(?}B&@q>cwZ_(cqx6G{#_@f#^_U2K+0;^u^2 z=T7`b%=(c09zFm`S%CumdjY7f>&gQ_wFcmcAHohg0W4vDW&gbRSJ#Gl#6J43pc6k+ zicV>1HzfAafB(4mz%N3b@SEg(@C#t30VppJp;sOtFZw4W9VxQUi~b4p^4oFm_p>Iv zLLD5TJ|5=3zu(WAa%w^FV;RQ}^!8)$xnZ|Qh*DI$&D;ivQo9)IiXQBj<2CTtJ3paU zrjMlEi?aWO-nsqFoTnOjG=^d(z?insp=QZ3GJu616=Pj<`+lOwk72t&$U<1A%gg1> zn%a8R7i*3+E@#kzJ8deGo-eEK6?dhKhy02{&zCLh7xrL01W1#9&-%sFQ!ciyzvT%T zAPOx@YUtZiCWE|&S>8a)xSt{{pkYqex7&SyUK6ZS^~Pm_wc&@FGd$P+YEj>RLa%H; z$y(b=29z>aAH+Yq`-G;g5&Bv(05@u~=z;kY`LpAT!v2I_;eN0E+pRbE+5YWCKHihI zZ1-`L5!!GG`vV%=SzC_*j6PsJIDwEq(bt&cSg?8c!BzG3COxh454YE(&oyY=m$g)H zf5t&uyX!@t->$!Xe_rqk&GPglYiNypjfO@Rtm)W4nl?n}Yve25sV`uD5a{y=XOf&FOnV--r({uCNzyH+yc5VN^R`yAY z{Rhs-_3lOzD9B<5|FsGKZ^+3{T?00$ed}qGyVj#nll6$?_Kf3zO+%?Cx$S#^Edo7u zD%ELLkGL^?8A`C~-CH@EjXwXtGzV!e)-CFvSIBhC<^b%sV7tk(vSv&5PZ)FR(OZA3 z1kK4xi`U;O({BaY#xU3;F*U#c`Llz5go-pw8A@(C-W>L~<^UlZ>|%wTF{F%fe=$<(Bluu;v+3Y z>fm2kJ?6j&`ONfe48VTO2W<{81GZ$+dh#$Z{trJ8v9LsIwb^W6*}gtZ{5R=+ zcfCjNk@Y?L`#;bvY0-Z<{g+?=<>r&`#Kkks-XI@P?9Zutc42b*neHzq6A9a%eyp&B zrDF%E#oL{ApE=uNjpe@ia?W1goH@Pjc#tiMJX;QXS5(uvIkor0ThzW?Z&5$)IX!Yt z?LP7*`8QwgKJq5{<3s5+_s`CyjQ`{LhWPiM??{85+;%SF-F&A^|K->Je)InyGfqVI HE4~2$3suJ2 diff --git a/tests/core/atoms/skzcam_files/adsorbate_slab_embedded_cluster.npy.gz b/tests/core/atoms/skzcam_files/adsorbate_slab_embedded_cluster.npy.gz new file mode 100644 index 0000000000000000000000000000000000000000..6fbc5fc72c6c3c33b884050bd21a3710c13c5dbb GIT binary patch literal 3228 zcmZXTc{~&TAIC*~=}Sqzk;M9jR>>8y99tz4qSKM#>s(^yzFCLdOqSf1ql8$FW-&*| zk~6W+Im)LG|6zGWOX`fq zfu9+%uoGWyf7x!s%aeR{ls`6!nC%xfQ7#|pFg84!RQ1)Ya;7db zd`$bv^RIrJEc-DQx|R#`px%EP!k{a8zRbr7{_d+o_F#r<2fC%FY^3dB2xRIacn zj#l;d?0c7&XKOEiaK119UX{&Ohl^mGndJAr7I&&R4tB3%5$fgnn%w(flTmuxV?X_7 z{vT7_7U9=4AR1*lXUNKZHy23zzK17apBA4+$1#Sx;$9o}^%%^(IK|D-c~@ALm6@3e zsaEJh4PD6$J*Z=~Ah!`NOASBPfhF~)tW{_7%`^u%q1$G&KnNsMiru>~5fJ5dtg86D zoui{X9_{>>X0U3ro#2zkaF}f0f~;QUqSl7R74t7*+@W-tz)+2WVORAV>Ni?YY_SWX zk;I!ZIf~13PLt>-YE!oL5;nKOv6tM|?dEh83@zrGMV>#xmc*C@KQ@n1F^R}j^a5zm zy{|ULI>WoNajBTTsKTsSixDAsz3?Z$ z*o8ZxM31iwP}$)enlLEPU9V5Fg`^fH&Z0`>&Vndm|WM=INRr=-4^mt z+p5OMaT}h>w){|*8?QZq3yqmkv0jOeUWnjJM@J28u3u-;49LWRK5H&+;h~fM$CZm? zjH0sunYBevM|i1u)llK(Ef(`pz*GLC+f#ELd4QYDYYkFGg{daRsFMaqg(0-(0u=a6 zfe%Q*`xL#mc}d+5f#hyl}ZT;?@GcZcXSlc&`T%=G_;M-;{-X(5~1{g3gvk!MfW4iNi)SvlG zW&JBdSRsYS`awpuhM=i*Q48WrKx3Ml0ahXg+R&dXIEWX5ep!=7Wr@zPK`B^bl7K?2 z%u+v|=e6Zupu;0YSl~pc_Yqo6{17R|PAee88H$ zK&EOAP)h;Iwn4On&!&IQnIRVdmaDD$>qnF-R7G=nxQJNL*jr-|>CDV6!KlWJdx;e( zvXwVQE3XNE#_}F$ilbJ9y7zEOuYsx~U+6*EV#HwUcG*~%9xFO5UGI*KbvL52ECHRW z|HzG}qTt|YdVD4}LcxKpDfxF6^=XmH>@4CmKVw2xYiBd7-y(e0v=Z=+{Ig-izq8v^ zp{>RVomQ2mbkP5P*jjPMy@3d~4RLztcf?%0k7$b;0$4bpc$8}An7&rEzM{kHK!{o; z7Q_dWn3N^Z?qd&5*%7Z3ZKzW5AhmpkN~L%}U8eMjwa+oti0u=5V>vJWf@Maz+tz?1)QuM|JUUphWh zKOJPmJf5_5;`oNrw0OHizbZxlT#i67evAHm8iY{Upw4OvUI$-3k9duLXSILaxqH*% zJQOFmoZlsap)Gza=#s{;rGWCmVnnvkD2%m9_+`<`ZY?j{##?A=PwX!Yq|i`h7(r0OG4_(`U&Ele_F{~&7!z+ zOBg>WRXJ);lqzJ6-#OI5KAYecql{1gbwd3Rai>0w!UkpoX1W=1`j04^z-;SZwi&R` zINzS7XCms~pO5T=E~v1vyA5J}@p~tm!6nc72_j0_8l(>-B6(J`MN!VTvb73@CAvfz z=|UNNBChphp%3zz{v+|kgEK?CBTFv^>>J<#*cQ_EV|uwIL`hTJGBFK~bj^!@&zDx@ z`Skw0_ygZFiOShE(&{Xeqb-1JvOX?lBch8w@UwNbdAo({HZ9P5iRG6X!s_4hn9=?w zrimY7a&@<)VE;_<-)RZ^Cwy<$OR&mglU^hlLkPg)gNm-HST8idKW_K8i2S>{yCb(_ z(4nYg-Vb>}orai@1W&X7ACqMnwy~8qLnJj`jZYNs{3JkP*a<%gyOAXO)oEa-){-=Vg%$^3l=NP~5p579Ig&Q(0^c;7Oks@J+GNV=1bNKSS2z9eVe@pkdIH~`!J;WSNifkOVf=8uST@NzUaZw};$MAG1`8G8PGtH`a1vtNOE4e!}@ck>oT$EJoEdDS*podnsw%DqV zBNWj-y^-N{awN7u)rTZI!Ymzae3@wG?+m?Mo*6SoSy9~i+t#nms!II>WiiU+Sx3)OcA&r&>mj&)xIsO`gP0ZYDH4QlI>F|a7GNP+)OZ1=BlAlME6V?Larx% zqxXzS9y9ya^|HVlXIPO9} z8g6ZyU%MIfdH2j=)(2}-AGmu-`ooc`20@B`{WBotpiY`Ga00yfHf9;JsB7rjqvBn? zZG4v$!Y4R)xFu)R?Rd#KoPT*vHXeC9fRp&NA!+2|L=vqD<q2G>AAXi4KTFEO0Y&G>E zo~Y@m)@n*a6@Dy|Q>C*xS{R>vwiZ1K%-TlFDBQ{}dw7-foIYeX87nUlN&d7TA$VIT zd*j>IN2Mmq!4>t3*X{%hG99tg(5T z!?RxG=Rh#b$Nk8t#Z^kmhMPmlhC`ca6#@ zs(>&&W>q#X6kN5zb{=2Ljp0Yi}kl?L{2Y$)p~AaPI4|;zcil+z()J^urI< zcHF%$JfS1t%dg@2Uj;RlmQJZdjJ3S%UH~c|2PJ>(^(WGndy0ClpR*$L!Lnup##w`N zo!Y?3dBT4+v>iK=Fh`tImf2P7di*b{6RTv^clRfOldGXwXuJHdya9D$Gj-t9E(7pC zVGd6#>haR}4;Px|pwYhukH8bZ_lLCz`v9&c^)QBSjnvs$p; z-~+pBvc&mgdv!vRNuRvIVtDl--|8-cofMbG?8Y6>F5KC=vBx>C$a81i33)H|*tDMY wLc>v{Bd%gT?S2d`3OvE7!yn1yFPXz>% literal 0 HcmV?d00001 diff --git a/tests/core/atoms/test_skzcam.py b/tests/core/atoms/test_skzcam.py index fe406a7430..be1f69a30d 100644 --- a/tests/core/atoms/test_skzcam.py +++ b/tests/core/atoms/test_skzcam.py @@ -3,36 +3,43 @@ from copy import deepcopy from pathlib import Path +import gzip import numpy as np import pytest from ase import Atoms from ase.calculators.calculator import compare_atoms from ase.io import read from numpy.testing import assert_allclose, assert_equal +import shutil from quacc.atoms.skzcam import ( MRCCInputGenerator, ORCAInputGenerator, - _find_cation_shells, - _get_anion_coordination, + CreateSKZCAMClusters, _get_atom_distances, - _get_ecp_region, - convert_pun_to_atoms, create_atom_coord_string, - create_skzcam_clusters, - get_cluster_info_from_slab, - insert_adsorbate_to_embedded_cluster, + ) FILE_DIR = Path(__file__).parent +@pytest.fixture() +def skzcam_clusters(): + return CreateSKZCAMClusters(adsorbate_indices=[0,1], slab_center_indices=[32], atom_oxi_states = {'Mg': 2.0, 'O': -2.0}, adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz"), pun_file=None) @pytest.fixture() -def embedded_cluster(): - return convert_pun_to_atoms( - Path(FILE_DIR, "skzcam_files", "mgo_shells_cluster.pun.gz"), - {"Mg": 2.0, "O": -2.0}, - ) +def slab_embedded_cluster(skzcam_clusters): + return skzcam_clusters.convert_pun_to_atoms( + pun_file = Path(FILE_DIR, "skzcam_files", "ChemShell_Cluster.pun.gz") ) + +@pytest.fixture() +def distance_matrix(slab_embedded_cluster): + return slab_embedded_cluster.get_all_distances() + +@pytest.fixture() +def embedded_adsorbed_cluster(): + with gzip.open(Path(FILE_DIR, "skzcam_files", 'adsorbate_slab_embedded_cluster.npy.gz'),'r') as file: + return np.load(file,allow_pickle=True).item()['atoms'] @pytest.fixture() @@ -92,31 +99,23 @@ def orca_input_generator(embedded_adsorbed_cluster, element_info): @pytest.fixture() -def embedded_adsorbed_cluster(): - embedded_cluster, quantum_cluster_indices, ecp_region_indices = ( - create_skzcam_clusters( - Path(FILE_DIR, "skzcam_files", "mgo_shells_cluster.pun.gz"), - [0, 0, 2], - {"Mg": 2.0, "O": -2.0}, - shell_max=2, - ecp_dist=3, - write_clusters=False, - ) - ) - adsorbate = Atoms( - "CO", positions=[[0.0, 0.0, 0.0], [0.0, 0.0, 1.128]], pbc=[False, False, False] - ) +def adsorbate_slab_embedded_cluster(skzcam_clusters): + skzcam_clusters.convert_pun_to_atoms('mgo_shells_cluster_shortened.pun.gz') + # Get quantum cluster and ECP region indices + skzcam_clusters.center_position = [0, 0, 2] + skzcam_clusters.pun_file = Path('mgo_shells_cluster_shortened.pun.gz') + skzcam_clusters.adsorbate = Atoms('CO', positions=[[0.0, 0.0, 0.0], [0.0, 0.0, 1.128]], pbc=[False, False, False]) + skzcam_clusters.adsorbate_vector_from_slab = [0.0, 0.0, 2.0] - embedded_adsorbed_cluster, quantum_cluster_indices, ecp_region_indices = ( - insert_adsorbate_to_embedded_cluster( - embedded_cluster=embedded_cluster, - adsorbate=adsorbate, - adsorbate_vector_from_slab=[0.0, 0.0, 2.0], - quantum_cluster_indices=quantum_cluster_indices, - ecp_region_indices=ecp_region_indices, - ) + skzcam_clusters.run_skzcam( + shell_max=2, + ecp_dist=3.0, + shell_width=0.01, + write_clusters=True, + write_clusters_path='./', ) - return embedded_adsorbed_cluster + + return skzcam_clusters.adsorbate_slab_embedded_cluster @pytest.fixture() @@ -143,10 +142,6 @@ def element_info(): } -@pytest.fixture() -def distance_matrix(embedded_cluster): - return embedded_cluster.get_all_distances() - def test_MRCCInputGenerator_init(embedded_adsorbed_cluster, element_info): # Check what happens if multiplicities is not provided @@ -1161,298 +1156,65 @@ def test_ORCAInputGenerator_create_point_charge_file(orca_input_generator, tmp_p ) -def test_get_cluster_info_from_slab(): - ( - adsorbate, - slab, - slab_first_atom_idx, - center_position, - adsorbate_vector_from_slab, - ) = get_cluster_info_from_slab( - adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "NO_MgO.poscar.gz"), - slab_center_indices=[32, 33], - adsorbate_indices=[0, 1], - ) - - # Check adsorbate matches reference - assert_allclose( - adsorbate.get_positions(), - np.array( - [ - [5.39130495, 4.07523845, 15.96981134], - [5.88635842, 4.84892196, 16.72270959], - ] - ), - rtol=1e-05, - atol=1e-07, - ) - assert_equal(adsorbate.get_atomic_numbers().tolist(), [7, 8]) - - # Check slab matches reference - assert_allclose( - slab.get_positions()[::10], - np.array( - [ - [0.0, 6.33073849, 7.5], - [0.0, 4.22049233, 9.61024616], - [4.2206809, 6.32743192, 11.73976183], - [4.2019821, 4.21892378, 13.89202884], - [0.0, 2.11024616, 9.61024616], - [4.22049233, 0.0, 7.5], - [4.22098271, 2.10239745, 13.86181098], - ] - ), - rtol=1e-05, - atol=1e-07, - ) - assert_equal( - slab.get_atomic_numbers().tolist(), - [ - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 12, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - ], - ) - - # Check first atom index of slab - assert slab_first_atom_idx == 30 - - # Check center_position matches reference - assert_allclose( - center_position, - np.array([1.06307888, -1.06176564, 2.4591779]), - rtol=1e-05, - atol=1e-07, - ) - - # Check vector distance of adsorbate from first center atom (corresponding to first atom index) of slab matches reference - assert_allclose( - adsorbate_vector_from_slab, - np.array([1.18932285, -0.14368533, 2.0777825]), - rtol=1e-05, - atol=1e-07, - ) - - -def test_generate_chemshell_cluster(tmp_path): - from quacc.atoms.skzcam import generate_chemshell_cluster - - # First create the slab - slab = read(Path(FILE_DIR, "skzcam_files", "NO_MgO.poscar.gz"))[2:] - - # Run ChemShell - generate_chemshell_cluster( - slab=slab, - slab_center_idx=30, - atom_oxi_states={"Mg": 2.0, "O": -2.0}, - filepath=tmp_path, - chemsh_radius_active=15.0, - chemsh_radius_cluster=25.0, - write_xyz_file=True, - ) +def test_CreateSKZCAMClusters_run_chemshell(skzcam_clusters, tmp_path): + + # Test if xyz file doesn't get written when write_xyz_file=False + # skzcam_clusters_nowrite = deepcopy(skzcam_clusters) + # skzcam_clusters_nowrite.convert_slab_to_atoms() + # skzcam_clusters_nowrite.run_chemshell( + # filepath= tmp_path / "ChemShell_Cluster.pun", + # chemsh_radius_active=5.0, + # chemsh_radius_cluster=10.0, + # write_xyz_file=False, + # ) + # assert not os.path.isfile(tmp_path / "ChemShell_Cluster.xyz") + + with ( + gzip.open( + Path(FILE_DIR, "skzcam_files", "REF_ChemShell_Cluster.xyz.gz"), "rb" + ) as f_in, + Path(tmp_path, "ChemShell_Cluster.xyz").open(mode="wb") as f_out, + ): + shutil.copyfileobj(f_in, f_out) + # skzcam_clusters.convert_slab_to_atoms() + # skzcam_clusters.run_chemshell( + # filepath= tmp_path / "ChemShell_Cluster.pun", + # chemsh_radius_active=5.0, + # chemsh_radius_cluster=10.0, + # write_xyz_file=True, + # ) # Read the output .xyz file - chemshell_embedded_cluster = read(tmp_path / "ChemShell_cluster.xyz") + chemshell_embedded_cluster = read(tmp_path / "ChemShell_Cluster.xyz") # Check that the positions and atomic numbers match reference assert_allclose( chemshell_embedded_cluster.get_positions()[::100], - np.array( - [ - [0.00000000e00, 0.00000000e00, 0.00000000e00], - [-2.09173593e00, -2.10867761e00, -6.39202884e00], - [2.12875640e00, -6.32916994e00, -6.39202884e00], - [-2.09273725e00, 1.05516878e01, -2.16301583e00], - [2.12875640e00, -1.05496623e01, -6.39202884e00], - [6.34924872e00, -1.05496623e01, -6.39202884e00], - [1.05725789e01, -1.05444085e01, -2.15965963e00], - [1.47875715e01, 6.33408913e00, -2.16464681e00], - [6.34924872e00, -1.47701546e01, -6.39202884e00], - [1.69010014e01, 6.33551965e00, -2.15224877e00], - [1.05697410e01, -1.47701546e01, -6.39202884e00], - [1.05637735e01, 1.68825241e01, -2.17052139e00], - [-1.68651820e01, 1.26649992e01, -5.68710477e-02], - [-1.89763671e01, -1.05478802e01, -2.16464681e00], - [1.05697410e01, -1.89906469e01, -6.39202884e00], - [-2.31906127e01, -4.21607826e00, -1.24998430e-02], - [1.47951600e01, 1.89994594e01, -5.11097275e-02], - [-2.31941976e01, -6.32916994e00, -6.39202884e00], - ] - ), + np.array([[ 0.00000000e+00, 0.00000000e+00, -7.72802046e-03], + [-2.11024616e+00, 2.11024616e+00, -6.38586825e+00], + [ 6.33073849e+00, -2.11024616e+00, -6.38586825e+00], + [-1.09499282e+01, -4.53560876e+00, 4.95687508e+00]]), rtol=1e-05, atol=1e-07, ) assert_equal( - chemshell_embedded_cluster.get_atomic_numbers()[::20].tolist(), - [ - 12, - 12, - 12, - 8, - 12, - 8, - 8, - 12, - 8, - 12, - 8, - 8, - 12, - 8, - 12, - 8, - 8, - 12, - 8, - 12, - 8, - 8, - 12, - 8, - 8, - 8, - 8, - 12, - 8, - 12, - 8, - 8, - 12, - 12, - 12, - 8, - 8, - 12, - 8, - 12, - 8, - 8, - 12, - 8, - 12, - 12, - 8, - 12, - 8, - 12, - 8, - 8, - 12, - 8, - 8, - 12, - 8, - 12, - 8, - 12, - 12, - 8, - 12, - 12, - 8, - 8, - 8, - 12, - 12, - 8, - 8, - 8, - 12, - 8, - 12, - 8, - 8, - 12, - 12, - 8, - 12, - 8, - 12, - 12, - 8, - 8, - 12, - 9, - 9, - 9, - ], + chemshell_embedded_cluster.get_atomic_numbers()[::40].tolist(), + [12, 12, 12, 8, 8, 8, 12, 9, 9], ) -def test_convert_pun_to_atoms(): - embedded_cluster = convert_pun_to_atoms( - pun_file=Path(FILE_DIR, "skzcam_files", "mgo_shells_cluster.pun.gz"), - atom_oxi_states={"Mg": 2.0, "O": -2.0}, - ) +def test_CreateSKZCAMClusters_convert_pun_to_atoms(skzcam_clusters): + + slab_embedded_cluster = skzcam_clusters.convert_pun_to_atoms(pun_file=Path(FILE_DIR, "skzcam_files", "ChemShell_Cluster.pun.gz")) + # Check that number of atoms matches our reference - assert len(embedded_cluster) == 390 + assert len(slab_embedded_cluster) == 390 # Check that last 10 elements of the oxi_state match our reference assert_allclose( - embedded_cluster.get_array("oxi_states")[-10:], + slab_embedded_cluster.get_array("oxi_states")[-10:], np.array( [ -0.80812511, @@ -1473,7 +1235,7 @@ def test_convert_pun_to_atoms(): # Check that first 10 elements of atom_type array match our reference assert_equal( - embedded_cluster.get_array("atom_type")[:10], + slab_embedded_cluster.get_array("atom_type")[:10], [ "cation", "anion", @@ -1490,110 +1252,133 @@ def test_convert_pun_to_atoms(): # Check that the positions of the atom matches assert_allclose( - embedded_cluster[200].position, + slab_embedded_cluster[200].position, np.array([6.33074029, -2.11024676, -6.37814205]), rtol=1e-05, atol=1e-07, ) -def test_insert_adsorbate_to_embedded_cluster(embedded_cluster): - # Create a CO molecule - adsorbate = Atoms( - "CO", positions=[[0.0, 0.0, 0.0], [0.0, 0.0, 1.128]], pbc=[False, False, False] - ) - # Insert the CO molecule to the embedded cluster - embedded_cluster, quantum_idx, ecp_idx = insert_adsorbate_to_embedded_cluster( - embedded_cluster=embedded_cluster, - adsorbate=adsorbate, - adsorbate_vector_from_slab=[0.0, 0.0, 2.0], - quantum_cluster_indices=[[0, 1, 3, 4], [5, 6, 7, 8]], - ecp_region_indices=[[0, 1, 3, 4], [5, 6, 7, 8]], - ) +def test_CreateSKZCAMClusters_convert_slab_to_atoms(): - # Check that the positions of the first 10 atoms of the embedded cluster matches the reference positions, oxi_states and atom_type + # Test for CO on MgO example + skzcam_clusters = CreateSKZCAMClusters(adsorbate_indices=[0,1], slab_center_indices=[32], atom_oxi_states = {'Mg': 2.0, 'O': -2.0}, adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz"), pun_file=None) + skzcam_clusters.convert_slab_to_atoms() + + # Check adsorbate matches reference assert_allclose( - embedded_cluster.get_positions()[:10], - np.array( - [ - [0.0, 0.0, 2.0], - [0.0, 0.0, 3.128], - [0.0, 0.0, 0.0], - [-2.12018426, 0.0, 0.00567209], - [0.0, 2.12018426, 0.00567209], - [2.12018426, 0.0, 0.00567209], - [0.0, -2.12018426, 0.00567209], - [0.0, 0.0, -2.14129966], - [-2.11144262, 2.11144262, -0.04367284], - [2.11144262, 2.11144262, -0.04367284], - ] - ), + skzcam_clusters.adsorbate.get_positions(), + np.array([[0. , 0. , 2.44102236], + [0. , 0. , 3.58784217]]), rtol=1e-05, atol=1e-07, ) + assert_equal(skzcam_clusters.adsorbate.get_atomic_numbers().tolist(), [6, 8]) + # Check slab matches reference + assert_allclose( + skzcam_clusters.slab.get_positions()[::10], + np.array([[ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [-2.11024616e+00, 0.00000000e+00, -6.37814023e+00], + [ 2.11024616e+00, 2.11024616e+00, -4.26789407e+00], + [ 2.10705227e+00, 0.00000000e+00, -2.14155146e+00], + [-4.22049233e+00, -2.11024616e+00, -4.26789407e+00], + [ 0.00000000e+00, -4.22049233e+00, -6.37814023e+00], + [ 0.00000000e+00, -2.12018365e+00, 5.67208927e-03]]), + rtol=1e-05, + atol=1e-07, + ) assert_equal( - embedded_cluster.get_chemical_symbols()[:10], - ["C", "O", "Mg", "O", "O", "O", "O", "O", "Mg", "Mg"], + skzcam_clusters.slab.get_atomic_numbers().tolist()[::10], + [12, 12, 12, 12, 8, 8, 8], ) + + # Check center_position matches reference assert_allclose( - embedded_cluster.get_array("oxi_states")[:10], - np.array([0.0, 0.0, 2.0, -2.0, -2.0, -2.0, -2.0, -2.0, 2.0, 2.0]), + skzcam_clusters.center_position, + np.array([0. , 0. , 3.09607306]), rtol=1e-05, atol=1e-07, ) - assert_equal( - embedded_cluster.get_array("atom_type")[:10], - [ - "adsorbate", - "adsorbate", - "cation", - "anion", - "anion", - "anion", - "anion", - "anion", - "cation", - "cation", - ], + + # Check vector distance of adsorbate from first center atom (corresponding to first atom index) of slab matches reference + assert_allclose( + skzcam_clusters.adsorbate_vector_from_slab, + np.array([0. , 0. , 2.44102236]), + rtol=1e-05, + atol=1e-07, ) - # Check that the quantum_idx and ecp_idx match the reference - assert_equal(quantum_idx, [[0, 1, 2, 3, 5, 6], [0, 1, 7, 8, 9, 10]]) - assert_equal(ecp_idx, [[2, 3, 5, 6], [7, 8, 9, 10]]) + # Test for NO on MgO example + skzcam_clusters = CreateSKZCAMClusters(adsorbate_indices=[0,1], slab_center_indices=[32,33], atom_oxi_states = {'Mg': 2.0, 'O': -2.0}, adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "NO_MgO.poscar.gz"), pun_file=None) + skzcam_clusters.convert_slab_to_atoms() + # Check adsorbate matches reference + assert_allclose( + skzcam_clusters.adsorbate.get_positions(), + np.array([[ 1.18932285, -0.14368533, 2.0777825 ], + [ 1.68437633, 0.62999818, 2.83068075]]), + rtol=1e-05, + atol=1e-07, + ) + assert_equal(skzcam_clusters.adsorbate.get_atomic_numbers().tolist(), [7, 8]) -def test_get_atom_distances(): - # Creating a H2 molecule as an Atoms object - h2_molecule = Atoms("H2", positions=[(0, 0, 0), (0, 0, 2)]) + # Check slab matches reference + assert_allclose( + skzcam_clusters.slab.get_positions()[::10], + np.array([[ 0. , 0. , 0. ], + [-4.2019821 , -2.10867761, -6.39202884], + [ 0.01851023, -4.21892378, -4.28178268], + [ 0.01903204, -2.105465 , -2.15224877], + [-4.2019821 , -2.10867761, -4.28178268], + [ 0.01851023, -4.21892378, -6.39202884], + [ 0.01900061, -2.11652633, -0.03021786]]), + rtol=1e-05, + atol=1e-07, + ) + assert_equal( + skzcam_clusters.slab.get_atomic_numbers().tolist()[::10], + [12, 12, 12, 12, 8, 8, 8], + ) - # Run _get_atom_distances function to get distance of h2 molecule atoms from a center position - atom_distances = _get_atom_distances( - embedded_cluster=h2_molecule, center_position=[2, 0, 0] + # Check center_position matches reference + assert_allclose( + skzcam_clusters.center_position, + np.array([ 1.06307888, -1.06176564, 2.47922285]), + rtol=1e-05, + atol=1e-07, + ) + + # Check vector distance of adsorbate from first center atom (corresponding to first atom index) of slab matches reference + assert_allclose( + skzcam_clusters.adsorbate_vector_from_slab, + np.array([ 1.18932285, -0.14368533, 2.0777825 ]), + rtol=1e-05, + atol=1e-07, ) - assert_allclose(atom_distances, np.array([2.0, 2.82842712]), rtol=1e-05, atol=1e-07) -def test_find_cation_shells(embedded_cluster): + +def test_CreateSKZCAMClusters_find_cation_shells(skzcam_clusters,slab_embedded_cluster): # Get distance of atoms from the center distances = _get_atom_distances( - embedded_cluster=embedded_cluster, center_position=[0, 0, 2] + atoms=slab_embedded_cluster, center_position=[0, 0, 2] ) # Find the cation shells from the distances - cation_shells, cation_shells_idx = _find_cation_shells( - embedded_cluster=embedded_cluster, distances=distances, shell_width=0.005 + cation_shells_distances , cation_shells_idx = skzcam_clusters._find_cation_shells( + slab_embedded_cluster=slab_embedded_cluster, distances=distances, shell_width=0.005 ) # As these list of lists do not have the same length, we flatten first 5 lists into a 1D list for comparison - cation_shells_flatten = [item for row in cation_shells[:5] for item in row] + cation_shells_distances_flatten = [item for row in cation_shells_distances[:5] for item in row] cation_shells_idx_flatten = [item for row in cation_shells_idx[:5] for item in row] # Check that these lists are correct assert_allclose( - cation_shells_flatten, + cation_shells_distances_flatten, np.array( [ 2.0, @@ -1621,10 +1406,10 @@ def test_find_cation_shells(embedded_cluster): ) -def test_get_anion_coordination(embedded_cluster, distance_matrix): +def test_CreateSKZCAMClusters_get_anion_coordination(skzcam_clusters,slab_embedded_cluster, distance_matrix): # Get the anions for the second SKZCAM shell - anion_shell_idx = _get_anion_coordination( - embedded_cluster=embedded_cluster, + anion_shell_idx = skzcam_clusters._get_anion_coordination( + slab_embedded_cluster=slab_embedded_cluster, cation_shell_indices=[9, 8, 6, 7], dist_matrix=distance_matrix, ) @@ -1635,10 +1420,10 @@ def test_get_anion_coordination(embedded_cluster, distance_matrix): ) -def test_get_ecp_region(embedded_cluster, distance_matrix): +def test_CreateSKZCAMClusters_get_ecp_region(skzcam_clusters,slab_embedded_cluster, distance_matrix): # Find the ECP region for the first cluster - ecp_region_idx = _get_ecp_region( - embedded_cluster=embedded_cluster, + ecp_region_idx = skzcam_clusters._get_ecp_region( + slab_embedded_cluster=slab_embedded_cluster, quantum_cluster_indices=[[0, 1, 2, 3, 4, 5]], dist_matrix=distance_matrix, ecp_dist=3, @@ -1647,13 +1432,79 @@ def test_get_ecp_region(embedded_cluster, distance_matrix): # Check ECP region indices match with reference assert_equal(ecp_region_idx[0], [6, 7, 8, 9, 10, 11, 12, 13, 18, 19, 20, 21, 22]) +def test_CreateSKZCAMClusters_create_adsorbate_slab_embedded_cluster(skzcam_clusters, slab_embedded_cluster): + + skzcam_clusters.slab_embedded_cluster = slab_embedded_cluster + skzcam_clusters.adsorbate = Atoms( + "CO", positions=[[0.0, 0.0, 0.0], [0.0, 0.0, 1.128]], pbc=[False, False, False] + ) + skzcam_clusters.adsorbate_vector_from_slab = [0.0, 0.0, 2.0] + + skzcam_clusters.create_adsorbate_slab_embedded_cluster( + quantum_cluster_indices=[[0, 1, 3, 4], [5, 6, 7, 8]], + ecp_region_indices=[[0, 1, 3, 4], [5, 6, 7, 8]], + ) + + # Check that the positions of the first 10 atoms of the embedded cluster matches the reference positions, oxi_states and atom_type + assert_allclose( + skzcam_clusters.adsorbate_slab_embedded_cluster.get_positions()[:10], + np.array( + [ + [0.0, 0.0, 2.0], + [0.0, 0.0, 3.128], + [0.0, 0.0, 0.0], + [-2.12018426, 0.0, 0.00567209], + [0.0, 2.12018426, 0.00567209], + [2.12018426, 0.0, 0.00567209], + [0.0, -2.12018426, 0.00567209], + [0.0, 0.0, -2.14129966], + [-2.11144262, 2.11144262, -0.04367284], + [2.11144262, 2.11144262, -0.04367284], + ] + ), + rtol=1e-05, + atol=1e-07, + ) + + assert_equal( + skzcam_clusters.adsorbate_slab_embedded_cluster.get_chemical_symbols()[:10], + ["C", "O", "Mg", "O", "O", "O", "O", "O", "Mg", "Mg"], + ) + assert_allclose( + skzcam_clusters.adsorbate_slab_embedded_cluster.get_array("oxi_states")[:10], + np.array([0.0, 0.0, 2.0, -2.0, -2.0, -2.0, -2.0, -2.0, 2.0, 2.0]), + rtol=1e-05, + atol=1e-07, + ) + assert_equal( + skzcam_clusters.adsorbate_slab_embedded_cluster.get_array("atom_type")[:10], + [ + "adsorbate", + "adsorbate", + "cation", + "anion", + "anion", + "anion", + "anion", + "anion", + "cation", + "cation", + ], + ) + + # Check that the quantum_idx and ecp_idx match the reference + assert_equal(skzcam_clusters.quantum_cluster_indices, [[0, 1, 2, 3, 5, 6], [0, 1, 7, 8, 9, 10]]) + assert_equal(skzcam_clusters.ecp_region_indices, [[2, 3, 5, 6], [7, 8, 9, 10]]) -def test_create_skzcam_clusters(tmp_path): + +def test_CreateSKZCAMClusters_run_skzcam(skzcam_clusters,tmp_path): # Get quantum cluster and ECP region indices - _, quantum_cluster_idx, ecp_region_idx = create_skzcam_clusters( - pun_file=Path(FILE_DIR, "skzcam_files", "mgo_shells_cluster.pun.gz"), - center_position=[0, 0, 2], - atom_oxi_states={"Mg": 2.0, "O": -2.0}, + skzcam_clusters.center_position = [0, 0, 2] + skzcam_clusters.pun_file = Path(FILE_DIR, "skzcam_files", "ChemShell_Cluster.pun.gz") + skzcam_clusters.adsorbate = Atoms('CO', positions=[[0.0, 0.0, 0.0], [0.0, 0.0, 1.128]], pbc=[False, False, False]) + skzcam_clusters.adsorbate_vector_from_slab = [0.0, 0.0, 2.0] + + skzcam_clusters.run_skzcam( shell_max=2, ecp_dist=3.0, shell_width=0.005, @@ -1663,66 +1514,41 @@ def test_create_skzcam_clusters(tmp_path): # Check quantum cluster indices match with reference assert_equal( - quantum_cluster_idx[1], - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 14, 15, 16, 17, 23, 24, 25, 26, 27, 28, 29, 30], - ) + skzcam_clusters.quantum_cluster_indices[1], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 17, 18, 19, 25, 26, 27, 28, 29, 30, 31, 32]) # Check ECP region indices match with reference assert_equal( - ecp_region_idx[1], - [ - 10, - 11, - 12, - 13, - 18, - 19, - 20, - 21, - 22, - 39, - 40, - 41, - 42, - 43, - 44, - 45, - 46, - 47, - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - ], + skzcam_clusters.ecp_region_indices[1], + [12, 13, 14, 15, 20, 21, 22, 23, 24, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 78, 79, 80, 81, 82, 83, 84, 85] +, ) # Read the written output and check that it matches with the reference positions and atomic numbers - skzcam_cluster = read(tmp_path / "SKZCAM_cluster_0.xyz") + skzcam_cluster_xyz = read(tmp_path / "SKZCAM_cluster_0.xyz") assert_allclose( - skzcam_cluster.get_positions(), - np.array( - [ - [0.0, 0.0, 0.0], - [-2.12018426, 0.0, 0.00567209], - [0.0, 2.12018426, 0.00567209], - [2.12018426, 0.0, 0.00567209], - [0.0, -2.12018426, 0.00567209], - [0.0, 0.0, -2.14129966], - ] - ), + skzcam_cluster_xyz.get_positions(), + np.array([[ 0. , 0. , 2. ], + [ 0. , 0. , 3.128 ], + [ 0. , 0. , 0. ], + [-2.12018426, 0. , 0.00567209], + [ 0. , 2.12018426, 0.00567209], + [ 2.12018426, 0. , 0.00567209], + [ 0. , -2.12018426, 0.00567209], + [ 0. , 0. , -2.14129966]]), rtol=1e-04, atol=1e-07, ) - assert_equal(skzcam_cluster.get_atomic_numbers().tolist(), [12, 8, 8, 8, 8, 8]) + assert_equal(skzcam_cluster_xyz.get_atomic_numbers().tolist(), [6,8,12, 8, 8, 8, 8, 8]) + +def test_get_atom_distances(): + # Creating a H2 molecule as an Atoms object + h2_molecule = Atoms("H2", positions=[(0, 0, 0), (0, 0, 2)]) + + # Run _get_atom_distances function to get distance of h2 molecule atoms from a center position + atom_distances = _get_atom_distances( + atoms=h2_molecule, center_position=[2, 0, 0] + ) + + assert_allclose(atom_distances, np.array([2.0, 2.82842712]), rtol=1e-05, atol=1e-07) \ No newline at end of file From 46367af7ca8d4b383f7720e9a718009d7d695c22 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 9 Jun 2024 14:58:24 +0000 Subject: [PATCH 06/29] pre-commit auto-fixes --- src/quacc/atoms/skzcam.py | 144 +++++++------ tests/core/atoms/conftest_noworking.py | 4 +- tests/core/atoms/test_skzcam.py | 287 +++++++++++++++++-------- 3 files changed, 280 insertions(+), 155 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index f2ee08d254..e1e11edd43 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -905,14 +905,15 @@ def create_atom_coord_string( return atom_coord_str + class CreateSKZCAMClusters: def __init__( - self, - adsorbate_indices: list[int], - slab_center_indices: list[int], - atom_oxi_states: dict[str, int], - adsorbate_slab_file: str | Path | None = None, - pun_file: str | Path | None = None, + self, + adsorbate_indices: list[int], + slab_center_indices: list[int], + atom_oxi_states: dict[str, int], + adsorbate_slab_file: str | Path | None = None, + pun_file: str | Path | None = None, ): """ Parameters @@ -931,16 +932,20 @@ def __init__( self.adsorbate_indices = adsorbate_indices self.slab_center_indices = slab_center_indices - self.slab_indices = None # This will be set later + self.slab_indices = None # This will be set later self.atom_oxi_states = atom_oxi_states # Check that the adsorbate_indices and slab_center_indices are not the same - if any([x in adsorbate_indices for x in slab_center_indices]): - raise ValueError("The adsorbate and slab center indices cannot be the same.") - + if any(x in adsorbate_indices for x in slab_center_indices): + raise ValueError( + "The adsorbate and slab center indices cannot be the same." + ) + # Check that the adsorbate_slab_file and pun_file are not both None if adsorbate_slab_file is None and pun_file is None: - raise ValueError("Either the adsorbate_slab_file or pun_file must be provided.") + raise ValueError( + "Either the adsorbate_slab_file or pun_file must be provided." + ) self.adsorbate_slab_file = adsorbate_slab_file self.pun_file = pun_file @@ -994,9 +999,7 @@ def run_skzcam( """ # Read the .pun file and create the embedded_cluster Atoms object - self.slab_embedded_cluster = self.convert_pun_to_atoms( - pun_file=self.pun_file - ) + self.slab_embedded_cluster = self.convert_pun_to_atoms(pun_file=self.pun_file) # Get distances of all atoms from the cluster center atom_center_distances = _get_atom_distances( @@ -1019,8 +1022,10 @@ def run_skzcam( shell_indices = cation_shells_idx[shell_idx] anion_coord_idx += [ self._get_anion_coordination( - slab_embedded_cluster=self.slab_embedded_cluster, cation_shell_indices = shell_indices, - dist_matrix = slab_embedded_cluster_all_dist, bond_dist = bond_dist + slab_embedded_cluster=self.slab_embedded_cluster, + cation_shell_indices=shell_indices, + dist_matrix=slab_embedded_cluster_all_dist, + bond_dist=bond_dist, ) ] @@ -1044,20 +1049,29 @@ def run_skzcam( ) # print(slab_quantum_cluster_indices) # Create the adsorbate_slab_embedded_cluster from slab_embedded_cluster and adsorbate atoms objects. This also sets the final quantum_cluster_indices and ecp_region_indices for the adsorbate_slab_embedded_cluster - self.create_adsorbate_slab_embedded_cluster(quantum_cluster_indices=slab_quantum_cluster_indices, ecp_region_indices=slab_ecp_region_indices) + self.create_adsorbate_slab_embedded_cluster( + quantum_cluster_indices=slab_quantum_cluster_indices, + ecp_region_indices=slab_ecp_region_indices, + ) # Write the quantum clusters to files if write_clusters: for idx in range(len(self.quantum_cluster_indices)): - quantum_atoms = self.adsorbate_slab_embedded_cluster[self.quantum_cluster_indices[idx]] + quantum_atoms = self.adsorbate_slab_embedded_cluster[ + self.quantum_cluster_indices[idx] + ] if write_include_ecp: - ecp_atoms = self.adsorbate_slab_embedded_cluster[self.ecp_region_indices[idx]] + ecp_atoms = self.adsorbate_slab_embedded_cluster[ + self.ecp_region_indices[idx] + ] ecp_atoms.set_chemical_symbols(np.array(["U"] * len(ecp_atoms))) cluster_atoms = quantum_atoms + ecp_atoms else: cluster_atoms = quantum_atoms - write(Path(write_clusters_path, f"SKZCAM_cluster_{idx}.xyz"), cluster_atoms) - + write( + Path(write_clusters_path, f"SKZCAM_cluster_{idx}.xyz"), + cluster_atoms, + ) @requires(has_chemshell, "ChemShell is not installed") def run_chemshell( @@ -1106,7 +1120,9 @@ def run_chemshell( ) # Save the final cluster to a .pun file - chemsh_slab_embedded_cluster.save(filename=Path(filepath).with_suffix(".pun"), fmt="pun") + chemsh_slab_embedded_cluster.save( + filename=Path(filepath).with_suffix(".pun"), fmt="pun" + ) self.pun_file = Path(filepath).with_suffix(".pun") if write_xyz_file: @@ -1115,11 +1131,7 @@ def run_chemshell( filename=Path(filepath).with_suffix(".xyz"), fmt="xyz" ) - - - def convert_slab_to_atoms( - self - ) -> None: + def convert_slab_to_atoms(self) -> None: """ Read the file containing the periodic slab and adsorbate (geometry optimized) and format the resulting Atoms object to be used to create a .pun file in ChemShell. @@ -1145,27 +1157,25 @@ def convert_slab_to_atoms( # Find indices (within adsorbate_slab) of the slab slab_indices = self.slab_center_indices + [ - i for i, _ in enumerate(adsorbate_slab) if i not in (self.adsorbate_indices + self.slab_center_indices) + i + for i, _ in enumerate(adsorbate_slab) + if i not in (self.adsorbate_indices + self.slab_center_indices) ] - # Create adsorbate and slab from adsorbate_slab + # Create adsorbate and slab from adsorbate_slab slab = adsorbate_slab[slab_indices] adsorbate = adsorbate_slab[self.adsorbate_indices] adsorbate.translate(-slab[0].position) slab.translate(-slab[0].position) - # Get the relative distance of the adsorbate from the first center atom of the slab as defined in the slab_center_indices - adsorbate_vector_from_slab = ( - adsorbate[0].position - slab[0].position - ) + adsorbate_vector_from_slab = adsorbate[0].position - slab[0].position # Get the center of the cluster from the slab_center_indices - slab_center_position = slab[:len(self.slab_center_indices)].get_positions().sum( - axis=0 - ) / len(self.slab_center_indices) - + slab_center_position = slab[ + : len(self.slab_center_indices) + ].get_positions().sum(axis=0) / len(self.slab_center_indices) # Add the height of the adsorbate from the slab along the z-direction relative to the slab_center adsorbate_com_z_disp = ( @@ -1173,8 +1183,7 @@ def convert_slab_to_atoms( ) center_position = ( - np.array([0.0, 0.0, adsorbate_com_z_disp]) - + slab_center_position + np.array([0.0, 0.0, adsorbate_com_z_disp]) + slab_center_position ) self.adsorbate = adsorbate @@ -1183,11 +1192,7 @@ def convert_slab_to_atoms( self.adsorbate_vector_from_slab = adsorbate_vector_from_slab self.center_position = center_position - - def convert_pun_to_atoms( - self, - pun_file: str | Path - ) -> Atoms: + def convert_pun_to_atoms(self, pun_file: str | Path) -> Atoms: """ Reads a .pun file and returns an ASE Atoms object containing the atomic coordinates, point charges/oxidation states, and atom types. @@ -1216,7 +1221,9 @@ def convert_pun_to_atoms( # Load the pun file as a list of strings with zopen(zpath(Path(pun_file))) as f: raw_pun_file = [ - line.rstrip().decode("utf-8") if isinstance(line, bytes) else line.rstrip() + line.rstrip().decode("utf-8") + if isinstance(line, bytes) + else line.rstrip() for line in f ] @@ -1270,7 +1277,6 @@ def convert_pun_to_atoms( return slab_embedded_cluster - def create_adsorbate_slab_embedded_cluster( self, quantum_cluster_indices: list[list[int]] | None = None, @@ -1297,35 +1303,41 @@ def create_adsorbate_slab_embedded_cluster( self.adsorbate.set_pbc(False) # Translate the adsorbate to the correct position relative to the slab - self.adsorbate.translate(self.slab_embedded_cluster[0].position - self.adsorbate[0].position + self.adsorbate_vector_from_slab) + self.adsorbate.translate( + self.slab_embedded_cluster[0].position + - self.adsorbate[0].position + + self.adsorbate_vector_from_slab + ) # Set oxi_state and atom_type arrays for the adsorbate self.adsorbate.set_array("oxi_states", np.array([0.0] * len(self.adsorbate))) - self.adsorbate.set_array("atom_type", np.array(["adsorbate"] * len(self.adsorbate))) + self.adsorbate.set_array( + "atom_type", np.array(["adsorbate"] * len(self.adsorbate)) + ) # Add the adsorbate to the embedded cluster - self.adsorbate_slab_embedded_cluster = self.adsorbate + self.slab_embedded_cluster + self.adsorbate_slab_embedded_cluster = ( + self.adsorbate + self.slab_embedded_cluster + ) # Update the quantum cluster and ECP region indices if quantum_cluster_indices is not None: quantum_cluster_indices = [ - list(range(len(self.adsorbate))) + [idx + len(self.adsorbate) for idx in cluster] + list(range(len(self.adsorbate))) + + [idx + len(self.adsorbate) for idx in cluster] for cluster in quantum_cluster_indices ] if ecp_region_indices is not None: ecp_region_indices = [ - [idx + len(self.adsorbate) for idx in cluster] for cluster in ecp_region_indices + [idx + len(self.adsorbate) for idx in cluster] + for cluster in ecp_region_indices ] self.quantum_cluster_indices = quantum_cluster_indices self.ecp_region_indices = ecp_region_indices - - - - - def _find_cation_shells(self, - slab_embedded_cluster: Atoms, distances: NDArray, shell_width: float = 0.1 + def _find_cation_shells( + self, slab_embedded_cluster: Atoms, distances: NDArray, shell_width: float = 0.1 ) -> list[list[int]]: """ Returns a list of lists containing the indices of the cations in each shell, based on distance from the embedded cluster center. @@ -1379,8 +1391,8 @@ def _find_cation_shells(self, return shells_distances, shells_indices - - def _get_anion_coordination(self, + def _get_anion_coordination( + self, slab_embedded_cluster: Atoms, cation_shell_indices: list[int], dist_matrix: NDArray, @@ -1422,8 +1434,8 @@ def _get_anion_coordination(self, return list(set(anion_coord_indices)) - - def _get_ecp_region(self, + def _get_ecp_region( + self, slab_embedded_cluster: Atoms, quantum_cluster_indices: list[int], dist_matrix: NDArray, @@ -1462,14 +1474,16 @@ def _get_ecp_region(self, if ( dist < ecp_dist and idx not in dummy_cation_indices - and slab_embedded_cluster.get_array("atom_type")[idx] == "cation" + and slab_embedded_cluster.get_array("atom_type")[idx] + == "cation" ): cluster_ecp_region_idx += [idx] ecp_region_indices += [list(set(cluster_ecp_region_idx))] return ecp_region_indices - + + def _get_atom_distances(atoms: Atoms, center_position: NDArray) -> NDArray: """ Returns the distance of all atoms from the center position of the embedded cluster @@ -1487,6 +1501,4 @@ def _get_atom_distances(atoms: Atoms, center_position: NDArray) -> NDArray: An array containing the distances of each atom in the Atoms object from the cluster center. """ - return np.array( - [np.linalg.norm(atom.position - center_position) for atom in atoms] - ) + return np.array([np.linalg.norm(atom.position - center_position) for atom in atoms]) diff --git a/tests/core/atoms/conftest_noworking.py b/tests/core/atoms/conftest_noworking.py index 774a1f32b1..ff60f216c8 100644 --- a/tests/core/atoms/conftest_noworking.py +++ b/tests/core/atoms/conftest_noworking.py @@ -9,9 +9,7 @@ FILE_DIR = Path(__file__).parent -def mock_run_chemshell( - slab, slab_center_idx, atom_oxi_states, filepath, **kwargs -): +def mock_run_chemshell(slab, slab_center_idx, atom_oxi_states, filepath, **kwargs): with ( gzip.open( Path(FILE_DIR, "skzcam_files", "REF_ChemShell_Cluster.xyz.gz"), "rb" diff --git a/tests/core/atoms/test_skzcam.py b/tests/core/atoms/test_skzcam.py index be1f69a30d..66f0d94bcc 100644 --- a/tests/core/atoms/test_skzcam.py +++ b/tests/core/atoms/test_skzcam.py @@ -1,45 +1,57 @@ from __future__ import annotations +import gzip +import shutil from copy import deepcopy from pathlib import Path -import gzip import numpy as np import pytest from ase import Atoms from ase.calculators.calculator import compare_atoms from ase.io import read from numpy.testing import assert_allclose, assert_equal -import shutil from quacc.atoms.skzcam import ( + CreateSKZCAMClusters, MRCCInputGenerator, ORCAInputGenerator, - CreateSKZCAMClusters, _get_atom_distances, create_atom_coord_string, - ) FILE_DIR = Path(__file__).parent + @pytest.fixture() def skzcam_clusters(): - return CreateSKZCAMClusters(adsorbate_indices=[0,1], slab_center_indices=[32], atom_oxi_states = {'Mg': 2.0, 'O': -2.0}, adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz"), pun_file=None) + return CreateSKZCAMClusters( + adsorbate_indices=[0, 1], + slab_center_indices=[32], + atom_oxi_states={"Mg": 2.0, "O": -2.0}, + adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz"), + pun_file=None, + ) + @pytest.fixture() def slab_embedded_cluster(skzcam_clusters): return skzcam_clusters.convert_pun_to_atoms( - pun_file = Path(FILE_DIR, "skzcam_files", "ChemShell_Cluster.pun.gz") ) + pun_file=Path(FILE_DIR, "skzcam_files", "ChemShell_Cluster.pun.gz") + ) + @pytest.fixture() def distance_matrix(slab_embedded_cluster): return slab_embedded_cluster.get_all_distances() + @pytest.fixture() def embedded_adsorbed_cluster(): - with gzip.open(Path(FILE_DIR, "skzcam_files", 'adsorbate_slab_embedded_cluster.npy.gz'),'r') as file: - return np.load(file,allow_pickle=True).item()['atoms'] + with gzip.open( + Path(FILE_DIR, "skzcam_files", "adsorbate_slab_embedded_cluster.npy.gz"), "r" + ) as file: + return np.load(file, allow_pickle=True).item()["atoms"] @pytest.fixture() @@ -100,11 +112,13 @@ def orca_input_generator(embedded_adsorbed_cluster, element_info): @pytest.fixture() def adsorbate_slab_embedded_cluster(skzcam_clusters): - skzcam_clusters.convert_pun_to_atoms('mgo_shells_cluster_shortened.pun.gz') + skzcam_clusters.convert_pun_to_atoms("mgo_shells_cluster_shortened.pun.gz") # Get quantum cluster and ECP region indices skzcam_clusters.center_position = [0, 0, 2] - skzcam_clusters.pun_file = Path('mgo_shells_cluster_shortened.pun.gz') - skzcam_clusters.adsorbate = Atoms('CO', positions=[[0.0, 0.0, 0.0], [0.0, 0.0, 1.128]], pbc=[False, False, False]) + skzcam_clusters.pun_file = Path("mgo_shells_cluster_shortened.pun.gz") + skzcam_clusters.adsorbate = Atoms( + "CO", positions=[[0.0, 0.0, 0.0], [0.0, 0.0, 1.128]], pbc=[False, False, False] + ) skzcam_clusters.adsorbate_vector_from_slab = [0.0, 0.0, 2.0] skzcam_clusters.run_skzcam( @@ -112,7 +126,7 @@ def adsorbate_slab_embedded_cluster(skzcam_clusters): ecp_dist=3.0, shell_width=0.01, write_clusters=True, - write_clusters_path='./', + write_clusters_path="./", ) return skzcam_clusters.adsorbate_slab_embedded_cluster @@ -142,7 +156,6 @@ def element_info(): } - def test_MRCCInputGenerator_init(embedded_adsorbed_cluster, element_info): # Check what happens if multiplicities is not provided mrcc_input_generator = MRCCInputGenerator( @@ -1157,7 +1170,6 @@ def test_ORCAInputGenerator_create_point_charge_file(orca_input_generator, tmp_p def test_CreateSKZCAMClusters_run_chemshell(skzcam_clusters, tmp_path): - # Test if xyz file doesn't get written when write_xyz_file=False # skzcam_clusters_nowrite = deepcopy(skzcam_clusters) # skzcam_clusters_nowrite.convert_slab_to_atoms() @@ -1170,11 +1182,11 @@ def test_CreateSKZCAMClusters_run_chemshell(skzcam_clusters, tmp_path): # assert not os.path.isfile(tmp_path / "ChemShell_Cluster.xyz") with ( - gzip.open( - Path(FILE_DIR, "skzcam_files", "REF_ChemShell_Cluster.xyz.gz"), "rb" - ) as f_in, - Path(tmp_path, "ChemShell_Cluster.xyz").open(mode="wb") as f_out, - ): + gzip.open( + Path(FILE_DIR, "skzcam_files", "REF_ChemShell_Cluster.xyz.gz"), "rb" + ) as f_in, + Path(tmp_path, "ChemShell_Cluster.xyz").open(mode="wb") as f_out, + ): shutil.copyfileobj(f_in, f_out) # skzcam_clusters.convert_slab_to_atoms() # skzcam_clusters.run_chemshell( @@ -1190,25 +1202,29 @@ def test_CreateSKZCAMClusters_run_chemshell(skzcam_clusters, tmp_path): # Check that the positions and atomic numbers match reference assert_allclose( chemshell_embedded_cluster.get_positions()[::100], - np.array([[ 0.00000000e+00, 0.00000000e+00, -7.72802046e-03], - [-2.11024616e+00, 2.11024616e+00, -6.38586825e+00], - [ 6.33073849e+00, -2.11024616e+00, -6.38586825e+00], - [-1.09499282e+01, -4.53560876e+00, 4.95687508e+00]]), + np.array( + [ + [0.00000000e00, 0.00000000e00, -7.72802046e-03], + [-2.11024616e00, 2.11024616e00, -6.38586825e00], + [6.33073849e00, -2.11024616e00, -6.38586825e00], + [-1.09499282e01, -4.53560876e00, 4.95687508e00], + ] + ), rtol=1e-05, atol=1e-07, ) assert_equal( chemshell_embedded_cluster.get_atomic_numbers()[::40].tolist(), - [12, 12, 12, 8, 8, 8, 12, 9, 9], + [12, 12, 12, 8, 8, 8, 12, 9, 9], ) - def test_CreateSKZCAMClusters_convert_pun_to_atoms(skzcam_clusters): - - slab_embedded_cluster = skzcam_clusters.convert_pun_to_atoms(pun_file=Path(FILE_DIR, "skzcam_files", "ChemShell_Cluster.pun.gz")) - + slab_embedded_cluster = skzcam_clusters.convert_pun_to_atoms( + pun_file=Path(FILE_DIR, "skzcam_files", "ChemShell_Cluster.pun.gz") + ) + # Check that number of atoms matches our reference assert len(slab_embedded_cluster) == 390 @@ -1259,18 +1275,21 @@ def test_CreateSKZCAMClusters_convert_pun_to_atoms(skzcam_clusters): ) - def test_CreateSKZCAMClusters_convert_slab_to_atoms(): - # Test for CO on MgO example - skzcam_clusters = CreateSKZCAMClusters(adsorbate_indices=[0,1], slab_center_indices=[32], atom_oxi_states = {'Mg': 2.0, 'O': -2.0}, adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz"), pun_file=None) + skzcam_clusters = CreateSKZCAMClusters( + adsorbate_indices=[0, 1], + slab_center_indices=[32], + atom_oxi_states={"Mg": 2.0, "O": -2.0}, + adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz"), + pun_file=None, + ) skzcam_clusters.convert_slab_to_atoms() # Check adsorbate matches reference assert_allclose( skzcam_clusters.adsorbate.get_positions(), - np.array([[0. , 0. , 2.44102236], - [0. , 0. , 3.58784217]]), + np.array([[0.0, 0.0, 2.44102236], [0.0, 0.0, 3.58784217]]), rtol=1e-05, atol=1e-07, ) @@ -1279,25 +1298,29 @@ def test_CreateSKZCAMClusters_convert_slab_to_atoms(): # Check slab matches reference assert_allclose( skzcam_clusters.slab.get_positions()[::10], - np.array([[ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], - [-2.11024616e+00, 0.00000000e+00, -6.37814023e+00], - [ 2.11024616e+00, 2.11024616e+00, -4.26789407e+00], - [ 2.10705227e+00, 0.00000000e+00, -2.14155146e+00], - [-4.22049233e+00, -2.11024616e+00, -4.26789407e+00], - [ 0.00000000e+00, -4.22049233e+00, -6.37814023e+00], - [ 0.00000000e+00, -2.12018365e+00, 5.67208927e-03]]), + np.array( + [ + [0.00000000e00, 0.00000000e00, 0.00000000e00], + [-2.11024616e00, 0.00000000e00, -6.37814023e00], + [2.11024616e00, 2.11024616e00, -4.26789407e00], + [2.10705227e00, 0.00000000e00, -2.14155146e00], + [-4.22049233e00, -2.11024616e00, -4.26789407e00], + [0.00000000e00, -4.22049233e00, -6.37814023e00], + [0.00000000e00, -2.12018365e00, 5.67208927e-03], + ] + ), rtol=1e-05, atol=1e-07, ) assert_equal( skzcam_clusters.slab.get_atomic_numbers().tolist()[::10], - [12, 12, 12, 12, 8, 8, 8], + [12, 12, 12, 12, 8, 8, 8], ) # Check center_position matches reference assert_allclose( skzcam_clusters.center_position, - np.array([0. , 0. , 3.09607306]), + np.array([0.0, 0.0, 3.09607306]), rtol=1e-05, atol=1e-07, ) @@ -1305,20 +1328,27 @@ def test_CreateSKZCAMClusters_convert_slab_to_atoms(): # Check vector distance of adsorbate from first center atom (corresponding to first atom index) of slab matches reference assert_allclose( skzcam_clusters.adsorbate_vector_from_slab, - np.array([0. , 0. , 2.44102236]), + np.array([0.0, 0.0, 2.44102236]), rtol=1e-05, atol=1e-07, ) # Test for NO on MgO example - skzcam_clusters = CreateSKZCAMClusters(adsorbate_indices=[0,1], slab_center_indices=[32,33], atom_oxi_states = {'Mg': 2.0, 'O': -2.0}, adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "NO_MgO.poscar.gz"), pun_file=None) + skzcam_clusters = CreateSKZCAMClusters( + adsorbate_indices=[0, 1], + slab_center_indices=[32, 33], + atom_oxi_states={"Mg": 2.0, "O": -2.0}, + adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "NO_MgO.poscar.gz"), + pun_file=None, + ) skzcam_clusters.convert_slab_to_atoms() # Check adsorbate matches reference assert_allclose( skzcam_clusters.adsorbate.get_positions(), - np.array([[ 1.18932285, -0.14368533, 2.0777825 ], - [ 1.68437633, 0.62999818, 2.83068075]]), + np.array( + [[1.18932285, -0.14368533, 2.0777825], [1.68437633, 0.62999818, 2.83068075]] + ), rtol=1e-05, atol=1e-07, ) @@ -1327,25 +1357,29 @@ def test_CreateSKZCAMClusters_convert_slab_to_atoms(): # Check slab matches reference assert_allclose( skzcam_clusters.slab.get_positions()[::10], - np.array([[ 0. , 0. , 0. ], - [-4.2019821 , -2.10867761, -6.39202884], - [ 0.01851023, -4.21892378, -4.28178268], - [ 0.01903204, -2.105465 , -2.15224877], - [-4.2019821 , -2.10867761, -4.28178268], - [ 0.01851023, -4.21892378, -6.39202884], - [ 0.01900061, -2.11652633, -0.03021786]]), + np.array( + [ + [0.0, 0.0, 0.0], + [-4.2019821, -2.10867761, -6.39202884], + [0.01851023, -4.21892378, -4.28178268], + [0.01903204, -2.105465, -2.15224877], + [-4.2019821, -2.10867761, -4.28178268], + [0.01851023, -4.21892378, -6.39202884], + [0.01900061, -2.11652633, -0.03021786], + ] + ), rtol=1e-05, atol=1e-07, ) assert_equal( skzcam_clusters.slab.get_atomic_numbers().tolist()[::10], - [12, 12, 12, 12, 8, 8, 8], + [12, 12, 12, 12, 8, 8, 8], ) # Check center_position matches reference assert_allclose( skzcam_clusters.center_position, - np.array([ 1.06307888, -1.06176564, 2.47922285]), + np.array([1.06307888, -1.06176564, 2.47922285]), rtol=1e-05, atol=1e-07, ) @@ -1353,27 +1387,31 @@ def test_CreateSKZCAMClusters_convert_slab_to_atoms(): # Check vector distance of adsorbate from first center atom (corresponding to first atom index) of slab matches reference assert_allclose( skzcam_clusters.adsorbate_vector_from_slab, - np.array([ 1.18932285, -0.14368533, 2.0777825 ]), + np.array([1.18932285, -0.14368533, 2.0777825]), rtol=1e-05, atol=1e-07, ) - - -def test_CreateSKZCAMClusters_find_cation_shells(skzcam_clusters,slab_embedded_cluster): +def test_CreateSKZCAMClusters_find_cation_shells( + skzcam_clusters, slab_embedded_cluster +): # Get distance of atoms from the center distances = _get_atom_distances( atoms=slab_embedded_cluster, center_position=[0, 0, 2] ) # Find the cation shells from the distances - cation_shells_distances , cation_shells_idx = skzcam_clusters._find_cation_shells( - slab_embedded_cluster=slab_embedded_cluster, distances=distances, shell_width=0.005 + cation_shells_distances, cation_shells_idx = skzcam_clusters._find_cation_shells( + slab_embedded_cluster=slab_embedded_cluster, + distances=distances, + shell_width=0.005, ) # As these list of lists do not have the same length, we flatten first 5 lists into a 1D list for comparison - cation_shells_distances_flatten = [item for row in cation_shells_distances[:5] for item in row] + cation_shells_distances_flatten = [ + item for row in cation_shells_distances[:5] for item in row + ] cation_shells_idx_flatten = [item for row in cation_shells_idx[:5] for item in row] # Check that these lists are correct @@ -1406,7 +1444,9 @@ def test_CreateSKZCAMClusters_find_cation_shells(skzcam_clusters,slab_embedded_c ) -def test_CreateSKZCAMClusters_get_anion_coordination(skzcam_clusters,slab_embedded_cluster, distance_matrix): +def test_CreateSKZCAMClusters_get_anion_coordination( + skzcam_clusters, slab_embedded_cluster, distance_matrix +): # Get the anions for the second SKZCAM shell anion_shell_idx = skzcam_clusters._get_anion_coordination( slab_embedded_cluster=slab_embedded_cluster, @@ -1420,7 +1460,9 @@ def test_CreateSKZCAMClusters_get_anion_coordination(skzcam_clusters,slab_embedd ) -def test_CreateSKZCAMClusters_get_ecp_region(skzcam_clusters,slab_embedded_cluster, distance_matrix): +def test_CreateSKZCAMClusters_get_ecp_region( + skzcam_clusters, slab_embedded_cluster, distance_matrix +): # Find the ECP region for the first cluster ecp_region_idx = skzcam_clusters._get_ecp_region( slab_embedded_cluster=slab_embedded_cluster, @@ -1432,10 +1474,12 @@ def test_CreateSKZCAMClusters_get_ecp_region(skzcam_clusters,slab_embedded_clust # Check ECP region indices match with reference assert_equal(ecp_region_idx[0], [6, 7, 8, 9, 10, 11, 12, 13, 18, 19, 20, 21, 22]) -def test_CreateSKZCAMClusters_create_adsorbate_slab_embedded_cluster(skzcam_clusters, slab_embedded_cluster): +def test_CreateSKZCAMClusters_create_adsorbate_slab_embedded_cluster( + skzcam_clusters, slab_embedded_cluster +): skzcam_clusters.slab_embedded_cluster = slab_embedded_cluster - skzcam_clusters.adsorbate = Atoms( + skzcam_clusters.adsorbate = Atoms( "CO", positions=[[0.0, 0.0, 0.0], [0.0, 0.0, 1.128]], pbc=[False, False, False] ) skzcam_clusters.adsorbate_vector_from_slab = [0.0, 0.0, 2.0] @@ -1493,15 +1537,22 @@ def test_CreateSKZCAMClusters_create_adsorbate_slab_embedded_cluster(skzcam_clus ) # Check that the quantum_idx and ecp_idx match the reference - assert_equal(skzcam_clusters.quantum_cluster_indices, [[0, 1, 2, 3, 5, 6], [0, 1, 7, 8, 9, 10]]) + assert_equal( + skzcam_clusters.quantum_cluster_indices, + [[0, 1, 2, 3, 5, 6], [0, 1, 7, 8, 9, 10]], + ) assert_equal(skzcam_clusters.ecp_region_indices, [[2, 3, 5, 6], [7, 8, 9, 10]]) -def test_CreateSKZCAMClusters_run_skzcam(skzcam_clusters,tmp_path): +def test_CreateSKZCAMClusters_run_skzcam(skzcam_clusters, tmp_path): # Get quantum cluster and ECP region indices skzcam_clusters.center_position = [0, 0, 2] - skzcam_clusters.pun_file = Path(FILE_DIR, "skzcam_files", "ChemShell_Cluster.pun.gz") - skzcam_clusters.adsorbate = Atoms('CO', positions=[[0.0, 0.0, 0.0], [0.0, 0.0, 1.128]], pbc=[False, False, False]) + skzcam_clusters.pun_file = Path( + FILE_DIR, "skzcam_files", "ChemShell_Cluster.pun.gz" + ) + skzcam_clusters.adsorbate = Atoms( + "CO", positions=[[0.0, 0.0, 0.0], [0.0, 0.0, 1.128]], pbc=[False, False, False] + ) skzcam_clusters.adsorbate_vector_from_slab = [0.0, 0.0, 2.0] skzcam_clusters.run_skzcam( @@ -1515,40 +1566,104 @@ def test_CreateSKZCAMClusters_run_skzcam(skzcam_clusters,tmp_path): # Check quantum cluster indices match with reference assert_equal( skzcam_clusters.quantum_cluster_indices[1], - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 17, 18, 19, 25, 26, 27, 28, 29, 30, 31, 32]) + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 16, + 17, + 18, + 19, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + ], + ) # Check ECP region indices match with reference assert_equal( skzcam_clusters.ecp_region_indices[1], - [12, 13, 14, 15, 20, 21, 22, 23, 24, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 78, 79, 80, 81, 82, 83, 84, 85] -, + [ + 12, + 13, + 14, + 15, + 20, + 21, + 22, + 23, + 24, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + ], ) # Read the written output and check that it matches with the reference positions and atomic numbers skzcam_cluster_xyz = read(tmp_path / "SKZCAM_cluster_0.xyz") assert_allclose( skzcam_cluster_xyz.get_positions(), - np.array([[ 0. , 0. , 2. ], - [ 0. , 0. , 3.128 ], - [ 0. , 0. , 0. ], - [-2.12018426, 0. , 0.00567209], - [ 0. , 2.12018426, 0.00567209], - [ 2.12018426, 0. , 0.00567209], - [ 0. , -2.12018426, 0.00567209], - [ 0. , 0. , -2.14129966]]), + np.array( + [ + [0.0, 0.0, 2.0], + [0.0, 0.0, 3.128], + [0.0, 0.0, 0.0], + [-2.12018426, 0.0, 0.00567209], + [0.0, 2.12018426, 0.00567209], + [2.12018426, 0.0, 0.00567209], + [0.0, -2.12018426, 0.00567209], + [0.0, 0.0, -2.14129966], + ] + ), rtol=1e-04, atol=1e-07, ) - assert_equal(skzcam_cluster_xyz.get_atomic_numbers().tolist(), [6,8,12, 8, 8, 8, 8, 8]) + assert_equal( + skzcam_cluster_xyz.get_atomic_numbers().tolist(), [6, 8, 12, 8, 8, 8, 8, 8] + ) + def test_get_atom_distances(): # Creating a H2 molecule as an Atoms object h2_molecule = Atoms("H2", positions=[(0, 0, 0), (0, 0, 2)]) # Run _get_atom_distances function to get distance of h2 molecule atoms from a center position - atom_distances = _get_atom_distances( - atoms=h2_molecule, center_position=[2, 0, 0] - ) + atom_distances = _get_atom_distances(atoms=h2_molecule, center_position=[2, 0, 0]) - assert_allclose(atom_distances, np.array([2.0, 2.82842712]), rtol=1e-05, atol=1e-07) \ No newline at end of file + assert_allclose(atom_distances, np.array([2.0, 2.82842712]), rtol=1e-05, atol=1e-07) From b0af38072943bb2feef2d2e2dbcf723e1e75c0ff Mon Sep 17 00:00:00 2001 From: benshi97 Date: Sun, 9 Jun 2024 16:11:50 +0100 Subject: [PATCH 07/29] Better variable definitions --- src/quacc/atoms/skzcam.py | 34 +++++++-------- tests/core/atoms/test_skzcam.py | 76 ++++++++++++++++----------------- 2 files changed, 53 insertions(+), 57 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index e1e11edd43..d69a00199e 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -162,7 +162,7 @@ class MultiplicityDict(TypedDict): class MRCCInputGenerator: def __init__( self, - embedded_adsorbed_cluster: Atoms, + adsorbate_slab_embedded_cluster: Atoms, quantum_cluster_indices: list[int], ecp_region_indices: list[int], element_info: dict[ElementStr, ElementInfo] | None = None, @@ -172,7 +172,7 @@ def __init__( """ Parameters ---------- - embedded_adsorbed_cluster + adsorbate_slab_embedded_cluster The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This object is created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. quantum_cluster_indices A list containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. @@ -196,7 +196,7 @@ def __init__( "An atom in the quantum cluster is also in the ECP region." ) - self.embedded_adsorbed_cluster = embedded_adsorbed_cluster + self.adsorbate_slab_embedded_cluster = adsorbate_slab_embedded_cluster self.quantum_cluster_indices = quantum_cluster_indices self.ecp_region_indices = ecp_region_indices self.element_info = element_info @@ -208,10 +208,10 @@ def __init__( self.multiplicities = multiplicities # Create the adsorbate-slab complex quantum cluster and ECP region cluster - self.adsorbate_slab_cluster = self.embedded_adsorbed_cluster[ + self.adsorbate_slab_cluster = self.adsorbate_slab_embedded_cluster[ self.quantum_cluster_indices ] - self.ecp_region = self.embedded_adsorbed_cluster[self.ecp_region_indices] + self.ecp_region = self.adsorbate_slab_embedded_cluster[self.ecp_region_indices] # Get the indices of the adsorbates from the quantum cluster self.adsorbate_indices = [ @@ -467,12 +467,12 @@ def generate_point_charge_block(self) -> str: """ # Get the oxi_states arrays from the embedded_cluster - oxi_states = self.embedded_adsorbed_cluster.get_array("oxi_states") + oxi_states = self.adsorbate_slab_embedded_cluster.get_array("oxi_states") # Get the number of point charges for this system. There is a point charge associated with each capped ECP as well. pc_region_indices = [ atom.index - for atom in self.embedded_adsorbed_cluster + for atom in self.adsorbate_slab_embedded_cluster if atom.index not in self.quantum_cluster_indices ] @@ -481,7 +481,7 @@ def generate_point_charge_block(self) -> str: # Add the ecp_region indices for i in pc_region_indices: - position = self.embedded_adsorbed_cluster[i].position + position = self.adsorbate_slab_embedded_cluster[i].position pc_block += f" {position[0]:-16.11f} {position[1]:-16.11f} {position[2]:-16.11f} {oxi_states[i]:-16.11f}\n" return pc_block @@ -490,7 +490,7 @@ def generate_point_charge_block(self) -> str: class ORCAInputGenerator: def __init__( self, - embedded_adsorbed_cluster: Atoms, + adsorbate_slab_embedded_cluster: Atoms, quantum_cluster_indices: list[int], ecp_region_indices: list[int], element_info: dict[ElementStr, ElementInfo] | None = None, @@ -504,7 +504,7 @@ def __init__( """ Parameters ---------- - embedded_adsorbed_cluster + adsorbate_slab_embedded_cluster The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This object is created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. quantum_cluster_indices A list containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. @@ -536,7 +536,7 @@ def __init__( "An atom in the quantum cluster is also in the ECP region." ) - self.embedded_adsorbed_cluster = embedded_adsorbed_cluster + self.adsorbate_slab_embedded_cluster = adsorbate_slab_embedded_cluster self.quantum_cluster_indices = quantum_cluster_indices self.ecp_region_indices = ecp_region_indices self.element_info = element_info @@ -548,10 +548,10 @@ def __init__( self.multiplicities = multiplicities # Create the adsorbate-slab complex quantum cluster and ECP region cluster - self.adsorbate_slab_cluster = self.embedded_adsorbed_cluster[ + self.adsorbate_slab_cluster = self.adsorbate_slab_embedded_cluster[ self.quantum_cluster_indices ] - self.ecp_region = self.embedded_adsorbed_cluster[self.ecp_region_indices] + self.ecp_region = self.adsorbate_slab_embedded_cluster[self.ecp_region_indices] # Get the indices of the adsorbates from the quantum cluster self.adsorbate_indices = [ @@ -839,19 +839,19 @@ def create_point_charge_file(self, pc_file: str | Path) -> None: """ # Get the oxi_states arrays from the embedded_cluster - oxi_states = self.embedded_adsorbed_cluster.get_array("oxi_states") + oxi_states = self.adsorbate_slab_embedded_cluster.get_array("oxi_states") # Get the number of point charges for this system total_indices = self.quantum_cluster_indices + self.ecp_region_indices - num_pc = len(self.embedded_adsorbed_cluster) - len(total_indices) + num_pc = len(self.adsorbate_slab_embedded_cluster) - len(total_indices) counter = 0 with Path.open(pc_file, "w") as f: # Write the number of point charges first f.write(f"{num_pc}\n") - for i in range(len(self.embedded_adsorbed_cluster)): + for i in range(len(self.adsorbate_slab_embedded_cluster)): if i not in total_indices: counter += 1 - position = self.embedded_adsorbed_cluster[i].position + position = self.adsorbate_slab_embedded_cluster[i].position if counter != num_pc: f.write( f"{oxi_states[i]:-16.11f} {position[0]:-16.11f} {position[1]:-16.11f} {position[2]:-16.11f}\n" diff --git a/tests/core/atoms/test_skzcam.py b/tests/core/atoms/test_skzcam.py index 66f0d94bcc..17a082432d 100644 --- a/tests/core/atoms/test_skzcam.py +++ b/tests/core/atoms/test_skzcam.py @@ -47,7 +47,7 @@ def distance_matrix(slab_embedded_cluster): @pytest.fixture() -def embedded_adsorbed_cluster(): +def adsorbate_slab_embedded_cluster(): with gzip.open( Path(FILE_DIR, "skzcam_files", "adsorbate_slab_embedded_cluster.npy.gz"), "r" ) as file: @@ -55,9 +55,9 @@ def embedded_adsorbed_cluster(): @pytest.fixture() -def mrcc_input_generator(embedded_adsorbed_cluster, element_info): +def mrcc_input_generator(adsorbate_slab_embedded_cluster, element_info): return MRCCInputGenerator( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, + adsorbate_slab_embedded_cluster=adsorbate_slab_embedded_cluster, quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], element_info=element_info, @@ -67,7 +67,7 @@ def mrcc_input_generator(embedded_adsorbed_cluster, element_info): @pytest.fixture() -def orca_input_generator(embedded_adsorbed_cluster, element_info): +def orca_input_generator(adsorbate_slab_embedded_cluster, element_info): pal_nprocs_block = {"nprocs": 1, "maxcore": 5000} method_block = {"Method": "hf", "RI": "on", "RunTyp": "Energy"} @@ -97,7 +97,7 @@ def orca_input_generator(embedded_adsorbed_cluster, element_info): end""" } return ORCAInputGenerator( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, + adsorbate_slab_embedded_cluster=adsorbate_slab_embedded_cluster, quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], element_info=element_info, @@ -109,29 +109,6 @@ def orca_input_generator(embedded_adsorbed_cluster, element_info): ecp_info=ecp_info, ) - -@pytest.fixture() -def adsorbate_slab_embedded_cluster(skzcam_clusters): - skzcam_clusters.convert_pun_to_atoms("mgo_shells_cluster_shortened.pun.gz") - # Get quantum cluster and ECP region indices - skzcam_clusters.center_position = [0, 0, 2] - skzcam_clusters.pun_file = Path("mgo_shells_cluster_shortened.pun.gz") - skzcam_clusters.adsorbate = Atoms( - "CO", positions=[[0.0, 0.0, 0.0], [0.0, 0.0, 1.128]], pbc=[False, False, False] - ) - skzcam_clusters.adsorbate_vector_from_slab = [0.0, 0.0, 2.0] - - skzcam_clusters.run_skzcam( - shell_max=2, - ecp_dist=3.0, - shell_width=0.01, - write_clusters=True, - write_clusters_path="./", - ) - - return skzcam_clusters.adsorbate_slab_embedded_cluster - - @pytest.fixture() def element_info(): return { @@ -156,10 +133,10 @@ def element_info(): } -def test_MRCCInputGenerator_init(embedded_adsorbed_cluster, element_info): +def test_MRCCInputGenerator_init(adsorbate_slab_embedded_cluster, element_info): # Check what happens if multiplicities is not provided mrcc_input_generator = MRCCInputGenerator( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, + adsorbate_slab_embedded_cluster=adsorbate_slab_embedded_cluster, quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], element_info=element_info, @@ -173,7 +150,7 @@ def test_MRCCInputGenerator_init(embedded_adsorbed_cluster, element_info): } mrcc_input_generator = MRCCInputGenerator( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, + adsorbate_slab_embedded_cluster=adsorbate_slab_embedded_cluster, quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], element_info=element_info, @@ -182,7 +159,7 @@ def test_MRCCInputGenerator_init(embedded_adsorbed_cluster, element_info): ) assert not compare_atoms( - mrcc_input_generator.embedded_adsorbed_cluster, embedded_adsorbed_cluster + mrcc_input_generator.adsorbate_slab_embedded_cluster, adsorbate_slab_embedded_cluster ) assert_equal(mrcc_input_generator.quantum_cluster_indices, [0, 1, 2, 3, 4, 5, 6, 7]) assert_equal(mrcc_input_generator.adsorbate_indices, [0, 1]) @@ -205,7 +182,7 @@ def test_MRCCInputGenerator_init(embedded_adsorbed_cluster, element_info): ValueError, match="An atom in the quantum cluster is also in the ECP region." ): mrcc_input_generator = MRCCInputGenerator( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, + adsorbate_slab_embedded_cluster=adsorbate_slab_embedded_cluster, quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], ecp_region_indices=[7, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], element_info=element_info, @@ -555,7 +532,7 @@ def test_MRCCInputGenerator_generate_point_charge_block(mrcc_input_generator): ) -def test_ORCAInputGenerator_init(embedded_adsorbed_cluster, element_info): +def test_ORCAInputGenerator_init(adsorbate_slab_embedded_cluster, element_info): pal_nprocs_block = {"nprocs": 1, "maxcore": 5000} method_block = {"Method": "hf", "RI": "on", "RunTyp": "Energy"} @@ -585,7 +562,7 @@ def test_ORCAInputGenerator_init(embedded_adsorbed_cluster, element_info): end""" } orca_input_generator = ORCAInputGenerator( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, + adsorbate_slab_embedded_cluster=adsorbate_slab_embedded_cluster, quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], element_info=element_info, @@ -604,7 +581,7 @@ def test_ORCAInputGenerator_init(embedded_adsorbed_cluster, element_info): } orca_input_generator = ORCAInputGenerator( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, + adsorbate_slab_embedded_cluster=adsorbate_slab_embedded_cluster, quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], ecp_region_indices=[8, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], element_info=element_info, @@ -617,7 +594,7 @@ def test_ORCAInputGenerator_init(embedded_adsorbed_cluster, element_info): ) assert not compare_atoms( - orca_input_generator.embedded_adsorbed_cluster, embedded_adsorbed_cluster + orca_input_generator.adsorbate_slab_embedded_cluster, adsorbate_slab_embedded_cluster ) assert_equal(orca_input_generator.quantum_cluster_indices, [0, 1, 2, 3, 4, 5, 6, 7]) assert_equal(orca_input_generator.adsorbate_indices, [0, 1]) @@ -645,7 +622,7 @@ def test_ORCAInputGenerator_init(embedded_adsorbed_cluster, element_info): ValueError, match="An atom in the quantum cluster is also in the ECP region." ): orca_input_generator = ORCAInputGenerator( - embedded_adsorbed_cluster=embedded_adsorbed_cluster, + adsorbate_slab_embedded_cluster=adsorbate_slab_embedded_cluster, quantum_cluster_indices=[0, 1, 2, 3, 4, 5, 6, 7], ecp_region_indices=[7, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24], element_info=element_info, @@ -825,8 +802,8 @@ def test_ORCAInputGenerator_generate_input(orca_input_generator): ) -def test_create_atom_coord_string(embedded_adsorbed_cluster): - atom = embedded_adsorbed_cluster[0] +def test_create_atom_coord_string(adsorbate_slab_embedded_cluster): + atom = adsorbate_slab_embedded_cluster[0] # First let's try the case where it's a normal atom. atom_coord_string = create_atom_coord_string(atom=atom) @@ -1168,6 +1145,25 @@ def test_ORCAInputGenerator_create_point_charge_file(orca_input_generator, tmp_p atol=1e-07, ) +def test_CreateSKZCAMClusters_init(): + skzcam_clusters = CreateSKZCAMClusters(adsorbate_indices=[0,1], slab_center_indices=[32], atom_oxi_states = {'Mg': 2.0, 'O': -2.0}, adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz"), pun_file='test.pun') + + assert_equal(skzcam_clusters.adsorbate_indices, [0, 1]) + assert skzcam_clusters.slab_center_indices == [32] + assert skzcam_clusters.atom_oxi_states == {'Mg': 2.0, 'O': -2.0} + assert skzcam_clusters.adsorbate_slab_file == Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz") + assert skzcam_clusters.pun_file == 'test.pun' + + # Check if error raised if adsorbate_indices and slab_center_indices overlap + with pytest.raises(ValueError, match="The adsorbate and slab center indices cannot be the same."): + skzcam_clusters = CreateSKZCAMClusters(adsorbate_indices=[0,1], slab_center_indices=[0], atom_oxi_states = {'Mg': 2.0, 'O': -2.0}, adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz"), pun_file='test.pun') + + # Check if error raised if both adsorbate_slab_file and pun_file are None + with pytest.raises(ValueError, match="Either the adsorbate_slab_file or pun_file must be provided."): + skzcam_clusters = CreateSKZCAMClusters(adsorbate_indices=[0,1], slab_center_indices=[32], atom_oxi_states = {'Mg': 2.0, 'O': -2.0}, adsorbate_slab_file=None, pun_file=None) + + + def test_CreateSKZCAMClusters_run_chemshell(skzcam_clusters, tmp_path): # Test if xyz file doesn't get written when write_xyz_file=False From a00901e9994d01fcef466d5daa480125a6346f37 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 9 Jun 2024 15:12:03 +0000 Subject: [PATCH 08/29] pre-commit auto-fixes --- tests/core/atoms/test_skzcam.py | 50 +++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/tests/core/atoms/test_skzcam.py b/tests/core/atoms/test_skzcam.py index 17a082432d..925e22f488 100644 --- a/tests/core/atoms/test_skzcam.py +++ b/tests/core/atoms/test_skzcam.py @@ -109,6 +109,7 @@ def orca_input_generator(adsorbate_slab_embedded_cluster, element_info): ecp_info=ecp_info, ) + @pytest.fixture() def element_info(): return { @@ -159,7 +160,8 @@ def test_MRCCInputGenerator_init(adsorbate_slab_embedded_cluster, element_info): ) assert not compare_atoms( - mrcc_input_generator.adsorbate_slab_embedded_cluster, adsorbate_slab_embedded_cluster + mrcc_input_generator.adsorbate_slab_embedded_cluster, + adsorbate_slab_embedded_cluster, ) assert_equal(mrcc_input_generator.quantum_cluster_indices, [0, 1, 2, 3, 4, 5, 6, 7]) assert_equal(mrcc_input_generator.adsorbate_indices, [0, 1]) @@ -594,7 +596,8 @@ def test_ORCAInputGenerator_init(adsorbate_slab_embedded_cluster, element_info): ) assert not compare_atoms( - orca_input_generator.adsorbate_slab_embedded_cluster, adsorbate_slab_embedded_cluster + orca_input_generator.adsorbate_slab_embedded_cluster, + adsorbate_slab_embedded_cluster, ) assert_equal(orca_input_generator.quantum_cluster_indices, [0, 1, 2, 3, 4, 5, 6, 7]) assert_equal(orca_input_generator.adsorbate_indices, [0, 1]) @@ -1145,24 +1148,47 @@ def test_ORCAInputGenerator_create_point_charge_file(orca_input_generator, tmp_p atol=1e-07, ) + def test_CreateSKZCAMClusters_init(): - skzcam_clusters = CreateSKZCAMClusters(adsorbate_indices=[0,1], slab_center_indices=[32], atom_oxi_states = {'Mg': 2.0, 'O': -2.0}, adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz"), pun_file='test.pun') + skzcam_clusters = CreateSKZCAMClusters( + adsorbate_indices=[0, 1], + slab_center_indices=[32], + atom_oxi_states={"Mg": 2.0, "O": -2.0}, + adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz"), + pun_file="test.pun", + ) assert_equal(skzcam_clusters.adsorbate_indices, [0, 1]) assert skzcam_clusters.slab_center_indices == [32] - assert skzcam_clusters.atom_oxi_states == {'Mg': 2.0, 'O': -2.0} - assert skzcam_clusters.adsorbate_slab_file == Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz") - assert skzcam_clusters.pun_file == 'test.pun' + assert skzcam_clusters.atom_oxi_states == {"Mg": 2.0, "O": -2.0} + assert skzcam_clusters.adsorbate_slab_file == Path( + FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz" + ) + assert skzcam_clusters.pun_file == "test.pun" # Check if error raised if adsorbate_indices and slab_center_indices overlap - with pytest.raises(ValueError, match="The adsorbate and slab center indices cannot be the same."): - skzcam_clusters = CreateSKZCAMClusters(adsorbate_indices=[0,1], slab_center_indices=[0], atom_oxi_states = {'Mg': 2.0, 'O': -2.0}, adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz"), pun_file='test.pun') + with pytest.raises( + ValueError, match="The adsorbate and slab center indices cannot be the same." + ): + skzcam_clusters = CreateSKZCAMClusters( + adsorbate_indices=[0, 1], + slab_center_indices=[0], + atom_oxi_states={"Mg": 2.0, "O": -2.0}, + adsorbate_slab_file=Path(FILE_DIR, "skzcam_files", "CO_MgO.poscar.gz"), + pun_file="test.pun", + ) # Check if error raised if both adsorbate_slab_file and pun_file are None - with pytest.raises(ValueError, match="Either the adsorbate_slab_file or pun_file must be provided."): - skzcam_clusters = CreateSKZCAMClusters(adsorbate_indices=[0,1], slab_center_indices=[32], atom_oxi_states = {'Mg': 2.0, 'O': -2.0}, adsorbate_slab_file=None, pun_file=None) - - + with pytest.raises( + ValueError, match="Either the adsorbate_slab_file or pun_file must be provided." + ): + skzcam_clusters = CreateSKZCAMClusters( + adsorbate_indices=[0, 1], + slab_center_indices=[32], + atom_oxi_states={"Mg": 2.0, "O": -2.0}, + adsorbate_slab_file=None, + pun_file=None, + ) def test_CreateSKZCAMClusters_run_chemshell(skzcam_clusters, tmp_path): From ce0e10641ac584095f0ef418cdb724cf13c724f9 Mon Sep 17 00:00:00 2001 From: Andrew Rosen Date: Sun, 9 Jun 2024 12:57:46 -0700 Subject: [PATCH 09/29] Clean up docstrings --- src/quacc/atoms/skzcam.py | 55 ++++++++++++--------------------------- 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index d69a00199e..6f459988f0 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -160,6 +160,10 @@ class MultiplicityDict(TypedDict): class MRCCInputGenerator: + """ + A class to generate the skzcam input for the MRCC ASE calculator. + """ + def __init__( self, adsorbate_slab_embedded_cluster: Atoms, @@ -239,9 +243,6 @@ def generate_input(self) -> BlockInfo: """ Creates the mrccblocks input for the MRCC ASE calculator. - Parameters - ---------- - Returns ------- BlockInfo @@ -266,9 +267,6 @@ def generate_basis_ecp_block(self) -> None: """ Generates the basis and ECP block for the MRCC input file. - Parameters - ---------- - Returns ------- None @@ -347,9 +345,6 @@ def generate_coords_block(self) -> None: """ Generates the coordinates block for the MRCC input file. This includes the coordinates of the quantum cluster, the ECP region, and the point charges. It will return three strings for the adsorbate-slab complex, adsorbate and slab. - Parameters - ---------- - Returns ------- None @@ -457,9 +452,6 @@ def generate_point_charge_block(self) -> str: """ Create the point charge block for the MRCC input file. This requires the embedded_cluster Atoms object containing both atom_type and oxi_states arrays, as well as the indices of the quantum cluster and ECP region. Such arrays are created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. - Parameters - ---------- - Returns ------- str @@ -488,6 +480,9 @@ def generate_point_charge_block(self) -> str: class ORCAInputGenerator: + """ + A class to generate the skzcam input for the ORCA ASE calculator. + """ def __init__( self, adsorbate_slab_embedded_cluster: Atoms, @@ -584,9 +579,6 @@ def generate_input(self) -> None: """ Creates the orcablocks input for the ORCA ASE calculator. - Parameters - ---------- - Returns ------- None @@ -605,10 +597,6 @@ def generate_coords_block(self) -> None: """ Generates the coordinates block for the ORCA input file. This includes the coordinates of the quantum cluster, the ECP region, and the point charges. It will return three strings for the adsorbate-slab complex, adsorbate and slab. - Parameters - ---------- - - Returns ------- None @@ -706,9 +694,6 @@ def generate_preamble_block(self) -> str: """ From the quantum cluster Atoms object, generate the ORCA input preamble for the basis, method, pal, and scf blocks. - Parameters - ---------- - Returns ------- None @@ -907,6 +892,9 @@ def create_atom_coord_string( class CreateSKZCAMClusters: + """ + A class to create the quantum clusters and ECP regions for the SKZCAM protocol. + """ def __init__( self, adsorbate_indices: list[int], @@ -914,7 +902,7 @@ def __init__( atom_oxi_states: dict[str, int], adsorbate_slab_file: str | Path | None = None, pun_file: str | Path | None = None, - ): + ) -> None: """ Parameters ---------- @@ -928,6 +916,10 @@ def __init__( The path to the file containing the adsorbate molecule on the surface slab. It can be in any format that ASE can read. pun_file The path to the .pun file containing the atomic coordinates and charges of the adsorbate-slab complex. This file should be generated by ChemShell. If it is None, then ChemShell wil be used to create this file. + + Returns + ------- + None """ self.adsorbate_indices = adsorbate_indices @@ -1140,16 +1132,7 @@ def convert_slab_to_atoms(self) -> None: Returns ------- - Atoms - The Atoms object of the adsorbate molecule. - Atoms - The Atoms object of the surface slab. - int - The index of the first atom of the slab as listed in slab_center_indices. - NDArray - The position of the center of the cluster. - NDArray - The vector from the center of the slab to the center of mass of the adsorbate. + None """ # Get the necessary information for the cluster from a provided slab file (in any format that ASE can read) @@ -1201,8 +1184,6 @@ def convert_pun_to_atoms(self, pun_file: str | Path) -> Atoms: ---------- pun_file The path to the .pun file created by ChemShell to be read. - atom_oxi_states - A dictionary containing the atomic symbols as keys and the oxidation states as values. Returns ------- @@ -1287,8 +1268,6 @@ def create_adsorbate_slab_embedded_cluster( Parameters ---------- - adsorbate_vector_from_slab - The vector from the first atom of the embedded cluster to the center of mass of the adsorbate. quantum_cluster_indices A list of lists containing the indices of the atoms in each quantum cluster. ecp_region_indices @@ -1338,7 +1317,7 @@ def create_adsorbate_slab_embedded_cluster( def _find_cation_shells( self, slab_embedded_cluster: Atoms, distances: NDArray, shell_width: float = 0.1 - ) -> list[list[int]]: + ) -> tuple[list[list[int]],list[list[int]]]: """ Returns a list of lists containing the indices of the cations in each shell, based on distance from the embedded cluster center. This is achieved by clustering the data based on the DBSCAN clustering algorithm. From 7436ea050fb9b4f6b82364579cac06ee0c7aa914 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 9 Jun 2024 19:58:00 +0000 Subject: [PATCH 10/29] pre-commit auto-fixes --- src/quacc/atoms/skzcam.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index 6f459988f0..7e6260257e 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -483,6 +483,7 @@ class ORCAInputGenerator: """ A class to generate the skzcam input for the ORCA ASE calculator. """ + def __init__( self, adsorbate_slab_embedded_cluster: Atoms, @@ -895,6 +896,7 @@ class CreateSKZCAMClusters: """ A class to create the quantum clusters and ECP regions for the SKZCAM protocol. """ + def __init__( self, adsorbate_indices: list[int], @@ -1317,7 +1319,7 @@ def create_adsorbate_slab_embedded_cluster( def _find_cation_shells( self, slab_embedded_cluster: Atoms, distances: NDArray, shell_width: float = 0.1 - ) -> tuple[list[list[int]],list[list[int]]]: + ) -> tuple[list[list[int]], list[list[int]]]: """ Returns a list of lists containing the indices of the cations in each shell, based on distance from the embedded cluster center. This is achieved by clustering the data based on the DBSCAN clustering algorithm. From 115da6ffc48f07e3c2bd87b5802d69ce71a8cb6d Mon Sep 17 00:00:00 2001 From: Andrew Rosen Date: Sun, 9 Jun 2024 12:59:53 -0700 Subject: [PATCH 11/29] Fix conftest --- tests/core/atoms/{conftest_noworking.py => conftest.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename tests/core/atoms/{conftest_noworking.py => conftest.py} (82%) diff --git a/tests/core/atoms/conftest_noworking.py b/tests/core/atoms/conftest.py similarity index 82% rename from tests/core/atoms/conftest_noworking.py rename to tests/core/atoms/conftest.py index ff60f216c8..c2a826f7de 100644 --- a/tests/core/atoms/conftest_noworking.py +++ b/tests/core/atoms/conftest.py @@ -21,8 +21,8 @@ def mock_run_chemshell(slab, slab_center_idx, atom_oxi_states, filepath, **kwarg @pytest.fixture(autouse=True) def patch_run_chemshell(monkeypatch): - from quacc.atoms import skzcam + from quacc.atoms.skzcam import CreateSKZCAMClusters monkeypatch.setattr( - skzcam, "CreateSKZCAMClusters.run_chemshell", mock_run_chemshell + CreateSKZCAMClusters, "run_chemshell", mock_run_chemshell ) From 96f03655ed2f518d83c5f4ca7faee012dec0ae53 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 9 Jun 2024 20:00:06 +0000 Subject: [PATCH 12/29] pre-commit auto-fixes --- tests/core/atoms/conftest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/core/atoms/conftest.py b/tests/core/atoms/conftest.py index c2a826f7de..5a720e2409 100644 --- a/tests/core/atoms/conftest.py +++ b/tests/core/atoms/conftest.py @@ -23,6 +23,4 @@ def mock_run_chemshell(slab, slab_center_idx, atom_oxi_states, filepath, **kwarg def patch_run_chemshell(monkeypatch): from quacc.atoms.skzcam import CreateSKZCAMClusters - monkeypatch.setattr( - CreateSKZCAMClusters, "run_chemshell", mock_run_chemshell - ) + monkeypatch.setattr(CreateSKZCAMClusters, "run_chemshell", mock_run_chemshell) From e063664674a6585926b093b04442b6d5efb31b1c Mon Sep 17 00:00:00 2001 From: Andrew Rosen Date: Sun, 9 Jun 2024 13:01:06 -0700 Subject: [PATCH 13/29] Fix docs --- src/quacc/atoms/skzcam.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index 7e6260257e..85993c7e31 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -1129,9 +1129,6 @@ def convert_slab_to_atoms(self) -> None: """ Read the file containing the periodic slab and adsorbate (geometry optimized) and format the resulting Atoms object to be used to create a .pun file in ChemShell. - Parameters - ---------- - Returns ------- None From 5625900707cab346d11622cc18bbf532dd65f8dc Mon Sep 17 00:00:00 2001 From: Andrew Rosen Date: Sun, 9 Jun 2024 13:08:49 -0700 Subject: [PATCH 14/29] small cleanup --- src/quacc/atoms/skzcam.py | 58 +++++++++++++++------------------------ 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index 85993c7e31..af22f08eb0 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -194,22 +194,18 @@ def __init__( None """ - # Check that none of the indices in quantum_cluster_indices are in ecp_region_indices - if not np.all([x not in ecp_region_indices for x in quantum_cluster_indices]): - raise ValueError( - "An atom in the quantum cluster is also in the ECP region." - ) - self.adsorbate_slab_embedded_cluster = adsorbate_slab_embedded_cluster self.quantum_cluster_indices = quantum_cluster_indices self.ecp_region_indices = ecp_region_indices self.element_info = element_info + self.include_cp = include_cp + self.multiplicities = {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} if multiplicities is None else multiplicities - # Set multiplicities - if multiplicities is None: - self.multiplicities = {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} - else: - self.multiplicities = multiplicities + # Check that none of the indices in quantum_cluster_indices are in ecp_region_indices + if not np.all([x not in self.ecp_region_indices for x in self.quantum_cluster_indices]): + raise ValueError( + "An atom in the quantum cluster is also in the ECP region." + ) # Create the adsorbate-slab complex quantum cluster and ECP region cluster self.adsorbate_slab_cluster = self.adsorbate_slab_embedded_cluster[ @@ -234,8 +230,6 @@ def __init__( self.adsorbate_cluster = self.adsorbate_slab_cluster[self.adsorbate_indices] self.slab_cluster = self.adsorbate_slab_cluster[self.slab_indices] - self.include_cp = include_cp - # Initialize the mrccblocks input strings for the adsorbate-slab complex, adsorbate, and slab self.mrccblocks = {"adsorbate_slab": "", "adsorbate": "", "slab": ""} @@ -526,22 +520,22 @@ def __init__( None """ - # Check that none of the indices in quantum_cluster_indices are in ecp_region_indices - if not np.all([x not in ecp_region_indices for x in quantum_cluster_indices]): - raise ValueError( - "An atom in the quantum cluster is also in the ECP region." - ) - self.adsorbate_slab_embedded_cluster = adsorbate_slab_embedded_cluster self.quantum_cluster_indices = quantum_cluster_indices self.ecp_region_indices = ecp_region_indices self.element_info = element_info + self.include_cp = include_cp + self.multiplicities = {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} if multiplicities is None else multiplicities + self.pal_nprocs_block = pal_nprocs_block + self.method_block = method_block + self.scf_block = scf_block + self.ecp_info = ecp_info - # Set multiplicities - if multiplicities is None: - self.multiplicities = {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} - else: - self.multiplicities = multiplicities + # Check that none of the indices in quantum_cluster_indices are in ecp_region_indices + if not np.all([x not in self.ecp_region_indices for x in self.quantum_cluster_indices]): + raise ValueError( + "An atom in the quantum cluster is also in the ECP region." + ) # Create the adsorbate-slab complex quantum cluster and ECP region cluster self.adsorbate_slab_cluster = self.adsorbate_slab_embedded_cluster[ @@ -566,13 +560,6 @@ def __init__( self.adsorbate_cluster = self.adsorbate_slab_cluster[self.adsorbate_indices] self.slab_cluster = self.adsorbate_slab_cluster[self.slab_indices] - self.include_cp = include_cp - - self.pal_nprocs_block = pal_nprocs_block - self.method_block = method_block - self.scf_block = scf_block - self.ecp_info = ecp_info - # Initialize the orcablocks input strings for the adsorbate-slab complex, adsorbate, and slab self.orcablocks = {"adsorbate_slab": "", "adsorbate": "", "slab": ""} @@ -928,22 +915,21 @@ def __init__( self.slab_center_indices = slab_center_indices self.slab_indices = None # This will be set later self.atom_oxi_states = atom_oxi_states + self.adsorbate_slab_file = adsorbate_slab_file + self.pun_file = pun_file # Check that the adsorbate_indices and slab_center_indices are not the same - if any(x in adsorbate_indices for x in slab_center_indices): + if any(x in self.adsorbate_indices for x in self.slab_center_indices): raise ValueError( "The adsorbate and slab center indices cannot be the same." ) # Check that the adsorbate_slab_file and pun_file are not both None - if adsorbate_slab_file is None and pun_file is None: + if self.adsorbate_slab_file is None and self.pun_file is None: raise ValueError( "Either the adsorbate_slab_file or pun_file must be provided." ) - self.adsorbate_slab_file = adsorbate_slab_file - self.pun_file = pun_file - # Initialize the adsorbate, slab and adsorbate_slab Atoms object which contains the adsorbate, slab and adsorbate-slab complex respectively self.adsorbate = None self.slab = None From 3bee19c8c78b278880f78e419df089141bcc8491 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 9 Jun 2024 20:09:02 +0000 Subject: [PATCH 15/29] pre-commit auto-fixes --- src/quacc/atoms/skzcam.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index af22f08eb0..82732e2544 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -199,10 +199,16 @@ def __init__( self.ecp_region_indices = ecp_region_indices self.element_info = element_info self.include_cp = include_cp - self.multiplicities = {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} if multiplicities is None else multiplicities + self.multiplicities = ( + {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} + if multiplicities is None + else multiplicities + ) # Check that none of the indices in quantum_cluster_indices are in ecp_region_indices - if not np.all([x not in self.ecp_region_indices for x in self.quantum_cluster_indices]): + if not np.all( + [x not in self.ecp_region_indices for x in self.quantum_cluster_indices] + ): raise ValueError( "An atom in the quantum cluster is also in the ECP region." ) @@ -525,14 +531,20 @@ def __init__( self.ecp_region_indices = ecp_region_indices self.element_info = element_info self.include_cp = include_cp - self.multiplicities = {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} if multiplicities is None else multiplicities + self.multiplicities = ( + {"adsorbate_slab": 1, "adsorbate": 1, "slab": 1} + if multiplicities is None + else multiplicities + ) self.pal_nprocs_block = pal_nprocs_block self.method_block = method_block self.scf_block = scf_block self.ecp_info = ecp_info # Check that none of the indices in quantum_cluster_indices are in ecp_region_indices - if not np.all([x not in self.ecp_region_indices for x in self.quantum_cluster_indices]): + if not np.all( + [x not in self.ecp_region_indices for x in self.quantum_cluster_indices] + ): raise ValueError( "An atom in the quantum cluster is also in the ECP region." ) From 0c8e03a4f82928f4d39971750e0d16ccd08286e4 Mon Sep 17 00:00:00 2001 From: Andrew Rosen Date: Sun, 9 Jun 2024 13:09:42 -0700 Subject: [PATCH 16/29] Remove stray comment --- src/quacc/atoms/skzcam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index 82732e2544..bd0ee97569 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -1039,7 +1039,7 @@ def run_skzcam( dist_matrix=slab_embedded_cluster_all_dist, ecp_dist=ecp_dist, ) - # print(slab_quantum_cluster_indices) + # Create the adsorbate_slab_embedded_cluster from slab_embedded_cluster and adsorbate atoms objects. This also sets the final quantum_cluster_indices and ecp_region_indices for the adsorbate_slab_embedded_cluster self.create_adsorbate_slab_embedded_cluster( quantum_cluster_indices=slab_quantum_cluster_indices, From db5cd04dd4e9b019a452a119e0b49037b363cd65 Mon Sep 17 00:00:00 2001 From: benshi97 Date: Mon, 10 Jun 2024 17:07:30 +0100 Subject: [PATCH 17/29] Fix docstring --- src/quacc/atoms/skzcam.py | 14 +++++------ tests/core/atoms/test_skzcam.py | 42 ++++++++++++++------------------- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index bd0ee97569..7315d7cbb6 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -177,11 +177,11 @@ def __init__( Parameters ---------- adsorbate_slab_embedded_cluster - The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This object is created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This object is created within the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. quantum_cluster_indices - A list containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + A list containing the indices of the atoms in each quantum cluster. These indices are created within the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. ecp_region_indices - A list containing the indices of the atoms in each ECP region. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + A list containing the indices of the atoms in each ECP region. These indices are provided by the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. element_info A dictionary with elements as keys which gives the (1) number of core electrons as 'core', (2) basis set as 'basis', (3) effective core potential as 'ecp', (4) resolution-of-identity/density-fitting auxiliary basis set for DFT/HF calculations as 'ri_scf_basis' and (5) resolution-of-identity/density-fitting for correlated wave-function methods as 'ri_cwft_basis'. include_cp @@ -450,7 +450,7 @@ def generate_coords_block(self) -> None: def generate_point_charge_block(self) -> str: """ - Create the point charge block for the MRCC input file. This requires the embedded_cluster Atoms object containing both atom_type and oxi_states arrays, as well as the indices of the quantum cluster and ECP region. Such arrays are created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + Create the point charge block for the MRCC input file. This requires the embedded_cluster Atoms object containing both atom_type and oxi_states arrays, as well as the indices of the quantum cluster and ECP region. Such arrays are created by the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. Returns ------- @@ -501,11 +501,11 @@ def __init__( Parameters ---------- adsorbate_slab_embedded_cluster - The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This object is created by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This object is created by the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. quantum_cluster_indices - A list containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + A list containing the indices of the atoms in each quantum cluster. These indices are provided by the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. ecp_region_indices - A list containing the indices of the atoms in each ECP region. These indices are provided by the [quacc.atoms.skzcam.create_skzcam_clusters][] function. + A list containing the indices of the atoms in each ECP region. These indices are provided by the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. element_info A dictionary with elements as keys which gives the (1) number of core electrons as 'core', (2) basis set as 'basis', (3) effective core potential as 'ecp', (4) resolution-of-identity/density-fitting auxiliary basis set for DFT/HF calculations as 'ri_scf_basis' and (5) resolution-of-identity/density-fitting for correlated wave-function methods as 'ri_cwft_basis'. include_cp diff --git a/tests/core/atoms/test_skzcam.py b/tests/core/atoms/test_skzcam.py index 925e22f488..e2d320e4eb 100644 --- a/tests/core/atoms/test_skzcam.py +++ b/tests/core/atoms/test_skzcam.py @@ -11,6 +11,7 @@ from ase.calculators.calculator import compare_atoms from ase.io import read from numpy.testing import assert_allclose, assert_equal +import os from quacc.atoms.skzcam import ( CreateSKZCAMClusters, @@ -1193,30 +1194,23 @@ def test_CreateSKZCAMClusters_init(): def test_CreateSKZCAMClusters_run_chemshell(skzcam_clusters, tmp_path): # Test if xyz file doesn't get written when write_xyz_file=False - # skzcam_clusters_nowrite = deepcopy(skzcam_clusters) - # skzcam_clusters_nowrite.convert_slab_to_atoms() - # skzcam_clusters_nowrite.run_chemshell( - # filepath= tmp_path / "ChemShell_Cluster.pun", - # chemsh_radius_active=5.0, - # chemsh_radius_cluster=10.0, - # write_xyz_file=False, - # ) - # assert not os.path.isfile(tmp_path / "ChemShell_Cluster.xyz") - - with ( - gzip.open( - Path(FILE_DIR, "skzcam_files", "REF_ChemShell_Cluster.xyz.gz"), "rb" - ) as f_in, - Path(tmp_path, "ChemShell_Cluster.xyz").open(mode="wb") as f_out, - ): - shutil.copyfileobj(f_in, f_out) - # skzcam_clusters.convert_slab_to_atoms() - # skzcam_clusters.run_chemshell( - # filepath= tmp_path / "ChemShell_Cluster.pun", - # chemsh_radius_active=5.0, - # chemsh_radius_cluster=10.0, - # write_xyz_file=True, - # ) + skzcam_clusters_nowrite = deepcopy(skzcam_clusters) + skzcam_clusters_nowrite.convert_slab_to_atoms() + skzcam_clusters_nowrite.run_chemshell( + filepath= tmp_path / "ChemShell_Cluster.pun", + chemsh_radius_active=5.0, + chemsh_radius_cluster=10.0, + write_xyz_file=False, + ) + assert not os.path.isfile(tmp_path / "ChemShell_Cluster.xyz") + + skzcam_clusters.convert_slab_to_atoms() + skzcam_clusters.run_chemshell( + filepath= tmp_path / "ChemShell_Cluster.pun", + chemsh_radius_active=5.0, + chemsh_radius_cluster=10.0, + write_xyz_file=True, + ) # Read the output .xyz file chemshell_embedded_cluster = read(tmp_path / "ChemShell_Cluster.xyz") From c784ebdb58115f36612c4d8e45ef75241af301fc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:07:43 +0000 Subject: [PATCH 18/29] pre-commit auto-fixes --- tests/core/atoms/test_skzcam.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/core/atoms/test_skzcam.py b/tests/core/atoms/test_skzcam.py index e2d320e4eb..33163c831a 100644 --- a/tests/core/atoms/test_skzcam.py +++ b/tests/core/atoms/test_skzcam.py @@ -1,7 +1,7 @@ from __future__ import annotations import gzip -import shutil +import os from copy import deepcopy from pathlib import Path @@ -11,7 +11,6 @@ from ase.calculators.calculator import compare_atoms from ase.io import read from numpy.testing import assert_allclose, assert_equal -import os from quacc.atoms.skzcam import ( CreateSKZCAMClusters, @@ -1197,7 +1196,7 @@ def test_CreateSKZCAMClusters_run_chemshell(skzcam_clusters, tmp_path): skzcam_clusters_nowrite = deepcopy(skzcam_clusters) skzcam_clusters_nowrite.convert_slab_to_atoms() skzcam_clusters_nowrite.run_chemshell( - filepath= tmp_path / "ChemShell_Cluster.pun", + filepath=tmp_path / "ChemShell_Cluster.pun", chemsh_radius_active=5.0, chemsh_radius_cluster=10.0, write_xyz_file=False, @@ -1206,7 +1205,7 @@ def test_CreateSKZCAMClusters_run_chemshell(skzcam_clusters, tmp_path): skzcam_clusters.convert_slab_to_atoms() skzcam_clusters.run_chemshell( - filepath= tmp_path / "ChemShell_Cluster.pun", + filepath=tmp_path / "ChemShell_Cluster.pun", chemsh_radius_active=5.0, chemsh_radius_cluster=10.0, write_xyz_file=True, From a94c5ee96923afbba1c1ff79c6b624af726cf00f Mon Sep 17 00:00:00 2001 From: benshi97 Date: Mon, 10 Jun 2024 17:43:42 +0100 Subject: [PATCH 19/29] Order functions better --- src/quacc/atoms/skzcam.py | 416 ++++++++++++++++---------------- tests/core/atoms/test_skzcam.py | 38 +-- 2 files changed, 228 insertions(+), 226 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index 7315d7cbb6..606c2687ec 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -250,20 +250,20 @@ def generate_input(self) -> BlockInfo: """ # Create the blocks for the basis sets (basis, basis_sm, dfbasis_scf, dfbasis_cor, ecp) - self.generate_basis_ecp_block() + self._generate_basis_ecp_block() # Create the blocks for the coordinates - self.generate_coords_block() + self._generate_coords_block() # Create the point charge block and add it to the adsorbate-slab complex and slab blocks - point_charge_block = self.generate_point_charge_block() + point_charge_block = self._generate_point_charge_block() self.mrccblocks["adsorbate_slab"] += point_charge_block self.mrccblocks["slab"] += point_charge_block # Combine the blocks return self.mrccblocks - def generate_basis_ecp_block(self) -> None: + def _generate_basis_ecp_block(self) -> None: """ Generates the basis and ECP block for the MRCC input file. @@ -276,16 +276,16 @@ def generate_basis_ecp_block(self) -> None: def _create_basis_block(quantum_region, ecp_region=None): return f""" basis_sm=atomtype -{self.create_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: 'def2-SVP' for element in self.element_info})} +{self._create_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: 'def2-SVP' for element in self.element_info})} basis=atomtype -{self.create_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: self.element_info[element]['basis'] for element in self.element_info})} +{self._create_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: self.element_info[element]['basis'] for element in self.element_info})} dfbasis_scf=atomtype -{self.create_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: self.element_info[element]['ri_scf_basis'] for element in self.element_info})} +{self._create_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: self.element_info[element]['ri_scf_basis'] for element in self.element_info})} dfbasis_cor=atomtype -{self.create_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: self.element_info[element]['ri_cwft_basis'] for element in self.element_info})} +{self._create_atomtype_basis(quantum_region=quantum_region, ecp_region=ecp_region, element_basis_info={element: self.element_info[element]['ri_cwft_basis'] for element in self.element_info})} """ if self.include_cp: @@ -309,7 +309,7 @@ def _create_basis_block(quantum_region, ecp_region=None): quantum_region=self.adsorbate_cluster, ecp_region=None ) - def create_atomtype_basis( + def _create_atomtype_basis( self, quantum_region: Atoms, element_basis_info: dict[ElementStr, str], @@ -341,7 +341,7 @@ def create_atomtype_basis( return basis_str - def generate_coords_block(self) -> None: + def _generate_coords_block(self) -> None: """ Generates the coordinates block for the MRCC input file. This includes the coordinates of the quantum cluster, the ECP region, and the point charges. It will return three strings for the adsorbate-slab complex, adsorbate and slab. @@ -448,7 +448,7 @@ def generate_coords_block(self) -> None: self.mrccblocks["slab"] += create_atom_coord_string(atom=atom) self.mrccblocks["slab"] += ecp_region_block - def generate_point_charge_block(self) -> str: + def _generate_point_charge_block(self) -> str: """ Create the point charge block for the MRCC input file. This requires the embedded_cluster Atoms object containing both atom_type and oxi_states arrays, as well as the indices of the quantum cluster and ECP region. Such arrays are created by the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. @@ -585,15 +585,52 @@ def generate_input(self) -> None: """ # First generate the preamble block - self.generate_preamble_block() + self._generate_preamble_block() # Create the blocks for the coordinates - self.generate_coords_block() + self._generate_coords_block() # Combine the blocks return self.orcablocks - def generate_coords_block(self) -> None: + def create_point_charge_file(self, pc_file: str | Path) -> None: + """ + Create a point charge file that can be read by ORCA. This requires the embedded_cluster Atoms object containing both atom_type and oxi_states arrays, as well as the indices of the quantum cluster and ECP region. + + Parameters + ---------- + pc_file + A file containing the point charges to be written by ORCA. + + Returns + ------- + None + """ + + # Get the oxi_states arrays from the embedded_cluster + oxi_states = self.adsorbate_slab_embedded_cluster.get_array("oxi_states") + + # Get the number of point charges for this system + total_indices = self.quantum_cluster_indices + self.ecp_region_indices + num_pc = len(self.adsorbate_slab_embedded_cluster) - len(total_indices) + counter = 0 + with Path.open(pc_file, "w") as f: + # Write the number of point charges first + f.write(f"{num_pc}\n") + for i in range(len(self.adsorbate_slab_embedded_cluster)): + if i not in total_indices: + counter += 1 + position = self.adsorbate_slab_embedded_cluster[i].position + if counter != num_pc: + f.write( + f"{oxi_states[i]:-16.11f} {position[0]:-16.11f} {position[1]:-16.11f} {position[2]:-16.11f}\n" + ) + else: + f.write( + f"{oxi_states[i]:-16.11f} {position[0]:-16.11f} {position[1]:-16.11f} {position[2]:-16.11f}" + ) + + def _generate_coords_block(self) -> None: """ Generates the coordinates block for the ORCA input file. This includes the coordinates of the quantum cluster, the ECP region, and the point charges. It will return three strings for the adsorbate-slab complex, adsorbate and slab. @@ -649,7 +686,7 @@ def generate_coords_block(self) -> None: # Create the coords section for the ECP region ecp_region_coords_section = "" for i, atom in enumerate(self.ecp_region): - atom_ecp_info = self.format_ecp_info( + atom_ecp_info = self._format_ecp_info( atom_ecp_info=self.ecp_info[atom.symbol] ) ecp_region_coords_section += create_atom_coord_string( @@ -663,7 +700,7 @@ def generate_coords_block(self) -> None: self.orcablocks["slab"] += f"{ecp_region_coords_section}end\nend\n" self.orcablocks["adsorbate"] += "end\nend\n" - def format_ecp_info(self, atom_ecp_info: str) -> str: + def _format_ecp_info(self, atom_ecp_info: str) -> str: """ Formats the ECP info so that it can be inputted to ORCA without problems. @@ -690,7 +727,7 @@ def format_ecp_info(self, atom_ecp_info: str) -> str: # Extract content between "NewECP" and "end", exclusive of "end", then add correctly formatted "NewECP" and "end" return f"NewECP\n{atom_ecp_info[start_pos:end_pos].strip()}\nend\n" - def generate_preamble_block(self) -> str: + def _generate_preamble_block(self) -> str: """ From the quantum cluster Atoms object, generate the ORCA input preamble for the basis, method, pal, and scf blocks. @@ -809,86 +846,7 @@ def generate_preamble_block(self) -> str: self.orcablocks["adsorbate"] += preamble_input self.orcablocks["slab"] += preamble_input - def create_point_charge_file(self, pc_file: str | Path) -> None: - """ - Create a point charge file that can be read by ORCA. This requires the embedded_cluster Atoms object containing both atom_type and oxi_states arrays, as well as the indices of the quantum cluster and ECP region. - - Parameters - ---------- - pc_file - A file containing the point charges to be written by ORCA. - - Returns - ------- - None - """ - - # Get the oxi_states arrays from the embedded_cluster - oxi_states = self.adsorbate_slab_embedded_cluster.get_array("oxi_states") - - # Get the number of point charges for this system - total_indices = self.quantum_cluster_indices + self.ecp_region_indices - num_pc = len(self.adsorbate_slab_embedded_cluster) - len(total_indices) - counter = 0 - with Path.open(pc_file, "w") as f: - # Write the number of point charges first - f.write(f"{num_pc}\n") - for i in range(len(self.adsorbate_slab_embedded_cluster)): - if i not in total_indices: - counter += 1 - position = self.adsorbate_slab_embedded_cluster[i].position - if counter != num_pc: - f.write( - f"{oxi_states[i]:-16.11f} {position[0]:-16.11f} {position[1]:-16.11f} {position[2]:-16.11f}\n" - ) - else: - f.write( - f"{oxi_states[i]:-16.11f} {position[0]:-16.11f} {position[1]:-16.11f} {position[2]:-16.11f}" - ) - - -def create_atom_coord_string( - atom: Atom, - is_ghost_atom: bool = False, - atom_ecp_info: str | None = None, - pc_charge: float | None = None, -) -> str: - """ - Creates a string containing the Atom symbol and coordinates for both MRCC and ORCA, with additional information for atoms in the ECP region as well as ghost atoms. - - Parameters - ---------- - atom - The ASE Atom (not Atoms) object containing the atomic coordinates. - is_ghost_atom - If True, then the atom is a ghost atom. - atom_ecp_info - If not None, then assume this is an atom in the ECP region and adds the ECP info. - pc_charge - The point charge value for the ECP region atom. - - Returns - ------- - str - The atom symbol and coordinates in the ORCA input file format. - """ - - # If ecp_info is not None and ghost_atom is True, raise an error - if atom_ecp_info and is_ghost_atom: - raise ValueError("ECP info cannot be provided for ghost atoms.") - - # Check that pc_charge is a float if atom_ecp_info is not None - if atom_ecp_info and pc_charge is None: - raise ValueError("Point charge value must be given for atoms with ECP info.") - - if is_ghost_atom: - atom_coord_str = f"{(atom.symbol + ':').ljust(3)} {' '*16} {atom.position[0]:-16.11f} {atom.position[1]:-16.11f} {atom.position[2]:-16.11f}\n" - elif atom_ecp_info is not None: - atom_coord_str = f"{(atom.symbol + '>').ljust(3)} {pc_charge:-16.11f} {atom.position[0]:-16.11f} {atom.position[1]:-16.11f} {atom.position[2]:-16.11f}\n{atom_ecp_info}" - else: - atom_coord_str = f"{atom.symbol.ljust(3)} {' '*16} {atom.position[0]:-16.11f} {atom.position[1]:-16.11f} {atom.position[2]:-16.11f}\n" - return atom_coord_str class CreateSKZCAMClusters: @@ -955,6 +913,113 @@ def __init__( self.quantum_cluster_indices = None self.ecp_region_indices = None + def convert_slab_to_atoms(self) -> None: + """ + Read the file containing the periodic slab and adsorbate (geometry optimized) and format the resulting Atoms object to be used to create a .pun file in ChemShell. + + Returns + ------- + None + """ + + # Get the necessary information for the cluster from a provided slab file (in any format that ASE can read) + adsorbate_slab = read(self.adsorbate_slab_file) + + # Find indices (within adsorbate_slab) of the slab + slab_indices = self.slab_center_indices + [ + i + for i, _ in enumerate(adsorbate_slab) + if i not in (self.adsorbate_indices + self.slab_center_indices) + ] + + # Create adsorbate and slab from adsorbate_slab + slab = adsorbate_slab[slab_indices] + adsorbate = adsorbate_slab[self.adsorbate_indices] + + adsorbate.translate(-slab[0].position) + slab.translate(-slab[0].position) + + # Get the relative distance of the adsorbate from the first center atom of the slab as defined in the slab_center_indices + adsorbate_vector_from_slab = adsorbate[0].position - slab[0].position + + # Get the center of the cluster from the slab_center_indices + slab_center_position = slab[ + : len(self.slab_center_indices) + ].get_positions().sum(axis=0) / len(self.slab_center_indices) + + # Add the height of the adsorbate from the slab along the z-direction relative to the slab_center + adsorbate_com_z_disp = ( + adsorbate.get_center_of_mass()[2] - slab_center_position[2] + ) + + center_position = ( + np.array([0.0, 0.0, adsorbate_com_z_disp]) + slab_center_position + ) + + self.adsorbate = adsorbate + self.slab = slab + self.adsorbate_slab = adsorbate_slab + self.adsorbate_vector_from_slab = adsorbate_vector_from_slab + self.center_position = center_position + + @requires(has_chemshell, "ChemShell is not installed") + def run_chemshell( + self, + filepath: str | Path, + chemsh_radius_active: float = 40.0, + chemsh_radius_cluster: float = 60.0, + chemsh_bq_layer: float = 6.0, + write_xyz_file: bool = False, + ) -> None: + """ + Run ChemShell to create an embedded cluster from a slab. + + Parameters + ---------- + filepath + The location where the ChemShell output files will be written. + chemsh_radius_active + The radius of the active region in Angstroms. This 'active' region is simply region where the charge fitting is performed to ensure correct Madelung potential; it can be a relatively large value. + chemsh_radius_cluster + The radius of the total embedded cluster in Angstroms. + chemsh_bq_layer + The height above the surface to place some additional fitting point charges in Angstroms; simply for better reproduction of the electrostatic potential close to the adsorbate. + write_xyz_file + Whether to write an XYZ file of the cluster for visualisation. + + Returns + ------- + None + """ + from chemsh.io.tools import convert_atoms_to_frag + + # Convert ASE Atoms to ChemShell Fragment object + slab_frag = convert_atoms_to_frag(self.slab, connect_mode="ionic", dim="2D") + + # Add the atomic charges to the fragment + slab_frag.addCharges(self.atom_oxi_states) + + # Create the chemshell cluster (i.e., add electrostatic fitting charges) from the fragment + chemsh_slab_embedded_cluster = slab_frag.construct_cluster( + origin=0, + radius_cluster=chemsh_radius_cluster / Bohr, + radius_active=chemsh_radius_active / Bohr, + bq_layer=chemsh_bq_layer / Bohr, + adjust_charge="coordination_scaled", + ) + + # Save the final cluster to a .pun file + chemsh_slab_embedded_cluster.save( + filename=Path(filepath).with_suffix(".pun"), fmt="pun" + ) + self.pun_file = Path(filepath).with_suffix(".pun") + + if write_xyz_file: + # XYZ for visualisation + chemsh_slab_embedded_cluster.save( + filename=Path(filepath).with_suffix(".xyz"), fmt="xyz" + ) + def run_skzcam( self, shell_max: int = 10, @@ -991,7 +1056,7 @@ def run_skzcam( """ # Read the .pun file and create the embedded_cluster Atoms object - self.slab_embedded_cluster = self.convert_pun_to_atoms(pun_file=self.pun_file) + self.slab_embedded_cluster = self._convert_pun_to_atoms(pun_file=self.pun_file) # Get distances of all atoms from the cluster center atom_center_distances = _get_atom_distances( @@ -1041,7 +1106,7 @@ def run_skzcam( ) # Create the adsorbate_slab_embedded_cluster from slab_embedded_cluster and adsorbate atoms objects. This also sets the final quantum_cluster_indices and ecp_region_indices for the adsorbate_slab_embedded_cluster - self.create_adsorbate_slab_embedded_cluster( + self._create_adsorbate_slab_embedded_cluster( quantum_cluster_indices=slab_quantum_cluster_indices, ecp_region_indices=slab_ecp_region_indices, ) @@ -1065,114 +1130,7 @@ def run_skzcam( cluster_atoms, ) - @requires(has_chemshell, "ChemShell is not installed") - def run_chemshell( - self, - filepath: str | Path, - chemsh_radius_active: float = 40.0, - chemsh_radius_cluster: float = 60.0, - chemsh_bq_layer: float = 6.0, - write_xyz_file: bool = False, - ) -> None: - """ - Run ChemShell to create an embedded cluster from a slab. - - Parameters - ---------- - filepath - The location where the ChemShell output files will be written. - chemsh_radius_active - The radius of the active region in Angstroms. This 'active' region is simply region where the charge fitting is performed to ensure correct Madelung potential; it can be a relatively large value. - chemsh_radius_cluster - The radius of the total embedded cluster in Angstroms. - chemsh_bq_layer - The height above the surface to place some additional fitting point charges in Angstroms; simply for better reproduction of the electrostatic potential close to the adsorbate. - write_xyz_file - Whether to write an XYZ file of the cluster for visualisation. - - Returns - ------- - None - """ - from chemsh.io.tools import convert_atoms_to_frag - - # Convert ASE Atoms to ChemShell Fragment object - slab_frag = convert_atoms_to_frag(self.slab, connect_mode="ionic", dim="2D") - - # Add the atomic charges to the fragment - slab_frag.addCharges(self.atom_oxi_states) - - # Create the chemshell cluster (i.e., add electrostatic fitting charges) from the fragment - chemsh_slab_embedded_cluster = slab_frag.construct_cluster( - origin=0, - radius_cluster=chemsh_radius_cluster / Bohr, - radius_active=chemsh_radius_active / Bohr, - bq_layer=chemsh_bq_layer / Bohr, - adjust_charge="coordination_scaled", - ) - - # Save the final cluster to a .pun file - chemsh_slab_embedded_cluster.save( - filename=Path(filepath).with_suffix(".pun"), fmt="pun" - ) - self.pun_file = Path(filepath).with_suffix(".pun") - - if write_xyz_file: - # XYZ for visualisation - chemsh_slab_embedded_cluster.save( - filename=Path(filepath).with_suffix(".xyz"), fmt="xyz" - ) - - def convert_slab_to_atoms(self) -> None: - """ - Read the file containing the periodic slab and adsorbate (geometry optimized) and format the resulting Atoms object to be used to create a .pun file in ChemShell. - - Returns - ------- - None - """ - - # Get the necessary information for the cluster from a provided slab file (in any format that ASE can read) - adsorbate_slab = read(self.adsorbate_slab_file) - - # Find indices (within adsorbate_slab) of the slab - slab_indices = self.slab_center_indices + [ - i - for i, _ in enumerate(adsorbate_slab) - if i not in (self.adsorbate_indices + self.slab_center_indices) - ] - - # Create adsorbate and slab from adsorbate_slab - slab = adsorbate_slab[slab_indices] - adsorbate = adsorbate_slab[self.adsorbate_indices] - - adsorbate.translate(-slab[0].position) - slab.translate(-slab[0].position) - - # Get the relative distance of the adsorbate from the first center atom of the slab as defined in the slab_center_indices - adsorbate_vector_from_slab = adsorbate[0].position - slab[0].position - - # Get the center of the cluster from the slab_center_indices - slab_center_position = slab[ - : len(self.slab_center_indices) - ].get_positions().sum(axis=0) / len(self.slab_center_indices) - - # Add the height of the adsorbate from the slab along the z-direction relative to the slab_center - adsorbate_com_z_disp = ( - adsorbate.get_center_of_mass()[2] - slab_center_position[2] - ) - - center_position = ( - np.array([0.0, 0.0, adsorbate_com_z_disp]) + slab_center_position - ) - - self.adsorbate = adsorbate - self.slab = slab - self.adsorbate_slab = adsorbate_slab - self.adsorbate_vector_from_slab = adsorbate_vector_from_slab - self.center_position = center_position - - def convert_pun_to_atoms(self, pun_file: str | Path) -> Atoms: + def _convert_pun_to_atoms(self, pun_file: str | Path) -> Atoms: """ Reads a .pun file and returns an ASE Atoms object containing the atomic coordinates, point charges/oxidation states, and atom types. @@ -1255,7 +1213,7 @@ def convert_pun_to_atoms(self, pun_file: str | Path) -> Atoms: return slab_embedded_cluster - def create_adsorbate_slab_embedded_cluster( + def _create_adsorbate_slab_embedded_cluster( self, quantum_cluster_indices: list[list[int]] | None = None, ecp_region_indices: list[list[int]] | None = None, @@ -1478,3 +1436,47 @@ def _get_atom_distances(atoms: Atoms, center_position: NDArray) -> NDArray: """ return np.array([np.linalg.norm(atom.position - center_position) for atom in atoms]) + + +def create_atom_coord_string( + atom: Atom, + is_ghost_atom: bool = False, + atom_ecp_info: str | None = None, + pc_charge: float | None = None, +) -> str: + """ + Creates a string containing the Atom symbol and coordinates for both MRCC and ORCA, with additional information for atoms in the ECP region as well as ghost atoms. + + Parameters + ---------- + atom + The ASE Atom (not Atoms) object containing the atomic coordinates. + is_ghost_atom + If True, then the atom is a ghost atom. + atom_ecp_info + If not None, then assume this is an atom in the ECP region and adds the ECP info. + pc_charge + The point charge value for the ECP region atom. + + Returns + ------- + str + The atom symbol and coordinates in the ORCA input file format. + """ + + # If ecp_info is not None and ghost_atom is True, raise an error + if atom_ecp_info and is_ghost_atom: + raise ValueError("ECP info cannot be provided for ghost atoms.") + + # Check that pc_charge is a float if atom_ecp_info is not None + if atom_ecp_info and pc_charge is None: + raise ValueError("Point charge value must be given for atoms with ECP info.") + + if is_ghost_atom: + atom_coord_str = f"{(atom.symbol + ':').ljust(3)} {' '*16} {atom.position[0]:-16.11f} {atom.position[1]:-16.11f} {atom.position[2]:-16.11f}\n" + elif atom_ecp_info is not None: + atom_coord_str = f"{(atom.symbol + '>').ljust(3)} {pc_charge:-16.11f} {atom.position[0]:-16.11f} {atom.position[1]:-16.11f} {atom.position[2]:-16.11f}\n{atom_ecp_info}" + else: + atom_coord_str = f"{atom.symbol.ljust(3)} {' '*16} {atom.position[0]:-16.11f} {atom.position[1]:-16.11f} {atom.position[2]:-16.11f}\n" + + return atom_coord_str \ No newline at end of file diff --git a/tests/core/atoms/test_skzcam.py b/tests/core/atoms/test_skzcam.py index e2d320e4eb..ce779afd95 100644 --- a/tests/core/atoms/test_skzcam.py +++ b/tests/core/atoms/test_skzcam.py @@ -37,7 +37,7 @@ def skzcam_clusters(): @pytest.fixture() def slab_embedded_cluster(skzcam_clusters): - return skzcam_clusters.convert_pun_to_atoms( + return skzcam_clusters._convert_pun_to_atoms( pun_file=Path(FILE_DIR, "skzcam_files", "ChemShell_Cluster.pun.gz") ) @@ -193,7 +193,7 @@ def test_MRCCInputGenerator_init(adsorbate_slab_embedded_cluster, element_info): ) -def test_MRCCInputGenerator_create_eint_blocks(mrcc_input_generator): +def test_MRCCInputGenerator_generate_input(mrcc_input_generator): mrcc_input_generator_nocp = deepcopy(mrcc_input_generator) mrcc_input_generator_nocp.include_cp = False @@ -291,9 +291,9 @@ def test_MRCCInputGenerator_generate_basis_ecp_block(mrcc_input_generator): mrcc_input_generator_nocp = deepcopy(mrcc_input_generator) mrcc_input_generator_nocp.include_cp = False - mrcc_input_generator_nocp.generate_basis_ecp_block() + mrcc_input_generator_nocp._generate_basis_ecp_block() - mrcc_input_generator.generate_basis_ecp_block() + mrcc_input_generator._generate_basis_ecp_block() reference_mrcc_blocks_collated = { "adsorbate_slab": [ @@ -371,14 +371,14 @@ def test_MRCCInputGenerator_generate_basis_ecp_block(mrcc_input_generator): def test_MRCCInputGenerator_create_atomtype_basis(mrcc_input_generator): - generated_basis_block_without_ecp = mrcc_input_generator.create_atomtype_basis( + generated_basis_block_without_ecp = mrcc_input_generator._create_atomtype_basis( quantum_region=mrcc_input_generator.adsorbate_slab_cluster, element_basis_info={ element: mrcc_input_generator.element_info[element]["ri_cwft_basis"] for element in mrcc_input_generator.element_info }, ) - generated_basis_block_with_ecp = mrcc_input_generator.create_atomtype_basis( + generated_basis_block_with_ecp = mrcc_input_generator._create_atomtype_basis( quantum_region=mrcc_input_generator.adsorbate_slab_cluster, element_basis_info={ element: mrcc_input_generator.element_info[element]["ri_cwft_basis"] @@ -398,9 +398,9 @@ def test_MRCCInputGenerator_generate_coords_block(mrcc_input_generator): mrcc_input_generator_nocp = deepcopy(mrcc_input_generator) mrcc_input_generator_nocp.include_cp = False - mrcc_input_generator_nocp.generate_coords_block() + mrcc_input_generator_nocp._generate_coords_block() - mrcc_input_generator.generate_coords_block() + mrcc_input_generator._generate_coords_block() reference_block_collated = { "adsorbate_slab": { @@ -509,7 +509,7 @@ def test_MRCCInputGenerator_generate_coords_block(mrcc_input_generator): def test_MRCCInputGenerator_generate_point_charge_block(mrcc_input_generator): - generated_point_charge_block = mrcc_input_generator.generate_point_charge_block() + generated_point_charge_block = mrcc_input_generator._generate_point_charge_block() generated_point_charge_block_shortened = [ float(x) for x in generated_point_charge_block.split()[5::180] @@ -850,9 +850,9 @@ def test_ORCAInputGenerator_generate_coords_block(orca_input_generator): orca_input_generator_nocp = deepcopy(orca_input_generator) orca_input_generator_nocp.include_cp = False - orca_input_generator_nocp.generate_coords_block() + orca_input_generator_nocp._generate_coords_block() - orca_input_generator.generate_coords_block() + orca_input_generator._generate_coords_block() reference_block_collated = { "adsorbate_slab": { @@ -1026,7 +1026,7 @@ def test_ORCAInputGenerator_generate_coords_block(orca_input_generator): def test_ORCAInputGenerator_format_ecp_info(orca_input_generator): with pytest.raises(ValueError): - orca_input_generator.format_ecp_info(atom_ecp_info="dummy_info\nN_core0\nend") + orca_input_generator._format_ecp_info(atom_ecp_info="dummy_info\nN_core0\nend") atom_ecp_info = """ NewECP @@ -1036,7 +1036,7 @@ def test_ORCAInputGenerator_format_ecp_info(orca_input_generator): 1 1.732000000 14.676000000 2 end """ - formatted_atom_ecp_info = orca_input_generator.format_ecp_info( + formatted_atom_ecp_info = orca_input_generator._format_ecp_info( atom_ecp_info=atom_ecp_info ) assert ( @@ -1052,7 +1052,7 @@ def test_ORCAInputGenerator_generate_preamble_block(orca_input_generator): orca_input_generator_3 = deepcopy(orca_input_generator) # Generate the orca input preamble - orca_input_generator_1.generate_preamble_block() + orca_input_generator_1._generate_preamble_block() assert ( orca_input_generator_1.orcablocks["adsorbate_slab"] @@ -1090,7 +1090,7 @@ def test_ORCAInputGenerator_generate_preamble_block(orca_input_generator): }, } orca_input_generator_2.element_info = element_info - orca_input_generator_2.generate_preamble_block() + orca_input_generator_2._generate_preamble_block() assert ( orca_input_generator_2.orcablocks["adsorbate_slab"] @@ -1102,7 +1102,7 @@ def test_ORCAInputGenerator_generate_preamble_block(orca_input_generator): orca_input_generator_3.method_block = None orca_input_generator_3.pal_nprocs_block = None orca_input_generator_3.element_info = None - orca_input_generator_3.generate_preamble_block() + orca_input_generator_3._generate_preamble_block() assert ( orca_input_generator_3.orcablocks["adsorbate_slab"] @@ -1113,7 +1113,7 @@ def test_ORCAInputGenerator_generate_preamble_block(orca_input_generator): with pytest.raises(ValueError): element_info_error = {"C": element_info["C"]} orca_input_generator_3.element_info = element_info_error - orca_input_generator_3.generate_preamble_block() + orca_input_generator_3._generate_preamble_block() def test_ORCAInputGenerator_create_point_charge_file(orca_input_generator, tmp_path): @@ -1237,7 +1237,7 @@ def test_CreateSKZCAMClusters_run_chemshell(skzcam_clusters, tmp_path): def test_CreateSKZCAMClusters_convert_pun_to_atoms(skzcam_clusters): - slab_embedded_cluster = skzcam_clusters.convert_pun_to_atoms( + slab_embedded_cluster = skzcam_clusters._convert_pun_to_atoms( pun_file=Path(FILE_DIR, "skzcam_files", "ChemShell_Cluster.pun.gz") ) @@ -1500,7 +1500,7 @@ def test_CreateSKZCAMClusters_create_adsorbate_slab_embedded_cluster( ) skzcam_clusters.adsorbate_vector_from_slab = [0.0, 0.0, 2.0] - skzcam_clusters.create_adsorbate_slab_embedded_cluster( + skzcam_clusters._create_adsorbate_slab_embedded_cluster( quantum_cluster_indices=[[0, 1, 3, 4], [5, 6, 7, 8]], ecp_region_indices=[[0, 1, 3, 4], [5, 6, 7, 8]], ) From 9de94f97465831a1889a7e077a8493cc36970f1e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:43:55 +0000 Subject: [PATCH 20/29] pre-commit auto-fixes --- src/quacc/atoms/skzcam.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index 606c2687ec..34be041438 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -847,8 +847,6 @@ def _generate_preamble_block(self) -> str: self.orcablocks["slab"] += preamble_input - - class CreateSKZCAMClusters: """ A class to create the quantum clusters and ECP regions for the SKZCAM protocol. @@ -1479,4 +1477,4 @@ def create_atom_coord_string( else: atom_coord_str = f"{atom.symbol.ljust(3)} {' '*16} {atom.position[0]:-16.11f} {atom.position[1]:-16.11f} {atom.position[2]:-16.11f}\n" - return atom_coord_str \ No newline at end of file + return atom_coord_str From ec2c53fa8a9851397d2981ea7a68e2b0af299b81 Mon Sep 17 00:00:00 2001 From: "Andrew S. Rosen" Date: Mon, 10 Jun 2024 10:06:30 -0700 Subject: [PATCH 21/29] Update conftest.py --- tests/core/atoms/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/atoms/conftest.py b/tests/core/atoms/conftest.py index 5a720e2409..3a224f05c3 100644 --- a/tests/core/atoms/conftest.py +++ b/tests/core/atoms/conftest.py @@ -9,7 +9,7 @@ FILE_DIR = Path(__file__).parent -def mock_run_chemshell(slab, slab_center_idx, atom_oxi_states, filepath, **kwargs): +def mock_run_chemshell(*args, filepath=".", **kwargs): with ( gzip.open( Path(FILE_DIR, "skzcam_files", "REF_ChemShell_Cluster.xyz.gz"), "rb" From ac07716bc74761f72a5bcb854aee64e2936d7639 Mon Sep 17 00:00:00 2001 From: benshi97 Date: Tue, 11 Jun 2024 10:58:26 +0100 Subject: [PATCH 22/29] Fix unit tests --- tests/core/atoms/conftest.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/core/atoms/conftest.py b/tests/core/atoms/conftest.py index 3a224f05c3..74e85fc546 100644 --- a/tests/core/atoms/conftest.py +++ b/tests/core/atoms/conftest.py @@ -9,14 +9,23 @@ FILE_DIR = Path(__file__).parent -def mock_run_chemshell(*args, filepath=".", **kwargs): - with ( - gzip.open( - Path(FILE_DIR, "skzcam_files", "REF_ChemShell_Cluster.xyz.gz"), "rb" - ) as f_in, - Path(filepath, "ChemShell_Cluster.xyz").open(mode="wb") as f_out, - ): - shutil.copyfileobj(f_in, f_out) +def mock_run_chemshell(*args, filepath=".", write_xyz_file = False, **kwargs): + if write_xyz_file: + with ( + gzip.open( + Path(FILE_DIR, "skzcam_files", "REF_ChemShell_Cluster.xyz.gz"), "rb" + ) as f_in, + Path(filepath).with_suffix(".xyz").open(mode="wb") as f_out, + ): + shutil.copyfileobj(f_in, f_out) + else: + with ( + gzip.open( + Path(FILE_DIR, "skzcam_files", "ChemShell_Cluster.pun.gz"), "rb" + ) as f_in, + Path(filepath).with_suffix(".pun").open(mode="wb") as f_out, + ): + shutil.copyfileobj(f_in, f_out) @pytest.fixture(autouse=True) From d30569b9ea89cb006a865d2a1eaf63f60a872a71 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:00:56 +0000 Subject: [PATCH 23/29] pre-commit auto-fixes --- tests/core/atoms/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/atoms/conftest.py b/tests/core/atoms/conftest.py index 74e85fc546..7ac9abca26 100644 --- a/tests/core/atoms/conftest.py +++ b/tests/core/atoms/conftest.py @@ -9,7 +9,7 @@ FILE_DIR = Path(__file__).parent -def mock_run_chemshell(*args, filepath=".", write_xyz_file = False, **kwargs): +def mock_run_chemshell(*args, filepath=".", write_xyz_file=False, **kwargs): if write_xyz_file: with ( gzip.open( From a228b251a0a762e96a401fcb484f96f5ff09bc43 Mon Sep 17 00:00:00 2001 From: benshi97 Date: Tue, 11 Jun 2024 11:28:05 +0100 Subject: [PATCH 24/29] Add attribute descriptions. --- src/quacc/atoms/skzcam.py | 221 ++++++++++++++++++++++++-------- tests/core/atoms/test_skzcam.py | 14 +- 2 files changed, 171 insertions(+), 64 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index 34be041438..f8a9de204d 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -17,6 +17,11 @@ from ase.atom import Atom from numpy.typing import NDArray + class SKZCAMOutput(TypedDict): + adsorbate_slab_embedded_cluster: Atoms + quantum_cluster_indices_set: list[list[int]] + ecp_region_indices_set: list[list[int]] + class ElementInfo(TypedDict): core: int basis: str @@ -161,7 +166,37 @@ class MultiplicityDict(TypedDict): class MRCCInputGenerator: """ - A class to generate the skzcam input for the MRCC ASE calculator. + A class to generate the SKZCAM input for the MRCC ASE calculator. + + Attributes + ---------- + adsorbate_slab_embedded_cluster + The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This object is created within the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. + quantum_cluster_indices + A list containing the indices of the atoms in one quantum cluster. These indices are created within the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. + ecp_region_indices + A list containing the indices of the atoms in one ECP region. These indices are provided by the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. + element_info + A dictionary with elements as keys which gives the (1) number of core electrons as 'core', (2) basis set as 'basis', (3) effective core potential as 'ecp', (4) resolution-of-identity/density-fitting auxiliary basis set for DFT/HF calculations as 'ri_scf_basis' and (5) resolution-of-identity/density-fitting for correlated wave-function methods as 'ri_cwft_basis'. + include_cp + If True, the coords strings will include the counterpoise correction (i.e., ghost atoms) for the adsorbate and slab. + multiplicities + The multiplicity of the adsorbate-slab complex, adsorbate and slab respectively, with the keys 'adsorbate_slab', 'adsorbate', and 'slab'. + adsorbate_slab_cluster + The ASE Atoms object for the quantum cluster of the adsorbate-slab complex. + ecp_region + The ASE Atoms object for the ECP region. + adsorbate_indices + The indices of the adsorbates from the adsorbate_slab_cluster quantum cluster. + slab_indices + The indices of the slab from the adsorbate_slab_cluster quantum cluster. + The ECP region cluster. + adsorbate_cluster + The ASE Atoms object for the quantum cluster of the adsorbate. + slab_cluster + The ASE Atoms object for the quantum cluster of the slab. + mrccblocks + The MRCC input block (to be put in 'mrccblocks' parameter) as a string for the adsorbate-slab complex, the adsorbate, and the slab in a dictionary with the keys 'adsorbate_slab', 'adsorbate', and 'slab' respectively. """ def __init__( @@ -179,9 +214,9 @@ def __init__( adsorbate_slab_embedded_cluster The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This object is created within the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. quantum_cluster_indices - A list containing the indices of the atoms in each quantum cluster. These indices are created within the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. + A list containing the indices of the atoms in one quantum cluster. These indices are created within the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. ecp_region_indices - A list containing the indices of the atoms in each ECP region. These indices are provided by the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. + A list containing the indices of the atoms in the corresponding ECP region of one quantum cluster. These indices are provided by the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. element_info A dictionary with elements as keys which gives the (1) number of core electrons as 'core', (2) basis set as 'basis', (3) effective core potential as 'ecp', (4) resolution-of-identity/density-fitting auxiliary basis set for DFT/HF calculations as 'ri_scf_basis' and (5) resolution-of-identity/density-fitting for correlated wave-function methods as 'ri_cwft_basis'. include_cp @@ -214,30 +249,30 @@ def __init__( ) # Create the adsorbate-slab complex quantum cluster and ECP region cluster - self.adsorbate_slab_cluster = self.adsorbate_slab_embedded_cluster[ + self.adsorbate_slab_cluster : Atoms = self.adsorbate_slab_embedded_cluster[ self.quantum_cluster_indices ] - self.ecp_region = self.adsorbate_slab_embedded_cluster[self.ecp_region_indices] + self.ecp_region : Atoms = self.adsorbate_slab_embedded_cluster[self.ecp_region_indices] # Get the indices of the adsorbates from the quantum cluster - self.adsorbate_indices = [ + self.adsorbate_indices : list[int] = [ i for i in range(len(self.adsorbate_slab_cluster)) if self.adsorbate_slab_cluster.get_array("atom_type")[i] == "adsorbate" ] # Get the indices of the slab from the quantum cluster - self.slab_indices = [ + self.slab_indices : list[int] = [ i for i in range(len(self.adsorbate_slab_cluster)) if self.adsorbate_slab_cluster.get_array("atom_type")[i] != "adsorbate" ] # Create the adsorbate and slab quantum clusters - self.adsorbate_cluster = self.adsorbate_slab_cluster[self.adsorbate_indices] - self.slab_cluster = self.adsorbate_slab_cluster[self.slab_indices] + self.adsorbate_cluster : Atoms = self.adsorbate_slab_cluster[self.adsorbate_indices] + self.slab_cluster : Atoms = self.adsorbate_slab_cluster[self.slab_indices] # Initialize the mrccblocks input strings for the adsorbate-slab complex, adsorbate, and slab - self.mrccblocks = {"adsorbate_slab": "", "adsorbate": "", "slab": ""} + self.mrccblocks : BlockInfo = {"adsorbate_slab": "", "adsorbate": "", "slab": ""} def generate_input(self) -> BlockInfo: """ @@ -481,7 +516,45 @@ def _generate_point_charge_block(self) -> str: class ORCAInputGenerator: """ - A class to generate the skzcam input for the ORCA ASE calculator. + A class to generate the SKZCAM input for the ORCA ASE calculator. + + Attributes + ---------- + adsorbate_slab_embedded_cluster + The ASE Atoms object containing the atomic coordinates and atomic charges from the .pun file, as well as the atom type. This object is created by the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. + quantum_cluster_indices + A list containing the indices of the atoms in one quantum cluster. These indices are provided by the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. + ecp_region_indices + A list containing the indices of the atoms in the ECP region of one quantum cluster. These indices are provided by the [quacc.atoms.skzcam.CreateSKZCAMClusters][] class. + element_info + A dictionary with elements as keys which gives the (1) number of core electrons as 'core', (2) basis set as 'basis', (3) effective core potential as 'ecp', (4) resolution-of-identity/density-fitting auxiliary basis set for DFT/HF calculations as 'ri_scf_basis' and (5) resolution-of-identity/density-fitting for correlated wave-function methods as 'ri_cwft_basis'. + include_cp + If True, the coords strings will include the counterpoise correction (i.e., ghost atoms) for the adsorbate and slab. + multiplicities + The multiplicity of the adsorbate-slab complex, adsorbate and slab respectively, with the keys 'adsorbate_slab', 'adsorbate', and 'slab'. + pal_nprocs_block + A dictionary with the number of processors for the PAL block as 'nprocs' and the maximum memory-per-core in megabytes blocks as 'maxcore'. + method_block + A dictionary that contains the method block for the ORCA input file. The key is the ORCA setting and the value is that setting's value. + scf_block + A dictionary that contains the SCF block for the ORCA input file. The key is the ORCA setting and the value is that setting's value. + ecp_info + A dictionary with the ECP data (in ORCA format) for the cations in the ECP region. The keys are the element symbols and the values are the ECP data. + adsorbate_slab_cluster + The ASE Atoms object for the quantum cluster of the adsorbate-slab complex. + ecp_region + The ASE Atoms object for the ECP region. + adsorbate_indices + The indices of the adsorbates from the adsorbate_slab_cluster quantum cluster. + slab_indices + The indices of the slab from the adsorbate_slab_cluster quantum cluster. + adsorbate_cluster + The ASE Atoms object for the quantum cluster of the adsorbate. + slab_cluster + The ASE Atoms object for the quantum cluster of the slab. + orcablocks + The ORCA input block (to be put in 'orcablocks' parameter) as a string for the adsorbate-slab complex, the adsorbate, and the slab in a dictionary with the keys 'adsorbate_slab', 'adsorbate', and 'slab' respectively. + """ def __init__( @@ -550,38 +623,39 @@ def __init__( ) # Create the adsorbate-slab complex quantum cluster and ECP region cluster - self.adsorbate_slab_cluster = self.adsorbate_slab_embedded_cluster[ + self.adsorbate_slab_cluster : Atoms = self.adsorbate_slab_embedded_cluster[ self.quantum_cluster_indices ] - self.ecp_region = self.adsorbate_slab_embedded_cluster[self.ecp_region_indices] + self.ecp_region : Atoms = self.adsorbate_slab_embedded_cluster[self.ecp_region_indices] # Get the indices of the adsorbates from the quantum cluster - self.adsorbate_indices = [ + self.adsorbate_indices : list[int] = [ i for i in range(len(self.adsorbate_slab_cluster)) if self.adsorbate_slab_cluster.get_array("atom_type")[i] == "adsorbate" ] # Get the indices of the slab from the quantum cluster - self.slab_indices = [ + self.slab_indices : list[int] = [ i for i in range(len(self.adsorbate_slab_cluster)) if self.adsorbate_slab_cluster.get_array("atom_type")[i] != "adsorbate" ] # Create the adsorbate and slab quantum clusters - self.adsorbate_cluster = self.adsorbate_slab_cluster[self.adsorbate_indices] - self.slab_cluster = self.adsorbate_slab_cluster[self.slab_indices] + self.adsorbate_cluster : Atoms = self.adsorbate_slab_cluster[self.adsorbate_indices] + self.slab_cluster : Atoms = self.adsorbate_slab_cluster[self.slab_indices] # Initialize the orcablocks input strings for the adsorbate-slab complex, adsorbate, and slab - self.orcablocks = {"adsorbate_slab": "", "adsorbate": "", "slab": ""} + self.orcablocks : BlockInfo = {"adsorbate_slab": "", "adsorbate": "", "slab": ""} - def generate_input(self) -> None: + def generate_input(self) -> BlockInfo: """ Creates the orcablocks input for the ORCA ASE calculator. Returns ------- - None + BlockInfo + The ORCA input block (to be put in 'orcablocks' parameter) as a string for the adsorbate-slab complex, the adsorbate, and the slab in a dictionary with the keys 'adsorbate_slab', 'adsorbate', and 'slab' respectively. """ # First generate the preamble block @@ -850,6 +924,35 @@ def _generate_preamble_block(self) -> str: class CreateSKZCAMClusters: """ A class to create the quantum clusters and ECP regions for the SKZCAM protocol. + + Attributes + ---------- + adsorbate_indices + The indices of the atoms that make up the adsorbate molecule. + slab_center_indices + The indices of the atoms that make up the 'center' of the slab right beneath the adsorbate. + slab_indices + The indices of the atoms that make up the slab. + atom_oxi_states + A dictionary with the element symbol as the key and its oxidation state as the value. + adsorbate_slab_file + The path to the file containing the adsorbate molecule on the surface slab. It can be in any format that ASE can read. + pun_file + The path to the .pun file containing the atomic coordinates and charges of the adsorbate-slab complex. This file should be generated by ChemShell. If it is None, then ChemShell wil be used to create this file. + adsorbate + The ASE Atoms object containing the atomic coordinates of the adsorbate. + slab + The ASE Atoms object containing the atomic coordinates of the slab. + adsorbate_slab + The ASE Atoms object containing the atomic coordinates of the adsorbate-slab complex. + adsorbate_slab_embedded_cluster + The ASE Atoms object containing the atomic coordinates, atomic charges and atom type (i.e., point charge or cation/anion) from the .pun file for the embedded cluster of the adsorbate-slab complex. + slab_embedded_cluster + The ASE Atoms object containing the atomic coordinates, atomic charges and atom type (i.e., point charge or cation/anion) from the .pun file for the embedded cluster of the slab. + quantum_cluster_indices_set + A list of lists of indices of the atoms in the set of quantum clusters created by the SKZCAM protocol + ecp_region_indices_set + A list of lists of indices of the atoms in the ECP region for the set of quantum clusters created by the SKZCAM protocol """ def __init__( @@ -899,17 +1002,17 @@ def __init__( ) # Initialize the adsorbate, slab and adsorbate_slab Atoms object which contains the adsorbate, slab and adsorbate-slab complex respectively - self.adsorbate = None - self.slab = None - self.adsorbate_slab = None + self.adsorbate : Atoms | None + self.slab : Atoms | None + self.adsorbate_slab : Atoms | None # Initialize the embedded_adsorbate_slab_cluster, and embedded_slab_cluster Atoms object which are the embedded cluster for the adsorbate-slab complex and slab respectively - self.adsorbate_slab_embedded_cluster = None - self.slab_embedded_cluster = None + self.adsorbate_slab_embedded_cluster : Atoms | None = None + self.slab_embedded_cluster : Atoms | None = None # Initialize the quantum cluster indices and ECP region indices - self.quantum_cluster_indices = None - self.ecp_region_indices = None + self.quantum_cluster_indices_set : list[list[int]] | None = None + self.ecp_region_indices_set : list[list[int]] | None = None def convert_slab_to_atoms(self) -> None: """ @@ -1027,7 +1130,7 @@ def run_skzcam( write_clusters: bool = False, write_clusters_path: str | Path = ".", write_include_ecp: bool = False, - ) -> None: + ) -> SKZCAMOutput: """ From a provided .pun file (generated by ChemShell), this function creates quantum clusters using the SKZCAM protocol. It will return the embedded cluster Atoms object and the indices of the atoms in the quantum clusters and the ECP region. The number of clusters created is controlled by the rdf_max parameter. @@ -1085,39 +1188,39 @@ def run_skzcam( ] # Create the quantum clusters by summing up the indices of the cations and their coordinating anions - slab_quantum_cluster_indices = [] + slab_quantum_cluster_indices_set = [] dummy_cation_indices = [] dummy_anion_indices = [] for shell_idx in range(shell_max): dummy_cation_indices += cation_shells_idx[shell_idx] dummy_anion_indices += anion_coord_idx[shell_idx] - slab_quantum_cluster_indices += [ + slab_quantum_cluster_indices_set += [ list(set(dummy_cation_indices + dummy_anion_indices)) ] # Get the ECP region for each quantum cluster - slab_ecp_region_indices = self._get_ecp_region( + slab_ecp_region_indices_set = self._get_ecp_region( slab_embedded_cluster=self.slab_embedded_cluster, - quantum_cluster_indices=slab_quantum_cluster_indices, + quantum_cluster_indices_set=slab_quantum_cluster_indices_set, dist_matrix=slab_embedded_cluster_all_dist, ecp_dist=ecp_dist, ) - # Create the adsorbate_slab_embedded_cluster from slab_embedded_cluster and adsorbate atoms objects. This also sets the final quantum_cluster_indices and ecp_region_indices for the adsorbate_slab_embedded_cluster + # Create the adsorbate_slab_embedded_cluster from slab_embedded_cluster and adsorbate atoms objects. This also sets the final quantum_cluster_indices_set and ecp_region_indices_set for the adsorbate_slab_embedded_cluster self._create_adsorbate_slab_embedded_cluster( - quantum_cluster_indices=slab_quantum_cluster_indices, - ecp_region_indices=slab_ecp_region_indices, + quantum_cluster_indices_set=slab_quantum_cluster_indices_set, + ecp_region_indices_set=slab_ecp_region_indices_set, ) # Write the quantum clusters to files if write_clusters: - for idx in range(len(self.quantum_cluster_indices)): + for idx in range(len(self.quantum_cluster_indices_set)): quantum_atoms = self.adsorbate_slab_embedded_cluster[ - self.quantum_cluster_indices[idx] + self.quantum_cluster_indices_set[idx] ] if write_include_ecp: ecp_atoms = self.adsorbate_slab_embedded_cluster[ - self.ecp_region_indices[idx] + self.ecp_region_indices_set[idx] ] ecp_atoms.set_chemical_symbols(np.array(["U"] * len(ecp_atoms))) cluster_atoms = quantum_atoms + ecp_atoms @@ -1128,6 +1231,10 @@ def run_skzcam( cluster_atoms, ) + return {'adsorbate_slab_embedded_cluster': self.adsorbate_slab_embedded_cluster, + 'quantum_cluster_indices_set': self.quantum_cluster_indices_set, + 'ecp_region_indices_set': self.ecp_region_indices_set} + def _convert_pun_to_atoms(self, pun_file: str | Path) -> Atoms: """ Reads a .pun file and returns an ASE Atoms object containing the atomic coordinates, @@ -1213,17 +1320,17 @@ def _convert_pun_to_atoms(self, pun_file: str | Path) -> Atoms: def _create_adsorbate_slab_embedded_cluster( self, - quantum_cluster_indices: list[list[int]] | None = None, - ecp_region_indices: list[list[int]] | None = None, + quantum_cluster_indices_set: list[list[int]] | None = None, + ecp_region_indices_set: list[list[int]] | None = None, ) -> None: """ Insert the adsorbate into the embedded cluster and update the quantum cluster and ECP region indices. Parameters ---------- - quantum_cluster_indices + quantum_cluster_indices_set A list of lists containing the indices of the atoms in each quantum cluster. - ecp_region_indices + ecp_region_indices_set A list of lists containing the indices of the atoms in the ECP region for each quantum cluster. Returns @@ -1253,20 +1360,20 @@ def _create_adsorbate_slab_embedded_cluster( ) # Update the quantum cluster and ECP region indices - if quantum_cluster_indices is not None: - quantum_cluster_indices = [ + if quantum_cluster_indices_set is not None: + quantum_cluster_indices_set = [ list(range(len(self.adsorbate))) + [idx + len(self.adsorbate) for idx in cluster] - for cluster in quantum_cluster_indices + for cluster in quantum_cluster_indices_set ] - if ecp_region_indices is not None: - ecp_region_indices = [ + if ecp_region_indices_set is not None: + ecp_region_indices_set = [ [idx + len(self.adsorbate) for idx in cluster] - for cluster in ecp_region_indices + for cluster in ecp_region_indices_set ] - self.quantum_cluster_indices = quantum_cluster_indices - self.ecp_region_indices = ecp_region_indices + self.quantum_cluster_indices_set = quantum_cluster_indices_set + self.ecp_region_indices_set = ecp_region_indices_set def _find_cation_shells( self, slab_embedded_cluster: Atoms, distances: NDArray, shell_width: float = 0.1 @@ -1369,7 +1476,7 @@ def _get_anion_coordination( def _get_ecp_region( self, slab_embedded_cluster: Atoms, - quantum_cluster_indices: list[int], + quantum_cluster_indices_set: list[int], dist_matrix: NDArray, ecp_dist: float = 6.0, ) -> list[list[int]]: @@ -1380,8 +1487,8 @@ def _get_ecp_region( ---------- slab_embedded_cluster The ASE Atoms object containing the atomic coordinates AND the atom types (i.e. cation or anion). - quantum_cluster_indices - A list of lists containing the indices of the atoms in each quantum cluster. + quantum_cluster_indices_set + A list of lists containing the indices of the atoms in each quantum cluster. dist_matrix A matrix containing the distances between each pair of atoms in the embedded cluster. ecp_dist @@ -1393,11 +1500,11 @@ def _get_ecp_region( A list of lists containing the indices of the atoms in the ECP region for each quantum cluster. """ - ecp_region_indices = [] + ecp_region_indices_set = [] dummy_cation_indices = [] # Iterate over the quantum clusters and find the atoms within the ECP distance of each quantum cluster - for cluster in quantum_cluster_indices: + for cluster in quantum_cluster_indices_set: dummy_cation_indices += cluster cluster_ecp_region_idx = [] for atom_idx in dummy_cation_indices: @@ -1411,9 +1518,9 @@ def _get_ecp_region( ): cluster_ecp_region_idx += [idx] - ecp_region_indices += [list(set(cluster_ecp_region_idx))] + ecp_region_indices_set += [list(set(cluster_ecp_region_idx))] - return ecp_region_indices + return ecp_region_indices_set def _get_atom_distances(atoms: Atoms, center_position: NDArray) -> NDArray: diff --git a/tests/core/atoms/test_skzcam.py b/tests/core/atoms/test_skzcam.py index afbaadd89f..d3a145401f 100644 --- a/tests/core/atoms/test_skzcam.py +++ b/tests/core/atoms/test_skzcam.py @@ -1481,7 +1481,7 @@ def test_CreateSKZCAMClusters_get_ecp_region( # Find the ECP region for the first cluster ecp_region_idx = skzcam_clusters._get_ecp_region( slab_embedded_cluster=slab_embedded_cluster, - quantum_cluster_indices=[[0, 1, 2, 3, 4, 5]], + quantum_cluster_indices_set=[[0, 1, 2, 3, 4, 5]], dist_matrix=distance_matrix, ecp_dist=3, ) @@ -1500,8 +1500,8 @@ def test_CreateSKZCAMClusters_create_adsorbate_slab_embedded_cluster( skzcam_clusters.adsorbate_vector_from_slab = [0.0, 0.0, 2.0] skzcam_clusters._create_adsorbate_slab_embedded_cluster( - quantum_cluster_indices=[[0, 1, 3, 4], [5, 6, 7, 8]], - ecp_region_indices=[[0, 1, 3, 4], [5, 6, 7, 8]], + quantum_cluster_indices_set=[[0, 1, 3, 4], [5, 6, 7, 8]], + ecp_region_indices_set=[[0, 1, 3, 4], [5, 6, 7, 8]], ) # Check that the positions of the first 10 atoms of the embedded cluster matches the reference positions, oxi_states and atom_type @@ -1553,10 +1553,10 @@ def test_CreateSKZCAMClusters_create_adsorbate_slab_embedded_cluster( # Check that the quantum_idx and ecp_idx match the reference assert_equal( - skzcam_clusters.quantum_cluster_indices, + skzcam_clusters.quantum_cluster_indices_set, [[0, 1, 2, 3, 5, 6], [0, 1, 7, 8, 9, 10]], ) - assert_equal(skzcam_clusters.ecp_region_indices, [[2, 3, 5, 6], [7, 8, 9, 10]]) + assert_equal(skzcam_clusters.ecp_region_indices_set, [[2, 3, 5, 6], [7, 8, 9, 10]]) def test_CreateSKZCAMClusters_run_skzcam(skzcam_clusters, tmp_path): @@ -1580,7 +1580,7 @@ def test_CreateSKZCAMClusters_run_skzcam(skzcam_clusters, tmp_path): # Check quantum cluster indices match with reference assert_equal( - skzcam_clusters.quantum_cluster_indices[1], + skzcam_clusters.quantum_cluster_indices_set[1], [ 0, 1, @@ -1611,7 +1611,7 @@ def test_CreateSKZCAMClusters_run_skzcam(skzcam_clusters, tmp_path): # Check ECP region indices match with reference assert_equal( - skzcam_clusters.ecp_region_indices[1], + skzcam_clusters.ecp_region_indices_set[1], [ 12, 13, From ed3383af4d108e0759c6db095922b8dd28b53e22 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:28:18 +0000 Subject: [PATCH 25/29] pre-commit auto-fixes --- src/quacc/atoms/skzcam.py | 60 +++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index f8a9de204d..9924a60ddb 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -249,30 +249,34 @@ def __init__( ) # Create the adsorbate-slab complex quantum cluster and ECP region cluster - self.adsorbate_slab_cluster : Atoms = self.adsorbate_slab_embedded_cluster[ + self.adsorbate_slab_cluster: Atoms = self.adsorbate_slab_embedded_cluster[ self.quantum_cluster_indices ] - self.ecp_region : Atoms = self.adsorbate_slab_embedded_cluster[self.ecp_region_indices] + self.ecp_region: Atoms = self.adsorbate_slab_embedded_cluster[ + self.ecp_region_indices + ] # Get the indices of the adsorbates from the quantum cluster - self.adsorbate_indices : list[int] = [ + self.adsorbate_indices: list[int] = [ i for i in range(len(self.adsorbate_slab_cluster)) if self.adsorbate_slab_cluster.get_array("atom_type")[i] == "adsorbate" ] # Get the indices of the slab from the quantum cluster - self.slab_indices : list[int] = [ + self.slab_indices: list[int] = [ i for i in range(len(self.adsorbate_slab_cluster)) if self.adsorbate_slab_cluster.get_array("atom_type")[i] != "adsorbate" ] # Create the adsorbate and slab quantum clusters - self.adsorbate_cluster : Atoms = self.adsorbate_slab_cluster[self.adsorbate_indices] - self.slab_cluster : Atoms = self.adsorbate_slab_cluster[self.slab_indices] + self.adsorbate_cluster: Atoms = self.adsorbate_slab_cluster[ + self.adsorbate_indices + ] + self.slab_cluster: Atoms = self.adsorbate_slab_cluster[self.slab_indices] # Initialize the mrccblocks input strings for the adsorbate-slab complex, adsorbate, and slab - self.mrccblocks : BlockInfo = {"adsorbate_slab": "", "adsorbate": "", "slab": ""} + self.mrccblocks: BlockInfo = {"adsorbate_slab": "", "adsorbate": "", "slab": ""} def generate_input(self) -> BlockInfo: """ @@ -623,30 +627,34 @@ def __init__( ) # Create the adsorbate-slab complex quantum cluster and ECP region cluster - self.adsorbate_slab_cluster : Atoms = self.adsorbate_slab_embedded_cluster[ + self.adsorbate_slab_cluster: Atoms = self.adsorbate_slab_embedded_cluster[ self.quantum_cluster_indices ] - self.ecp_region : Atoms = self.adsorbate_slab_embedded_cluster[self.ecp_region_indices] + self.ecp_region: Atoms = self.adsorbate_slab_embedded_cluster[ + self.ecp_region_indices + ] # Get the indices of the adsorbates from the quantum cluster - self.adsorbate_indices : list[int] = [ + self.adsorbate_indices: list[int] = [ i for i in range(len(self.adsorbate_slab_cluster)) if self.adsorbate_slab_cluster.get_array("atom_type")[i] == "adsorbate" ] # Get the indices of the slab from the quantum cluster - self.slab_indices : list[int] = [ + self.slab_indices: list[int] = [ i for i in range(len(self.adsorbate_slab_cluster)) if self.adsorbate_slab_cluster.get_array("atom_type")[i] != "adsorbate" ] # Create the adsorbate and slab quantum clusters - self.adsorbate_cluster : Atoms = self.adsorbate_slab_cluster[self.adsorbate_indices] - self.slab_cluster : Atoms = self.adsorbate_slab_cluster[self.slab_indices] + self.adsorbate_cluster: Atoms = self.adsorbate_slab_cluster[ + self.adsorbate_indices + ] + self.slab_cluster: Atoms = self.adsorbate_slab_cluster[self.slab_indices] # Initialize the orcablocks input strings for the adsorbate-slab complex, adsorbate, and slab - self.orcablocks : BlockInfo = {"adsorbate_slab": "", "adsorbate": "", "slab": ""} + self.orcablocks: BlockInfo = {"adsorbate_slab": "", "adsorbate": "", "slab": ""} def generate_input(self) -> BlockInfo: """ @@ -1002,17 +1010,17 @@ def __init__( ) # Initialize the adsorbate, slab and adsorbate_slab Atoms object which contains the adsorbate, slab and adsorbate-slab complex respectively - self.adsorbate : Atoms | None - self.slab : Atoms | None - self.adsorbate_slab : Atoms | None + self.adsorbate: Atoms | None + self.slab: Atoms | None + self.adsorbate_slab: Atoms | None # Initialize the embedded_adsorbate_slab_cluster, and embedded_slab_cluster Atoms object which are the embedded cluster for the adsorbate-slab complex and slab respectively - self.adsorbate_slab_embedded_cluster : Atoms | None = None - self.slab_embedded_cluster : Atoms | None = None + self.adsorbate_slab_embedded_cluster: Atoms | None = None + self.slab_embedded_cluster: Atoms | None = None # Initialize the quantum cluster indices and ECP region indices - self.quantum_cluster_indices_set : list[list[int]] | None = None - self.ecp_region_indices_set : list[list[int]] | None = None + self.quantum_cluster_indices_set: list[list[int]] | None = None + self.ecp_region_indices_set: list[list[int]] | None = None def convert_slab_to_atoms(self) -> None: """ @@ -1231,9 +1239,11 @@ def run_skzcam( cluster_atoms, ) - return {'adsorbate_slab_embedded_cluster': self.adsorbate_slab_embedded_cluster, - 'quantum_cluster_indices_set': self.quantum_cluster_indices_set, - 'ecp_region_indices_set': self.ecp_region_indices_set} + return { + "adsorbate_slab_embedded_cluster": self.adsorbate_slab_embedded_cluster, + "quantum_cluster_indices_set": self.quantum_cluster_indices_set, + "ecp_region_indices_set": self.ecp_region_indices_set, + } def _convert_pun_to_atoms(self, pun_file: str | Path) -> Atoms: """ @@ -1488,7 +1498,7 @@ def _get_ecp_region( slab_embedded_cluster The ASE Atoms object containing the atomic coordinates AND the atom types (i.e. cation or anion). quantum_cluster_indices_set - A list of lists containing the indices of the atoms in each quantum cluster. + A list of lists containing the indices of the atoms in each quantum cluster. dist_matrix A matrix containing the distances between each pair of atoms in the embedded cluster. ecp_dist From 14dda7e7d9887b16983568aceed0e981f38b1eea Mon Sep 17 00:00:00 2001 From: Andrew Rosen Date: Fri, 28 Jun 2024 20:52:15 -0700 Subject: [PATCH 26/29] Revert "Merge branch 'main' into class_format" This reverts commit 2aa9eadd83923caf4f6a56a4d3e1e466fdc3fb1e, reversing changes made to d285d20da5d90e033f9ba8df067be068507fcb91. --- .github/PULL_REQUEST_TEMPLATE.md | 6 +- .github/workflows/tests.yaml | 121 ++++++++----- .sourcery.yaml | 5 + CHANGELOG.md | 11 +- docs/about/contributors.md | 1 - docs/install/codes.md | 7 +- docs/user/recipes/recipes_list.md | 2 +- docs/user/settings/settings.md | 23 +-- src/quacc/recipes/emt/md.py | 85 --------- src/quacc/recipes/espresso/_base.py | 5 +- src/quacc/recipes/mlp/_base.py | 15 +- src/quacc/recipes/onetep/_base.py | 5 +- src/quacc/recipes/vasp/core.py | 13 -- src/quacc/runners/ase.py | 133 +++----------- src/quacc/schemas/_aliases/ase.py | 26 +-- src/quacc/schemas/ase.py | 78 +------- src/quacc/settings.py | 66 +++---- src/quacc/wflow_tools/decorators.py | 4 +- src/quacc/wflow_tools/job_patterns.py | 167 ------------------ .../recipes/emt_recipes/test_emt_recipes.py | 73 -------- tests/core/wflow/test_job_patterns.py | 88 --------- tests/covalent/test_customizers.py | 25 --- tests/dask/test_customizers.py | 20 --- tests/prefect/test_customizers.py | 25 --- tests/prefect/test_map_partitions.py | 88 --------- tests/redun/test_customizers.py | 20 --- tests/requirements-dask.txt | 2 +- tests/requirements-jobflow.txt | 2 +- tests/requirements-mlp.txt | 2 +- tests/requirements-parsl.txt | 2 +- tests/requirements-phonons.txt | 2 +- tests/requirements-prefect.txt | 2 +- tests/requirements-redun.txt | 2 +- tests/requirements.txt | 6 +- 34 files changed, 197 insertions(+), 935 deletions(-) create mode 100644 .sourcery.yaml delete mode 100644 src/quacc/recipes/emt/md.py delete mode 100644 src/quacc/wflow_tools/job_patterns.py delete mode 100644 tests/core/wflow/test_job_patterns.py delete mode 100644 tests/prefect/test_map_partitions.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d3d2a9f4f1..2ceb4c2f5c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,9 +4,9 @@ ### Requirements -- [ ] My PR is focused on a [single feature addition or bugfix](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/getting-started/best-practices-for-pull-requests#write-small-prs). -- [ ] My PR has relevant, comprehensive [unit tests](https://quantum-accelerators.github.io/quacc/dev/contributing.html#unit-tests). -- [ ] My PR is on a [custom branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-and-deleting-branches-within-your-repository) (i.e. is _not_ named `main`). +- Your PR should be focused on a [single feature addition or bugfix](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/getting-started/best-practices-for-pull-requests#write-small-prs). +- Your PR should have relevant, comprehensive [unit tests](https://quantum-accelerators.github.io/quacc/dev/contributing.html#unit-tests). +- Your PR should be on a custom branch and _not_ be named `main`. ### Notes diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 171e6e7040..a14a50ec6a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -38,7 +38,7 @@ jobs: - name: Install pip packages run: | pip install uv - uv pip install --system -r tests/requirements.txt "quacc[dev] @ ." + uv pip install --system -r tests/requirements.txt -r tests/requirements-mp.txt "quacc[dev] @ ." - name: Run tests with pytest (w/ coverage) if: matrix.python-version != '3.12' @@ -55,12 +55,50 @@ jobs: path: "coverage.xml" retention-days: 1 + tests-engines-covalent: + runs-on: ubuntu-latest + strategy: + fail-fast: true + + defaults: + run: + shell: bash -l {0} + + steps: + - name: Check out repo + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + cache: pip + cache-dependency-path: tests/requirements**.txt + + - name: Install pip packages + run: | + pip install uv + uv pip install --system -r tests/requirements.txt -r tests/requirements-covalent.txt -r tests/requirements-phonons.txt "quacc[dev] @ ." + + - name: Start Covalent server + run: covalent start + + - name: Run tests with pytest + run: pytest --durations=10 tests/covalent --cov=quacc --cov-report=xml + + - name: Upload code coverage report to Artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ github.job }} coverage report + path: "coverage.xml" + retention-days: 1 + tests-engines: runs-on: ubuntu-latest strategy: fail-fast: true matrix: - wflow_engine: [covalent, dask, parsl, prefect, redun, jobflow] + wflow_engine: [dask, parsl, redun, jobflow] defaults: run: @@ -73,7 +111,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: ${{ matrix.wflow_engine == 'covalent' && '3.10' || '3.11' }} + python-version: "3.11" cache: pip cache-dependency-path: tests/requirements**.txt @@ -93,10 +131,6 @@ jobs: pip install uv uv pip install --system -r tests/requirements.txt -r tests/requirements-${{ matrix.wflow_engine }}.txt -r tests/requirements-phonons.txt "quacc[dev] @ ." - - name: Start Covalent server - if: matrix.wflow_engine == 'covalent' - run: covalent start - - name: Run tests with pytest run: pytest --durations=10 tests/${{ matrix.wflow_engine }} --cov=quacc --cov-report=xml @@ -113,10 +147,12 @@ jobs: path: "coverage.xml" retention-days: 1 - tests-psi4: + tests-engines2: + runs-on: ubuntu-latest strategy: fail-fast: true - runs-on: ubuntu-latest + matrix: + wflow_engine: [prefect] defaults: run: @@ -133,33 +169,22 @@ jobs: cache: pip cache-dependency-path: tests/requirements**.txt - - name: Set up conda - uses: conda-incubator/setup-miniconda@v3 - with: - python-version: ${{ matrix.python-version }} - activate-environment: quacc-env - - - name: Install conda packages - run: | - conda install -n base conda-libmamba-solver - conda install psi4 -c conda-forge --solver libmamba - - name: Install pip packages run: | pip install uv - uv pip install --system -r tests/requirements.txt "quacc[dev] @ ." + uv pip install --system -r tests/requirements.txt -r tests/requirements-${{ matrix.wflow_engine }}.txt -r tests/requirements-phonons.txt "quacc[dev] @ ." - name: Run tests with pytest - run: pytest -k 'psi4' --durations=10 --cov=quacc --cov-report=xml + run: pytest --durations=10 tests/${{ matrix.wflow_engine }} --cov=quacc --cov-report=xml - name: Upload code coverage report to Artifact uses: actions/upload-artifact@v4 with: - name: ${{ github.job }} coverage report + name: ${{ github.job }} ${{ matrix.wflow_engine }} coverage report path: "coverage.xml" retention-days: 1 - tests-defects-phonons-espresso: + tests-psi4: strategy: fail-fast: true runs-on: ubuntu-latest @@ -186,15 +211,17 @@ jobs: activate-environment: quacc-env - name: Install conda packages - run: conda install -c conda-forge qe + run: | + conda install -n base conda-libmamba-solver + conda install psi4 -c conda-forge --solver libmamba - name: Install pip packages run: | pip install uv - uv pip install --system -r tests/requirements.txt -r tests/requirements-defects.txt -r tests/requirements-phonons.txt "quacc[dev] @ ." + uv pip install --system -r tests/requirements.txt "quacc[dev] @ ." - name: Run tests with pytest - run: pytest -k 'defects or phonon or espresso' --durations=10 --cov=quacc --cov-report=xml + run: pytest -k 'psi4' --durations=10 --cov=quacc --cov-report=xml - name: Upload code coverage report to Artifact uses: actions/upload-artifact@v4 @@ -203,7 +230,7 @@ jobs: path: "coverage.xml" retention-days: 1 - tests-tblite-dftbplus: + tests-defects-phonons-espresso: strategy: fail-fast: true runs-on: ubuntu-latest @@ -230,15 +257,15 @@ jobs: activate-environment: quacc-env - name: Install conda packages - run: conda install -c conda-forge dftbplus + run: conda install -c conda-forge qe - name: Install pip packages run: | pip install uv - uv pip install --system -r tests/requirements.txt -r tests/requirements-tblite.txt -r tests/requirements-phonons.txt "quacc[dev] @ ." + uv pip install --system -r tests/requirements.txt -r tests/requirements-defects.txt -r tests/requirements-phonons.txt "quacc[dev] @ ." - name: Run tests with pytest - run: pytest -k 'dftb or tblite' --durations=10 --cov=quacc --cov-report=xml + run: pytest -k 'defects or phonon or espresso' --durations=10 --cov=quacc --cov-report=xml - name: Upload code coverage report to Artifact uses: actions/upload-artifact@v4 @@ -247,7 +274,7 @@ jobs: path: "coverage.xml" retention-days: 1 - tests-qchem-sella: + tests-tblite-dftbplus: strategy: fail-fast: true runs-on: ubuntu-latest @@ -274,15 +301,15 @@ jobs: activate-environment: quacc-env - name: Install conda packages - run: conda install -c conda-forge openbabel + run: conda install -c conda-forge dftbplus - name: Install pip packages run: | pip install uv - uv pip install --system -r tests/requirements.txt -r tests/requirements-sella.txt "quacc[dev] @ ." + uv pip install --system -r tests/requirements.txt -r tests/requirements-tblite.txt -r tests/requirements-phonons.txt "quacc[dev] @ ." - name: Run tests with pytest - run: pytest -k 'qchem or sella' --durations=10 --cov=quacc --cov-report=xml + run: pytest -k 'dftb or tblite' --durations=10 --cov=quacc --cov-report=xml - name: Upload code coverage report to Artifact uses: actions/upload-artifact@v4 @@ -291,7 +318,7 @@ jobs: path: "coverage.xml" retention-days: 1 - tests-mlps: + tests-qchem-sella: strategy: fail-fast: true runs-on: ubuntu-latest @@ -311,13 +338,22 @@ jobs: cache: pip cache-dependency-path: tests/requirements**.txt + - name: Set up conda + uses: conda-incubator/setup-miniconda@v3 + with: + python-version: ${{ matrix.python-version }} + activate-environment: quacc-env + + - name: Install conda packages + run: conda install -c conda-forge openbabel + - name: Install pip packages run: | pip install uv - uv pip install --system -r tests/requirements.txt -r tests/requirements-mlp.txt -r tests/requirements-newtonnet.txt -r tests/requirements-sella.txt -r tests/requirements-phonons.txt "quacc[dev] @ ." + uv pip install --system -r tests/requirements.txt -r tests/requirements-sella.txt "quacc[dev] @ ." - name: Run tests with pytest - run: pytest -k 'mlp or newtonnet' --durations=10 --cov=quacc --cov-report=xml + run: pytest -k 'qchem or sella' --durations=10 --cov=quacc --cov-report=xml - name: Upload code coverage report to Artifact uses: actions/upload-artifact@v4 @@ -326,7 +362,7 @@ jobs: path: "coverage.xml" retention-days: 1 - tests-mp: + tests-mlps: strategy: fail-fast: true runs-on: ubuntu-latest @@ -349,10 +385,10 @@ jobs: - name: Install pip packages run: | pip install uv - uv pip install --system -r tests/requirements.txt -r tests/requirements-mp.txt "quacc[dev] @ ." + uv pip install --system -r tests/requirements.txt -r tests/requirements-mlp.txt -r tests/requirements-newtonnet.txt -r tests/requirements-sella.txt -r tests/requirements-phonons.txt "quacc[dev] @ ." - name: Run tests with pytest - run: pytest -k 'mp_' --durations=10 --cov=quacc --cov-report=xml + run: pytest -k 'mlp or newtonnet' --durations=10 --cov=quacc --cov-report=xml - name: Upload code coverage report to Artifact uses: actions/upload-artifact@v4 @@ -364,13 +400,14 @@ jobs: codecov: needs: - tests-core + - tests-engines-covalent - tests-engines + - tests-engines2 - tests-psi4 - tests-defects-phonons-espresso - tests-tblite-dftbplus - tests-qchem-sella - tests-mlps - - tests-mp runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.sourcery.yaml b/.sourcery.yaml new file mode 100644 index 0000000000..f8b248f4e5 --- /dev/null +++ b/.sourcery.yaml @@ -0,0 +1,5 @@ +github: + ignore_labels: + - sourcery-ignore +rule_settings: + python_version: "3.9" diff --git a/CHANGELOG.md b/CHANGELOG.md index b00a1ea095..2b93e363e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,16 +8,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added -- Added a function `from quacc import get_settings` to fetch the current settings on a thread -- Added a mechanism to update the settings on-the-fly via a special `settings_swap` keyword argument that can be passed to the decorators when a workflow engine is used. +- Added a mechanism to update the settings on-the-fly via a special `settings_swap` keyword argument that can be passed to the decorators. -### Fixed +### Changed -- Fixed an edge-case that can occur in multithreading environments where in-memory changes to the `QuaccSettings` could carry over to a concurrent thread +- The global `SETTINGS` variable is gone! Modifying a global variable can cause havoc in multithreaded environments and was never compatible with workflow engines. Refer to the [Modifying Settings](https://quantum-accelerators.github.io/quacc/user/settings/settings.html) page for the recommended approach. For most users without an active workflow engine, this will be the `with change_settings()` context manager. -### Removed +### Fixed -- The global `SETTINGS` variable is gone! Modifying a global variable can cause havoc in multithreaded environments and was never compatible with workflow engines. Refer to the [Modifying Settings](https://quantum-accelerators.github.io/quacc/user/settings/settings.html) page for the recommended approach. For most users without an active workflow engine, this will be the `with change_settings()` context manager. +- Fixed an edge-case that can occur in multithreading environments where in-memory changes to the `QuaccSettings` could carry over to a concurrent thread ## [0.9.5] diff --git a/docs/about/contributors.md b/docs/about/contributors.md index b5dbf78fd3..1321e9c5a7 100644 --- a/docs/about/contributors.md +++ b/docs/about/contributors.md @@ -27,7 +27,6 @@ Additional contributions were made by the individuals listed [here](https://gith - [@zulissimeta](https://github.com/zulissimeta): Dask support - [@yw-fang](https://github.com/yw-fang): VASP Non-SCF recipe - [@espottesmith](http://github.com/espottesmith): Some ORCA and Q-Chem workflows, mostly re: IRC -- [@honghuikim](https://github.com/honghuikim): Dynamic changing of quacc settings with workflow engines ## Inspiration diff --git a/docs/install/codes.md b/docs/install/codes.md index c6cbedbab8..7d639417f6 100644 --- a/docs/install/codes.md +++ b/docs/install/codes.md @@ -84,12 +84,7 @@ export QUACC_ORCA_CMD="/path/to/orca/orca" ## Psi4 -If you plan to use Psi4 with quacc, you will need to install it prior to use. This can be done as follows: - -```bash -conda install -n base conda-libmamba-solver -conda install psi4 -c conda-forge --solver libmamba -``` +If you plan to use Psi4 with quacc, you will need to install it prior to use. This can be done as described in the [Psi4 installation guide](https://psicode.org/installs/latest/). ## Q-Chem diff --git a/docs/user/recipes/recipes_list.md b/docs/user/recipes/recipes_list.md index d4f287b8e6..9085e09417 100644 --- a/docs/user/recipes/recipes_list.md +++ b/docs/user/recipes/recipes_list.md @@ -29,7 +29,6 @@ The list of available quacc recipes is shown below. The "Req'd Extras" column sp | ------------------------ | ---------------- | ------------------------------------------------------- | ---------------- | | EMT Static | `#!Python @job` | [quacc.recipes.emt.core.static_job][] | | | EMT Relax | `#!Python @job` | [quacc.recipes.emt.core.relax_job][] | | -| EMT MD | `#!Python @job` | [quacc.recipes.emt.md.md_job][] | | | EMT Bulk to Defects | `#!Python @flow` | [quacc.recipes.emt.defects.bulk_to_defects_flow][] | `quacc[defects]` | | EMT Bulk to Slabs | `#!Python @flow` | [quacc.recipes.emt.slabs.bulk_to_slabs_flow][] | | | EMT Phonons | `#!Python @flow` | [quacc.recipes.emt.phonons.phonon_flow][] | `quacc[phonons]` | @@ -149,6 +148,7 @@ The list of available quacc recipes is shown below. The "Req'd Extras" column sp | ORCA ASE Relax | `#!Python @job` | [quacc.recipes.orca.core.ase_relax_job][] | | | ORCA ASE Quasi-IRC Perturb | `#!Python @job` | [quacc.recipes.orca.core.ase_quasi_irc_perturb_job][] | | + ## Psi4 diff --git a/docs/user/settings/settings.md b/docs/user/settings/settings.md index 1978725444..46eb35d544 100644 --- a/docs/user/settings/settings.md +++ b/docs/user/settings/settings.md @@ -51,32 +51,30 @@ export QUACC_WORKFLOW_ENGINE=None If you want to define quacc settings on-the-fly without writing them to a YAML file or using environment variables, you can do so using the context handler function [quacc.settings.change_settings][] as follows: ```python -from ase.build import bulk from quacc import change_settings -from quacc.recipes.emt.core import relax_job - -atoms = bulk("Cu") with change_settings({"GZIP_FILES": False}): - result = relax_job(atoms) + pass # Your calculation here ``` !!! Important "Active Workflow Engine" When deploying calculations via a workflow engine, changes to in-memory global variables on the local machine will not be reflected on the remote machine. Instead, this should be done via a custom `settings_swap` keyword argument that is supported by the `@job` decorator. + Essentially, the following two blocks of code are functionally the same: + ```python from quacc import job - @job(settings_swap={"GZIP_FILES": False}) # (1)! + @job(settings_swap={"GZIP_FILES"}) # (1)! def add(a, b): return a + b ``` 1. This is the same as doing - ```python + ```python from quacc import change_settings, job @@ -89,15 +87,12 @@ with change_settings({"GZIP_FILES": False}): If using a pre-made `@job`, you can simply redecorate it so that it supports your custom settings: ```python - from ase.build import bulk - from quacc import redecorate, job - from quacc.recipes.emt.core import relax_job + from quacc import redecorate + from quacc.recipes.emt.core import static_job - atoms = bulk("Cu") - relax_job_ = redecorate(relax_job, job(settings_swap={"GZIP_FILES": False})) - results = relax_job_(atoms) + static_job_ = redecorate(static_job, settings_swap={"GZIP_FILES": False}) ``` !!! Tip "When is This Method Ideal?" - This approach is ideal for fine-tuned modifications to settings within your workflow. + This approach is ideal for fine-tuned modifications to settings within your workflow and for debugging scenarios (e.g. in a Jupyter Notebook). diff --git a/src/quacc/recipes/emt/md.py b/src/quacc/recipes/emt/md.py deleted file mode 100644 index 830c6ef19b..0000000000 --- a/src/quacc/recipes/emt/md.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -Molecular Dynamics recipes for EMT. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from ase.calculators.emt import EMT -from ase.md.verlet import VelocityVerlet -from ase.units import bar, fs - -from quacc import Remove, job -from quacc.runners.ase import Runner -from quacc.schemas.ase import summarize_md_run -from quacc.utils.dicts import recursive_dict_merge - -if TYPE_CHECKING: - from ase.atoms import Atoms - from ase.md.md import MolecularDynamics - - from quacc.runners.ase import MDParams - from quacc.schemas._aliases.ase import DynSchema - from quacc.utils.files import Filenames, SourceDirectory - - -@job -def md_job( - atoms: Atoms, - dynamics: MolecularDynamics = VelocityVerlet, - steps: int = 1000, - timestep_fs: float = 1.0, - temperature_K: float | None = None, - pressure_bar: float | None = None, - md_params: MDParams | None = None, - copy_files: SourceDirectory | dict[SourceDirectory, Filenames] | None = None, - **calc_kwargs, -) -> DynSchema: - """ - Carry out a Molecular Dynamics calculation. - - Parameters - ---------- - atoms - Atoms object - dynamics - ASE `MolecularDynamics` class to use, from `ase.md.md.MolecularDynamics`. - steps - Number of MD steps to run. - timestep_fs - Time step in fs. - temperature_K - Temperature in K, if applicable for the given ensemble. - pressure_bar - Pressure in bar, if applicable for the given ensemble. - md_params - Dictionary of custom kwargs for the MD run. For a list of available - keys, refer to [quacc.runners.ase.Runner.run_md][]. - copy_files - Files to copy (and decompress) from source to the runtime directory. - **calc_kwargs - Custom kwargs for the EMT calculator. Set a value to - `quacc.Remove` to remove a pre-existing key entirely. For a list of available - keys, refer to the `ase.calculators.emt.EMT` calculator. - - Returns - ------- - DynSchema - Dictionary of results, specified in [quacc.schemas.ase.summarize_md_run][]. - See the type-hint for the data structure. - """ - md_defaults = { - "steps": steps, - "dynamics_kwargs": { - "timestep": timestep_fs * fs, - "temperature_K": temperature_K if temperature_K else Remove, - "pressure_au": pressure_bar * bar if pressure_bar else Remove, - }, - } - md_params = recursive_dict_merge(md_defaults, md_params) - - calc = EMT(**calc_kwargs) - dyn = Runner(atoms, calc, copy_files=copy_files).run_md(dynamics, **md_params) - - return summarize_md_run(dyn, additional_fields={"name": "EMT MD"}) diff --git a/src/quacc/recipes/espresso/_base.py b/src/quacc/recipes/espresso/_base.py index 7ae729a7c5..dbe99eab45 100644 --- a/src/quacc/recipes/espresso/_base.py +++ b/src/quacc/recipes/espresso/_base.py @@ -142,8 +142,9 @@ def run_and_summarize_opt( opt_defaults The default optimization parameters. opt_params - Dictionary of custom kwargs for the optimization process. For a list - of available keys, refer to [quacc.runners.ase.Runner.run_opt][]. + Dictionary of parameters to pass to the optimizer. pass "optimizer" + to change the optimizer being used. "fmax" and "max_steps" are commonly + used keywords. See the ASE documentation for more information. additional_fields Any additional fields to supply to the summarizer. copy_files diff --git a/src/quacc/recipes/mlp/_base.py b/src/quacc/recipes/mlp/_base.py index f086a30546..a172868d08 100644 --- a/src/quacc/recipes/mlp/_base.py +++ b/src/quacc/recipes/mlp/_base.py @@ -5,6 +5,7 @@ import logging from functools import lru_cache from typing import TYPE_CHECKING +from warnings import warn if TYPE_CHECKING: from typing import Literal @@ -21,6 +22,11 @@ def pick_calculator( """ Adapted from `matcalc.util.get_universal_calculator`. + .. deprecated:: 0.7.6 + method `mace` will be removed in a later version, it is replaced by 'mace-mp-0' + which more accurately reflects the nature of the model and allows for versioning + in the future. + Parameters ---------- method @@ -56,10 +62,17 @@ def pick_calculator( calc = CHGNetCalculator(**kwargs) - elif method.lower() == "mace-mp-0": + elif method.lower() in ["mace-mp-0", "mace"]: from mace import __version__ from mace.calculators import mace_mp + if method.lower() == "mace": + warn( + "'mace' is deprecated and support will be removed. Use 'mace-mp-0' instead!", + DeprecationWarning, + stacklevel=3, + ) + if "default_dtype" not in kwargs: kwargs["default_dtype"] = "float64" calc = mace_mp(**kwargs) diff --git a/src/quacc/recipes/onetep/_base.py b/src/quacc/recipes/onetep/_base.py index 5e66083f74..6567f20da7 100644 --- a/src/quacc/recipes/onetep/_base.py +++ b/src/quacc/recipes/onetep/_base.py @@ -82,8 +82,9 @@ def run_and_summarize_opt( opt_defaults The default optimization parameters. opt_params - Dictionary of custom kwargs for the optimization process. For a list - of available keys, refer to [quacc.runners.ase.Runner.run_opt][]. + Dictionary of parameters to pass to the optimizer. pass "optimizer" + to change the optimizer being used. "fmax" and "max_steps" are commonly + used keywords. See the ASE documentation for more information. additional_fields Any additional fields to supply to the summarizer. copy_files diff --git a/src/quacc/recipes/vasp/core.py b/src/quacc/recipes/vasp/core.py index fdb0b5f525..b30de1d62c 100644 --- a/src/quacc/recipes/vasp/core.py +++ b/src/quacc/recipes/vasp/core.py @@ -4,7 +4,6 @@ from pathlib import Path from typing import TYPE_CHECKING, Literal -from warnings import warn import numpy as np from monty.os.path import zpath @@ -108,12 +107,6 @@ def relax_job( Dictionary of results from [quacc.schemas.vasp.vasp_summarize_run][]. See the type-hint for the data structure. """ - if relax_cell: - warn( - "The `relax_cell` parameter will default to `False` by default in a future version for internal consistency throughout quacc. Please set `relax_cell=True` directly.", - DeprecationWarning, - stacklevel=3, - ) calc_defaults = { "ediffg": -0.02, "isif": 3 if relax_cell else 2, @@ -226,12 +219,6 @@ def ase_relax_job( VaspASEOptSchema Dictionary of results. See the type-hint for the data structure. """ - if relax_cell: - warn( - "The `relax_cell` parameter will default to `False` by default in a future version for internal consistency throughout quacc. Please set `relax_cell=True` directly.", - DeprecationWarning, - stacklevel=3, - ) calc_defaults = {"lcharg": False, "lwave": False, "nsw": 0} opt_defaults = {"relax_cell": relax_cell} return run_and_summarize_opt( diff --git a/src/quacc/runners/ase.py b/src/quacc/runners/ase.py index 95aec078c7..b8ef2dbe1a 100644 --- a/src/quacc/runners/ase.py +++ b/src/quacc/runners/ase.py @@ -2,7 +2,6 @@ from __future__ import annotations -import logging import sys from importlib.util import find_spec from shutil import copy, copytree @@ -12,12 +11,6 @@ from ase.calculators import calculator from ase.filters import FrechetCellFilter from ase.io import Trajectory, read -from ase.md.md import MolecularDynamics -from ase.md.velocitydistribution import ( - MaxwellBoltzmannDistribution, - Stationary, - ZeroRotation, -) from ase.optimize import BFGS from ase.optimize.sciopt import SciPyOptimizer from ase.vibrations import Vibrations @@ -30,8 +23,6 @@ from quacc.runners.prep import terminate from quacc.utils.dicts import recursive_dict_merge -LOGGER = logging.getLogger(__name__) - has_sella = bool(find_spec("sella")) @@ -41,8 +32,7 @@ from ase.atoms import Atoms from ase.calculators.calculator import Calculator - from ase.optimize.optimize import Dynamics - from np.random import Generator + from ase.optimize.optimize import Optimizer from quacc.utils.files import Filenames, SourceDirectory @@ -51,44 +41,30 @@ class OptParams(TypedDict, total=False): Type hint for `opt_params` used throughout quacc. """ - relax_cell: bool - fmax: float | None + fmax: float max_steps: int - optimizer: Dynamics - optimizer_kwargs: dict[str, Any] | None + optimizer: Optimizer # default = BFGS + optimizer_kwargs: OptimizerKwargs | None store_intermediate_results: bool fn_hook: Callable | None run_kwargs: dict[str, Any] | None - class MDParams(TypedDict, total=False): + class OptimizerKwargs(TypedDict, total=False): """ - Type hint for `md_params` used throughout quacc. + Type hint for `optimizer_kwargs` in [quacc.runners.ase.Runner.run_opt][]. """ - dynamics: MolecularDynamics - dynamics_kwargs: dict[str, Any] | None - steps: int - maxwell_boltzmann_kwargs: MaxwellBoltzmanDistributionKwargs | None - set_com_stationary: bool - set_zero_rotation: bool + restart: Path | str | None # default = None + append_trajectory: bool # default = False class VibKwargs(TypedDict, total=False): """ Type hint for `vib_kwargs` in [quacc.runners.ase.Runner.run_vib][]. """ - indices: list[int] | None - delta: float - nfree: int - - class MaxwellBoltzmanDistributionKwargs(TypedDict, total=False): - """ - Type hint for `maxwell_boltzmann_kwargs` in [quacc.runners.ase.Runner.run_md][]. - """ - - temperature_K: float - force_temp: bool - rng: Generator | None + indices: list[int] | None # default = None + delta: float # default = 0.01 + nfree: int # default = 2 class Runner(BaseRunner): @@ -191,12 +167,12 @@ def run_opt( relax_cell: bool = False, fmax: float | None = 0.01, max_steps: int = 1000, - optimizer: Dynamics = BFGS, - optimizer_kwargs: dict[str, Any] | None = None, + optimizer: Optimizer = BFGS, + optimizer_kwargs: OptimizerKwargs | None = None, store_intermediate_results: bool = False, fn_hook: Callable | None = None, run_kwargs: dict[str, Any] | None = None, - ) -> Dynamics: + ) -> Optimizer: """ This is a wrapper around the optimizers in ASE. @@ -227,8 +203,8 @@ def run_opt( Returns ------- - Dynamics - The ASE Dynamics object following an optimization. + Optimizer + The ASE Optimizer object. """ # Set defaults settings = get_settings() @@ -248,12 +224,8 @@ def run_opt( raise ValueError(msg) # Handle optimizer kwargs - if ( - issubclass(optimizer, (SciPyOptimizer, MolecularDynamics)) - or optimizer.__name__ == "IRC" - ): + if issubclass(optimizer, SciPyOptimizer) or optimizer.__name__ == "IRC": # https://gitlab.com/ase/ase/-/issues/1476 - # https://gitlab.com/ase/ase/-/merge_requests/3310 optimizer_kwargs.pop("restart", None) if optimizer.__name__ == "Sella": self._set_sella_kwargs(optimizer_kwargs) @@ -268,17 +240,15 @@ def run_opt( self.atoms = FrechetCellFilter(self.atoms) # Run optimization - full_run_kwargs = {"fmax": fmax, "steps": max_steps, **run_kwargs} - if issubclass(optimizer, MolecularDynamics): - full_run_kwargs.pop("fmax") try: with traj, optimizer(self.atoms, **optimizer_kwargs) as dyn: - if issubclass(optimizer, (SciPyOptimizer, MolecularDynamics)): + if issubclass(optimizer, SciPyOptimizer): # https://gitlab.coms/ase/ase/-/issues/1475 - # https://gitlab.com/ase/ase/-/issues/1497 - dyn.run(**full_run_kwargs) + dyn.run(fmax=fmax, steps=max_steps, **run_kwargs) else: - for i, _ in enumerate(dyn.irun(**full_run_kwargs)): + for i, _ in enumerate( + dyn.irun(fmax=fmax, steps=max_steps, **run_kwargs) + ): if store_intermediate_results: self._copy_intermediate_files( i, @@ -339,65 +309,6 @@ def run_vib(self, vib_kwargs: VibKwargs | None = None) -> Vibrations: return vib - def run_md( - self, - dynamics: MolecularDynamics, - dynamics_kwargs: dict[str, Any] | None = None, - steps: int = 1000, - maxwell_boltzmann_kwargs: MaxwellBoltzmanDistributionKwargs | None = None, - set_com_stationary: bool = False, - set_zero_rotation: bool = False, - ) -> MolecularDynamics: - """ - Run an ASE-based MD in a scratch directory and copy the results back to - the original directory. - - Parameters - ---------- - dynamics - MolecularDynamics class to use, from `ase.md.md.MolecularDynamics`. - dynamics_kwargs - Dictionary of kwargs for the dynamics. Takes all valid kwargs for ASE - MolecularDynamics classes. - steps - Maximum number of steps to run - maxwell_boltzmann_kwargs - If specified, a `MaxwellBoltzmannDistribution` will be applied to the atoms - based on `ase.md.velocitydistribution.MaxwellBoltzmannDistribution` with the - specified keyword arguments. - set_com_stationary - Whether to set the center-of-mass momentum to zero. This would be applied after - any `MaxwellBoltzmannDistribution` is set. - set_zero_rotation - Whether to set the total angular momentum to zero. This would be applied after - any `MaxwellBoltzmannDistribution` is set. - - Returns - ------- - MolecularDymamics - The ASE MolecularDynamics object. - """ - - # Set defaults - dynamics_kwargs = dynamics_kwargs or {} - maxwell_boltzmann_kwargs = maxwell_boltzmann_kwargs or {} - settings = get_settings() - dynamics_kwargs["logfile"] = "-" if settings.DEBUG else self.tmpdir / "md.log" - - if maxwell_boltzmann_kwargs: - MaxwellBoltzmannDistribution(self.atoms, **maxwell_boltzmann_kwargs) - if set_com_stationary: - Stationary(self.atoms) - if set_zero_rotation: - ZeroRotation(self.atoms) - - return self.run_opt( - fmax=None, - max_steps=steps, - optimizer=dynamics, - optimizer_kwargs=dynamics_kwargs, - ) - def _copy_intermediate_files( self, step_number: int, files_to_ignore: list[Path] | None = None ) -> None: diff --git a/src/quacc/schemas/_aliases/ase.py b/src/quacc/schemas/_aliases/ase.py index f529a3f4f4..fc736c6e9b 100644 --- a/src/quacc/schemas/_aliases/ase.py +++ b/src/quacc/schemas/_aliases/ase.py @@ -19,17 +19,8 @@ class Parameters(TypedDict): """Dictionary of parameters from atoms.calc.parameters""" -class ParametersDyn(TypedDict): - """Dictionary of parameters from Dynamics.todict()""" - - -class TrajectoryLog(TypedDict): - """Dictionary of parameters related to the MD trajectory""" - - # ASE units - kinetic_energy: float - temperature: float - time: float +class ParametersOpt(TypedDict): + """Dictionary of parameters from Optimizer.todict()""" class RunSchema(AtomsSchema): @@ -46,21 +37,12 @@ class RunSchema(AtomsSchema): class OptSchema(RunSchema): """Schema for [quacc.schemas.ase.summarize_opt_run][]""" - parameters_opt: ParametersDyn + parameters_opt: ParametersOpt # from dyn.todict() converged: bool trajectory: list[Atoms] trajectory_results: list[Results] -class DynSchema(RunSchema): - """Schema for [quacc.schemas.ase.summarize_md_run][]""" - - parameters_md: ParametersDyn - trajectory: list[Atoms] - trajectory_log: TrajectoryLog - trajectory_results: list[Results] - - class ParametersVib(TypedDict): delta: float direction: str @@ -91,7 +73,6 @@ class PhononSchema(RunSchema): class ParametersThermo(TypedDict): - # ASE units temperature: float pressure: float sigma: int @@ -102,7 +83,6 @@ class ParametersThermo(TypedDict): class ThermoResults(TypedDict): - # ASE units energy: float enthalpy: float entropy: float diff --git a/src/quacc/schemas/ase.py b/src/quacc/schemas/ase.py index 5bc44bf7f1..33d18bc048 100644 --- a/src/quacc/schemas/ase.py +++ b/src/quacc/schemas/ase.py @@ -22,13 +22,11 @@ from ase.atoms import Atoms from ase.io import Trajectory - from ase.md.md import MolecularDynamics from ase.optimize.optimize import Optimizer from ase.thermochemistry import IdealGasThermo from maggma.core import Store from quacc.schemas._aliases.ase import ( - DynSchema, OptSchema, RunSchema, ThermoSchema, @@ -173,6 +171,8 @@ def summarize_opt_run( if not trajectory: trajectory = read(dyn.trajectory.filename, index=":") trajectory_results = [atoms.calc.results for atoms in trajectory] + for traj_atoms in trajectory: + traj_atoms.calc = None initial_atoms = trajectory[0] final_atoms = get_final_atoms_from_dynamics(dyn) @@ -213,80 +213,6 @@ def summarize_opt_run( ) -def summarize_md_run( - dyn: MolecularDynamics, - trajectory: Trajectory | list[Atoms] | None = None, - charge_and_multiplicity: tuple[int, int] | None = None, - move_magmoms: bool = True, - additional_fields: dict[str, Any] | None = None, - store: Store | bool | None = None, -) -> DynSchema: - """ - Get tabulated results from an ASE Atoms trajectory and store them in a database- - friendly format. This is meant to be compatible with all calculator types. - - Parameters - ---------- - dyn - ASE MolecularDynamics object. - trajectory - ASE Trajectory object or list[Atoms] from reading a trajectory file. If - None, the trajectory must be found in `dyn.trajectory.filename`. - charge_and_multiplicity - Charge and spin multiplicity of the Atoms object, only used for Molecule - metadata. - move_magmoms - Whether to move the final magmoms of the original Atoms object to the - initial magmoms of the returned Atoms object. - additional_fields - Additional fields to add to the task document. - store - Maggma Store object to store the results in. If None, - `QuaccSettings.STORE` will be used. - - Returns - ------- - DynSchema - Dictionary representation of the task document - """ - settings = get_settings() - base_task_doc = summarize_opt_run( - dyn, - trajectory=trajectory, - check_convergence=False, - charge_and_multiplicity=charge_and_multiplicity, - move_magmoms=move_magmoms, - store=None, - ) - del base_task_doc["converged"] - - # Clean up the opt parameters - parameters_md = base_task_doc.pop("parameters_opt") - parameters_md.pop("logfile", None) - - trajectory_log = [] - for t, atoms in enumerate(base_task_doc["trajectory"]): - trajectory_log.append( - { - "kinetic_energy": atoms.get_kinetic_energy(), - "temperature": atoms.get_temperature(), - "time": t * parameters_md["timestep"], - } - ) - - md_fields = {"parameters_md": parameters_md, "trajectory_log": trajectory_log} - - # Create a dictionary of the inputs/outputs - unsorted_task_doc = base_task_doc | md_fields | additional_fields - - return finalize_dict( - unsorted_task_doc, - base_task_doc["dir_name"], - gzip_file=settings.GZIP_FILES, - store=store, - ) - - def summarize_vib_and_thermo( vib: Vibrations, igt: IdealGasThermo, diff --git a/src/quacc/settings.py b/src/quacc/settings.py index 3f5a3755e1..4e43a497f7 100644 --- a/src/quacc/settings.py +++ b/src/quacc/settings.py @@ -488,33 +488,6 @@ def validate_espresso_parallel_cmd( return v - @staticmethod - def _use_custom_config_settings(settings: dict[str, Any]) -> dict[str, Any]: - """Parse user settings from a custom YAML. - - Parameters - ---------- - settings - Initial settings. - - Returns - ------- - dict - Updated settings based on the custom YAML. - """ - config_file_path = ( - Path(settings.get("CONFIG_FILE", _DEFAULT_CONFIG_FILE_PATH)) - .expanduser() - .resolve() - ) - - new_settings = {} # type: dict - if config_file_path.exists() and config_file_path.stat().st_size > 0: - new_settings |= loadfn(config_file_path) - - new_settings.update(settings) - return new_settings - @model_validator(mode="before") @classmethod def load_user_settings(cls, settings: dict[str, Any]) -> dict[str, Any]: @@ -532,7 +505,34 @@ def load_user_settings(cls, settings: dict[str, Any]) -> dict[str, Any]: dict Loaded settings. """ - return _type_handler(cls._use_custom_config_settings(settings)) + return _type_handler(_use_custom_config_settings(settings)) + + +def _use_custom_config_settings(settings: dict[str, Any]) -> dict[str, Any]: + """Parse user settings from a custom YAML. + + Parameters + ---------- + settings : dict + Initial settings. + + Returns + ------- + dict + Updated settings based on the custom YAML. + """ + config_file_path = ( + Path(settings.get("CONFIG_FILE", _DEFAULT_CONFIG_FILE_PATH)) + .expanduser() + .resolve() + ) + + new_settings = {} # type: dict + if config_file_path.exists() and config_file_path.stat().st_size > 0: + new_settings |= loadfn(config_file_path) + + new_settings.update(settings) + return new_settings def _type_handler(settings: dict[str, Any]) -> dict[str, Any]: @@ -560,7 +560,7 @@ def _type_handler(settings: dict[str, Any]) -> dict[str, Any]: @contextmanager -def change_settings(changes: dict[str, Any] | None): +def change_settings(changes: dict[str, Any] | None) -> None: """ Temporarily change an attribute of an object. @@ -568,6 +568,10 @@ def change_settings(changes: dict[str, Any] | None): ---------- changes Dictionary of changes to make formatted as attribute: value. + + Returns + ------- + None """ from quacc import _internally_set_settings, get_settings @@ -598,13 +602,13 @@ def change_settings_wrap(func: Callable, changes: dict[str, Any]) -> Callable: Callable The wrapped function. """ - original_func = func._original_func if getattr(func, "_changed", False) else func + original_func = func._original_func if getattr(func, "__changed__", False) else func @wraps(original_func) def wrapper(*args, **kwargs): with change_settings(changes): return original_func(*args, **kwargs) - wrapper._changed = True + wrapper.__changed__ = True wrapper._original_func = original_func return wrapper diff --git a/src/quacc/wflow_tools/decorators.py b/src/quacc/wflow_tools/decorators.py index 5370ad11b1..0d8ee5e50f 100644 --- a/src/quacc/wflow_tools/decorators.py +++ b/src/quacc/wflow_tools/decorators.py @@ -626,8 +626,8 @@ def wrapper( ): return func(*f_args, **f_kwargs) - if getattr(func, "_changed", False): - wrapper._changed = func._changed + if getattr(func, "__changed__", False): + wrapper.__changed__ = func.__changed__ wrapper._original_func = func._original_func wrapper.__name__ = func.__name__ return wrapper diff --git a/src/quacc/wflow_tools/job_patterns.py b/src/quacc/wflow_tools/job_patterns.py deleted file mode 100644 index 0fd659ca09..0000000000 --- a/src/quacc/wflow_tools/job_patterns.py +++ /dev/null @@ -1,167 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from quacc.wflow_tools.customizers import strip_decorator -from quacc.wflow_tools.decorators import job - -if TYPE_CHECKING: - from typing import Any, Callable - - -@job -def partition(list_to_partition: list, num_partitions: int) -> list[Any]: - """ - Given a list, partition it into n roughly equal lists - - Parameters - ---------- - list_to_partition - the list to partition - num_partitions - the number of partitions to output - - Returns - ------- - list[Any] - n lists constructed from a - """ - k, m = divmod(len(list_to_partition), num_partitions) - return [ - list_to_partition[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] - for i in range(num_partitions) - ] - - -def map_partitioned_lists( - func: Callable, - num_partitions: int, - unmapped_kwargs: dict[str, Any] | None = None, - **mapped_kwargs: dict[str, list[list[Any]]], -) -> list[Any]: - """ - Given list-of-lists parameters (say a list of batches that we want to map over), - apply func to each element of each list - - For example: - - ```python - @job - def testjob(**kwargs): - print(kwargs) - - - @flow - def testflow(): - num_partitions = 2 - result = map_partitioned_lists( - testjob, - num_partitions, - test_arg_1=partition([1, 2, 3, 4, 5], num_partitions), - test_arg_2=partition(["a", "b", "c", "d", "e"], num_partitions), - ) - - - testflow() - ``` - - should yield - - ```python - {"test_arg_1": 1, "test_arg_2": "a"} - {"test_arg_1": 2, "test_arg_2": "b"} - {"test_arg_1": 3, "test_arg_2": "c"} - {"test_arg_1": 4, "test_arg_2": "d"} - {"test_arg_1": 5, "test_arg_2": "e"} - ``` - - regardless of the number of partitions. - - - Parameters - ---------- - func - The function to map. - num_partitions - the length of each kwarg in mapped_kwargs - unmapped_kwargs - Dictionary of kwargs to pass to func that shouldn't be mapped - mapped_kwargs - kwargs of the form key=list[...] that should be mapped over - - Returns - ------- - list[Any] - list of results from calling func(**(mapped_kwargs | unmapped_kwargs)) for each - kwargs in mapped_kwargs - """ - - return [ - map_partition( - strip_decorator(func), - unmapped_kwargs=unmapped_kwargs, - **{k: mapped_kwargs[k][i] for k in mapped_kwargs}, - ) - for i in range(num_partitions) - ] - - -@job -def map_partition( - func: Callable, unmapped_kwargs: dict[str, Any] | None = None, **mapped_kwargs -) -> list[Any]: - """ - Job to apply a function to each set of elements in mapped_kwargs. - - Parameters - ---------- - func - The function to map. - unmapped_kwargs - Dictionary of kwargs to pass to func that shouldn't be mapped - mapped_kwargs - kwargs of the form key=list[...] that should be mapped over - - Returns - ------- - list[Any] - list of results from calling func(**mapped_kwargs, **unmapped_kwargs) for each - kwargs in mapped_kwargs - """ - return kwarg_map(func, unmapped_kwargs=unmapped_kwargs, **mapped_kwargs) - - -def kwarg_map( - func: Callable, unmapped_kwargs: dict[str, Any] | None = None, **mapped_kwargs -) -> list[Any]: - """ - A helper function for when you want to construct a chain of objects with individual arguments for each one. Can - be easier to read than a list expansion. - - Adapted from https://stackoverflow.com/a/36575917 (CC-by-SA 3.0) - - Parameters - ---------- - func - The function to map. - unmapped_kwargs - Dictionary of kwargs to pass to func that shouldn't be mapped - mapped_kwargs - kwargs of the form key=list[...] that should be mapped over - - Returns - ------- - list[Any] - List of results from calling func(**mapped_kwargs, **unmapped_kwargs) for each - kwargs in mapped_kwargs - """ - unmapped_kwargs = unmapped_kwargs or {} - - all_lens = [len(v) for v in mapped_kwargs.values()] - n_elements = all_lens[0] - if not all(n_elements == le for le in all_lens): - raise AssertionError(f"Inconsistent lengths: {all_lens}") - return [ - func(**{k: v[i] for k, v in iter(mapped_kwargs.items())}, **unmapped_kwargs) - for i in range(n_elements) - ] diff --git a/tests/core/recipes/emt_recipes/test_emt_recipes.py b/tests/core/recipes/emt_recipes/test_emt_recipes.py index 58f70a48fa..47f13eb390 100644 --- a/tests/core/recipes/emt_recipes/test_emt_recipes.py +++ b/tests/core/recipes/emt_recipes/test_emt_recipes.py @@ -1,23 +1,16 @@ from __future__ import annotations -import logging from pathlib import Path import numpy as np import pytest from ase.build import bulk, molecule from ase.constraints import FixAtoms -from ase.md.npt import NPT from ase.optimize import FIRE -from ase.units import fs from quacc.recipes.emt.core import relax_job, static_job -from quacc.recipes.emt.md import md_job from quacc.recipes.emt.slabs import bulk_to_slabs_flow -LOGGER = logging.getLogger(__name__) -LOGGER.propagate = True - def test_static_job(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) @@ -94,72 +87,6 @@ def test_relax_job(tmp_path, monkeypatch): assert output["results"]["energy"] == pytest.approx(0.04996032884581858) -def test_md_job1(): - atoms = molecule("H2O") - old_positions = atoms.positions.copy() - output = md_job(atoms, steps=500) - assert output["parameters"]["asap_cutoff"] is False - assert len(output["trajectory"]) == 501 - assert output["name"] == "EMT MD" - assert output["parameters_md"]["timestep"] == pytest.approx(1.0 * fs) - assert output["trajectory_log"][-1]["temperature"] == pytest.approx(1575.886) - assert output["trajectory_log"][0]["temperature"] == pytest.approx(0.0) - assert output["trajectory_log"][1]["temperature"] == pytest.approx(759.680) - assert output["trajectory_log"][10]["time"] == pytest.approx(10 * fs) - assert atoms.positions == pytest.approx(old_positions) - - -def test_md_job2(): - atoms = molecule("H2O") - old_positions = atoms.positions.copy() - - output = md_job( - atoms, - timestep_fs=0.5, - steps=20, - md_params={ - "maxwell_boltzmann_kwargs": { - "temperature_K": 1000, - "rng": np.random.default_rng(seed=42), - }, - "set_com_stationary": True, - "set_zero_rotation": True, - }, - ) - assert output["parameters"]["asap_cutoff"] is False - assert len(output["trajectory"]) == 21 - assert output["name"] == "EMT MD" - assert output["parameters_md"]["timestep"] == pytest.approx(0.5 * fs) - assert output["trajectory_log"][-1]["temperature"] == pytest.approx(1023.384) - assert output["trajectory_log"][0]["temperature"] == pytest.approx(915.678) - assert output["trajectory_log"][1]["temperature"] == pytest.approx(1060.650) - assert output["trajectory_log"][10]["time"] == pytest.approx(10 * 0.5 * fs) - assert atoms.positions == pytest.approx(old_positions) - - -def test_md_job3(): - atoms = molecule("H2O", vacuum=10.0) - output = md_job( - atoms, - dynamics=NPT, - timestep_fs=1.0, - temperature_K=1000, - steps=500, - md_params={"dynamics_kwargs": {"ttime": 50 * fs}}, - ) - assert output["parameters"]["asap_cutoff"] is False - assert len(output["trajectory"]) == 500 - assert output["name"] == "EMT MD" - assert output["trajectory_log"][0]["temperature"] == pytest.approx(759.8829) - assert output["trajectory_results"][-1]["energy"] == pytest.approx(2.0363759) - - -def test_md_job_error(): - atoms = molecule("H2O") - with pytest.raises(ValueError, match="Quacc does not support"): - md_job(atoms, md_params={"dynamics_kwargs": {"trajectory": "md.traj"}}) - - def test_slab_dynamic_jobs(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) diff --git a/tests/core/wflow/test_job_patterns.py b/tests/core/wflow/test_job_patterns.py deleted file mode 100644 index 3af97a4e19..0000000000 --- a/tests/core/wflow/test_job_patterns.py +++ /dev/null @@ -1,88 +0,0 @@ -from __future__ import annotations - -import numpy as np -import pytest - -from quacc import job -from quacc.wflow_tools.job_patterns import ( - kwarg_map, - map_partition, - map_partitioned_lists, - partition, -) - - -def test_partition(): - @job - def simple_list(): - return list(range(10)) - - partitioned_list = partition(simple_list(), 3) - assert len(partitioned_list) == 3 - np.testing.assert_allclose(partitioned_list[0], [0, 1, 2, 3]) - - -def test_kwarg_map(): - def test_fun(a, b): - return {"a": a, "b": b} - - assert kwarg_map(test_fun, a=[1, 2], b=["c", "d"]) == [ - {"a": 1, "b": "c"}, - {"a": 2, "b": "d"}, - ] - assert kwarg_map(test_fun, unmapped_kwargs={"b": 1}, a=[1, 2]) == [ - {"a": 1, "b": 1}, - {"a": 2, "b": 1}, - ] - with pytest.raises(AssertionError, match="Inconsistent lengths"): - kwarg_map(test_fun, a=[1, 2, 3], b=[1, 2]) - - -def test_map_partitioned_lists(): - @job - def simple_list(): - return list(range(10)) - - def testfun(a, const=2): - return a * const - - num_partitions = 4 - assert map_partitioned_lists( - testfun, - a=partition(simple_list(), num_partitions), - num_partitions=num_partitions, - )[0] == [0, 2, 4] - assert map_partitioned_lists( - testfun, - unmapped_kwargs={"const": 3}, - a=partition(simple_list(), num_partitions), - num_partitions=num_partitions, - )[0] == [0, 3, 6] - num_partitions = 2 - assert map_partitioned_lists( - testfun, - a=partition(simple_list(), num_partitions), - num_partitions=num_partitions, - )[0] == [0, 2, 4, 6, 8] - assert map_partitioned_lists( - testfun, - unmapped_kwargs={"const": 3}, - a=partition(simple_list(), num_partitions), - num_partitions=num_partitions, - )[1] == [15, 18, 21, 24, 27] - - -def test_map_partition(): - def test_fun(a, b): - return {"a": a, "b": b} - - assert map_partition(test_fun, a=[1, 2], b=["c", "d"]) == [ - {"a": 1, "b": "c"}, - {"a": 2, "b": "d"}, - ] - assert map_partition(test_fun, unmapped_kwargs={"b": 1}, a=[1, 2]) == [ - {"a": 1, "b": 1}, - {"a": 2, "b": 1}, - ] - with pytest.raises(AssertionError): - kwarg_map(test_fun, a=[1, 2, 3], b=[1, 2]) diff --git a/tests/covalent/test_customizers.py b/tests/covalent/test_customizers.py index c240fd835c..b66b2c43a3 100644 --- a/tests/covalent/test_customizers.py +++ b/tests/covalent/test_customizers.py @@ -166,28 +166,3 @@ def write_file_flow(name="flow.txt", job_decorators=None): wait=True, ) assert Path(tmp_dir2 / "flow.txt").exists() - - -def test_double_change_settings_redecorate_job(tmp_path_factory): - tmp_dir1 = tmp_path_factory.mktemp("dir1") - tmp_dir2 = tmp_path_factory.mktemp("dir2") - - @job - def write_file_job(name="job.txt"): - with open(Path(get_settings().RESULTS_DIR, name), "w") as f: - f.write("test file") - - write_file_job = redecorate( - write_file_job, job(settings_swap={"RESULTS_DIR": tmp_dir1}) - ) - write_file_job = redecorate( - write_file_job, job(settings_swap={"RESULTS_DIR": tmp_dir2}) - ) - - @flow - def my_flow(): - return write_file_job() - - ct.get_result(ct.dispatch(my_flow)(), wait=True) - assert not Path(tmp_dir1 / "job.txt").exists() - assert Path(tmp_dir2 / "job.txt").exists() diff --git a/tests/dask/test_customizers.py b/tests/dask/test_customizers.py index 324421ae4c..4f8a8994ad 100644 --- a/tests/dask/test_customizers.py +++ b/tests/dask/test_customizers.py @@ -113,23 +113,3 @@ def write_file_flow(name="flow.txt", job_decorators=None): ) ).result() assert Path(tmp_dir2 / "flow.txt").exists() - - -def test_double_change_settings_redecorate_job(tmp_path_factory): - tmp_dir1 = tmp_path_factory.mktemp("dir1") - tmp_dir2 = tmp_path_factory.mktemp("dir2") - - @job - def write_file_job(name="job.txt"): - with open(Path(get_settings().RESULTS_DIR, name), "w") as f: - f.write("test file") - - write_file_job = redecorate( - write_file_job, job(settings_swap={"RESULTS_DIR": tmp_dir1}) - ) - write_file_job = redecorate( - write_file_job, job(settings_swap={"RESULTS_DIR": tmp_dir2}) - ) - client.compute(write_file_job()).result() - assert not Path(tmp_dir1 / "job.txt").exists() - assert Path(tmp_dir2 / "job.txt").exists() diff --git a/tests/prefect/test_customizers.py b/tests/prefect/test_customizers.py index b80d34728f..b089d696d3 100644 --- a/tests/prefect/test_customizers.py +++ b/tests/prefect/test_customizers.py @@ -73,28 +73,3 @@ def write_file_flow(name="flow.txt", job_decorators=None): job_decorators={"write_file_job": job(settings_swap={"RESULTS_DIR": tmp_dir2})} ).result() assert Path(tmp_dir2 / "flow.txt").exists() - - -def test_double_change_settings_redecorate_job(tmp_path_factory): - tmp_dir1 = tmp_path_factory.mktemp("dir1") - tmp_dir2 = tmp_path_factory.mktemp("dir2") - - @job - def write_file_job(name="job.txt"): - with open(Path(get_settings().RESULTS_DIR, name), "w") as f: - f.write("test file") - - write_file_job = redecorate( - write_file_job, job(settings_swap={"RESULTS_DIR": tmp_dir1}) - ) - write_file_job = redecorate( - write_file_job, job(settings_swap={"RESULTS_DIR": tmp_dir2}) - ) - - @flow - def my_flow(): - return write_file_job() - - my_flow().result() - assert not Path(tmp_dir1 / "job.txt").exists() - assert Path(tmp_dir2 / "job.txt").exists() diff --git a/tests/prefect/test_map_partitions.py b/tests/prefect/test_map_partitions.py deleted file mode 100644 index f0ff6581cc..0000000000 --- a/tests/prefect/test_map_partitions.py +++ /dev/null @@ -1,88 +0,0 @@ -from __future__ import annotations - -import numpy as np -import pytest - -from quacc import flow, job -from quacc.wflow_tools.job_patterns import ( - kwarg_map, - map_partition, - map_partitioned_lists, - partition, -) - - -def test_partition(): - @job - def simple_list(): - return list(range(10)) - - @flow - def test_flow(): - return partition(simple_list(), 3) - - partitioned_list = test_flow().result() - assert len(partitioned_list) == 3 - np.testing.assert_allclose(partitioned_list[0], [0, 1, 2, 3]) - - -def test_kwarg_map(): - def test_fun(a, b): - return {"a": a, "b": b} - - assert kwarg_map(test_fun, a=[1, 2], b=["c", "d"]) == [ - {"a": 1, "b": "c"}, - {"a": 2, "b": "d"}, - ] - assert kwarg_map(test_fun, unmapped_kwargs={"b": 1}, a=[1, 2]) == [ - {"a": 1, "b": 1}, - {"a": 2, "b": 1}, - ] - with pytest.raises(AssertionError, match="Inconsistent lengths"): - kwarg_map(test_fun, a=[1, 2, 3], b=[1, 2]) - - -def test_map_partitioned_lists(): - @job - def simple_list(): - return list(range(10)) - - def testfun(a, const=2): - return a * const - - @flow - def test_flow(func, num_partitions, unmapped_kwargs=None): - return map_partitioned_lists( - func, - unmapped_kwargs=unmapped_kwargs, - a=partition(simple_list(), num_partitions), - num_partitions=num_partitions, - ) - - num_partitions = 4 - assert test_flow(testfun, num_partitions)[0].result() == [0, 2, 4] - assert test_flow(testfun, num_partitions, unmapped_kwargs={"const": 3})[ - 0 - ].result() == [0, 3, 6] - num_partitions = 2 - assert test_flow(testfun, num_partitions)[0].result() == [0, 2, 4, 6, 8] - assert test_flow(testfun, num_partitions, unmapped_kwargs={"const": 3})[ - 1 - ].result() == [15, 18, 21, 24, 27] - - -def test_map_partition(): - def test_fun(a, b): - return {"a": a, "b": b} - - @flow - def test_flow1(): - return map_partition(test_fun, a=[1, 2], b=["c", "d"]) - - assert test_flow1().result() == [{"a": 1, "b": "c"}, {"a": 2, "b": "d"}] - - @flow - def test_flow2(): - return map_partition(test_fun, unmapped_kwargs={"b": 1}, a=[1, 2]) - - assert test_flow2().result() == [{"a": 1, "b": 1}, {"a": 2, "b": 1}] diff --git a/tests/redun/test_customizers.py b/tests/redun/test_customizers.py index 4fe8e5c7c1..202516c0c6 100644 --- a/tests/redun/test_customizers.py +++ b/tests/redun/test_customizers.py @@ -77,23 +77,3 @@ def write_file_flow(name="flow.txt", job_decorators=None): ) ) assert Path(tmp_dir2 / "flow.txt").exists() - - -def test_double_change_settings_redecorate_job(tmp_path_factory, scheduler): - tmp_dir1 = tmp_path_factory.mktemp("dir1") - tmp_dir2 = tmp_path_factory.mktemp("dir2") - - @job - def write_file_job(name="job.txt"): - with open(Path(get_settings().RESULTS_DIR, name), "w") as f: - f.write("test file") - - write_file_job = redecorate( - write_file_job, job(settings_swap={"RESULTS_DIR": tmp_dir1}) - ) - write_file_job = redecorate( - write_file_job, job(settings_swap={"RESULTS_DIR": tmp_dir2}) - ) - scheduler.run(write_file_job()) - assert not Path(tmp_dir1 / "job.txt").exists() - assert Path(tmp_dir2 / "job.txt").exists() diff --git a/tests/requirements-dask.txt b/tests/requirements-dask.txt index fa02fd0386..159d41094a 100644 --- a/tests/requirements-dask.txt +++ b/tests/requirements-dask.txt @@ -1,2 +1,2 @@ -dask[distributed]==2024.6.2 +dask[distributed]==2024.6.0 dask-jobqueue==0.8.5 diff --git a/tests/requirements-jobflow.txt b/tests/requirements-jobflow.txt index 79c32f6ff6..9f4299ac5a 100644 --- a/tests/requirements-jobflow.txt +++ b/tests/requirements-jobflow.txt @@ -1,3 +1,3 @@ jobflow==0.1.17 -jobflow-remote==0.1.2 +jobflow-remote==0.1.1 fireworks==2.0.3 diff --git a/tests/requirements-mlp.txt b/tests/requirements-mlp.txt index 49d71218c9..e4c84e4af7 100644 --- a/tests/requirements-mlp.txt +++ b/tests/requirements-mlp.txt @@ -1,4 +1,4 @@ chgnet==0.3.8 matgl==1.1.2 -mace-torch==0.3.5 +mace-torch==0.3.4 torch-dftd==0.4.0 diff --git a/tests/requirements-parsl.txt b/tests/requirements-parsl.txt index 518f6cd29d..afee1f1bda 100644 --- a/tests/requirements-parsl.txt +++ b/tests/requirements-parsl.txt @@ -1 +1 @@ -parsl[monitoring]==2024.6.24 +parsl[monitoring]==2024.6.10 diff --git a/tests/requirements-phonons.txt b/tests/requirements-phonons.txt index 2cc276575b..89da8120c8 100644 --- a/tests/requirements-phonons.txt +++ b/tests/requirements-phonons.txt @@ -1,2 +1,2 @@ -phonopy==2.24.3 +phonopy==2.24.2 seekpath==2.1.0 diff --git a/tests/requirements-prefect.txt b/tests/requirements-prefect.txt index d525ec1e9d..2b63b550ec 100644 --- a/tests/requirements-prefect.txt +++ b/tests/requirements-prefect.txt @@ -1,2 +1,2 @@ dask-jobqueue==0.8.5 -prefect[dask]==2.19.7 +prefect[dask]==2.19.5 diff --git a/tests/requirements-redun.txt b/tests/requirements-redun.txt index 8412786799..7df46945d7 100644 --- a/tests/requirements-redun.txt +++ b/tests/requirements-redun.txt @@ -1 +1 @@ -redun==0.21.0 +redun==0.19.5 diff --git a/tests/requirements.txt b/tests/requirements.txt index 0336347b64..d6a0e01c09 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,13 +1,13 @@ ase==3.23.0 cclib==1.8.1 -custodian==2024.6.24 +custodian==2024.4.18 emmet-core==0.83.6 -maggma==0.68.6 +maggma==0.68.4 monty==2024.5.24 numpy==1.26.4 psutil==5.9.8 pydantic==2.7.4 -pydantic-settings==2.3.4 +pydantic-settings==2.3.3 pymatgen==2024.6.10 ruamel.yaml==0.18.6 typer==0.12.3 From 5630cee1536bf97983189ce918750c29d21147b7 Mon Sep 17 00:00:00 2001 From: "Andrew S. Rosen" Date: Fri, 28 Jun 2024 20:56:46 -0700 Subject: [PATCH 27/29] Update test_mrcc.py --- tests/core/calculators/mrcc/test_mrcc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/calculators/mrcc/test_mrcc.py b/tests/core/calculators/mrcc/test_mrcc.py index 77da885842..fda354de02 100644 --- a/tests/core/calculators/mrcc/test_mrcc.py +++ b/tests/core/calculators/mrcc/test_mrcc.py @@ -3,7 +3,7 @@ import pytest from ase.atoms import Atoms -from quacc import SETTINGS +from quacc import get_settings from quacc.calculators.mrcc.mrcc import MRCC, MrccProfile, _get_version_from_mrcc_header @@ -20,7 +20,7 @@ def test_mrcc_version_from_string(): def test_mrcc_singlepoint(tmp_path): calc = MRCC( - profile=MrccProfile(command=SETTINGS.MRCC_CMD), + profile=MrccProfile(command=get_settings().MRCC_CMD), mrccinput={"calc": "PBE", "basis": "STO-3G"}, mrccblocks="symm=off", directory=tmp_path, From 65619fbf57905bb762bbc9b8320c7f845b5c388a Mon Sep 17 00:00:00 2001 From: Andrew Rosen Date: Mon, 1 Jul 2024 08:59:09 -0700 Subject: [PATCH 28/29] Fix hinting --- src/quacc/atoms/skzcam.py | 14 +++++++------- src/quacc/types.py | 5 +++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/quacc/atoms/skzcam.py b/src/quacc/atoms/skzcam.py index 3d88931e1e..ff75f6a0c9 100644 --- a/src/quacc/atoms/skzcam.py +++ b/src/quacc/atoms/skzcam.py @@ -16,14 +16,14 @@ if TYPE_CHECKING: from ase.atom import Atom from numpy.typing import NDArray - from typing import TypedDict - class SKZCAMOutput(TypedDict): - adsorbate_slab_embedded_cluster: Atoms - quantum_cluster_indices_set: list[list[int]] - ecp_region_indices_set: list[list[int]] - - from quacc.types import BlockInfo, ElementInfo, ElementStr, MultiplicityDict + from quacc.types import ( + BlockInfo, + ElementInfo, + ElementStr, + MultiplicityDict, + SKZCAMOutput, + ) has_chemshell = find_spec("chemsh") is not None diff --git a/src/quacc/types.py b/src/quacc/types.py index 2556f37c30..a37169962f 100644 --- a/src/quacc/types.py +++ b/src/quacc/types.py @@ -248,6 +248,11 @@ class FindAdsSitesKwargs(TypedDict, total=False): # ----------- Atoms (skzcam) handling type hints ----------- + class SKZCAMOutput(TypedDict): + adsorbate_slab_embedded_cluster: Atoms + quantum_cluster_indices_set: list[list[int]] + ecp_region_indices_set: list[list[int]] + class ElementInfo(TypedDict): core: int basis: str From 760b52c44455bff342b7b98f0c83d6d2c7dddf91 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:00:57 +0000 Subject: [PATCH 29/29] pre-commit auto-fixes --- src/quacc/types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/quacc/types.py b/src/quacc/types.py index a37169962f..e93be954f2 100644 --- a/src/quacc/types.py +++ b/src/quacc/types.py @@ -101,7 +101,6 @@ class MaxwellBoltzmanDistributionKwargs(TypedDict, total=False): # ----------- Atoms handling type hints ----------- - ElementStr = Literal[ "H", "He",