diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e8dcb3..d3d1a87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for the novel "g-xTB" method (working title: GP3-xTB) - A function which contracts the coordinates after the initial generation. - A function which is able to printout the xyz coordinates to the terminal similar to the `.xyz` layout. +- Elements 87 to 103 are accessible via the element composition. If `xtb` is the engine, the elements will be replaced by their lighter homologues. ### Breaking Changes - Removal of the `dist_threshold` flag and in the `-toml` file. diff --git a/src/mindlessgen/generator/main.py b/src/mindlessgen/generator/main.py index 237d1ec..e0c0df8 100644 --- a/src/mindlessgen/generator/main.py +++ b/src/mindlessgen/generator/main.py @@ -58,31 +58,12 @@ def generator(config: ConfigManager) -> tuple[list[Molecule] | None, int]: if config.general.verbosity > 0: print(config) - # lower number of the available cores and the configured parallelism + config.check_config(verbosity=config.general.verbosity) + num_cores = min(mp.cpu_count(), config.general.parallel) - if config.general.parallel > mp.cpu_count(): - warnings.warn( - f"Number of cores requested ({config.general.parallel}) is greater " - + f"than the number of available cores ({mp.cpu_count()})." - + f"Using {num_cores} cores instead." - ) if config.general.verbosity > 0: print(f"Running with {num_cores} cores.") - if num_cores > 1 and config.general.verbosity > 0: - # raise warning that parallelization will disable verbosity - warnings.warn( - "Parallelization will disable verbosity during iterative search. " - + "Set '--verbosity 0' or '-P 1' to avoid this warning, or simply ignore it." - ) - if num_cores > 1 and config.postprocess.debug: - # raise warning that debugging of postprocessing will disable parallelization - warnings.warn( - "Debug output might seem to be redundant due to the parallel processes " - + "with possibly similar errors in parallel mode. " - + "Don't be confused!" - ) - # Check if the file "mindless.molecules" exists. If yes, append to it. if Path(MINDLESS_MOLECULES_FILE).is_file(): if config.general.verbosity > 0: diff --git a/src/mindlessgen/molecules/miscellaneous.py b/src/mindlessgen/molecules/miscellaneous.py index a735eb8..8ed4424 100644 --- a/src/mindlessgen/molecules/miscellaneous.py +++ b/src/mindlessgen/molecules/miscellaneous.py @@ -16,14 +16,15 @@ def set_random_charge(ati: np.ndarray, verbosity: int = 1) -> tuple[int, int]: if verbosity > 1: print(f"Number of protons in molecule: {nel}") - if np.any(np.isin(ati, get_lanthanides())): - ### Special mode for lanthanides + if np.any(np.isin(ati, get_lanthanides() + get_actinides())): + ### Special mode for lanthanides and actinides # -> always high spin - # -> Divide the molecule into Ln3+ ions and negative "ligands" + # -> Divide the molecule into Ln3+/Ac3+ ions and negative "ligands" # -> The ligands are the remaining protons are assumed to be low spin uhf = 0 charge = 0 ln_protons = 0 + ac_protons = 0 for atom in ati: if atom in get_lanthanides(): if atom < 64: @@ -33,9 +34,20 @@ def set_random_charge(ati: np.ndarray, verbosity: int = 1) -> tuple[int, int]: ln_protons += ( atom - 3 + 1 ) # subtract 3 to get the number of protons in the Ln3+ ion - ligand_protons = nel - ln_protons + elif atom in get_actinides(): + if atom < 96: + uhf += atom - 88 + else: + uhf += 102 - atom + ac_protons += ( + atom - 3 + 1 + ) # subtract 3 to get the number of protons in the Ln3+ ion + ligand_protons = nel - ln_protons - ac_protons if verbosity > 2: - print(f"Number of protons from Ln^3+ ions: {ln_protons}") + if np.any(np.isin(ati, get_lanthanides())): + print(f"Number of protons from Ln^3+ ions: {ln_protons}") + if np.any(np.isin(ati, get_actinides())): + print(f"Number of protons from Ac^3+ ions: {ac_protons}") print( f"Number of protons from ligands (assuming negative charge): {ligand_protons}" ) diff --git a/src/mindlessgen/prog/config.py b/src/mindlessgen/prog/config.py index 6bd0020..2fd3457 100644 --- a/src/mindlessgen/prog/config.py +++ b/src/mindlessgen/prog/config.py @@ -6,6 +6,8 @@ from pathlib import Path from abc import ABC, abstractmethod +import warnings +import multiprocessing as mp import toml from ..molecules import PSE_NUMBERS @@ -735,6 +737,68 @@ def __init__(self, config_file: str | Path | None = None): if config_file: self.load_from_toml(config_file) + def check_config(self, verbosity: int = 1) -> None: + """ + Checks ConfigClass for any incompatibilities that are imaginable + """ + + # lower number of the available cores and the configured parallelism + num_cores = min(mp.cpu_count(), self.general.parallel) + if self.general.parallel > mp.cpu_count() and verbosity > -1: + warnings.warn( + f"Number of cores requested ({self.general.parallel}) is greater " + + f"than the number of available cores ({mp.cpu_count()})." + + f"Using {num_cores} cores instead." + ) + + if num_cores > 1 and self.postprocess.debug and verbosity > -1: + # raise warning that debugging of postprocessing will disable parallelization + warnings.warn( + "Debug output might seem to be redundant due to the parallel processes " + + "with possibly similar errors in parallel mode. " + + "Don't be confused!" + ) + + if num_cores > 1 and verbosity > 0: + # raise warning that parallelization will disable verbosity + warnings.warn( + "Parallelization will disable verbosity during iterative search. " + + "Set '--verbosity 0' or '-P 1' to avoid this warning, or simply ignore it." + ) + + if self.refine.engine == "xtb": + # Check for f-block elements in forbidden elements + if self.generate.forbidden_elements: + if verbosity > 0: + f_block_elements = set(range(56, 71)) | set(range(88, 103)) + lanthanides = set(range(56, 71)) + if not all( + elem in self.generate.forbidden_elements for elem in lanthanides + ) or any( + elem in f_block_elements + for elem in self.generate.element_composition + ): + warnings.warn( + "f-block elements could be within the molecule. xTB does not treat f electrons explicitly. In this case UHF is set to 0." + ) + + # Check for super heavy elements in forbidden elements + super_heavy_elements = set(range(86, 102)) + if self.generate.element_composition and any( + elem in super_heavy_elements + for elem in self.generate.element_composition + ): + if verbosity > 0: + warnings.warn( + "xTB does not treat super heavy elements. Atomic numbers are temporarily reduced by 32 to their lighter homologues and then replaced with the correct atom number." + ) + # Check if postprocessing is turned off + if not self.general.postprocess: + if verbosity > 0: + warnings.warn( + "Postprocessing is turned off. The structure will not be relaxed." + ) + def get_all_identifiers(self): """ Returns the identifiers of all subconfiguration classes, e.g. "orca", "refinement", ... diff --git a/src/mindlessgen/qm/xtb.py b/src/mindlessgen/qm/xtb.py index 5dedc36..f80d2be 100644 --- a/src/mindlessgen/qm/xtb.py +++ b/src/mindlessgen/qm/xtb.py @@ -36,6 +36,11 @@ def optimize( """ Optimize a molecule using xtb. """ + super_heavy_elements = False + ati_original = molecule.ati.copy() + if np.any(molecule.ati > 85): + super_heavy_elements = True + molecule.ati[molecule.ati > 85] -= 32 if np.any(np.isin(molecule.ati, get_lanthanides())): check_ligand_uhf(molecule.ati, molecule.charge) # Store the original UHF value and set uhf to 0 @@ -82,12 +87,21 @@ def optimize( if np.any(np.isin(molecule.ati, get_lanthanides())): # Reset the UHF value to the original value before returning the optimized molecule. optimized_molecule.uhf = uhf_original + if super_heavy_elements: + # Reset the atomic numbers to the original values before returning the optimized molecule. + optimized_molecule.ati = ati_original + optimized_molecule.atlist = molecule.atlist return optimized_molecule def singlepoint(self, molecule: Molecule, verbosity: int = 1) -> str: """ Perform a single-point calculation using xtb. """ + super_heavy_elements = False + ati_original = molecule.ati.copy() + if np.any(molecule.ati > 85): + super_heavy_elements = True + molecule.ati[molecule.ati > 85] -= 32 if np.any(np.isin(molecule.ati, get_lanthanides())): check_ligand_uhf(molecule.ati, molecule.charge) # Store the original UHF value and set uhf to 0 @@ -128,6 +142,9 @@ def singlepoint(self, molecule: Molecule, verbosity: int = 1) -> str: if np.any(np.isin(molecule.ati, get_lanthanides())): molecule.uhf = uhf_original + if super_heavy_elements: + # Reset the atomic numbers to the original values before returning the optimized molecule. + molecule.ati = ati_original return xtb_log_out def check_gap( diff --git a/test/test_molecules/test_miscellaneous.py b/test/test_molecules/test_miscellaneous.py index 68b68e5..3c6e328 100644 --- a/test/test_molecules/test_miscellaneous.py +++ b/test/test_molecules/test_miscellaneous.py @@ -37,7 +37,18 @@ 7, ), # Gd (64), C, C -> Lanthanide case, UHF = 7, CHRG = 1 (np.array([57, 7, 0]), [0], 1), # Ce (58), O, H -> Lanthanide case, UHF = 1 - (np.array([69, 7, 0]), [0], 1), # Ce (58), O, H -> Lanthanide case, UHF = 1 + (np.array([69, 7, 0]), [0], 1), # Yb (70), O, H -> Lanthanide case, UHF = 1 + (np.array([5, 7, 0, 98]), [0], 4), # Es(99), O, C, H -> Actinides case, UHF = 4 + ( + np.array([59, 92, 0, 0]), + [0], + 7, + ), # Nd(60), Np(93), H, H -> Lanthanide and Actinide case, UHF = 7 + ( + np.array([59, 91, 92, 5, 7, 0]), + [0], + 10, + ), # Nd(60), U(92), Np(93), H, H -> Lanthanide and Actinides case, UHF = 10 ], ids=[ "B-N-H (standard, odd)", @@ -49,6 +60,9 @@ "Gd-C-N (lanthanide)", "Ce-O-H (lanthanide)", "Yb-O-H (lanthanide)", + "Es-O-C-H (actinides)", + "Nd-Np-H-H (lanthanide and actinide)", + "Nd-Eu-Np-H-H (lanthanide and actinides)", ], ) def test_set_random_charge(atom_types, expected_charges, expected_uhf):