diff --git a/.gitignore b/.gitignore index 81d1b79..fbb8695 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ venv/ .coverage .coverage.* .cache +.hypothesis coverage.xml # Sphinx documentation diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bf28d03..f54c70e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,32 @@ Changelog ========= +Release 2.0.0 +------------- + +New Jobs: + - Automatic QM region selection for QM/MM. + - Job that tries to find a new reaction starting from a transition state guess only. + - Job that carries out a fast dissociation reaction trial protocol after optimizing the input structure. + - Remove `scine_step_refinement` job. + +New Settings: + - Add the option `allow_exhaustive_product_decomposition` to jobs that carry out reaction trials, which allows products to decompose and re-optimize them until no further decomposition is observed. + - Add the option `always_add_barrierless_step_for_reactive_complex` to jobs that carry out reaction trials, which enables that a barrierless elementary step is added for the reactive complex formation of a bimolecular reaction irrespective of the complexation energy. + - Add option `store_structures_with_frequency` and `store_structures_with_fraction` that allow storing a portion of structures per sub-task. + - Add option `spin_propensity_ts_check` to `scine_react_ts_guess` job to specify a spin multiplicity range to be checked for the transition state different to the spin multiplicity range to be checked for the reactants and products (`spin_propensity_check`) + +Technical changes: + - Ensure that the calculated spin states are considered in the calculation of the dissociation energy in the `DissociationCut` jobs and in structure optimizing jobs (`GeometryOptimization` and `ReactTsGuess`) + - Write normalized modes in database. + - Improve dependency handling and add more typehints. + - Optimize only unique structures of the endpoints of an IRC calculation. + - Deduplicate code for analyzing both sides of the IRC part in the `scine_react_job`. + - Stricter conditions to distribute charges if `expect_charge_separation` is set to `True` by prohibiting changing + already changed charge. + - Comply docstrings with numpy styling. + - Introduce Enums for ReaDuct calls. + Release 1.3.0 ------------- diff --git a/README.rst b/README.rst index e76f85d..6d4ba9f 100644 --- a/README.rst +++ b/README.rst @@ -242,7 +242,14 @@ release as archived on `Zenodo `_ (DOI In addition, we kindly request you to cite the following article when using Puffin: J. P. Unsleber, S. A. Grimmel, M. Reiher, "Chemoton 2.0: Autonomous Exploration of Chemical Reaction Networks", -arXiv:2202.13011 [physics.chem-ph]. +*J. Chem. Theory Comput.*, **2022**, *18*, 5393. + +Furthermore, when publishing results obtained with any SCINE module, please cite the following paper: + +T. Weymuth, J. P. Unsleber, P. L. Türtscher, M. Steiner, J.-G. Sobez, C. H. Müller, M. Mörchen, +V. Klasovita, S. A. Grimmel, M. Eckhoff, K.-S. Csizi, F. Bosia, M. Bensberg, M. Reiher, +"SCINE—Software for chemical interaction networks", *J. Chem. Phys.*, **2024**, *160*, 222501 +(DOI `10.1063/5.0206974 `_). Support and Contact diff --git a/conanfile.txt b/conanfile.txt index be64a01..9e05ecf 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,16 +1,18 @@ [requires] -scine_utilities/9.0.0@ -scine_molassembler/2.0.1@ -scine_database/1.3.0@ -scine_xtb_wrapper/3.0.0@ -scine_sparrow/5.0.0@ -scine_readuct/5.1.0@ +scine_utilities/10.0.0@ +scine_molassembler/3.0.0@ +scine_database/1.4.0@ +scine_xtb_wrapper/3.0.1@ +scine_sparrow/5.1.0@ +scine_swoose/2.1.0@ +scine_readuct/6.0.0@ [options] scine_utilities:python=True scine_molassembler:python=True scine_database:python=True scine_sparrow:python=True +scine_swoose:python=True scine_xtb_wrapper:python=True scine_readuct:python=True diff --git a/container/apptainer/README.rst b/container/apptainer/README.rst index 7e651c1..543a99e 100644 --- a/container/apptainer/README.rst +++ b/container/apptainer/README.rst @@ -52,3 +52,4 @@ A more complete run could thus look like this: apptainer run --bind /scratch/puffin:/socket \ --bind /scratch/puffin/jobs:/jobs \ puffin.sif + diff --git a/dev b/dev index 518ab3c..2712fd3 160000 --- a/dev +++ b/dev @@ -1 +1 @@ -Subproject commit 518ab3c7f8a0a724081fcd7ed518c669724bcd37 +Subproject commit 2712fd3204d703c777244b077066ad75c6bc0db1 diff --git a/docs/source/api.rst b/docs/source/api.rst index 7e4f212..8758e0e 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -26,13 +26,6 @@ Module: jobloop .. automodule:: scine_puffin.jobloop :members: -Module: jobs.templates.job --------------------------- - -.. automodule:: scine_puffin.jobs.templates.job - :members: - - Module: programs.program ------------------------ diff --git a/docs/source/conf.py b/docs/source/conf.py index 6b2a178..aa69433 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -4,6 +4,8 @@ import sphinx_rtd_theme import scine_puffin import recommonmark +import sys +import pathlib from recommonmark.transform import AutoStructify # -- General configuration ------------------------------------------------ @@ -25,7 +27,6 @@ 'sphinx.ext.githubpages', 'sphinx.ext.intersphinx', 'sphinx.ext.mathjax', - 'sphinx_autodoc_typehints', 'sphinx.ext.viewcode', 'matplotlib.sphinxext.plot_directive', 'numpydoc', @@ -177,6 +178,14 @@ 'Miscellaneous'), ] +autodoc_default_options = { + "autosummary": True, + "members": True, + "undoc-members": True, + "inherited-members": True, + "class-doc-from": "both", +} + # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'python': ('https://docs.python.org/3/', None), @@ -184,3 +193,6 @@ 'scipy': ('https://docs.scipy.org/doc/scipy-1.7.1/', None), 'matplotlib': ('https://matplotlib.org/stable', None), } +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. +sys.path.insert(0, pathlib.Path(__file__).parents[2].resolve().as_posix()) diff --git a/docs/source/index.rst b/docs/source/index.rst index 9a209a7..6ce86b4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,4 +15,5 @@ Puffin programs jobs api + utilities changelog diff --git a/docs/source/jobs.rst b/docs/source/jobs.rst index 96c09da..f9d70f2 100644 --- a/docs/source/jobs.rst +++ b/docs/source/jobs.rst @@ -12,77 +12,8 @@ the custom ``module``/``Calculator``. For more information about this interface please see the documentation of SCINE: Core and possibly also SCINE: Utils. -Conformer Generation -```````````````````` -.. autoclass:: scine_puffin.jobs.conformers.Conformers +.. autosummary:: + :toctree: generated + :recursive: -SCINE: AFIR -``````````` -.. autoclass:: scine_puffin.jobs.scine_afir.ScineAfir - -SCINE: Bond Orders -`````````````````` -.. autoclass:: scine_puffin.jobs.scine_bond_orders.ScineBondOrders - -SCINE: Geometry Optimization -```````````````````````````` -.. autoclass:: scine_puffin.jobs.scine_geometry_optimization.ScineGeometryOptimization - -SCINE: Hessian -`````````````````` -.. autoclass:: scine_puffin.jobs.scine_hessian.ScineHessian - -SCINE: IRC Scan -`````````````````` -.. autoclass:: scine_puffin.jobs.scine_irc_scan.ScineIrcScan - -SCINE: Artificial Force Induced Reaction Probe -`````````````````````````````````````````````` -.. autoclass:: scine_puffin.jobs.scine_react_complex_afir.ScineReactComplexAfir - -SCINE: Newton Trajectory Reaction Probe -``````````````````````````````````````` -.. autoclass:: scine_puffin.jobs.scine_react_complex_nt.ScineReactComplexNt - -SCINE: Newton Trajectory 2 Reaction Probe -````````````````````````````````````````` -.. autoclass:: scine_puffin.jobs.scine_react_complex_nt2.ScineReactComplexNt2 - -SCINE: Single Point -``````````````````` -.. autoclass:: scine_puffin.jobs.scine_single_point.ScineSinglePoint - -SCINE: Transition State Optimization -```````````````````````````````````` -.. autoclass:: scine_puffin.jobs.scine_ts_optimization.ScineTsOptimization - -Specialized Jobs ----------------- - -Gaussian: Partial Charges - Charge Model 5 -`````````````````````````````````````````` -.. autoclass:: scine_puffin.jobs.gaussian_charge_model_5.GaussianChargeModel5 - -Orca: Geometry Optimization -``````````````````````````` -.. autoclass:: scine_puffin.jobs.orca_geometry_optimization.OrcaGeometryOptimization - -Turbomole: Geometry Optimization -````````````````````````````````` -.. autoclass:: scine_puffin.jobs.turbomole_geometry_optimization.TurbomoleGeometryOptimization - -Turbomole: Single Point -```````````````````````` -.. autoclass:: scine_puffin.jobs.turbomole_single_point.TurbomoleSinglePoint - -Turbomole: Hessian -``````````````````` -.. autoclass:: scine_puffin.jobs.turbomole_hessian.TurbomoleHessian - -Turbomole: Bond Orders -``````````````````````` -.. autoclass:: scine_puffin.jobs.turbomole_bond_orders.TurbomoleBondOrders - -Debugging: Sleep -```````````````` -.. autoclass:: scine_puffin.jobs.sleep.Sleep + scine_puffin.jobs diff --git a/docs/source/utilities.rst b/docs/source/utilities.rst new file mode 100644 index 0000000..7cc0b8f --- /dev/null +++ b/docs/source/utilities.rst @@ -0,0 +1,10 @@ +Utilities +========= + +A module that gathers various modules with helper functions for multiple jobs. + +.. autosummary:: + :toctree: generated + :recursive: + + scine_puffin.utilities diff --git a/scine_puffin/_version.py b/scine_puffin/_version.py index 96cdefe..9d6b8f5 100644 --- a/scine_puffin/_version.py +++ b/scine_puffin/_version.py @@ -3,4 +3,4 @@ See LICENSE.txt for details. """ -__version__ = "1.3.0" +__version__ = "2.0.0" diff --git a/scine_puffin/bootstrap.py b/scine_puffin/bootstrap.py index 888e615..5ddd1ce 100644 --- a/scine_puffin/bootstrap.py +++ b/scine_puffin/bootstrap.py @@ -18,7 +18,7 @@ def bootstrap(config: Configuration): Parameters ----------. - config :: scine_puffin.config.Configuration + config : scine_puffin.config.Configuration The current configuration of the Puffin. """ # Prepare directories @@ -71,25 +71,9 @@ def bootstrap(config: Configuration): utils = Utils(config.programs()["utils"]) utils.install(utils_build_dir, install_dir, config["resources"]["cores"]) - # Install all other programs - for program_name, settings in config.programs().items(): - if program_name in ['core', 'utils'] or not settings["available"]: - continue - print("") - print("Preparing " + program_name.capitalize() + "...") - print("") - module = importlib.import_module("scine_puffin.programs." + program_name) - class_ = getattr(module, program_name.capitalize()) - program = class_(settings) - program_build_dir = os.path.join(build_dir, program_name) - program.install(program_build_dir, install_dir, config["resources"]["cores"]) - - # Setup environment - # General setup + # setup Python path already now for crosslinking for Python type stubs env = {} - executables = {} python_version = sys.version_info - executables["OMP_NUM_THREADS"] = str(config["resources"]["cores"]) env["PYTHONPATH"] = ( os.path.join( install_dir, @@ -121,6 +105,25 @@ def bootstrap(config: Configuration): "dist-packages", ) ) + os.environ["PYTHONPATH"] = env["PYTHONPATH"] + + # Install all other programs + for program_name, settings in config.programs().items(): + if program_name in ['core', 'utils'] or not settings["available"]: + continue + print("") + print("Preparing " + program_name.capitalize() + "...") + print("") + module = importlib.import_module("scine_puffin.programs." + program_name) + class_ = getattr(module, program_name.capitalize()) + program = class_(settings) + program_build_dir = os.path.join(build_dir, program_name) + program.install(program_build_dir, install_dir, config["resources"]["cores"]) + + # Setup environment + # General setup + executables = {} + executables["OMP_NUM_THREADS"] = str(config["resources"]["cores"]) env["PATH"] = os.path.join(install_dir, "bin") env["LD_LIBRARY_PATH"] = os.path.join(install_dir, "lib") + ":" + os.path.join(install_dir, "lib64") env["SCINE_MODULE_PATH"] = os.path.join(install_dir, "lib") + ":" + os.path.join(install_dir, "lib64") diff --git a/scine_puffin/config.py b/scine_puffin/config.py index 546a867..5d8248d 100644 --- a/scine_puffin/config.py +++ b/scine_puffin/config.py @@ -20,14 +20,14 @@ def dict_generator(indict: Dict[str, Any], pre: Optional[List[str]] = None): Parameters ---------- - indict :: dict + indict : dict The dictionary to traverse. - pre :: dict + pre : dict The parent dictionary (used for recursion). Yields ------ - key_chain :: List[str] + key_chain : List[str] A list of keys from top level to bottom level for each end in the tree of possible value fields in the given dictionary. """ @@ -79,85 +79,85 @@ class Configuration: In detail, the options in the configuration are: **daemon** - mode :: str + mode : str The mode to run the Puffin in, options are ``release`` and ``debug``. The ``release`` mode will fork the main process and run in a daemonized mode while the ``debug`` mode will run in the current shell, reporting any output and errors to ``stdout`` and ``stderr``. - job_dir :: str + job_dir : str The path to the directory containing the currently running job. - software_dir :: str + software_dir : str The path to the directory containing the software bootstrapped from sources. The Puffin will generate and fill this directory upon bootstrapping. - error_dir :: str + error_dir : str If existent, the Puffin instance will archive all failed jobs into this directory. - archive_dir :: str + archive_dir : str If existent, the Puffin instance will archive all correctly completed jobs into this directory. - uuid :: str + uuid : str A unique name for the Puffin instance. This can be set by the user, if not, a unique ID will automatically be generated. - pid :: str + pid : str The path to the file identifying the PID of the Puffin instance. Automatically populated on start-up if left empty. - pid_dir :: str + pid_dir : str The path to a folder holding the file identifying the PID of the Puffin instance. - log :: str + log : str The path to the logfile of the Puffin instance. - stop :: str + stop : str The path to a file that if existent will prompt the Puffin instance to stop taking new jobs and shut down instead. The instance will finish any running job though. - remove_stop_file :: bool + remove_stop_file : bool Upon finding a stop file the daemon will stop, if this option is set to ``True`` the found file will be deleted allowing instant restarts. In cases where multiple puffins depend on the same stop file it may be required to keep the stop file, setting this option to ``False`` - cycle_time_in_s :: float + cycle_time_in_s : float The time in between scans of the database for new jobs that can be run. - timeout_in_h :: float + timeout_in_h : float The number of hours the Puffin instance should stay alive. Once this limit is reached, the Puffin is shut down and its running job will be killed and re-flagged as `new`. - idle_timeout_in_h :: float + idle_timeout_in_h : float The number of hours the Puffin instance should stay alive. After receiving the last job, once the limit is reached, the Puffin is shut down. Any accepted job will reset the timer. A negative value disables this feature and make the Puffin run until the ``timeout_in_h`` is reached independent of it being idle the entire time. - touch_time_in_s :: float + touch_time_in_s : float The time in seconds in between the attempts of the puffin to touch a calculation it is running in the database. In practice each Puffin will search for jobs in the database that are set as running but are not touched and reset them, as they indicate that the executing puffin has crashed. See ``job_reset_time_in_s`` for more information. - job_reset_time_in_s :: float + job_reset_time_in_s : float The time in seconds that may have passed since the last touch on pending jobs before they are considered dead and are reset to be worked by another puffin. Note: The time in this setting has to be larger than the ``touch_time_in_s`` of all Puffins working on the same database to work! - repeated_failure_stop :: int + repeated_failure_stop : int The number of consecutive failed jobs that are allowed before the Puffin stops in order to avoid failing all jobs in a DB due to e.g. hardware issues. Failed jobs will be reset to new and rerun by other Puffins. Should always be greater than 1. - max_number_of_jobs :: int + max_number_of_jobs : int The maximum number of jobs a single Puffin will carry out (complete or failed), before gracefully exiting. Any negative number or zero disables this setting; by default it is disabled. - enforce_memory_limit :: bool + enforce_memory_limit : bool If the given memory limit should be enforced (i.e., a job is killed as soon as it reaches it) or not. The puffin still continues to work on other calculations either way. **database** - ip :: str + ip : str The IP at which the database server to connect with is found. - port :: int + port : int The port at which the database server to connect with is found. - name :: str + name : str The name of the database on the database server to connect with. Multiple databases (with multiple names) can be given as comma seperated list: ``name_one,name_two,name_three``. The databases will be used in @@ -166,19 +166,19 @@ class Configuration: second one will be considered by the Puffin instance. **resources** - cores :: int + cores : int The number of threads the executed jobs are allowed to use. Note that only jobs that are below this value in their requirements will be accepted by the Puffin instance. - memory :: float + memory : float The total amount of memory the Puffin and its jobs are allowed to use. Given in GB. Note that only jobs that are below this value in their requirements will be accepted by the Puffin instance. - disk :: float + disk : float The total amount of disk space the Puffin and its jobs are allowed to use. Given in GB. Note that only jobs that are below this value in their requirements will be accepted by the Puffin instance. - ssh_keys :: List[str] + ssh_keys : List[str] Any SSH keys needed by the Puffin in order to connect to the database or to bootstrap programs. @@ -186,24 +186,24 @@ class Configuration: The specific details for each program are given in their respective documentation. However, common options are: - available :: bool + available : bool The switch whether the program shall be available to Puffin. Any programs set to be unavailable will not be bootstrapped. - source :: str + source : str The link to the source location of the given program, usually a https link to a git repository - root :: str + root : str The folder at which the program is already installed at. This will request a non source based bootstrapping of the program. - version :: str + version : str The version of the program to use. Can also be a git tag or commit SHA. The default version of a configuration file can be generated using ``python3 -m puffin configure`` (if no environment variables are set). """ - def __init__(self): - self._data = {} + def __init__(self) -> None: + self._data: Dict[str, Dict[str, Any]] = {} self._data["database"] = {"ip": "127.0.0.1", "port": 27017, "name": "default"} self._data["resources"] = { "cores": 1, @@ -237,7 +237,7 @@ def __init__(self): "available": False, "source": "https://github.com/qcscine/ams_wrapper.git", "root": "", - "version": "0.0.0", + "version": "develop", "march": "native", "cxx_compiler_flags": "", "cmake_flags": "", @@ -246,7 +246,7 @@ def __init__(self): "available": True, "source": "https://github.com/qcscine/readuct.git", "root": "", - "version": "5.1.0", + "version": "6.0.0", "march": "native", "cxx_compiler_flags": "", "cmake_flags": "", @@ -255,7 +255,7 @@ def __init__(self): "available": True, "source": "https://github.com/qcscine/core.git", "root": "", - "version": "6.0.0", + "version": "6.0.1", "march": "native", "cxx_compiler_flags": "", "cmake_flags": "", @@ -264,7 +264,7 @@ def __init__(self): "available": True, "source": "https://github.com/qcscine/utilities.git", "root": "", - "version": "9.0.0", + "version": "10.0.0", "march": "native", "cxx_compiler_flags": "", "cmake_flags": "", @@ -273,7 +273,7 @@ def __init__(self): "available": True, "source": "https://github.com/qcscine/database.git", "root": "", - "version": "1.3.0", + "version": "1.4.0", "march": "native", "cxx_compiler_flags": "", "cmake_flags": "", @@ -282,7 +282,7 @@ def __init__(self): "available": True, "source": "https://github.com/qcscine/sparrow.git", "root": "", - "version": "5.0.0", + "version": "5.1.0", "march": "native", "cxx_compiler_flags": "", "cmake_flags": "", @@ -291,7 +291,7 @@ def __init__(self): "available": True, "source": "https://github.com/qcscine/molassembler.git", "root": "", - "version": "2.0.1", + "version": "3.0.0", "march": "native", "cxx_compiler_flags": "", "cmake_flags": "", @@ -300,7 +300,7 @@ def __init__(self): 'available': False, 'source': 'https://github.com/qcscine/swoose.git', 'root': '', - 'version': '2.0.0', + 'version': '2.1.0', 'march': 'native', "cmake_flags": "", "cxx_compiler_flags": "", @@ -327,7 +327,7 @@ def __init__(self): "available": False, "source": "https://github.com/qcscine/serenity_wrapper.git", "root": "", - "version": "3.0.0", + "version": "3.1.0", "march": "native", "cxx_compiler_flags": "", "cmake_flags": "", @@ -342,7 +342,7 @@ def __init__(self): "available": False, "source": "https://github.com/qcscine/xtb_wrapper.git", "root": "", - "version": "3.0.0", + "version": "3.0.1", "march": "native", "cxx_compiler_flags": "", "cmake_flags": "", @@ -351,7 +351,7 @@ def __init__(self): "available": True, "source": "https://github.com/qcscine/kinetx.git", "root": "", - "version": "2.0.0", + "version": "3.0.0", "march": "native", "cxx_compiler_flags": "", "cmake_flags": "", @@ -372,7 +372,7 @@ def __init__(self): "available": False, "source": "https://github.com/qcscine/parrot.git", "root": "", - "version": "0.0.0" + "version": "develop" }, } @@ -382,12 +382,12 @@ def __getitem__(self, key: str) -> dict: Parameters ---------- - key :: str + key : str One of: ``daemon``, ``database``, ``resources``, ``programs``. Returns ------- - settings :: dict + settings : dict A sub-dict of the total configuration. """ return self._data[key] @@ -398,7 +398,7 @@ def database(self) -> dict: Returns ------- - settings :: dict + settings : dict A sub-dict of the total configuration. """ return self._data["database"] @@ -409,7 +409,7 @@ def resources(self) -> dict: Returns ------- - settings :: dict + settings : dict A sub-dict of the total configuration. """ return self._data["resources"] @@ -420,7 +420,7 @@ def daemon(self) -> dict: Returns ------- - settings :: dict + settings : dict A sub-dict of the total configuration. """ return self._data["daemon"] @@ -431,18 +431,18 @@ def programs(self) -> dict: Returns ------- - settings :: dict + settings : dict A sub-dict of the total configuration. """ return self._data["programs"] - def dump(self, path: str): + def dump(self, path: str) -> None: """ Dumps the current configuration into a .yaml file. Parameters ---------- - path :: str + path : str The file to dump the configuration into. """ # Parse environment @@ -454,11 +454,11 @@ def dump(self, path: str): with open(path, "w") as outfile: yaml.dump(self._data, outfile, default_flow_style=False) - def load(self, path: Optional[str] = None): + def load(self, path: Optional[str] = None) -> None: """ Loads the configuration. The configuration is initialized using the default values, then all settings given in the file (if there is one) - are applied. Finally all settings given as environment variables are + are applied. Finally, all settings given as environment variables are applied. Each setting in the config can be set via a corresponding environment @@ -474,7 +474,7 @@ def load(self, path: Optional[str] = None): Parameters ---------- - path :: str + path : str The file to read the configuration from. Default: ``None`` """ # Parse file @@ -506,7 +506,7 @@ def load(self, path: Optional[str] = None): if isinstance(current_value, bool): value = env[key].lower() in ["true", "1"] else: - value = type(current_value)(env[key]) + value = type(current_value)(env[key]) # type: ignore except BaseException as e: raise KeyError( "The environment variable '{}' can not be translated " @@ -527,16 +527,16 @@ def load(self, path: Optional[str] = None): f'{self._data["daemon"]["uuid"]}.pid' ) - def _apply_changes(self, to_dict: dict, from_dict: dict): + def _apply_changes(self, to_dict: dict, from_dict: dict) -> None: """ A small helper applying changes from one dictionary to another, checking the types and making sure only existing keys are mapped. Parameters ---------- - to_dict :: dict + to_dict : dict The dictionary to apply the changes to. - from_dict :: dict + from_dict : dict The dictionary to read the changes from. Raises diff --git a/scine_puffin/daemon.py b/scine_puffin/daemon.py index bf0fc06..fc0f69d 100644 --- a/scine_puffin/daemon.py +++ b/scine_puffin/daemon.py @@ -18,7 +18,7 @@ def shutdown(_signum, _frame): Parameters ---------- - _signum :: int + _signum : int Dummy variable to match the signal dependent function signature. _frame Dummy variable to match the signal dependent function signature. @@ -33,7 +33,7 @@ def check_environment(config: Configuration): Parameters ----------. - config :: scine_puffin.config.Configuration + config : scine_puffin.config.Configuration The current configuration of the Puffin. """ if "OMP_NUM_THREADS" in os.environ: @@ -51,7 +51,7 @@ def stop_daemon(config: Configuration): Parameters ----------. - config :: scine_puffin.config.Configuration + config : scine_puffin.config.Configuration The current configuration of the Puffin. """ # Generate stop file in order to stop after the current job @@ -74,9 +74,9 @@ def start_daemon(config: Configuration, detach: bool = True): Parameters ----------. - config :: scine_puffin.config.Configuration + config : scine_puffin.config.Configuration The current configuration of the Puffin. - detach :: bool + detach : bool If true, forks the daemon process and detaches it. """ check_environment(config) diff --git a/scine_puffin/jobloop.py b/scine_puffin/jobloop.py index beba38b..df89ff2 100644 --- a/scine_puffin/jobloop.py +++ b/scine_puffin/jobloop.py @@ -30,9 +30,9 @@ def _log(config: Configuration, message: str): Parameters ---------- - config :: Configuration + config : Configuration The configuration of the puffin - message :: str + message : str The message that is padded """ if config["daemon"]["log"]: @@ -49,9 +49,9 @@ def slow_connect(manager, config: Configuration) -> None: Parameters ---------- - manager :: scine_database.Manager + manager : scine_database.Manager The database manager/connection. - config :: scine_puffin.config.Configuration + config : scine_puffin.config.Configuration The current configuration of the Puffin. """ import scine_database as db @@ -83,7 +83,7 @@ def kill_daemon(config: Configuration) -> None: Parameters ---------- - config :: scine_puffin.config.Configuration + config : scine_puffin.config.Configuration The current configuration of the Puffin. """ # Remove stop file if present @@ -122,9 +122,9 @@ def loop(config: Configuration, available_jobs: dict) -> None: Parameters ---------- - config :: scine_puffin.config.Configuration + config : scine_puffin.config.Configuration The current configuration of the Puffin. - available_jobs :: dict + available_jobs : dict The dictionary of available jobs, given the current config and runtime environment. """ @@ -219,11 +219,11 @@ def _check_touch_of_pending_jobs( Parameters ---------- - manager :: scine_database.Manager + manager : scine_database.Manager The database connection - calculations :: scine_database.Collection + calculations : scine_database.Collection The collection holding all calculations - reset_delta :: int + reset_delta : int The time difference after which a job is assumed to be dead. Time given in seconds. """ @@ -246,7 +246,7 @@ def check_setup(config: Configuration) -> Dict[str, str]: Parameters ---------- - config :: scine_puffin.config.Configuration + config : scine_puffin.config.Configuration The current configuration of the Puffin. """ scine_database = util.find_spec("scine_database") @@ -336,14 +336,14 @@ def _loop_impl( Parameters ---------- - config :: scine_puffin.config.Configuration + config : scine_puffin.config.Configuration The current configuration of the Puffin. - available_jobs :: dict + available_jobs : dict A dictionary of all jobs that are available to this Puffin. - JOB :: multiprocessing.Array + JOB : multiprocessing.Array Possibly a shared array of chars (string) to share the current jobs ID with external code. Default: ``None`` - CURRENT_DB :: multiprocessing.Array + CURRENT_DB : multiprocessing.Array The name of the current database, used to sync the two threads in case of multi-database usage of a single Puffin. """ diff --git a/scine_puffin/jobs/conformers.py b/scine_puffin/jobs/conformers.py index db9d967..c3e353d 100644 --- a/scine_puffin/jobs/conformers.py +++ b/scine_puffin/jobs/conformers.py @@ -1,17 +1,27 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING, List +import math + from scine_puffin.config import Configuration from .templates.job import calculation_context, job_configuration_wrapper from .templates.scine_connectivity_job import ConnectivityJob from ..utilities import masm_helper +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class Conformers(ConnectivityJob): - """ + __doc__ = (""" A job generating all possible conformers (guesses) for a given structure with Molassembler. Currently, the structure must be a structure representing a single compound and not a non-covalently bonded complex. @@ -26,31 +36,12 @@ class Conformers(ConnectivityJob): any ``Calculation`` stored in a SCINE Database. Possible settings for this job are: - add_based_on_distance_connectivity :: bool - If ``True``, the structure's connectivity is derived from interatomic - distances via the utils.BondDetector: The bond orders used for - interpretation are set to the maximum between those given in the - ``bond_orders`` property and 1.0, whereever the utils.BondDetector - detects a bond. (default: True) - sub_based_on_distance_connectivity :: bool - If ``True``, the structure's connectivity is derived from interatomic - distances via the utils.BondDetector: The bond orders used for - interpretation are removed, whereever the utils.BondDetector does not - detect a bond. (default: True) - enforce_bond_order_model :: bool - If ``True``, only processes ``bond_orders`` that were generated with - the specified model. If ``False``, eventually falls back to any - ``bond_orders`` available for the structure. (default: True) - dihedral_retries :: int - The number of attempts to generate the dihedral decision - during conformer generation. (default: 100) - - **Required Packages** - - SCINE: Database (present by default) - - SCINE: molassembler (present by default) - - SCINE: Utils (present by default) - - **Generated Data** + """ + "\n" + + ConnectivityJob.optional_settings_doc() + "\n" + + ConnectivityJob.general_calculator_settings_docstring() + "\n" + + ConnectivityJob.generated_data_docstring() + "\n" + + + """ If successful the following data will be generated and added to the database: @@ -58,18 +49,22 @@ class Conformers(ConnectivityJob): A set of conformers guesses derived from the graph representation of the initial structure. All generated conformers will have a graph (``masm_cbor_graph``) and decision list set (``masm_decision_list``). - """ - def __init__(self): + """ + ConnectivityJob.required_packages_docstring() + ) + + def __init__(self) -> None: super().__init__() self.name = "Scine conformer generation" - @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: + @classmethod + def generated_data_docstring(cls) -> str: + return super().generated_data_docstring() + """ + """ - import scine_database as db + @job_configuration_wrapper + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: import scine_molassembler as masm - import math structure = db.Structure(calculation.get_structures()[0], self._structures) atoms = structure.get_atoms() @@ -154,5 +149,5 @@ def store(_, conformation): return self.postprocess_calculation_context() @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "molassembler", "utils"] diff --git a/scine_puffin/jobs/deprecated/final_conformer_deduplication.py.depr b/scine_puffin/jobs/deprecated/final_conformer_deduplication.py.depr index 24cbe32..1e3649f 100644 --- a/scine_puffin/jobs/deprecated/final_conformer_deduplication.py.depr +++ b/scine_puffin/jobs/deprecated/final_conformer_deduplication.py.depr @@ -7,9 +7,19 @@ See LICENSE.txt for details. import os import math from ast import literal_eval +from typing import Any, TYPE_CHECKING + from scine_puffin.config import Configuration from ..templates.job import Job, calculation_context, job_configuration_wrapper from ...utilities import masm_helper +from scine_puffin.utilities.imports import module_exists + +if module_exists("scine_database"): + import scine_database as db +elif TYPE_CHECKING: + import scine_database as db +else: + db = Any class FinalConformerDeduplication(Job): @@ -27,13 +37,13 @@ class FinalConformerDeduplication(Job): any ``Calculation`` stored in a SCINE Database. Possible settings for this job are: - use_distance_connectivity :: bool + use_distance_connectivity : bool If ``True``, the structure's connectivity is derived from interatomic distances via the utils.BondDetector: The bond orders used for interpretation are set to the maximum between those given in the ``bond_orders`` property and 1.0, whereever the utils.BondDetector detects a bond, and to 0.0 elsewhere. (default: True) - enforce_bond_order_model :: bool + enforce_bond_order_model : bool If ``True``, only processes ``bond_orders`` that were generated with the specified model. If ``False``, eventually falls back to any ``bond_orders`` available for the structure. (default: True) @@ -52,7 +62,7 @@ class FinalConformerDeduplication(Job): super().__init__() @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: # Import required packages import scine_database as db import scine_molassembler as masm @@ -212,5 +222,5 @@ class FinalConformerDeduplication(Job): return True @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "utils", "molassembler"] diff --git a/scine_puffin/jobs/deprecated/rdkit_conformers.py.depr b/scine_puffin/jobs/deprecated/rdkit_conformers.py.depr index a57b1f8..dc9b4ed 100644 --- a/scine_puffin/jobs/deprecated/rdkit_conformers.py.depr +++ b/scine_puffin/jobs/deprecated/rdkit_conformers.py.depr @@ -6,9 +6,20 @@ See LICENSE.txt for details. import os import sys +from typing import Any, TYPE_CHECKING + import scipy.sparse + from scine_puffin.config import Configuration from ..templates.job import Job, calculation_context, job_configuration_wrapper +from scine_puffin.utilities.imports import module_exists + +if module_exists("scine_database"): + import scine_database as db +elif TYPE_CHECKING: + import scine_database as db +else: + db = Any try: from rdkit import Chem @@ -31,18 +42,18 @@ class RdkitConformers(Job): any ``Calculation`` stored in a SCINE Database. Possible settings for this job are: - numConfs :: int + numConfs : int The number of conformers to be generated. By default it is 100. - maxAttempts :: int + maxAttempts : int The maximum number of trials that will be undertaken in order to generate conformers. By default it equals numConfs. - pruneRmsThresh :: float + pruneRmsThresh : float If set, the heavy atom RMSD is calculated and the generated conformers are pruned such that only those are kept that are at least pruneRmsThresh away from all priorly retained conformations. By default there is no pruning. - randomSeed :: int + randomSeed : int Seed for the random number generator employed during conformer generation. If set to -1 (default), the random number generator is not seeded. @@ -70,11 +81,11 @@ class RdkitConformers(Job): super().__init__() @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "utils", "rdkit"] @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: import scine_database as db diff --git a/scine_puffin/jobs/gaussian_charge_model_5.py b/scine_puffin/jobs/gaussian_charge_model_5.py index dc62eb3..0b54aa4 100644 --- a/scine_puffin/jobs/gaussian_charge_model_5.py +++ b/scine_puffin/jobs/gaussian_charge_model_5.py @@ -1,13 +1,21 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING, List import os import subprocess from scine_puffin.config import Configuration from .templates.job import Job, calculation_context, job_configuration_wrapper +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class GaussianChargeModel5(Job): @@ -35,7 +43,7 @@ class GaussianChargeModel5(Job): The ``cm5_charges`` calculated for the given structure. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.input_file = "gaussian_calc.inp" self.output_file = "gaussian_calc.out" @@ -92,9 +100,7 @@ def execute_program(self): ) @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - - import scine_database as db + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: # Gather all required collections structures = manager.get_collection("structures") @@ -172,5 +178,5 @@ def run(self, manager, calculation, config: Configuration) -> bool: return True @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "utils", "gaussian"] diff --git a/scine_puffin/jobs/graph.py b/scine_puffin/jobs/graph.py index 16e1e53..1148916 100644 --- a/scine_puffin/jobs/graph.py +++ b/scine_puffin/jobs/graph.py @@ -1,16 +1,25 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING, List + from scine_puffin.config import Configuration from .templates.job import calculation_context, job_configuration_wrapper from .templates.scine_connectivity_job import ConnectivityJob +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class Graph(ConnectivityJob): - """ + __doc__ = (""" A job generating the molassembler graph and decision lists of a structure. **Order Name** @@ -20,51 +29,33 @@ class Graph(ConnectivityJob): Optional settings are read from the ``settings`` field, which is part of any ``Calculation`` stored in a SCINE Database. Possible settings for this job are: - - add_based_on_distance_connectivity :: bool - If ``True``, the structure's connectivity is derived from interatomic - distances via the utils.BondDetector: The bond orders used for - interpretation are set to the maximum between those given in the - ``bond_orders`` property and 1.0, wherever the utils.BondDetector - detects a bond. (default: True) - sub_based_on_distance_connectivity :: bool - If ``True``, the structure's connectivity is derived from interatomic - distances via the utils.BondDetector: The bond orders used for - interpretation are removed, wherever the utils.BondDetector does not - detect a bond. (default: True) - enforce_bond_order_model :: bool - If ``True``, only processes ``bond_orders`` that were generated with - the specified model. If ``False``, eventually falls back to any ``bond_orders`` - available for the structure. (default: True) - - **Required Packages** - - SCINE: Database (present by default) - - SCINE: molassembler (present by default) - - SCINE: Utils (present by default) - - **Generated Data** - If successful the following data will be generated and added to the - database: - - Properties - - None - - Other - Graph representations of the structure will be added to the structures - ``graphs`` field. The added representations are: A representation of the - graph ``masm_cbor_graph``, and the decision representations of the existing - stereopermutators using a nearest neighbor fit ``masm_decision_list`` - Any previous graph representations of the structure will be overwritten. + """ + "\n" + + ConnectivityJob.optional_settings_doc() + "\n" + + ConnectivityJob.general_calculator_settings_docstring() + "\n" + + ConnectivityJob.generated_data_docstring() + "\n" + + """ + If successful the following data will be generated and added to the + database: + + Properties + - None + + Other + Graph representations of the structure will be added to the structures + ``graphs`` field. The added representations are: A representation of the + graph ``masm_cbor_graph``, and the decision representations of the existing + stereopermutators using a nearest neighbor fit ``masm_decision_list`` + Any previous graph representations of the structure will be overwritten. """ + + ConnectivityJob.required_packages_docstring() + ) - def __init__(self): + def __init__(self) -> None: super().__init__() self.name = "Scine Graph Job" @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - - import scine_database as db + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: # preprocessing of structure structure = db.Structure(calculation.get_structures()[0], self._structures) @@ -88,5 +79,5 @@ def run(self, manager, calculation, config: Configuration) -> bool: return self.postprocess_calculation_context() @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "molassembler", "utils"] diff --git a/scine_puffin/jobs/kinetx_kinetic_modeling.py b/scine_puffin/jobs/kinetx_kinetic_modeling.py index bf93340..37b015f 100644 --- a/scine_puffin/jobs/kinetx_kinetic_modeling.py +++ b/scine_puffin/jobs/kinetx_kinetic_modeling.py @@ -1,17 +1,26 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -from typing import List - -import scine_database as db +from typing import Any, TYPE_CHECKING, List, Dict from .templates.job import breakable, calculation_context, job_configuration_wrapper from .templates.kinetic_modeling_jobs import KineticModelingJob from ..utilities.compound_and_flask_helpers import get_compound_or_flask from scine_puffin.config import Configuration +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") class KinetxKineticModeling(KineticModelingJob): @@ -27,43 +36,43 @@ class KinetxKineticModeling(KineticModelingJob): ``kinetx_kinetic_modeling`` **Required Input** - model :: db.Model - The electronic structure model to flag the new properties with. + model : db.Model + The electronic structure model to flag the new properties with. **Required Settings** - aggregate_ids :: List[str] + aggregate_ids : List[str] The aggregate IDs (as strings). - reaction_ids :: List[str] + reaction_ids : List[str] The reaction IDs (as strings). - aggregate_types :: List[int] + aggregate_types : List[int] The aggregate types. 0 for compounds, 1 for flasks. - lhs_rates :: List[float] - The reaction rates for the forward reactions. - rhs_rates :: List[float] - The reaction rates for the backward reactions. + lhs_rates : List[float] + The reaction rates for the forward reactions. + rhs_rates : List[float] + The reaction rates for the backward reactions. **Optional Settings** Optional settings are read from the ``settings`` field, which is part of any ``Calculation`` stored in a SCINE Database. The following options are available: - time_step ::float + time_step : float The integration time step. - solver :: str + solver : str The name of the numerical differential equation solver. Options are "CashKarp5" (default), "ImplicitEuler", and "ExplicitEuler". - batch_interval :: int + batch_interval : int The numerical integration is done in batches of time steps. After each step the maximum concentration for each compound is updated. This is the size of each time-step batch. - n_batches :: int + n_batches : int The numerical integration is done in batches of time steps. After each step the maximum concentration for each compound is updated. This is the number of time-step batches. - energy_model_program :: str + energy_model_program : str The program with which the electronic structure model should be flagged. Default any. - convergence :: float + convergence : float Stop the numerical integration if the concentrations do not change more than this threshold between intervals. - concentration_label_postfix :: str + concentration_label_postfix : str Post fix to the property label. Default "". **Required Packages** @@ -82,10 +91,10 @@ class KinetxKineticModeling(KineticModelingJob): documents. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.name = "KiNetX kinetic modeling job" - self.settings = { + self.settings: Dict[str, Any] = { "energy_label": "electronic_energy", "time_step": 1e-8, "solver": "cash_karp_5", @@ -96,20 +105,20 @@ def __init__(self): "concentration_label_postfix": "" } self.model = db.Model("PM6", "PM6", "") - self._flask_decomposition = dict() @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: import scine_kinetx as kinetx with breakable(calculation_context(self)): self.settings.update(calculation.get_settings()) - aggregate_id_list = [db.ID(c_id_str) for c_id_str in self.settings["aggregate_ids"]] - aggregate_type_list = [db.CompoundOrFlask(a_type) for a_type in self.settings["aggregate_types"]] - reaction_ids = [db.ID(r_id_str) for r_id_str in self.settings["reaction_ids"]] - lhs_rates_per_reaction = self.settings["lhs_rates"] - rhs_rates_per_reaction = self.settings["rhs_rates"] - concentrations = self.settings["start_concentrations"] + aggregate_id_list = [db.ID(c_id_str) for c_id_str in self._get_setting_value_list("aggregate_ids")] + aggregate_type_list = [db.CompoundOrFlask(a_type) + for a_type in self._get_setting_value_list("aggregate_types")] + reaction_ids = [db.ID(r_id_str) for r_id_str in self._get_setting_value_list("reaction_ids")] + lhs_rates_per_reaction = self._get_setting_value_list("lhs_rates") + rhs_rates_per_reaction = self._get_setting_value_list("rhs_rates") + concentrations = self._get_setting_value_list("start_concentrations") self.model = calculation.get_model() n_reactions = len(reaction_ids) n_aggregates = len(aggregate_id_list) @@ -147,13 +156,19 @@ def run(self, manager, calculation, config: Configuration) -> bool: [reaction_flux, reaction_flux_forward, reaction_flux_backward], [self.c_max_label, self.c_final_label, self.c_flux_label], [self.r_flux_label, self.r_forward_label, self.r_backward_label], - results, self.settings["concentration_label_postfix"], True) + results, self.settings["concentration_label_postfix"]) # calculation.set_results(results) self._disable_all_aggregates() self.complete_job() return self.postprocess_calculation_context() + def _get_setting_value_list(self, name: str) -> List[Any]: + val = self.settings[name] + if not isinstance(val, list): + raise RuntimeError(f"Setting {name} must be a list.") + return val + def _add_all_aggregates(self, aggregate_id_list: List[db.ID], aggregate_type_list: List[db.CompoundOrFlask], network_builder) -> None: """ @@ -165,19 +180,19 @@ def _add_all_aggregates(self, aggregate_id_list: List[db.ID], aggregate_type_lis mass = self._calculate_weight(centroid) network_builder.add_compound(mass, a_id.string()) - def _calculate_weight(self, structure_id) -> float: - import scine_utilities as utils + @requires("utilities") + def _calculate_weight(self, structure_id: db.ID) -> float: """ Calculates the molecular weight, given a DB structure id. Attributes ---------- - structure :: db.Structure + structure_id : db.ID The structure of which to calculate the molecular weight. Returns ------- - weight :: float + weight : float The molecular weight in a.u. . """ structure = db.Structure(structure_id, self._structures) @@ -221,5 +236,5 @@ def check_mass_balance(self, lhs_stoichiometry, rhs_stoichiometry, aggregate_id_ raise AssertionError("Unbalanced masses in reaction. You are destroying/creating atoms!") @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "kinetx", "utils"] diff --git a/scine_puffin/jobs/orca_geometry_optimization.py b/scine_puffin/jobs/orca_geometry_optimization.py index 0d11970..e41a9e8 100644 --- a/scine_puffin/jobs/orca_geometry_optimization.py +++ b/scine_puffin/jobs/orca_geometry_optimization.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. @@ -8,9 +9,15 @@ import re import subprocess import glob -from typing import Any, List, Tuple +from typing import Any, List, Tuple, TYPE_CHECKING from scine_puffin.config import Configuration from .templates.job import Job, calculation_context, job_configuration_wrapper +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class OrcaGeometryOptimization(Job): @@ -28,24 +35,24 @@ class OrcaGeometryOptimization(Job): any ``Calculation`` stored in a SCINE Database. Possible settings for this job are: - convergence_max_iterations :: int + convergence_max_iterations : int The maximum number of geometry optimization cycles. - cartesian_constraints :: List[int] + cartesian_constraints : List[int] A list of atom indices of the atoms which positions will be constrained during the optimization. - max_scf_iterations :: int + max_scf_iterations : int The number of allowed SCF cycles until convergence. - self_consistence_criterion :: float + self_consistence_criterion : float The self consistence criterion corresponding to the maximum energy change between two SCF cycles resulting in convergence. - scf_damping :: bool + scf_damping : bool Switches damping on or off during the SCF by employing the Orca keyword SlowConv. The default is False. - calculate_hirshfeld_charges :: bool + calculate_hirshfeld_charges : bool Calculates atomic partial charges based on the Hirshfeld population analysis for the optimized structure and stores these into the database. The default is False. - transform_coordinates :: bool + transform_coordinates : bool Switch to transform the input coordinates from Cartesian to redundant internal coordinates. Setting this value to False and hence performing the calculation in Cartesian coordinates is helpful in rare occasions @@ -68,7 +75,7 @@ class OrcaGeometryOptimization(Job): The ``electronic_energy`` associated with the new structure. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.input_file = "orca_calc.inp" self.output_file = "orca_calc.out" @@ -144,6 +151,7 @@ def parse_output_file(self) -> Tuple[Any, float, list]: successful = False atomic_charges: List[float] = [] + optimized_energy = None with open(self.output_file, "r") as out: lines = out.readlines() for line_index, line in enumerate(lines): @@ -168,6 +176,8 @@ def parse_output_file(self) -> Tuple[Any, float, list]: atomic_charges.append(float(charge)) current_line_index += 1 if successful: + if optimized_energy is None: + raise RuntimeError("Optimization converged but no energy found in output file.") optimized_structure, _ = utils.io.read(self.output_structure) return optimized_structure, float(optimized_energy), atomic_charges else: @@ -186,9 +196,8 @@ def execute_program(self): ) @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: - import scine_database as db import scine_utilities as utils # Gather all required collections @@ -239,7 +248,7 @@ def run(self, manager, calculation, config: Configuration) -> bool: model, job, config["daemon"]["uuid"], - calculation.get_settings(), + dict(calculation.get_settings()), ) # Execute ORCA self.execute_program() @@ -340,5 +349,5 @@ def run(self, manager, calculation, config: Configuration) -> bool: return True @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "utils", "orca"] diff --git a/scine_puffin/jobs/rms_kinetic_modeling.py b/scine_puffin/jobs/rms_kinetic_modeling.py index 7c1c107..093772b 100644 --- a/scine_puffin/jobs/rms_kinetic_modeling.py +++ b/scine_puffin/jobs/rms_kinetic_modeling.py @@ -1,20 +1,26 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -import numpy as np -import scine_database as db -from typing import Optional, List from multiprocessing import Pool +from typing import Optional, List, TYPE_CHECKING, Any, Dict + +import numpy as np from .templates.job import breakable, calculation_context, job_configuration_wrapper from .templates.kinetic_modeling_jobs import KineticModelingJob from scine_puffin.config import Configuration - from ..utilities.rms_kinetic_model import RMSKineticModel from ..utilities.kinetic_modeling_sensitivity_analysis import RMSKineticModelingSensitivityAnalysis +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class RmsKineticModeling(KineticModelingJob): @@ -22,30 +28,32 @@ class RmsKineticModeling(KineticModelingJob): Micro-kinetic modeling with the puffin-interface to the reaction mechanism simulator (RMS). Note: Running jobs with RMS as a backend requires an installation of RMS (including its Python bindings). This is not supported through the Puffin bootstrapping. See programs/rms.py for more information. + **Order Name** - ``rms_kinetic_modeling`` + ``rms_kinetic_modeling`` + **Required Input** - model :: db.Model - The electronic structure model to flag the new properties with. + model : db.Model + The electronic structure model to flag the new properties with. **Required Settings** - aggregate_ids :: List[str] + aggregate_ids : List[str] The aggregate IDs (as strings). - reaction_ids :: List[str] + reaction_ids : List[str] The reaction IDs (as strings). - aggregate_types :: List[int] + aggregate_types : List[int] The aggregate types. 0 for compounds, 1 for flasks. - ea :: List[float] + ea : List[float] The activation energies for each reaction as the free energy difference to the reaction LHS (in J/mol). - enthalpies :: List[float] + enthalpies : List[float] The enthalpy of each aggregate (in J/mol). - entropies :: List[float] + entropies : List[float] The entropy of each aggregate (in J/mol). - arrhenius_prefactors :: List[float] + arrhenius_prefactors : List[float] The exponential prefactors. - arrhenius_temperature_exponents :: List[float] + arrhenius_temperature_exponents : List[float] The temperature exponents in the Arrhenius equation. - start_concentrations :: List[float + start_concentrations : List[float The start concentrations of each aggregate. **Optional Settings** @@ -53,44 +61,43 @@ class RmsKineticModeling(KineticModelingJob): any ``Calculation`` stored in a SCINE Database. The following options are available: - solver :: str + + solver : str ODE solver. Currently only "CVODE_BDF" is supported. - phase_type :: str + phase_type : str The reactor phase. Options are ideal_gas (assumes P=const, T=const), ideal_dilute_solution (assumes V=const, T=const). Default is "ideal_gas". - max_time :: float + max_time : float Maximum integration time in seconds. Default 3600.0. - energy_model_program :: str + energy_model_program : str The program with which the electronic structure model should be flagged. Default any. - viscosity :: float + viscosity : float The solvent viscosity (in Pa s). Needs phase=ideal_dilute_solution and diffusion_limited=true. If "none", the viscosity is taken from tabulated values. - reactor_solvent :: str + reactor_solvent : str The reactor solvent. If "none", the solvent in the electronic structure model is used if any. - site_density :: float - The density of surface sites. Default is "none". Requires phase=ideal_surface. Not fully supported yet. - diffusion_limited :: bool - If true, diffusion limits are enforced. Requires phase=ideal_dilute_solution. May lead to numerical - instability of the ODE solver. Default False. - reactor_temperature :: float - The reactor temperature (in K). If "none", the temperature in the model is used. Default "none". - reactor_pressure :: float - The reactor pressure (in Pa). If none, the pressure in the model is used. Default "none". - absolute_tolerance :: float - The absolute tolerance of the ODE solver. High values lead to a faster but less reliable integration. Default - 1e-20. - relative_tolerance :: float - The relative tolerance of the ODE solver. High values lead to a faster but less reliable integration. - Default 1e-6. - solvent_aggregate_str_id :: str - The aggregate ID of the solvent as a string. If "none", the solvent is assumed to be unreactive. - solvent_concentration :: float - The solvent concentraion. Defualt is 55.3 (mol/L). - - enforce_mass_balance :: bool - If true, the an error is raised for any non-balanced reaction. - - screen_sensitivities :: bool + site_density : float + The density of surface sites. Default is "none". Requires phase=ideal_surface. Not fully supported yet. + diffusion_limited : bool + If true, diffusion limits are enforced. Requires phase=ideal_dilute_solution. May lead to numerical + instability of the ODE solver. Default False. + reactor_temperature : float + The reactor temperature (in K). If "none", the temperature in the model is used. Default "none". + reactor_pressure : float + The reactor pressure (in Pa). If none, the pressure in the model is used. Default "none". + absolute_tolerance : float + The absolute tolerance of the ODE solver. High values lead to a faster but less reliable integration. Default + 1e-20. + relative_tolerance : float + The relative tolerance of the ODE solver. High values lead to a faster but less reliable integration. + Default 1e-6. + solvent_aggregate_str_id : str + The aggregate ID of the solvent as a string. If "none", the solvent is assumed to be unreactive. + solvent_concentration : float + The solvent concentration. Default is 55.3 (mol/L). + enforce_mass_balance : bool + If true, an error is raised for any non-balanced reaction. + screen_sensitivities : bool If true, only parameters associated to aggregates and reactions with significant concentration flux are considered in the sensitivity analysis (flux > oaat_vertex_flux_threshold | flux > oaat_vertex_flux_threshold). @@ -107,10 +114,11 @@ class RmsKineticModeling(KineticModelingJob): its centroid. The edge flux for each reaction is added to the centroid of the first aggregate on the reaction's LHS. Note, that the properties are NOT listed in the results to avoid large DB documents. """ - def __init__(self): + + def __init__(self) -> None: super().__init__() self.name: str = "RMS kinetic modeling job" - self.settings = { + self.settings: Dict[str, Any] = { "solver": "Recommended", "phase_type": "ideal_gas", "max_time": 3600.0, @@ -141,7 +149,6 @@ def __init__(self): } self.model: db.Model = db.Model("PM6", "PM6", "") self._rms_file_name: str = "chem.rms" - self._rms_aggregate_indices = [] self._solvent_a_index: Optional[int] = None self._viscosity: Optional[float] = None self._solvent: Optional[str] = None @@ -176,7 +183,7 @@ def use_n_cores(self, n_cores: int) -> int: return 1 @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "rms"] @job_configuration_wrapper diff --git a/scine_puffin/jobs/scine_afir.py b/scine_puffin/jobs/scine_afir.py index be091bb..4a242c1 100644 --- a/scine_puffin/jobs/scine_afir.py +++ b/scine_puffin/jobs/scine_afir.py @@ -1,17 +1,25 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING + from scine_puffin.config import Configuration from .templates.job import calculation_context, job_configuration_wrapper from .templates.scine_react_job import ReactJob +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class ScineAfir(ReactJob): - """ - A job optimizing a structure while applying an artificial force. + __doc__ = ("""A job optimizing a structure while applying an artificial force. **Order Name** ``scine_afir`` @@ -22,44 +30,35 @@ class ScineAfir(ReactJob): Possible settings for this job are: All settings recognized by ReaDuct's AFIR task. For a complete list see the - `ReaDuct manual `_ + `ReaDuct manual `_ The most common AFIR related ones being: - afir_afir_weak_forces :: bool + afir_afir_weak_forces : bool This activates an additional, weakly attractiveforce applied to all atom pairs. By default set to ``False``. - afir_afir_attractive :: bool + afir_afir_attractive : bool Specifies whether the artificial force is attractive or repulsive. By default set to ``True``, which means that the force is attractive. - afir_afir_rhs_list :: List[int] + afir_afir_rhs_list : List[int] This specifies list of indices of atoms to be artificially forced onto or away from those in the LHS list (see below). By default, this list is empty. Note that the first atom has the index zero. - afir_afir_lhs_list :: List[int] + afir_afir_lhs_list : List[int] This specifies list of indices of atoms to be artificially forced onto or away from those in the RHS list (see above). By default, this list is empty. Note that the first atom has the index zero. - afir_afir_energy_allowance :: float + afir_afir_energy_allowance : float The maximum amount of energy to be added by the artificial force, in kJ/mol. By default set to 1000 kJ/mol. - afir_afir_phase_in :: int + afir_afir_phase_in : int The number of steps over which the full force is gradually applied. By default set to 100. - All settings that are recognized by the SCF program chosen. - - Common examples are: - - max_scf_iterations :: int - The number of allowed SCF cycles until convergence. - - **Required Packages** - - SCINE: Database (present by default) - - SCINE: Readuct (present by default) - - SCINE: Utils (present by default) - - A program implementing the SCINE Calculator interface, e.g. Sparrow - - **Generated Data** + """ + "\n" + + ReactJob.optional_settings_doc() + "\n" + + ReactJob.general_calculator_settings_docstring() + "\n" + + ReactJob.generated_data_docstring() + "\n" + + """ If successful the following data will be generated and added to the database: @@ -69,7 +68,10 @@ class ScineAfir(ReactJob): The ``electronic_energy`` associated with the new structure. """ - def __init__(self): + + ReactJob.required_packages_docstring() + ) + + def __init__(self) -> None: super().__init__() self.name = "Scine AFIR Job" afir_defaults = { @@ -83,13 +85,12 @@ def __init__(self): self.settings = { **self.settings, "afir": afir_defaults, - "opt": opt_defaults, + self.opt_key: opt_defaults, } @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: - import scine_database as db import scine_readuct as readuct structure = db.Structure(calculation.get_structures()[0], self._structures) @@ -106,7 +107,7 @@ def run(self, manager, calculation, config: Configuration) -> bool: structure, calculation, calculation.get_settings(), config["resources"] ) if program_helper is not None: - program_helper.calculation_preprocessing(self.systems[keys[0]], calculation.get_settings()) + program_helper.calculation_preprocessing(self.get_system(keys[0]), calculation.get_settings()) """ AFIR Optimization """ print("Afir Settings:") @@ -115,28 +116,45 @@ def run(self, manager, calculation, config: Configuration) -> bool: self.throw_if_not_successful(success, self.systems, keys, ["energy"], "AFIR optimization failed:\n") """ Endpoint Optimization """ - product_names = self.optimize_structures( - "product", - [self.systems[self.output('afir')[0]].structure], + product_names, self.systems = self.optimize_structures( + "product", self.systems, + [self.get_system(self.output('afir')[0]).structure], [structure.get_charge()], [structure.get_multiplicity()], settings_manager.calculator_settings ) - - graph, self.systems = self.make_graph_from_calc(self.systems, keys[0]) - new_label = self.determine_new_label(structure, graph) - - new_structure = self.optimization_postprocessing(success, self.systems, product_names, structure, - new_label, program_helper, ['energy', 'bond_orders']) - self.store_property( - self._properties, - "bond_orders", - "SparseMatrixProperty", - self.systems[product_names[0]].get_results().bond_orders.matrix, - self._calculation.get_model(), - self._calculation, - new_structure, + if len(product_names) != 1: + self.raise_named_exception("Optimization of the product yielded multiple structures, " + "which is not expected") + lowest_name, names_within_range = self._get_propensity_names_within_range( + product_names[0], self.systems, self.settings[self.propensity_key]["energy_range_to_optimize"] ) - self.add_graph(new_structure, self.systems[product_names[0]].get_results().bond_orders) - + if lowest_name is None: + self.raise_named_exception("Product optimization was not successful") + raise RuntimeError("Unreachable") + + old_label = structure.get_label() + db_results = calculation.get_results() + for product in [lowest_name] + names_within_range: + graph, self.systems = self.make_graph_from_calc(self.systems, product) + new_label = self.determine_new_label(old_label, graph, structure.has_property("surface_atom_indices")) + + new_structure = self.optimization_postprocessing(success, self.systems, [product], structure, + new_label, program_helper, ['energy', 'bond_orders']) + bond_orders = self.get_system(product_names[0]).get_results().bond_orders + assert bond_orders is not None + + self.store_property( + self._properties, + "bond_orders", + "SparseMatrixProperty", + bond_orders.matrix, + self._calculation.get_model(), + self._calculation, + new_structure, + ) + self.add_graph(new_structure, bond_orders) + db_results += calculation.get_results() + + calculation.set_results(db_results) return self.postprocess_calculation_context() diff --git a/scine_puffin/jobs/scine_bond_orders.py b/scine_puffin/jobs/scine_bond_orders.py index 1d5b42f..38820e2 100644 --- a/scine_puffin/jobs/scine_bond_orders.py +++ b/scine_puffin/jobs/scine_bond_orders.py @@ -1,16 +1,29 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING + from scine_puffin.config import Configuration from .templates.job import calculation_context, job_configuration_wrapper from .templates.scine_connectivity_job import ConnectivityJob +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") class ScineBondOrders(ConnectivityJob): - """ + __doc__ = (""" A job calculating bond orders. **Order Name** @@ -21,42 +34,29 @@ class ScineBondOrders(ConnectivityJob): any ``Calculation`` stored in a SCINE Database. Possible settings for this job are: - only_distance_connectivity :: bool - Whether the bond orders shall be constructed via distance information only (True) - or from an electronic structure calculation (False). (default: False) - add_based_on_distance_connectivity :: bool - If ``True``, the structure's connectivity is derived from interatomic - distances via the utils.BondDetector: The bond orders used for - interpretation are set to the maximum between those given by an electronic structure - calculation and 1.0, whereever the utils.BondDetector - detects a bond. (default: True) - sub_based_on_distance_connectivity :: bool - If ``True``, the structure's connectivity is derived from interatomic - distances via the utils.BondDetector: The bond orders used given by an electronic structure - calculation are removed, whereever the utils.BondDetector does not - detect a bond. (default: True) - add_graph :: bool - Whether to add a molassembler graph and decision list to the structure - based on the determined bond orders. (default: True) - - All settings that are recognized by the SCF program chosen. - - Common examples are: - - max_scf_iterations :: int - The number of allowed SCF cycles until convergence. - - **Required Packages** - - SCINE: Database (present by default) - - SCINE: molassembler (present by default) - - SCINE: Readuct (present by default) - - SCINE: Utils (present by default) - - A program implementing the SCINE Calculator interface, e.g. Sparrow - **Generated Data** If successful the following data will be generated and added to the database: + Properties + The ``bond_orders`` (``SparseMatrixProperty``) are added. + Optionally the ``electronic_energy`` associated with the structure if it + is present in the results of provided by the calculator interface. + Other + If a graph is requested, graph representations of the structure will be + added to the structures ``graphs`` field. The added representations are: + A representation of the graph ``masm_cbor_graph``, and the decision + representations of the existing stereopermutators using a nearest + neighbour fit ``masm_decision_list``. + Any previous graph representations of the structure will be overwritten. + """ + "\n" + + ConnectivityJob.optional_settings_doc() + "\n" + + ConnectivityJob.general_calculator_settings_docstring() + "\n" + + ConnectivityJob.generated_data_docstring() + "\n" + + """ + If successful the following data will be generated and added to the + database: + Properties The ``bond_orders`` (``SparseMatrixProperty``) are added. Optionally the ``electronic_energy`` associated with the structure if it @@ -69,15 +69,16 @@ class ScineBondOrders(ConnectivityJob): neighbour fit ``masm_decision_list``. Any previous graph representations of the structure will be overwritten. """ + + ConnectivityJob.required_packages_docstring() + ) - def __init__(self): + def __init__(self) -> None: super().__init__() self.name = "Scine Bond Order Job" @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: - import scine_database as db import scine_readuct as readuct # Get structure @@ -97,22 +98,26 @@ def run(self, manager, calculation, config: Configuration) -> bool: bond_orders = self.distance_bond_orders(structure) # Bond order calculation with readuct else: + task_settings = utils.ValueCollection(settings_manager.task_settings) systems, keys = settings_manager.prepare_readuct_task( structure, calculation, - settings_manager.task_settings, + task_settings, config["resources"], ) if program_helper is not None: program_helper.calculation_preprocessing( - systems[keys[0]], settings_manager.task_settings + self.get_calc(keys[0], systems), task_settings ) systems, success = readuct.run_single_point_task( - systems, keys, require_bond_orders=True, **settings_manager.task_settings + systems, keys, require_bond_orders=True, **task_settings ) self.throw_if_not_successful(success, systems, keys) - bond_orders = systems[keys[0]].get_results().bond_orders + bond_orders = self.get_calc(keys[0], systems).get_results().bond_orders # type: ignore + if bond_orders is None: + self.raise_named_exception("No bond orders found in results.") + raise RuntimeError("Unreachable") # for mypy # Graph generation if add_graph: diff --git a/scine_puffin/jobs/scine_bspline_optimization.py b/scine_puffin/jobs/scine_bspline_optimization.py index fd68c14..db04e96 100644 --- a/scine_puffin/jobs/scine_bspline_optimization.py +++ b/scine_puffin/jobs/scine_bspline_optimization.py @@ -1,22 +1,34 @@ # -*- coding: utf-8 -*- +from __future__ import annotations + __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -import scine_database as db -import scine_utilities as utils - +import sys from copy import deepcopy +from typing import TYPE_CHECKING, Dict + from scine_puffin.config import Configuration from .templates.job import breakable, calculation_context, job_configuration_wrapper from .templates.scine_react_job import ReactJob from typing import Optional, List from scine_puffin.utilities.scine_helper import SettingsManager +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_database") class ScineBsplineOptimization(ReactJob): - """ + __doc__ = (""" The job interpolated between two structures. Generates a transition state guess, optimizes the transition state, and verifies the IRC. @@ -42,7 +54,7 @@ class ScineBsplineOptimization(ReactJob): any ``Calculation`` stored in a SCINE Database. All possible settings for this job are based on those available in SCINE ReaDuct. For a complete list see the - `ReaDuct manual `_ + `ReaDuct manual `_ Given that this job does more than one, in fact many separate calculations it is possible to target each individually with the settings. In order to @@ -62,63 +74,14 @@ class ScineBsplineOptimization(ReactJob): 3. Optimization of the structures obtained with the IRC scan : ``ircopt_*`` 4. Optimization of the products and reactants: ``opt_*`` - The following settings are recognized without a prepending flag: - - add_based_on_distance_connectivity :: bool - Whether to add the connectivity (i.e. add bonds) as derived from - atomic distances when graphs are generated. (default: True) - sub_based_on_distance_connectivity :: bool - Whether to subtract the connectivity (i.e. remove bonds) as derived from - atomic distances when graphs are generated. (default: True) - only_distance_connectivity :: bool - Whether to impose the connectivity solely from distances. (default: False) - imaginary_wavenumber_threshold :: float - Threshold value in inverse centimeters below which a wavenumber - is considered as imaginary when the transition state is analyzed. - Negative numbers are interpreted as imaginary. (default: 0.0) - spin_propensity_check :: int - The range to check for possible multiplicities for products. A value - of 2 (default) will check triplet and quintet for a singlet - and will check singlet, quintet und septet for triplet. - - Additionally, all settings that are recognized by the SCF program chosen. - are also available. These settings are not required to be prepended with - any flag. - - Common examples are: - - max_scf_iterations :: int - The number of allowed SCF cycles until convergence. - - **Required Packages** - - SCINE: Database (present by default) - - SCINE: molassembler (present by default) - - SCINE: Readuct (present by default) - - SCINE: Utils (present by default) - - A program implementing the SCINE Calculator interface, e.g. Sparrow - - **Generated Data** - If successful (technically and chemically) the following data will be - generated and added to the database: - - Elementary Steps - If found, a single new elementary step with the associated transition - state will be added to the database. - - Structures - The transition state (TS) and also the separated products and reactants - will be added to the database. - - Properties - The ``hessian`` (``DenseMatrixProperty``), ``frequencies`` - (``VectorProperty``), ``normal_modes`` (``DenseMatrixProperty``), - ``gibbs_energy_correction`` (``NumberProperty``) and - ``gibbs_free_energy`` (``NumberProperty``) of the TS will be - provided. The ``electronic_energy`` associated with the TS structure and - each of the products will be added to the database. - """ - - def __init__(self): + """ + "\n" + + ReactJob.optional_settings_doc() + "\n" + + ReactJob.general_calculator_settings_docstring() + "\n" + + ReactJob.generated_data_docstring() + "\n" + + ReactJob.required_packages_docstring() + ) + + def __init__(self) -> None: super().__init__() self.name = "Scine double ended transition state optimization from b-splines" self.exploration_key = "bspline" @@ -149,11 +112,11 @@ def __init__(self): "tsopt": tsopt_defaults, "irc": irc_defaults, "ircopt": ircopt_defaults, - "opt": opt_defaults, + self.opt_key: opt_defaults, } @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: import scine_readuct as readuct import scine_molassembler as masm @@ -313,9 +276,9 @@ def __check_barrierless_reactions(self, settings_manager: SettingsManager, opt_n p_fragments, p_graph, p_charges, p_multi, _ = self.__set_up_calculator( p_structure, settings_manager, p_name) # check graph of spline ends - opt_r_fragments, opt_r_graph, opt_r_charges, opt_r_multiplicities, _ =\ + opt_r_fragments, opt_r_graph, opt_r_charges, opt_r_multiplicities, _ = \ self.get_graph_charges_multiplicities(opt_name_reactant, charge) - opt_p_fragments, opt_p_graph, opt_p_charges, opt_p_multiplicities, _ =\ + opt_p_fragments, opt_p_graph, opt_p_charges, opt_p_multiplicities, _ = \ self.get_graph_charges_multiplicities(opt_name_product, charge) # create structures for optimized ends if ";" in opt_r_graph: @@ -356,14 +319,53 @@ def __prepare_structures(self, settings_manager, reactant_structure, products_st Optimize the input structures + generate graphs. """ # optimize spline ends - opt_name_reactant = self.optimize_structures("opt_reactant", [reactant_structure.get_atoms()], - [reactant_structure.get_charge()], - [reactant_structure.get_multiplicity()], - deepcopy(settings_manager.calculator_settings.as_dict())) - opt_name_product = self.optimize_structures("opt_product", [products_structure.get_atoms()], - [products_structure.get_charge()], - [products_structure.get_multiplicity()], - deepcopy(settings_manager.calculator_settings.as_dict())) + """ Reactant """ + opt_name_reactant, self.systems = self.optimize_structures("opt_reactant", self.systems, + [reactant_structure.get_atoms()], + [reactant_structure.get_charge()], + [reactant_structure.get_multiplicity()], + deepcopy( + settings_manager.calculator_settings.as_dict())) + if len(opt_name_reactant) != 1: + self.raise_named_exception("The optimization of the reactant structure failed.") + raise RuntimeError("Unreachable") + lowest_r_name, _ = self._get_propensity_names_within_range( + opt_name_reactant[0], self.systems, self.settings[self.propensity_key]["energy_range_to_optimize"] + ) + if lowest_r_name is None: + self.raise_named_exception("No reactant optimization was successful.") + raise RuntimeError("Unreachable") + if lowest_r_name != opt_name_reactant[0]: + sys.stderr.write(f"Warning: Detected a lower energy spin multiplicity of " + f"{self.get_multiplicity(self.get_system(lowest_r_name))} for the reactant.") + opt_name_reactant = [lowest_r_name] + + """ Product """ + opt_name_product, self.systems = self.optimize_structures("opt_product", self.systems, + [products_structure.get_atoms()], + [products_structure.get_charge()], + [products_structure.get_multiplicity()], + deepcopy( + settings_manager.calculator_settings.as_dict())) + if len(opt_name_product) != 1: + self.raise_named_exception("The optimization of the product structure failed.") + raise RuntimeError("Unreachable") + lowest_p_name, _ = self._get_propensity_names_within_range( + opt_name_product[0], self.systems, self.settings[self.propensity_key]["energy_range_to_optimize"] + ) + if lowest_p_name is None: + self.raise_named_exception("No product optimization was successful.") + raise RuntimeError("Unreachable") + if lowest_p_name != opt_name_product[0]: + sys.stderr.write(f"Warning: Detected a lower energy spin multiplicity of " + f"{self.get_multiplicity(self.get_system(lowest_p_name))} for the product.") + opt_name_product = [lowest_p_name] + + if self.get_multiplicity(self.get_system(opt_name_reactant[0])) != \ + self.get_multiplicity(self.get_system(opt_name_product[0])): + self.raise_named_exception("The optimized reactant and product have different spin multiplicities.") + raise RuntimeError("Unreachable") + _, opt_r_graph, opt_r_charges, opt_r_multiplicities, opt_r_decision_list = \ self.get_graph_charges_multiplicities(opt_name_reactant[0], products_structure.get_charge()) _, opt_p_graph, _, _, _ = \ @@ -383,15 +385,18 @@ def __create_complex_or_minimum(self, graph: str, calculator_name: str): """ label = db.Label.MINIMUM_OPTIMIZED if ";" not in graph else db.Label.COMPLEX_OPTIMIZED new_structure = self.create_new_structure(self.systems[calculator_name], label) + if self.ref_structure is None: + self.raise_named_exception("The reference structure is not set.") + raise RuntimeError("Unreachable") # for mypy self.transfer_properties(self.ref_structure, new_structure) bond_orders, self.systems = self.make_bond_orders_from_calc(self.systems, calculator_name) - self.store_energy(self.systems[calculator_name], new_structure) + self.store_energy(self.get_system(calculator_name), new_structure) self.store_bond_orders(bond_orders, new_structure) self.store_property( self._properties, "atomic_charges", "VectorProperty", - self.systems[calculator_name].get_results().atomic_charges, + self.get_system(calculator_name).get_results().atomic_charges, self._calculation.get_model(), self._calculation, new_structure, @@ -409,9 +414,11 @@ def __optimize_and_get_graphs_and_energies(self, fragment_base_name: str, fragme """ Optimize molecular fragments and return their names, graphs, and energies. """ - opt_fragment_names = self.optimize_structures(fragment_base_name, fragments, - charges, multiplicities, - deepcopy(settings_manager.calculator_settings.as_dict())) + opt_fragment_names, self.systems = ( + self.optimize_structures(fragment_base_name, self.systems, fragments, + charges, multiplicities, + deepcopy(settings_manager.calculator_settings.as_dict())) + ) opt_f_graphs = [] fragment_energies = [] for name, charge in zip(opt_fragment_names, charges): @@ -422,7 +429,7 @@ def __optimize_and_get_graphs_and_energies(self, fragment_base_name: str, fragme + self.name ) opt_f_graphs.append(opt_f_graph) - fragment_energies.append(self.systems[name].get_results().energy) + fragment_energies.append(self.get_system(name).get_results().energy) return opt_fragment_names, opt_f_graphs, fragment_energies @@ -453,8 +460,8 @@ def __assert_conserved_atom(self, lhs_names: List[str], rhs_names: List[str]): """ Assert that the number of atoms did not change between the calculators. """ - lhs_atoms = [self.systems[name].structure for name in lhs_names] - rhs_atoms = [self.systems[name].structure for name in rhs_names] + lhs_atoms = [self.get_system(name).structure for name in lhs_names] + rhs_atoms = [self.get_system(name).structure for name in rhs_names] lhs_counts = self.__get_elements_in_atom_collections(lhs_atoms) rhs_counts = self.__get_elements_in_atom_collections(rhs_atoms) print("Atom counts lhs", lhs_counts) @@ -463,7 +470,7 @@ def __assert_conserved_atom(self, lhs_names: List[str], rhs_names: List[str]): raise RuntimeError("Error: Non stoichiometric elementary step detected. The structures are likely wrong.") @staticmethod - def __get_elements_in_atom_collections(atom_collections): + def __get_elements_in_atom_collections(atom_collections: List[utils.AtomCollection]) -> Dict[str, int]: """ Builds a dictionary containing the element symbols and the number of their occurrence in a given atom collection. @@ -496,8 +503,9 @@ def __get_sorted_graphs_charges_multiplicities(self, names_one: List[str]): multies_one = [] graphs_one = [] for name in names_one: - charges_one.append(self.systems[name].settings[utils.settings_names.molecular_charge]) - multies_one.append(self.systems[name].settings[utils.settings_names.spin_multiplicity]) + system = self.get_system(name) + charges_one.append(system.settings[utils.settings_names.molecular_charge]) + multies_one.append(system.settings[utils.settings_names.spin_multiplicity]) graphs_one.append(self.make_graph_from_calc(self.systems, name)[0]) graphs, charges, multiplicities = ( list(start_val) diff --git a/scine_puffin/jobs/scine_conceptual_dft.py b/scine_puffin/jobs/scine_conceptual_dft.py index 76f723a..50a64ca 100644 --- a/scine_puffin/jobs/scine_conceptual_dft.py +++ b/scine_puffin/jobs/scine_conceptual_dft.py @@ -1,18 +1,29 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ + from copy import deepcopy +from typing import TYPE_CHECKING, List + +import numpy as np from scine_puffin.config import Configuration from scine_puffin.utilities import scine_helper from .templates.job import calculation_context, job_configuration_wrapper from .templates.scine_job import ScineJob +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class ScineConceptualDft(ScineJob): - """ + __doc__ = (""" A job calculating conceptual DFT properties for a given structure with a given model. The properties are extracted from the atomic charges and energies of the given structure, as well the structure with the same geometry but @@ -27,35 +38,24 @@ class ScineConceptualDft(ScineJob): any ``Calculation`` stored in a SCINE Database. Possible settings for this job are: - spin_multiplicity_plus :: int + spin_multiplicity_plus : int The spin multiplicity of the system with one additional electron. If not specified, the additional electron is assumed to pair up with any priorly existing unpaired electrons. I.e. the multiplicity is assumed to decrease by one for all open-shell structures and to be two if the start structure is closed-shell. - spin_multiplicity_minus :: int + spin_multiplicity_minus : int The spin multiplicity of the system with one electron less. If not specified, the deducted electron is assumed to be an unpaired one with the others not rearranging. I.e. the multiplicity is assumed to decrease by one for all open-shell structures and to be two if the start structure is closed-shell. - All settings that are recognized by the program chosen. - Furthermore, all settings that are commonly understood by any program - interface via the SCINE Calculator interface. - - Common examples are: - - max_scf_iterations :: int - The number of allowed SCF cycles until convergence. - - **Required Packages** - - SCINE: Database (present by default) - - SCINE: Readuct (present by default) - - SCINE: Utils (present by default) - - A program implementing the SCINE Calculator interface, e.g. Sparrow - - **Generated Data** + """ + "\n" + + ScineJob.optional_settings_doc() + "\n" + + ScineJob.general_calculator_settings_docstring() + "\n" + + ScineJob.generated_data_docstring() + "\n" + + """ If successful the following data will be generated and added to the database: @@ -70,15 +70,16 @@ class ScineConceptualDft(ScineJob): The ``hardness`` associated with the given structure. The ``softness`` associated with the given structure. """ + + ScineJob.required_packages_docstring() + ) - def __init__(self): + def __init__(self) -> None: super().__init__() self.name = "Scine Conceptual DFT Calculation Job" @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: - import scine_database as db import scine_readuct as readuct import scine_utilities as utils @@ -108,13 +109,13 @@ def run(self, manager, calculation, config: Configuration) -> bool: charge_minus = charge + 1 if "spin_multiplicity_plus" in calculation_settings: - multiplicity_plus = calculation_settings["spin_multiplicity_plus"] + multiplicity_plus: int = calculation_settings["spin_multiplicity_plus"] # type: ignore del calculation_settings["spin_multiplicity_plus"] else: multiplicity_plus = 2 if multiplicity == 1 else multiplicity - 1 if "spin_multiplicity_minus" in calculation_settings: - multiplicity_minus = calculation_settings["spin_multiplicity_minus"] + multiplicity_minus: int = calculation_settings["spin_multiplicity_minus"] # type: ignore del calculation_settings["spin_multiplicity_minus"] else: multiplicity_minus = 2 if multiplicity == 1 else multiplicity - 1 @@ -127,7 +128,7 @@ def run(self, manager, calculation, config: Configuration) -> bool: settings_manager.task_settings["require_charges"] = True if program_helper is not None: - program_helper.calculation_preprocessing(systems[keys[0]], calculation_settings) + program_helper.calculation_preprocessing(self.get_calc(keys[0], systems), calculation_settings) # N electron structure/structure of interest print("N ELECTRON CALCULATION") @@ -136,12 +137,13 @@ def run(self, manager, calculation, config: Configuration) -> bool: self.throw_if_not_successful( success, systems, keys, [ "energy", "atomic_charges"], "Single point calculation on N electron system failed.") - energy = systems[keys[0]].get_results().energy - atomic_charges = systems[keys[0]].get_results().atomic_charges + energy = self.get_energy(self.get_calc(keys[0], systems)) + atomic_charges = self.get_calc(keys[0], systems).get_results().atomic_charges + assert atomic_charges # N+1 electron "plus" system print("N+1 ELECTRON CALCULATION") - print("Charge: {:4d} Multiplicity: {:4d}".format(charge_plus, multiplicity_plus)) + print(f"Charge: {charge_plus} Multiplicity: {multiplicity_plus}") plus_calculator_settings = deepcopy(settings_manager.calculator_settings) plus_calculator_settings[utils.settings_names.molecular_charge] = charge_plus plus_calculator_settings[utils.settings_names.spin_multiplicity] = multiplicity_plus @@ -151,12 +153,13 @@ def run(self, manager, calculation, config: Configuration) -> bool: self.throw_if_not_successful( success, systems, ["plus"], [ "energy", "atomic_charges"], "Single point calculation on N+1 electron system failed.") - energy_plus = systems["plus"].get_results().energy - atomic_charges_plus = systems["plus"].get_results().atomic_charges + energy_plus = self.get_energy(self.get_calc("plus", systems)) + atomic_charges_plus = self.get_calc("plus", systems).get_results().atomic_charges + assert atomic_charges_plus # N-1 electron "minus" system print("N-1 ELECTRON CALCULATION") - print("Charge: {:4d} Multiplicity: {:4d}".format(charge_minus, multiplicity_minus)) + print(f"Charge: {charge_minus} Multiplicity: {multiplicity_plus}") minus_calculator_settings = deepcopy(settings_manager.calculator_settings) minus_calculator_settings[utils.settings_names.molecular_charge] = charge_minus minus_calculator_settings[utils.settings_names.spin_multiplicity] = multiplicity_minus @@ -166,13 +169,17 @@ def run(self, manager, calculation, config: Configuration) -> bool: self.throw_if_not_successful( success, systems, ["minus"], [ "energy", "atomic_charges"], "Single point calculation on N-1 electron system failed.") - energy_minus = systems["minus"].get_results().energy - atomic_charges_minus = systems["minus"].get_results().atomic_charges + energy_minus = self.get_energy(self.get_calc("minus", systems)) + atomic_charges_minus = self.get_calc("minus", systems).get_results().atomic_charges + assert atomic_charges_minus # Deduce conceptual DFT properties print("cDFT PROPERTIES") cDFT_container = utils.conceptual_dft.calculate( - energy, atomic_charges, energy_plus, atomic_charges_plus, energy_minus, atomic_charges_minus) + energy, np.asarray(atomic_charges), + energy_plus, np.asarray(atomic_charges_plus), + energy_minus, np.asarray(atomic_charges_minus) + ) # Calculation postprocessing self.verify_connection() @@ -181,7 +188,7 @@ def run(self, manager, calculation, config: Configuration) -> bool: db_results.clear() self._calculation.set_results(db_results) # update model - scine_helper.update_model(systems[keys[0]], self._calculation, self.config) + scine_helper.update_model(self.get_calc(keys[0], systems), self._calculation, self.config) # Store cDFT properties # Localized @@ -281,5 +288,5 @@ def run(self, manager, calculation, config: Configuration) -> bool: return self.postprocess_calculation_context() @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "readuct", "utils"] diff --git a/scine_puffin/jobs/scine_dissociation_cut.py b/scine_puffin/jobs/scine_dissociation_cut.py index a19cb0d..f828b70 100644 --- a/scine_puffin/jobs/scine_dissociation_cut.py +++ b/scine_puffin/jobs/scine_dissociation_cut.py @@ -1,29 +1,66 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from copy import copy +from typing import Any, Dict, List, Tuple, Union, Optional, TYPE_CHECKING +from itertools import combinations_with_replacement, permutations + +import numpy as np + from scine_puffin.config import Configuration from scine_puffin.utilities import masm_helper, scine_helper from scine_puffin.utilities.program_helper import ProgramHelper from .templates.job import breakable, calculation_context, job_configuration_wrapper from .templates.scine_react_job import ReactJob -from copy import copy -from typing import Any, Dict, List, Tuple, Union -from itertools import combinations_with_replacement, permutations -import numpy as np +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") +if module_exists("scine_readuct") or TYPE_CHECKING: + import scine_readuct as readuct +else: + readuct = MissingDependency("scine_readuct") class ScineDissociationCut(ReactJob): - """ + __doc__ = (""" + A job that tries to find a dissociation reaction by cutting one or more bonds in a single molecule and optimizing + the fragments. + + The list of calculations/steps done is the following: + + 1. Split system into multiple fragments. + 2. Optimize the individual fragments in various charge and spin multiplicity states. + 3. Determine lowest dissociation energy and corresponding charge and spin multiplicity states. + 4. Try to find a barrierless reaction by arranging the optimized fragments along the broken bond(s) at the + Van der Waals distances of the bonded atoms. + + **Order Name** + ``scine_dissociation_cut`` + + **Required Input** + The cut bonds have to be defined in the settings handed to the task. + + dissociations : int + This specifies list of indices of atoms pairs whose bonds should be cut. + Pairs are given in a flat list such that indices ``2i`` and ``2i+1`` form a pair. **Optional Settings** Optional settings are read from the ``settings`` field, which is part of any ``Calculation`` stored in a SCINE Database. All possible settings for this job are based on those available in SCINE ReaDuct. For a complete list see the - `ReaDuct manual `_ + `ReaDuct manual `_ Given that this job consists of two separate tasks, it is possible to target each individually with the settings. In order to @@ -42,44 +79,11 @@ class ScineDissociationCut(ReactJob): 1. Single product optimizations ``opt_*`` 2. Optimization of the reactive complex: ``rcopt_*`` - - - The following settings are recognized without a prepending flag: - - add_based_on_distance_connectivity :: bool - Whether to add the connectivity (i.e. add bonds) as derived from - atomic distances when graphs are generated. (default: True) - sub_based_on_distance_connectivity :: bool - Whether to subtract the connectivity (i.e. remove bonds) as derived from - atomic distances when graphs are generated. (default: True) - only_distance_connectivity :: bool - Whether to impose the connectivity solely from distances. (default: False) - spin_propensity_check :: int - The range to check for possible multiplicities for products. A value - of 2 (default) will check triplet and quintet for a singlet - and will check singlet, quintet und septet for triplet. - charge_propensity_check :: int - The range to check for possible charges for products. A value - of 1 (default) will check +1 and -1 charges for both products, - i.e. 6 different geometry optimizations for a single cut bond resulting in two molecules - - Additionally, all settings that are recognized by the chosen SCF program - are also available. These settings are not required to be prepended with - any flag. - - Common examples are: - - max_scf_iterations :: int - The number of allowed SCF cycles until convergence. - - **Required Packages** - - SCINE: Database (present by default) - - SCINE: Molassembler (present by default) - - SCINE: Readuct (present by default) - - SCINE: Utils (present by default) - - A program implementing the SCINE Calculator interface, e.g. Sparrow - - **Generated Data** + """ + "\n" + + ReactJob.optional_settings_doc() + "\n" + + ReactJob.general_calculator_settings_docstring() + "\n" + + ReactJob.generated_data_docstring() + "\n" + + """ If successful the following data will be generated and added to the database: Elementary Steps @@ -93,8 +97,10 @@ class ScineDissociationCut(ReactJob): ``electronic_energy`` (``NumberProperty``) of the given structure and all split products will be provided. """ + + ReactJob.required_packages_docstring() + ) - def __init__(self): + def __init__(self) -> None: super().__init__() self.name = "Scine React Job with bond cutting" opt_defaults: Dict[str, Any] = { @@ -112,15 +118,11 @@ def __init__(self): self.settings[self.rc_opt_system_name] = {**self.settings[self.rc_opt_system_name], **rcopt_defaults} self.diss = 'dissociations' self.charge_propensity = "charge_propensity_check" - self.settings[self.job_key][self.diss]: List[int] = list() + self.settings[self.job_key][self.diss] = list() self.settings[self.job_key][self.charge_propensity] = 1 @job_configuration_wrapper - def run(self, _, calculation, config: Configuration) -> bool: - - import scine_database as db - import scine_readuct as readuct - import scine_utilities as utils + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: # Everything that calls SCINE is enclosed in a try/except block with breakable(calculation_context(self)): @@ -128,149 +130,171 @@ def run(self, _, calculation, config: Configuration) -> bool: if len(calculation.get_structures()) != 1: self.raise_named_exception(f"{self.name} is only implemented for single molecule system.") settings_manager, program_helper = self.reactive_complex_preparations() - if self.settings[self.rc_opt_system_name]['optimizer'].lower() != 'bfgs': - # in case user specified different optimizer, delete default setting - del self.settings[self.rc_opt_system_name]['bfgs_min_iterations'] - dissociations: List[int] = self.settings[self.job_key][self.diss] - if not dissociations: - self.raise_named_exception(f"Bond dissociation information is missing. It has to be " - f"specified in the settings with '{self.diss}'.") - if len(dissociations) % 2 != 0: - self.raise_named_exception(f"Received an uneven number of entries in '{self.diss}', this does not " - f"correspond to the expected format.") - rc_atoms = self.systems[self.rc_key].structure - for d in dissociations: - if d < 0 or d >= len(rc_atoms): - self.raise_named_exception(f"Invalid entry '{d}' in '{self.diss}' for given structure " - f"with '{len(rc_atoms)}' nuclei.") - - """ gather reactant info """ - bond_orders, self.systems = self.make_bond_orders_from_calc(self.systems, self.rc_key) - if not self.expected_results_check(self.systems, self.rc_key, ['energy', 'atomic_charges'])[0]: - self.systems, success = readuct.run_sp_task(self.systems, [self.rc_key]) - self.throw_if_not_successful(success, self.systems, [self.rc_key], ['energy', 'atomic_charges']) - rc_energy = self.systems[self.rc_key].get_results().energy - rc_atomic_charges = self.systems[self.rc_key].get_results().atomic_charges - - """ cut bonds and determine new molecules """ - bond_breaks = [] - for i in range(0, len(dissociations), 2): - lhs = dissociations[i] - rhs = dissociations[i + 1] - bond_breaks.append((lhs, rhs, 0.0)) - mol_result = masm_helper.get_molecules_result( - rc_atoms, - bond_orders, - self.connectivity_settings, - calculation.get_model().periodic_boundaries, - self.surface_indices(db.Structure(calculation.get_structures()[0], self._structures)), - bond_breaks - ) - n_mols = len(mol_result.molecules) - if n_mols == 1: - self.raise_named_exception(f"Failed because the specified dissociations {dissociations} did not " - f"suffice to generate multiple molecules.") - # we split manually, because we need exact index mapping below - split_molecules = [utils.AtomCollection() for _ in range(n_mols)] - super_map: List[Tuple[int, int, int]] = [] - for source, target in enumerate(mol_result.component_map): - # this is the extra information we need - # it tells us which index in the split up molecule the atom has - new_index = len(split_molecules[target]) - split_molecules[target].push_back(rc_atoms[source]) - super_map.append((source, target, new_index)) - - """ Carry out various optimizations """ - propensity_range = self._get_propensity_range() - print(f"Specified dissociations {dissociations} lead to {n_mols} separate molecules") - print(f"Now optimizing each molecule with each a charge difference of {propensity_range} " - f"leading to {n_mols * len(propensity_range)} separate structure optimizations.") - - # get guess charges based on atomic charges of reactant - mol_charges = self._get_charge_per_molecule(super_map, rc_atomic_charges) - - # gather most probable number of electrons and charge per molecule - n_electrons = [] - guessed_charges: List[int] = [] - for i, molecule in enumerate(split_molecules): - electrons = sum(utils.ElementInfo.Z(e) for e in molecule.elements) - n_electrons.append(electrons) - guessed_charges.append(int(round(mol_charges[i]))) - - split_names: Dict[int, List[str]] = {} - product_single_energies: Dict[int, List[Union[float, None]]] = {} - for charge_diff in propensity_range: - # This assumes minimal multiplicity, multiplicities are also checked for other values in calculations - multiplicities = [(nel - charge - charge_diff) % 2 + 1 - for charge, nel in zip(guessed_charges, n_electrons)] - charges = [charge + charge_diff for charge in guessed_charges] - # We allow the optimizations to fail, but if this is the case the calculator becomes None - # The reason is that is reasonable that some charge combinations might not be possible - split_names[charge_diff] = self.optimize_structures(f"split_product_charge_difference_{charge_diff}", - split_molecules, - charges, - multiplicities, - settings_manager.calculator_settings, - stop_on_error=False - ) - # Now be careful due to possible None - energies = [] - for name in split_names[charge_diff]: - calc = self.systems[name] - energies.append(None if calc is None else calc.get_results().energy) - product_single_energies[charge_diff] = energies - - """ Deduce valid product energies """ - lowest_energy, lowest_combination, product_energies = \ - self._determine_diss_energies(product_single_energies, guessed_charges) - - # update model because job will be marked complete - scine_helper.update_model(self.systems[self.rc_key], calculation, self.config) - - """ Print and save results so far """ - self._print_dissociation_energies(product_energies, split_names, rc_energy, guessed_charges) - lowest_rhs_structures = self._save_dissociated_structures(split_names, lowest_combination, - product_single_energies, program_helper) - if lowest_energy < rc_energy: - calculation.set_comment(f"The dissociation(s) {dissociations} has a formal negative electronic " - "dissociation energy. This should be tested for a potential reaction with a " - "barrier") - raise breakable.Break - - """ Barrierless Reaction Check """ - # we need to write a newly generated reactive complex - # we directly overwrite reactive complex in systems map - self._setup_dissociated_reactive_complex(rc_atoms, super_map, bond_breaks, split_names, lowest_combination) - - # we optimize this new reactive complex and see whether we arrive at the original one - rc_opt_graph, _ = self.check_for_barrierless_reaction() - if rc_opt_graph is None: - # this means we got the same graph as in the original structure -> it was successful - print("Barrierless Reaction Found") - graphs = [] - product_names = [] - for mol in range(n_mols): - charge = lowest_combination[mol] - name = split_names[charge][mol] - product_names.append(name) - graphs.append(self.make_graph_from_calc(self.systems, name)[0]) - joined_graph = ";".join(graphs) - print("Barrierless dissociation product graph:") - print(joined_graph) - print("Start Graph:") - print(self.start_graph) - db_results = self._calculation.get_results() - # Save step - new_step = db.ElementaryStep() - new_step.link(self._elementary_steps) - new_step.create(self._calculation.get_structures(), [rhs.id() for rhs in lowest_rhs_structures]) - new_step.set_type(db.ElementaryStepType.BARRIERLESS) - db_results.add_elementary_step(new_step.id()) - self._calculation.set_comment(self.name + ": Barrierless reaction found.") - self._calculation.set_results(self._calculation.get_results() + db_results) + db_results = self._calculation.get_results() + db_results.clear() + self._calculation.set_results(db_results) + self._dissociation_impl(settings_manager, program_helper) return self.postprocess_calculation_context() + def _dissociation_impl(self, settings_manager: scine_helper.SettingsManager, + program_helper: Optional[ProgramHelper]) -> None: + + if self.settings[self.rc_opt_system_name]['optimizer'].lower() != 'bfgs': + # in case user specified different optimizer, delete default setting + del self.settings[self.rc_opt_system_name]['bfgs_min_iterations'] + dissociations: List[int] = self.settings[self.job_key][self.diss] + if not dissociations: + self.raise_named_exception(f"Bond dissociation information is missing. It has to be " + f"specified in the settings with '{self.diss}'.") + if len(dissociations) % 2 != 0: + self.raise_named_exception(f"Received an uneven number of entries in '{self.diss}', this does not " + f"correspond to the expected format.") + rc_atoms = self.get_system(self.rc_key).structure + for d in dissociations: + if d < 0 or d >= len(rc_atoms): + self.raise_named_exception(f"Invalid entry '{d}' in '{self.diss}' for given structure " + f"with '{len(rc_atoms)}' nuclei.") + + """ gather reactant info """ + bond_orders, self.systems = self.make_bond_orders_from_calc(self.systems, self.rc_key) + if not self.expected_results_check(self.systems, [self.rc_key], ['energy', 'atomic_charges'])[0]: + self.systems, success = readuct.run_sp_task(self.systems, [self.rc_key]) + self.throw_if_not_successful(success, self.systems, [self.rc_key], ['energy', 'atomic_charges']) + results = self.get_system(self.rc_key).get_results() + rc_energy = results.energy + rc_atomic_charges = results.atomic_charges + assert rc_atomic_charges is not None + + """ cut bonds and determine new molecules """ + bond_breaks = [] + for i in range(0, len(dissociations), 2): + lhs = dissociations[i] + rhs = dissociations[i + 1] + bond_breaks.append((lhs, rhs, 0.0)) + mol_result = masm_helper.get_molecules_result( + rc_atoms, + bond_orders, + self.connectivity_settings, + self._calculation.get_model().periodic_boundaries, + self.surface_indices(db.Structure(self._calculation.get_structures()[0], self._structures)), + bond_breaks + ) + n_mols = len(mol_result.molecules) + if n_mols == 1: + self.raise_named_exception(f"Failed because the specified dissociations {dissociations} did not " + f"suffice to generate multiple molecules.") + # we split manually, because we need the exact index mapping below + split_molecules = [utils.AtomCollection() for _ in range(n_mols)] + super_map: List[Tuple[int, int, int]] = [] + for source, target in enumerate(mol_result.component_map): + # this is the extra information we need + # it tells us which index in the split up molecule the atom has + new_index = len(split_molecules[target]) + split_molecules[target].push_back(rc_atoms[source]) + super_map.append((source, target, new_index)) + + """ Carry out various optimizations """ + propensity_range = self._get_propensity_range() + print(f"Specified dissociations {dissociations} lead to {n_mols} separate molecules") + print(f"Now optimizing each molecule with each a charge difference of {propensity_range} " + f"leading to {n_mols * len(propensity_range)} separate structure optimizations.") + + # get guess charges based on atomic charges of reactant + mol_charges = self._get_charge_per_molecule(super_map, rc_atomic_charges) + + # gather the most probable number of electrons and charge per molecule + n_electrons = [] + guessed_charges: List[int] = [] + for i, molecule in enumerate(split_molecules): + electrons = sum(utils.ElementInfo.Z(e) for e in molecule.elements) + n_electrons.append(electrons) + guessed_charges.append(int(round(mol_charges[i]))) + + split_names: Dict[int, List[str]] = {} + product_single_energies: Dict[int, List[Union[float, None]]] = {} + for charge_diff in propensity_range: + # This assumes minimal multiplicity, multiplicities are also checked for other values in calculations + multiplicities = [(nel - charge - charge_diff) % 2 + 1 + for charge, nel in zip(guessed_charges, n_electrons)] + charges = [charge + charge_diff for charge in guessed_charges] + # We allow the optimizations to fail, but if this is the case, the calculator becomes None + # The reason is that is reasonable that some charge combinations might not be possible + split_names[charge_diff], self.systems = self.optimize_structures( + f"split_product_charge_difference_{charge_diff}", + self.systems, + split_molecules, + charges, + multiplicities, + settings_manager.calculator_settings, + stop_on_error=False + ) + # now only continue with the lowest-energy spin multiplicity for each charge difference system + for i, name in enumerate(split_names[charge_diff]): + lowest_name, _ = self._get_propensity_names_within_range( + name, self.systems, self.settings[self.propensity_key]["energy_range_to_optimize"] + ) + if lowest_name is not None: + split_names[charge_diff][i] = lowest_name + # Now be careful due to possible None + energies = [] + for name in split_names[charge_diff]: + calc = self.systems[name] + energies.append(None if calc is None else calc.get_results().energy) + product_single_energies[charge_diff] = energies + + """ Deduce valid product energies """ + lowest_energy, lowest_combination, product_energies = \ + self._determine_diss_energies(product_single_energies, guessed_charges) + + # update model because job will be marked complete + scine_helper.update_model(self.get_system(self.rc_key), self._calculation, self.config) + + """ Print and save results so far """ + self._print_dissociation_energies(product_energies, split_names, rc_energy, guessed_charges) + lowest_rhs_structures = self._save_dissociated_structures(split_names, lowest_combination, + product_single_energies, program_helper) + if lowest_energy < rc_energy: + self._calculation.set_comment(f"The dissociation(s) {dissociations} has a formal negative electronic " + "dissociation energy. This should be tested for a potential reaction with a " + "barrier") + raise breakable.Break + + """ Barrierless Reaction Check """ + # we need to write a newly generated reactive complex + # we directly overwrite reactive complex in systems map + self._setup_dissociated_reactive_complex(rc_atoms, super_map, bond_breaks, split_names, lowest_combination) + + # we optimize this new reactive complex and see whether we arrive at the original one + rc_opt_graph, _ = self.check_for_barrierless_reaction() + if rc_opt_graph is None: + # this means we got the same graph as in the original structure -> it was successful + print("Barrierless Reaction Found") + graphs = [] + product_names = [] + for mol in range(n_mols): + charge = lowest_combination[mol] + name = split_names[charge][mol] + product_names.append(name) + graphs.append(self.make_graph_from_calc(self.systems, name)[0]) + joined_graph = ";".join(graphs) + print("Barrierless dissociation product graph:") + print(joined_graph) + print("Start Graph:") + print(self.start_graph) + db_results = self._calculation.get_results() + if self.ref_structure is None: + self.raise_named_exception("Internal error in Dissociation Job") + raise RuntimeError("unreachable") # for linter + # Save step + new_step = db.ElementaryStep() + new_step.link(self._elementary_steps) + new_step.create([self.ref_structure.id()], [rhs.id() for rhs in lowest_rhs_structures]) + new_step.set_type(db.ElementaryStepType.BARRIERLESS) + db_results.add_elementary_step(new_step.id()) + self._calculation.set_comment(self.name + ": Barrierless reaction found.") + self._calculation.set_results(self._calculation.get_results() + db_results) + def _get_propensity_range(self) -> List[int]: propensity_limit = self.settings[self.job_key][self.charge_propensity] if propensity_limit < 0: @@ -278,14 +302,13 @@ def _get_propensity_range(self) -> List[int]: f"but must be a positive number!") return list(range(-propensity_limit, propensity_limit + 1)) + @requires("utilities") def _determine_diss_energies(self, product_single_energies: Dict[int, List[Union[float, None]]], fragment_base_charges: List[int]) \ -> Tuple[float, Tuple[int], Dict[Tuple[int], Union[float, None]]]: - import scine_utilities as utils - propensity_range = self._get_propensity_range() n_mols = len(product_single_energies[0]) - rc_charge: int = self.systems[self.rc_key].settings[utils.settings_names.molecular_charge] + rc_charge = self.get_charge(self.get_system(self.rc_key)) # we can only compare total energies that have a total charge identical to that of the rc, # This gives us the combinations that are possible based on the x charge differences and n molecules valid_combinations = [comb for comb in combinations_with_replacement(propensity_range, n_mols) @@ -322,12 +345,12 @@ def _determine_diss_energies(self, product_single_energies: Dict[int, List[Union raise BaseException # only for linters return lowest_energy, lowest_combination, product_energies + @requires("utilities") def _print_dissociation_energies(self, product_energies: Dict[Tuple[int], Union[float, None]], split_names: Dict[int, List[str]], rc_energy: Union[float, None], fragment_base_charges: List[int]) -> None: - import scine_utilities as utils if rc_energy is None: self.raise_named_exception("Calculation of reactant failed") return # only for linters @@ -342,25 +365,23 @@ def _print_dissociation_energies(self, m_entry: Union[str, List[int]] = "Not converged" e_entry: Union[str, float] = "Not converged" else: - m_entry = [self.systems[split_names[charge][i]].settings[utils.settings_names.spin_multiplicity] + m_entry = [self.get_multiplicity(self.get_system(split_names[charge][i])) for i, charge in enumerate(charge_diffs)] e_entry = (energy - rc_energy) * utils.KJPERMOL_PER_HARTREE c_buffer = " " * (print_lengths[0] - len(c_entry)) m_buffer = " " * (print_lengths[1] - len(str(m_entry))) print(f"{c_entry}{c_buffer}| {m_entry}{m_buffer}| {e_entry}") + @requires("database") def _save_dissociated_structures(self, split_names: Dict[int, List[str]], lowest_combination: Tuple[int], product_single_energies: Dict[int, List[Union[float, None]]], - program_helper: Union[ProgramHelper, None]) -> list: - import scine_database as db - # clear results + program_helper: Union[ProgramHelper, None]) -> List[db.Structure]: db_results = self._calculation.get_results() - db_results.clear() - self._calculation.set_results(db_results) - + if self.ref_structure is None: + self.raise_named_exception("Internal error in Dissociation Job") + raise RuntimeError("unreachable") # for linter # store energy and bond orders for reactive complex, i.e. structure being dissociated - start_structure = db.Structure(self._calculation.get_structures()[0], self._structures) - self.store_energy(self.systems[self.rc_key], start_structure) + self.store_energy(self.get_system(self.rc_key), self.ref_structure) bond_orders, self.systems = self.make_bond_orders_from_calc(self.systems, self.rc_key) self.store_property( self._properties, @@ -369,7 +390,7 @@ def _save_dissociated_structures(self, split_names: Dict[int, List[str]], lowest bond_orders.matrix, self._calculation.get_model(), self._calculation, - start_structure + self.ref_structure ) # save structure we have optimized and sensible energies @@ -381,11 +402,11 @@ def _save_dissociated_structures(self, split_names: Dict[int, List[str]], lowest if energy is None: continue graph, self.systems = self.make_graph_from_calc(self.systems, name) - label = self._determine_new_label_based_on_graph(self.systems[name], graph) - rhs_structure = self.create_new_structure(self.systems[name], label) + label = self._determine_new_label_based_on_graph(self.get_system(name), graph) + rhs_structure = self.create_new_structure(self.get_system(name), label) db_results.add_structure(rhs_structure.id()) self.transfer_properties(self.ref_structure, rhs_structure) - self.store_energy(self.systems[name], rhs_structure) + self.store_energy(self.get_system(name), rhs_structure) bond_orders, self.systems = self.make_bond_orders_from_calc(self.systems, name) self.store_property( self._properties, @@ -415,13 +436,11 @@ def _save_dissociated_structures(self, split_names: Dict[int, List[str]], lowest ) return lowest_rhs_structures + @requires("readuct") def _setup_dissociated_reactive_complex(self, rc_atoms, super_map: List[Tuple[int, int, int]], bond_breaks: List[Tuple[int, int, float]], split_names: Dict[int, List[str]], lowest_combination: Tuple[int], ) -> None: - import scine_readuct as readuct - import scine_utilities as utils - n_mols = np.max(np.array(super_map)[:, 1]) + 1 frankenstein_rc_atoms = copy(rc_atoms) # fit each molecule to the corresponding atoms in the old structure separately @@ -430,7 +449,7 @@ def _setup_dissociated_reactive_complex(self, rc_atoms, super_map: List[Tuple[in for source, mol, mol_position in super_map: charge = lowest_combination[mol] name = split_names[charge][mol] - product = self.systems[name].structure + product = self.get_system(name).structure fitted_products[mol].push_back(product[mol_position]) target_sources[mol].push_back(rc_atoms[source]) for target, product in zip(target_sources, fitted_products): @@ -454,18 +473,24 @@ def _setup_dissociated_reactive_complex(self, rc_atoms, super_map: List[Tuple[in if super_map[i][1] == molecule_to_push: frankenstein_rc_atoms.set_position(i, pos + direction) # overwrite reactive complex in systems - self.systems[self.rc_key].positions = frankenstein_rc_atoms.positions + rc_calc = self.get_system(self.rc_key) + rc_calc.positions = frankenstein_rc_atoms.positions # check if we resemble the charge we expect self.systems, success = readuct.run_single_point_task(self.systems, [self.rc_key], require_charges=True) self.throw_if_not_successful(success, self.systems, [self.rc_key], ['atomic_charges']) - new_rc_charges = self.systems[self.rc_key].get_results().atomic_charges + rc_calc = self.get_system(self.rc_key) + new_rc_charges = rc_calc.get_results().atomic_charges + if new_rc_charges is None: + self.raise_named_exception("Atomic charges are missing for the reactive complex.") + raise RuntimeError("Unreachable") # only for linters + mol_charges = self._get_charge_per_molecule(super_map, new_rc_charges) rounded_mol_charges = [int(round(c)) for c in mol_charges] lowest_combination_charges = [] for mol, charge_diff in enumerate(lowest_combination): name = split_names[charge_diff][mol] - lowest_combination_charges.append(self.systems[name].settings[utils.settings_names.molecular_charge]) + lowest_combination_charges.append(self.get_system(name).settings[utils.settings_names.molecular_charge]) if rounded_mol_charges != lowest_combination_charges: self.raise_named_exception(f"The lowest energy charge separation {lowest_combination_charges} is not " f"present in our dissociated supermolecule, where we have a charge distribution " @@ -473,7 +498,7 @@ def _setup_dissociated_reactive_complex(self, rc_atoms, super_map: List[Tuple[in f"barrier without specialized methods.") @staticmethod - def _get_charge_per_molecule(super_map: List[Tuple[int, int, int]], total_atomic_charges: np.ndarray) \ + def _get_charge_per_molecule(super_map: List[Tuple[int, int, int]], total_atomic_charges: List[float]) \ -> np.ndarray: n_mols = np.max(np.array(super_map)[:, 1]) + 1 mol_charges = np.zeros(n_mols) diff --git a/scine_puffin/jobs/scine_dissociation_cut_with_optimization.py b/scine_puffin/jobs/scine_dissociation_cut_with_optimization.py new file mode 100644 index 0000000..68a4933 --- /dev/null +++ b/scine_puffin/jobs/scine_dissociation_cut_with_optimization.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +import sys +from typing import TYPE_CHECKING, Dict, Optional + +from scine_puffin.config import Configuration +from .templates.job import breakable, calculation_context, job_configuration_wrapper +from ..utilities.task_to_readuct_call import SubTaskToReaductCall +from .scine_dissociation_cut import ScineDissociationCut +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_database") +if module_exists("scine_molassembler") or TYPE_CHECKING: + import scine_molassembler as masm +else: + masm = MissingDependency("scine_database") + + +class ScineDissociationCutWithOptimization(ScineDissociationCut): + """ + Identical to :py:class:`ScineDissociationCut`, but does not assume that the given structure is a minimum, + e.g., because this job is carried out with a different model. + """ + + def __init__(self) -> None: + super().__init__() + self.name = "Scine React Job optimization and bond cutting" + + @job_configuration_wrapper + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: + + # Everything that calls SCINE is enclosed in a try/except block + with (breakable(calculation_context(self))): + """ sanity checks """ + if len(calculation.get_structures()) != 1: + self.raise_named_exception(f"{self.name} is only implemented for single molecule system.") + + # preprocessing of structure + self.ref_structure = db.Structure(calculation.get_structures()[0], self._structures) + settings_manager, program_helper = self.create_helpers(self.ref_structure) + + # Separate the calculation settings from the database into the task and calculator settings + # This overwrites any default settings by user settings + settings_manager.separate_settings(self._calculation.get_settings()) + settings_manager.correct_non_applicable_settings() + settings_manager.calculator_settings[utils.settings_names.molecular_charge] = \ + self.ref_structure.get_charge() + settings_manager.calculator_settings[utils.settings_names.spin_multiplicity] = \ + self.ref_structure.get_multiplicity() + self.sort_settings(settings_manager.task_settings) + """ Setup calculator """ + self.systems: Dict[str, Optional[utils.core.Calculator]] = {} + utils.io.write("system.xyz", self.ref_structure.get_atoms()) + self.systems[self.rc_key] = utils.core.load_system_into_calculator( + "system.xyz", + self._calculation.get_model().method_family, + **settings_manager.calculator_settings, + ) + if program_helper is not None: + program_helper.calculation_preprocessing( + self.get_system(self.rc_key), self._calculation.get_settings()) + + # Calculate bond orders and graph of reactive complex and compare to database graph of start structures + self.start_graph, self.systems = self.make_graph_from_calc(self.systems, self.rc_key) + + # optimize + self.systems = self.observed_readuct_call_with_throw(SubTaskToReaductCall.OPT, self.systems, + [self.rc_key], ['energy'], + "Optimization of initial structure failed.", + **self.settings[self.rc_opt_system_name]) + opt_graph, self.systems = self.make_graph_from_calc(self.systems, self.rc_opt_system_name) + if ";" in opt_graph: + self.raise_named_exception("Structure decomposed in optimization.") + calc = self.get_system(self.rc_opt_system_name) + self.ref_structure = db.Structure.make(calc.structure, + self.get_charge(calc), + self.get_multiplicity(calc), + self._calculation.get_model(), self.ref_structure.get_label(), + self._structures) + self.ref_structure.add_calculation(self._calculation.get_job().order, self._calculation.id()) + db_results = self._calculation.get_results() + db_results.clear() + db_results.add_structure(self.ref_structure.id()) + self._calculation.set_results(db_results) + if not masm.JsonSerialization.equal_molecules(self.start_graph, opt_graph): + sys.stderr.write("Warning: Graphs of optimized and initial structure differ.\n") + self.start_graph = opt_graph + + self._dissociation_impl(settings_manager, program_helper) + + return self.postprocess_calculation_context() diff --git a/scine_puffin/jobs/scine_geometry_optimization.py b/scine_puffin/jobs/scine_geometry_optimization.py index ea56da8..3bd88c0 100644 --- a/scine_puffin/jobs/scine_geometry_optimization.py +++ b/scine_puffin/jobs/scine_geometry_optimization.py @@ -1,17 +1,27 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +import sys +from typing import TYPE_CHECKING + from scine_puffin.config import Configuration from .templates.job import calculation_context, job_configuration_wrapper from .templates.scine_optimization_job import OptimizationJob -from .templates.scine_connectivity_job import ConnectivityJob +from .templates.scine_propensity_job import ScinePropensityJob +from scine_puffin.utilities.imports import module_exists, MissingDependency +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") -class ScineGeometryOptimization(OptimizationJob, ConnectivityJob): - """ + +class ScineGeometryOptimization(OptimizationJob, ScinePropensityJob): + __doc__ = (""" A job optimizing the geometry of a given structure, in search of a local minimum on the potential energy surface. Optimizing a given structure's geometry, generating a new minimum energy @@ -25,48 +35,40 @@ class ScineGeometryOptimization(OptimizationJob, ConnectivityJob): any ``Calculation`` stored in a SCINE Database. Possible settings for this job are: - All settings recognized by ReaDuct's geometry optimization task. + All settings recognized by ReaDuct's geometry optimization task, which must be prepended by ``opt`` prefix. Common examples are: - optimizer :: str + opt_optimizer : str The name of the optimizer to be used, e.g. 'bfgs', 'lbfgs', 'nr' or 'sd'. - convergence_max_iterations :: int + opt_convergence_max_iterations : int The maximum number of geometry optimization cycles. - convergence_delta_value :: float + opt_convergence_delta_value : float The convergence criterion for the electronic energy difference between two steps. - convergence_gradient_max_coefficient :: float + opt_convergence_gradient_max_coefficient : float The convergence criterion for the maximum absolute gradient. contribution. - convergence_step_rms :: float + opt_convergence_step_rms : float The convergence criterion for root mean square of the geometric gradient. - convergence_step_max_coefficient :: float + opt_convergence_step_max_coefficient : float The convergence criterion for the maximum absolute coefficient in the last step taken in the geometry optimization. - convergence_gradient_rms :: float + opt_convergence_gradient_rms : float The convergence criterion for root mean square of the last step taken in the geometry optimization. For a complete list see the - `ReaDuct manual `_ + `ReaDuct manual `_ - All settings that are recognized by the SCF program chosen. - - Common examples are: + """ + "\n" + + OptimizationJob.optional_settings_doc() + "\n" + ScinePropensityJob.optional_settings_doc() + "\n" + + ScinePropensityJob.general_calculator_settings_docstring() + "\n" + + ScinePropensityJob.generated_data_docstring() + "\n" + + """ - max_scf_iterations :: int - The number of allowed SCF cycles until convergence. - - **Required Packages** - - SCINE: Database (present by default) - - SCINE: Readuct (present by default) - - SCINE: Utils (present by default) - - A program implementing the SCINE Calculator interface, e.g. Sparrow - - **Generated Data** If successful the following data will be generated and added to the database: @@ -75,61 +77,94 @@ class ScineGeometryOptimization(OptimizationJob, ConnectivityJob): Properties The ``electronic_energy`` associated with the new structure. """ + + ScinePropensityJob.required_packages_docstring() + ) - def __init__(self): + def __init__(self) -> None: super().__init__() self.name = "Scine Geometry Optimization" + self.settings = { + **self.settings, + self.propensity_key: { + **self.settings[self.propensity_key], + "check_for_unimolecular_reaction": True, + "energy_range_to_save": 100.0, + "optimize_all": False, + "energy_range_to_optimize": 250.0, + "check": 0, + } + } @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - self.run_geometry_optimization(calculation, config) - return self.postprocess_calculation_context() - - def run_geometry_optimization(self, calculation, config): - import scine_database as db - import scine_readuct as readuct + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: from scine_utilities import settings_names as sn - # preprocessing of structure structure = db.Structure(calculation.get_structures()[0], self._structures) settings_manager, program_helper = self.create_helpers(structure) - # actual calculation with calculation_context(self): + """ preparation """ + if len(calculation.get_structures()) > 1: + raise RuntimeError(self.name + " is only meant for a single structure!") + settings_manager.separate_settings(self._calculation.get_settings()) + self.sort_settings(settings_manager.task_settings) + systems, keys = settings_manager.prepare_readuct_task( structure, calculation, calculation.get_settings(), config["resources"] ) if program_helper is not None: - program_helper.calculation_preprocessing(systems[keys[0]], calculation.get_settings()) - optimize_cell: bool = "unitcelloptimizer" in settings_manager.task_settings \ - and settings_manager.task_settings["unitcelloptimizer"] - systems, success = readuct.run_opt_task(systems, keys, **settings_manager.task_settings) - + program_helper.calculation_preprocessing(self.get_calc(keys[0], systems), calculation.get_settings()) + + optimize_cell: bool = "unitcelloptimizer" in self.settings[self.opt_key] \ + and len(self.settings[self.opt_key]["unitcelloptimizer"]) > 0 + opt_names, systems = self.optimize_structures( + "system", + systems, + [structure.get_atoms()], + [structure.get_charge()], + [structure.get_multiplicity()], + settings_manager.calculator_settings, + ) + if len(opt_names) != 1: + self.raise_named_exception("Optimization of the structure yielded multiple structures, " + "which is not expected") + lowest_name, _ = self._get_propensity_names_within_range( + opt_names[0], systems, self.settings[self.propensity_key]["energy_range_to_optimize"] + ) + if lowest_name is None: + self.raise_named_exception("No optimization was successful.") + raise RuntimeError("Unreachable") + if lowest_name != opt_names[0]: + sys.stderr.write(f"Warning: Specified the spin multiplicity '{structure.get_multiplicity()}', but " + f"the system reached a lower energy with the spin multiplicity " + f"'{self.get_multiplicity(self.get_calc(lowest_name, systems))}'.\n" + f"Continuing with the latter.\n") + opt_names[0] = lowest_name + + opt_calc = self.get_calc(opt_names[0], systems) if optimize_cell: # require to change the calculator settings, to avoid model completion failure model = calculation.get_model() old_pbc = model.periodic_boundaries - new_pbc = systems[keys[0]].settings[sn.periodic_boundaries] - systems[keys[0]].settings[sn.periodic_boundaries] = old_pbc + new_pbc = opt_calc.settings[sn.periodic_boundaries] + opt_calc.settings[sn.periodic_boundaries] = old_pbc # Graph generation - if success: - graph, systems = self.make_graph_from_calc(systems, keys[0]) - new_label = self.determine_new_label(structure, graph) - else: - new_label = db.Label.IRRELEVANT + graph, systems = self.make_graph_from_calc(systems, opt_names[0]) + old_label = structure.get_label() + new_label = self.determine_new_label(old_label, graph, structure.has_property("surface_atom_indices")) - if graph: - structure.set_graph("masm_cbor_graph", graph) - - t = self.optimization_postprocessing( - success, systems, keys, structure, new_label, program_helper + new_structure = self.optimization_postprocessing( + True, systems, opt_names, structure, new_label, program_helper ) + if graph: + new_structure.set_graph("masm_cbor_graph", graph) if optimize_cell: + assert isinstance(new_pbc, str) # update model of new structure to match the optimized unit cell - new_structure = db.Structure(calculation.get_results().structure_ids[0], self._structures) model = new_structure.get_model() model.periodic_boundaries = new_pbc new_structure.set_model(model) - return t + + return self.postprocess_calculation_context() diff --git a/scine_puffin/jobs/scine_geometry_validation.py b/scine_puffin/jobs/scine_geometry_validation.py index 519c3a9..fd87cb6 100644 --- a/scine_puffin/jobs/scine_geometry_validation.py +++ b/scine_puffin/jobs/scine_geometry_validation.py @@ -1,32 +1,47 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -import numpy as np +from typing import Any, TYPE_CHECKING, Tuple, Dict, List, Optional import sys +import numpy as np + from scine_puffin.config import Configuration from .templates.job import breakable, calculation_context, job_configuration_wrapper -from .templates.scine_react_job import HessianJob, OptimizationJob, ConnectivityJob - -# TODO: Guess this should inherit from a template - - -class ScineGeometryValidation(HessianJob, OptimizationJob, ConnectivityJob): - - def __init__(self): +from .templates.scine_hessian_job import HessianJob +from .templates.scine_optimization_job import OptimizationJob +from .templates.scine_connectivity_job import ConnectivityJob +from .templates.scine_job_with_observers import ScineJobWithObservers +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency +from scine_puffin.utilities.scine_helper import SettingsManager +from scine_puffin.utilities.task_to_readuct_call import SubTaskToReaductCall + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_database") + + +class ScineGeometryValidation(ScineJobWithObservers, HessianJob, OptimizationJob, ConnectivityJob): + + def __init__(self) -> None: super().__init__() self.name = "Scine Geometry Validation Job" self.validation_key = "val" self.opt_key = "opt" - self.job_key = self.validation_key val_defaults = { "imaginary_wavenumber_threshold": 0.0, "fix_distortion_step_size": -1.0, - "distortion_inversion_point": 2.0, + "distortion_inversion_point": 0.2, "optimization_attempts": 0, } opt_defaults = { @@ -35,22 +50,22 @@ def __init__(self): "geoopt_coordinate_system": "cartesianWithoutRotTrans" } - self.settings = { - "val": val_defaults, - "opt": opt_defaults, + self.settings: Dict[str, Dict[str, Any]] = { + **self.settings, + self.validation_key: val_defaults, + self.opt_key: opt_defaults, } self.start_graph = "" self.start_key = "" self.end_graph = "" self.end_key = "" - self.systems = {} - self.inputs = [] + self.systems: Dict[str, Optional[utils.core.Calculator]] = {} + self.inputs: List[str] = [] self.optimization_attempts_count = 0 @job_configuration_wrapper - def run(self, _, calculation, config: Configuration) -> bool: + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: - import scine_database as db import scine_readuct as readuct import scine_molassembler as masm @@ -73,7 +88,8 @@ def run(self, _, calculation, config: Configuration) -> bool: self.start_graph = structure.get_graph("masm_cbor_graph") if program_helper is not None: - program_helper.calculation_preprocessing(self.systems[self.start_key], calculation.get_settings()) + program_helper.calculation_preprocessing(self.get_calc(self.start_key, self.systems), + calculation.get_settings()) # # # # Extract Job settings self.sort_settings(settings_manager.task_settings) @@ -83,6 +99,7 @@ def run(self, _, calculation, config: Configuration) -> bool: opt_success = True clear_to_write = False self.end_key = self.start_key + exception_message = "" # Enter Run Loop for number of allowed attempts while self.optimization_attempts_count <= self.settings[self.validation_key]['optimization_attempts']: @@ -97,14 +114,14 @@ def run(self, _, calculation, config: Configuration) -> bool: "Hessian calculation failed.\n", ) # Process hessian calculation - _ = self.calculation_postprocessing(success, self.systems, self.inputs, [ - "energy", "hessian", "thermochemistry"]) + self.calculation_postprocessing(success, self.systems, self.inputs, [ + "energy", "hessian", "thermochemistry"]) # Frequency check - hessian_results = self.systems[self.end_key].get_results() + hessian_results = self.get_calc(self.end_key, self.systems).get_results() false_minimum, mode_container = self.has_wavenumber_below_threshold( hessian_results, - self.systems[self.end_key].structure, + self.get_calc(self.end_key, self.systems).structure, self.settings[self.validation_key]["imaginary_wavenumber_threshold"] ) @@ -112,8 +129,9 @@ def run(self, _, calculation, config: Configuration) -> bool: """ SP JOB """ # Copy calculator and delete its previous results end_sp_key = self.end_key + "_sp" - self.systems[end_sp_key] = self.systems[self.end_key].clone() - self.systems[end_sp_key].delete_results() + end_calc = self.get_calc(self.end_key, self.systems).clone() + end_calc.delete_results() + self.systems[end_sp_key] = end_calc # Check graph self.end_graph, self.systems = self.make_graph_from_calc(self.systems, end_sp_key) @@ -124,7 +142,7 @@ def run(self, _, calculation, config: Configuration) -> bool: print(self.end_graph) # Compare start and end graph if not masm.JsonSerialization.equal_molecules(self.start_graph, self.end_graph): - self._calculation.set_comment(self.name + ": End structure does not match starting structure.") + exception_message += "Final structure does not match starting structure. " clear_to_write = False else: clear_to_write = True @@ -148,8 +166,8 @@ def run(self, _, calculation, config: Configuration) -> bool: end_opt_key = "distorted_opt_" + str(self.optimization_attempts_count) self.settings[self.opt_key]["output"] = [end_opt_key] # # # Optimization, per default stop on error is false - self.systems, opt_success = readuct.run_opt_task( - self.systems, self.inputs, **self.settings[self.opt_key]) + self.systems, opt_success = self.observed_readuct_call( + SubTaskToReaductCall.OPT, self.systems, self.inputs, **self.settings[self.opt_key]) # Update inputs and end key for next round self.inputs = self.settings[self.opt_key]["output"] self.end_key = self.inputs[0] @@ -164,41 +182,47 @@ def run(self, _, calculation, config: Configuration) -> bool: self.verify_connection() if clear_to_write: - final_sp_results = self.systems[end_sp_key].get_results() + final_sp_results = self.get_calc(end_sp_key, self.systems).get_results() + if final_sp_results.bond_orders is None: + self.raise_named_exception("No bond orders found in results.") + raise RuntimeError("Unreachable") # for linters # # # Store Energy and Bond Orders overwrites existing results of identical model - self.store_energy(self.systems[end_sp_key], structure) + self.store_energy(self.get_calc(end_sp_key, self.systems), structure) self.store_property(self._properties, "bond_orders", "SparseMatrixProperty", final_sp_results.bond_orders.matrix, self._calculation.get_model(), self._calculation, structure) # Store hessian information - self.store_hessian_data(self.systems[self.end_key], structure) + self.store_hessian_data(self.get_calc(self.end_key, self.systems), structure) # Only overwrite positions, if an optimization was attempted if self.optimization_attempts_count != 0: # Overwrite positions org_atoms = structure.get_atoms() - position_shift = self.systems[self.end_key].structure.positions - org_atoms.positions + position_shift = self.get_calc(self.end_key, self.systems).structure.positions - org_atoms.positions # # # Store Position Shift self.store_property(self._properties, "position_shift", "DenseMatrixProperty", position_shift, self._calculation.get_model(), self._calculation, structure) - structure.set_atoms(self.systems[self.end_key].structure) + structure.set_atoms(self.get_calc(self.end_key, self.systems).structure) # # # Overwrite graph if structure has changed, decision list and idx map might have changed self.add_graph(structure, final_sp_results.bond_orders) else: self.store_hessian_data(self.systems[self.start_key], structure) self.capture_raw_output() self.raise_named_exception( + exception_message + "Structure could not be validated to be a minimum. Hessian information is stored anyway." ) return self.postprocess_calculation_context() - @staticmethod - # TODO: add proper typing - def has_wavenumber_below_threshold(calc_results, atoms, wavenumber_threshold: float): - import scine_utilities as utils - true_minimum = False + @requires("utilities") + def has_wavenumber_below_threshold(self, calc_results: utils.Results, atoms: utils.AtomCollection, + wavenumber_threshold: float) -> Tuple[bool, utils.normal_modes.container]: + false_minimum = False + if calc_results.hessian is None: + self.raise_named_exception("Results are missing a Hessian") + raise RuntimeError("Unreachable") # for linters # Get normal modes and frequencies modes_container = utils.normal_modes.calculate(calc_results.hessian, atoms.elements, atoms.positions) # Wavenumbers in cm-1 @@ -206,35 +230,35 @@ def has_wavenumber_below_threshold(calc_results, atoms, wavenumber_threshold: fl # Get minimal frequency min_wavenumber = np.min(wavenumbers) if min_wavenumber < 0.0 and abs(min_wavenumber) > wavenumber_threshold: - true_minimum = True + false_minimum = True - return true_minimum, modes_container + return false_minimum, modes_container - def _distort_structure_and_load_calculator(self, mode_container, settings_manager): - import scine_utilities as utils + @requires("utilities") + def _distort_structure_and_load_calculator(self, mode_container: utils.normal_modes.container, + settings_manager: SettingsManager) -> None: wavenumbers = np.asarray(mode_container.get_wave_numbers()) img_wavenumber_indices = np.where(wavenumbers < 0.0)[0] modes = [utils.normal_modes.mode(wavenumbers[i], mode_container.get_mode(i)) for i in img_wavenumber_indices] + max_steps = [utils.normal_modes.get_harmonic_inversion_point( + wavenumbers[i], self.settings[self.validation_key]['distortion_inversion_point']) + for i in img_wavenumber_indices] # Distortion according to inversion point - if self.settings[self.job_key]['fix_distortion_step_size'] == -1.0: - max_steps = [utils.normal_modes.get_harmonic_inversion_point( - wavenumbers[i], self.settings[self.job_key]['distortion_inversion_point']) - for i in img_wavenumber_indices] - else: - max_steps = [self.settings[self.job_key]['fix_distortion_step_size'] * len(modes)] - + if self.settings[self.validation_key]['fix_distortion_step_size'] != -1.0: + max_steps = np.array(max_steps) / np.max(max_steps) * \ + self.settings[self.validation_key]['fix_distortion_step_size'] # Only one direction, could be improved by distorting in other direction # # # Displace along modes with img wavenumbers and load calculator distorted_positions = utils.geometry.displace_along_modes( - self.systems[self.end_key].structure.positions, + self.get_calc(self.end_key, self.systems).structure.positions, modes, max_steps) distorted_key = "distorted_guess_" + str(self.optimization_attempts_count) xyz_name = distorted_key + ".xyz" # Write file and load into calculator distorted_atoms = utils.AtomCollection( - self.systems[self.end_key].structure.elements, distorted_positions) + self.get_calc(self.end_key, self.systems).structure.elements, distorted_positions) utils.io.write(xyz_name, distorted_atoms) distorted_calculator = utils.core.load_system_into_calculator( xyz_name, @@ -244,35 +268,3 @@ def _distort_structure_and_load_calculator(self, mode_container, settings_manage # Load into systems and update inputs for next step self.systems[distorted_key] = distorted_calculator self.inputs = [distorted_key] - - def sort_settings(self, task_settings: dict): - """ - Take settings of configured calculation and save them in class member. Throw exception for unknown settings. - - Notes - ----- - * Requires run configuration - * May throw exception - - Parameters - ---------- - task_settings :: dict - A dictionary from which the settings are taken - """ - self.extract_connectivity_settings_from_dict(task_settings) - # Dissect settings into individual user task_settings - for key, value in task_settings.items(): - for task in self.settings.keys(): - if task == self.job_key: - if key in self.settings[task].keys(): - self.settings[task][key] = value - break # found right task, leave inner loop - else: - indicator_length = len(task) + 1 # underscore to avoid ambiguities - if key[:indicator_length] == task + "_": - self.settings[task][key[indicator_length:]] = value - break # found right task, leave inner loop - else: - self.raise_named_exception( - "The key '{}' was not recognized.".format(key) - ) diff --git a/scine_puffin/jobs/scine_hessian.py b/scine_puffin/jobs/scine_hessian.py index 67c2ea2..38bec7a 100644 --- a/scine_puffin/jobs/scine_hessian.py +++ b/scine_puffin/jobs/scine_hessian.py @@ -1,12 +1,21 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING + from scine_puffin.config import Configuration from .templates.job import calculation_context, job_configuration_wrapper from .templates.scine_hessian_job import HessianJob +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class ScineHessian(HessianJob): @@ -25,13 +34,13 @@ class ScineHessian(HessianJob): All settings recognized by ReaDuct's Hessian task. For a complete list see the - `ReaDuct manual `_ + `ReaDuct manual `_ All settings that are recognized by the SCF program chosen. Common examples are: - max_scf_iterations :: int + max_scf_iterations : int The number of allowed SCF cycles until convergence. **Required Packages** @@ -53,13 +62,12 @@ class ScineHessian(HessianJob): is present in the results of provided by the calculator interface. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.name = "Scine Hessian Job" @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - import scine_database as db + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: import scine_readuct as readuct # preprocessing of structure @@ -77,7 +85,7 @@ def run(self, manager, calculation, config: Configuration) -> bool: structure, calculation, calculation.get_settings(), config["resources"] ) if program_helper is not None: - program_helper.calculation_preprocessing(systems[keys[0]], calculation.get_settings()) + program_helper.calculation_preprocessing(self.get_calc(keys[0], systems), calculation.get_settings()) systems, success = readuct.run_hessian_task(systems, keys, **settings_manager.task_settings) self.sp_postprocessing(success, systems, keys, structure, program_helper) diff --git a/scine_puffin/jobs/scine_irc_scan.py b/scine_puffin/jobs/scine_irc_scan.py index 60c6b73..f8ec78e 100644 --- a/scine_puffin/jobs/scine_irc_scan.py +++ b/scine_puffin/jobs/scine_irc_scan.py @@ -1,13 +1,22 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING + from ..utilities import scine_helper from scine_puffin.config import Configuration from .templates.job import calculation_context, job_configuration_wrapper from .templates.scine_optimization_job import OptimizationJob +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class ScineIrcScan(OptimizationJob): @@ -24,17 +33,17 @@ class ScineIrcScan(OptimizationJob): All settings recognized by ReaDuct's IRC task. For a complete list see the - `ReaDuct manual `_ + `ReaDuct manual `_ Common examples are: - stop_on_error :: bool + stop_on_error : bool If ``False``, the optimization does not need to fully converge but will be accepted as a success even if it reaches the maximum amounts of optimization cycles. Also, the resulting structures will be flagged as ``minimum_guess`` if this option is set ot be ``False``. (Default: ``True``) - irc_mode :: int + irc_mode : int The mode to follow during the IRC scan. By default, the first mode (0). (mode with the larges imaginary frequency will be followed). @@ -42,7 +51,7 @@ class ScineIrcScan(OptimizationJob): Common examples are: - max_scf_iterations :: int + max_scf_iterations : int The number of allowed SCF cycles until convergence. **Required Packages** @@ -65,14 +74,12 @@ class ScineIrcScan(OptimizationJob): structures. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.name = "Scine IRC Job" @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - - import scine_database as db + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: import scine_readuct as readuct # Get structure @@ -85,7 +92,7 @@ def run(self, manager, calculation, config: Configuration) -> bool: structure, calculation, calculation.get_settings(), config["resources"] ) if program_helper is not None: - program_helper.calculation_preprocessing(systems[keys[0]], calculation.get_settings()) + program_helper.calculation_preprocessing(self.get_calc(keys[0], systems), calculation.get_settings()) # Task specific operation settings_manager.task_settings["output"] = ["forward", "backward"] stop_on_error = settings_manager.task_settings.get("stop_on_error", True) @@ -95,7 +102,7 @@ def run(self, manager, calculation, config: Configuration) -> bool: results_check, results_err = self.expected_results_check(systems, ["forward", "backward"]) if not results_check: self.raise_named_exception(results_err) - scine_helper.update_model(systems[keys[0]], self._calculation, config) + scine_helper.update_model(self.get_calc(keys[0], systems), self._calculation, config) is_surface = structure.has_property("surface_atom_indices") label = db.Label.SURFACE_OPTIMIZED if is_surface else db.Label.MINIMUM_OPTIMIZED @@ -118,7 +125,7 @@ def run(self, manager, calculation, config: Configuration) -> bool: db_results.add_structure(new_structure.id()) # properties - self.store_energy(systems[name], new_structure) + self.store_energy(self.get_calc(name, systems), new_structure) self.transfer_properties(structure, new_structure) if program_helper is not None: diff --git a/scine_puffin/jobs/scine_qm_region_selection.py b/scine_puffin/jobs/scine_qm_region_selection.py new file mode 100644 index 0000000..958d956 --- /dev/null +++ b/scine_puffin/jobs/scine_qm_region_selection.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +from typing import List, TYPE_CHECKING + +from scine_puffin.config import Configuration +from scine_puffin.jobs.templates.job import calculation_context, job_configuration_wrapper +from scine_puffin.jobs.templates.scine_job import ScineJob +from scine_puffin.utilities.qm_mm_settings import prepare_optional_settings, is_qm_mm +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") + + +class ScineQmRegionSelection(ScineJob): + """ + This job implements the QM region selection presented in J. Chem. Theory Comput. 2021, 17, 3797-3813. + In this approach, the QM region is selected such that an error in the forces acting on a manually selected + set of atoms is minimized. For this purpose, a reference calculation for a very large QM region is run, then + the QM region is expanded systematically to generate a set of model systems. For these model systems, we calculate + the differences to the reference forces and select the smallest model system that is within 20% of the smallest + error of all model systems. + + The calculation requires the presence of bond orders ('bond_orders') and (optionally) atomic + charges ('atomic_charges'). Upon job completion, the "optimal" QM region is saved as a property ('qm_atoms'). + + **Order Name** + ``scine_qm_region_selection`` + + **Optional Settings** + Optional settings are read from the ``settings`` field, which is part of + any ``Calculation`` stored in a SCINE Database. + Possible settings for this job are: + + All settings that are recognized by the program chosen. + Furthermore, all settings that are commonly understood by any program + interface via the SCINE Calculator interface. + + Common examples are: + + electrostatic_embedding : bool + Use electrostatic embedding. + qm_region_center_atoms : List[int] + The indices of the atoms for which the forces are converged. + initial_radius : float + The radius of the smallest/initial QM region around the selected atoms. + cutting_probability : float + A parameter that controls the random construction of QM regions, controlling the probability to cut bonds + during the QM region expansion. If this is set to 1.0, the QM region is fixed by the radius and not sampled. + tol_percentage_error : float + Error percentage to tolerate with respect to the smallest error encountered in the candidate QM/MM models. + qm_region_max_size : int + Maximum number of atoms in the QM region. + qm_region_min_size : int + Minimum number of atoms in the QM region. + ref_max_size : int + Maximum number of atoms in the QM region of the reference calculation. + tol_percentage_sym_score : float + Only roughly symmetric QM regions. This score determines the acceptable tolerance. + + + **Required Packages** + - SCINE: Database (present by default) + - SCINE: Utils (present by default) + - SCINE: Swoose + - A program implementing the SCINE Calculator interface, e.g. Sparrow + + **Generated Data** + If successful the following data will be generated and added to the + database: + + Properties + The `qm_atoms` selected by the algorithm. + """ + + def __init__(self) -> None: + super().__init__() + self.name = "Scine QM Region Selection Job" + + @job_configuration_wrapper + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: + + import scine_swoose as swoose + + # preprocessing of structure + structure = db.Structure(calculation.get_structures()[0], self._structures) + settings_manager, program_helper = self.create_helpers(structure) + + # actual calculation + with calculation_context(self): + + if not is_qm_mm(calculation.get_model()): + raise RuntimeError("QM region selection for QM/MM is only possible if the electronic structure model" + "/method family is of type QM/MM. Your method family is: " + + calculation.get_model().method_family) + + prepare_optional_settings(structure, calculation, settings_manager, self._properties, skip_qm_atoms=True) + systems, keys = settings_manager.prepare_readuct_task( + structure, calculation, calculation.get_settings(), config["resources"] + ) + calculator = self.get_calc(keys[0], systems) + if program_helper is not None: + program_helper.calculation_preprocessing(calculator, calculation.get_settings()) + + qm_region_selector = swoose.QmRegionSelector() + qm_region_selector.set_underlying_calculator(calculator) + qm_region_selector.settings.update(settings_manager.task_settings) + qm_region_selector.settings["mm_connectivity_file"] = calculator.settings["mm_connectivity_file"] + qm_region_selector.settings["mm_parameter_file"] = calculator.settings["mm_parameter_file"] + qm_region_selector.generate_qm_region(structure.get_atoms()) + qm_atom_indices = qm_region_selector.get_qm_region_indices() + print("QM-region indices: ", qm_atom_indices, "\nNumber of atoms ", len(qm_atom_indices)) + self.save_results(structure, qm_atom_indices) + return self.postprocess_calculation_context() + + def save_results(self, structure: db.Structure, qm_atom_indices: List[int]) -> None: + self.store_property( + self._properties, + "qm_atoms", + "VectorProperty", + qm_atom_indices, + self._calculation.get_model(), + self._calculation, + structure, + ) + + @staticmethod + def required_programs() -> List[str]: + return ["database", "swoose", "utils"] diff --git a/scine_puffin/jobs/scine_react_complex_afir.py b/scine_puffin/jobs/scine_react_complex_afir.py index d002706..cf84a32 100644 --- a/scine_puffin/jobs/scine_react_complex_afir.py +++ b/scine_puffin/jobs/scine_react_complex_afir.py @@ -1,19 +1,27 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -import scine_molassembler as masm +from typing import TYPE_CHECKING from scine_puffin.config import Configuration from scine_puffin.utilities import scine_helper from .templates.job import breakable, calculation_context, job_configuration_wrapper from .templates.scine_react_job import ReactJob +from ..utilities.task_to_readuct_call import SubTaskToReaductCall +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class ScineReactComplexAfir(ReactJob): - """ + __doc__ = (""" A job that tries to enforce a reaction, given a reactive complex and its parts. The reactive complex is expected to be generated as two structures placed next to one another or, for intramolecular reactions, to be equal to the start structure. @@ -46,10 +54,10 @@ class ScineReactComplexAfir(ReactJob): intramolecular reaction is set up. Furthermore, the reactive sites in the complex that shall be pressed onto one another need to be given using: - afir_afir_rhs_list :: int + afir_afir_rhs_list : int This specifies list of indices of atoms to be artificially forced onto or away from those in the LHS list. - afir_afir_lhs_list :: int + afir_afir_lhs_list : int This specifies list of indices of atoms to be artificially forced onto or away from those in the RHS list. @@ -58,7 +66,7 @@ class ScineReactComplexAfir(ReactJob): any ``Calculation`` stored in a SCINE Database. All possible settings for this job are based on those available in SCINE Readuct. For a complete list see the - `ReaDuct manual `_ + `ReaDuct manual `_ Given that this job does more than one, in fact many separate calculations it is possible to target each individually with the settings. In order to @@ -79,116 +87,18 @@ class ScineReactComplexAfir(ReactJob): 4. TS optimization: ``tsopt_*`` 5. Validation using an IRC scan: ``irc_*`` 6. Optimization of the structures obtained with the IRC scan : ``ircopt_*`` - 7. Optimization of new products: ``opt_*`` - 8. (Optional) optimization of the reactive complex: ``rcopt_*`` - - The following options are available for the reactive complex generation: - - rc_x_alignment_0 :: List[float], length=9 - In case of two structures building the reactive complex, this option - describes a rotation of the first structure (index 0) that aligns - the reaction coordinate along the x-axis (pointing towards +x). - The rotation assumes that the geometric mean position of all - atoms in the reactive site (``afir_lhs_list``) is shifted into the - origin. - rc_x_alignment_1 :: List[float], length=9 - In case of two structures building the reactive complex, this option - describes a rotation of the second structure (index 1) that aligns - the reaction coordinate along the x-axis (pointing towards -x). - The rotation assumes that the geometric mean position of all - atoms in the reactive site (``afir_rhs_list``) is shifted into the - origin. - rc_x_rotation :: float - In case of two structures building the reactive complex, this option - describes a rotation angle around the x-axis of one of the two - structures after ``rc_x_alignment_0`` and ``rc_x_alignment_1`` have - been applied. - rc_x_spread :: float - In case of two structures building the reactive complex, this option - gives the distance by which the two structures are moved apart along - the x-axis after ``rc_x_alignment_0``, ``rc_x_alignment_1``, and - ``rc_x_rotation`` have been applied. - rc_displacement :: float - In case of two structures building the reactive complex, this option - adds a random displacement to all atoms (random direction, random - length). The maximum length of this displacement (per atom) is set to - be the value of this option. - rc_spin_multiplicity :: int - This option sets the ``spin_multiplicity`` of the reactive complex. - In case this is not given the ``spin_multiplicity`` of the initial - structure or minimal possible spin of the two initial structures is - used. - rc_molecular_charge :: int - This option sets the ``molecular_charge`` of the reactive complex. - In case this is not given the ``molecular_charge`` of the initial - structure or sum of the charges of the initial structures is used. - Note: If you set the ``rc_molecular_charge`` to a value different - from the sum of the start structures charges the possibly resulting - elementary steps will never be balanced but include removal or - addition of electrons. - rc_minimal_spin_multiplicity :: bool - True: The total spin multiplicity in a bimolecular reaction is - based on the assumption of total spin recombination (s + t = s; t + t = s; d + s = d; d + t = d) - False: No spin recombination is assumed (s + t = t; t + t = quin; d + s = d; d + t = quar) - (default: False) - - The following settings are recognized without a prepending flag: - - add_based_on_distance_connectivity :: bool - Whether to add the connectivity (i.e. add bonds) as derived from - atomic distances when graphs are generated. (default: True) - sub_based_on_distance_connectivity :: bool - Whether to subtract the connectivity (i.e. remove bonds) as derived from - atomic distances when graphs are generated. (default: True) - only_distance_connectivity :: bool - Whether to impose the connectivity solely from distances. (default: False) - imaginary_wavenumber_threshold :: float - Threshold value in inverse centimeters below which a wavenumber - is considered as imaginary when the transition state is analyzed. - Negative numbers are interpreted as imaginary. (default: 0.0) - spin_propensity_check :: int - The range to check for possible multiplicities for products. A value - of 2 (default) will check triplet and quintet for a singlet - and will check singlet, quintet und septet for triplet. - - Additionally all settings that are recognized by the SCF program chosen. - are also available. These settings are not required to be prepended with - any flag. - - Common examples are: - - max_scf_iterations :: int - The number of allowed SCF cycles until convergence. - - **Required Packages** - - SCINE: Database (present by default) - - SCINE: molassembler (present by default) - - SCINE: Readuct (present by default) - - SCINE: Utils (present by default) - - A program implementing the SCINE Calculator interface, e.g. Sparrow - - **Generated Data** - If successful (technically and chemically) the following data will be - generated and added to the database: - - Elementary Steps - If found, a single new elementary step with the associated transition - state will be added to the database. - - Structures - The transition state (TS) and also the separated products will be added - to the database. - - Properties - The ``hessian`` (``DenseMatrixProperty``), ``frequencies`` - (``VectorProperty``), ``normal_modes`` (``DenseMatrixProperty``), - ``gibbs_energy_correction`` (``NumberProperty``) and - ``gibbs_free_energy`` (``NumberProperty``) of the TS will be - provided. The ``electronic_energy`` associated with the TS structure and - each of the products will be added to the database. - """ - - def __init__(self): + 7. Analyze supersystem, derive individual products and assign charges: ``sp_*`` + 8. Optimization of new products: ``opt_*`` + 9. (Optional) optimization of the reactive complex: ``rcopt_*`` + + """ + "\n" + + ReactJob.optional_settings_doc() + "\n" + + ReactJob.general_calculator_settings_docstring() + "\n" + + ReactJob.generated_data_docstring() + "\n" + + ReactJob.required_packages_docstring() + ) + + def __init__(self) -> None: super().__init__() self.name = "Scine React Job with AFIR" self.exploration_key = "afir" @@ -224,12 +134,13 @@ def __init__(self): "tsopt": tsopt_defaults, "irc": irc_defaults, "ircopt": ircopt_defaults, - "opt": opt_defaults, + self.opt_key: opt_defaults } @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: + import scine_molassembler as masm import scine_readuct as readuct import scine_utilities as utils @@ -241,7 +152,7 @@ def run(self, manager, calculation, config: Configuration) -> bool: print("Afir Settings:") print(self.settings["afir"], "\n") self.systems, success = self.observed_readuct_call( - 'run_afir_task', self.systems, [self.rc_key], **self.settings["afir"] + SubTaskToReaductCall.AFIR, self.systems, [self.rc_key], **self.settings["afir"] ) if not success: self.verify_connection() @@ -258,16 +169,16 @@ def run(self, manager, calculation, config: Configuration) -> bool: # update model because job will be marked complete # use start calculator because afir might have last failed calculation scine_helper.update_model( - self.systems[self.rc_key], calculation, self.config + self.get_system(self.rc_key), calculation, self.config ) raise breakable.Break """ Endpoint Optimization """ inputs = self.output("afir") print("Endpoint Opt Settings:") - print(self.settings["opt"], "\n") + print(self.settings[self.opt_key], "\n") self.systems, success = self.observed_readuct_call( - 'run_optimization_task', self.systems, inputs, **self.settings["opt"] + SubTaskToReaductCall.OPT, self.systems, inputs, **self.settings[self.opt_key] ) self.throw_if_not_successful( success, @@ -285,24 +196,23 @@ def run(self, manager, calculation, config: Configuration) -> bool: end_charges, _, _ - ) = self.get_graph_charges_multiplicities(self.output("opt")[0], initial_charge) + ) = self.get_graph_charges_multiplicities(self.output(self.opt_key)[0], initial_charge) print("Start Graph:") print(self.start_graph) print("End Graph:") print(self.end_graph) - found_new_structures = bool(not masm.JsonSerialization.equal_molecules(self.start_graph, self.end_graph) or self.start_charges != end_charges) if not found_new_structures: self._calculation.set_comment("No new structure was discovered") scine_helper.update_model( - self.systems[self.output("opt")[0]], calculation, self.config + self.get_system(self.output(self.opt_key)[0]), calculation, self.config ) raise breakable.Break """ B-Spline Optimization """ - inputs = [self.rc_key] + self.output("opt") + inputs = [self.rc_key] + self.output(self.opt_key) print("\nBspline Settings:") print(self.settings["bspline"], "\n") self.systems, success = readuct.run_bspline_task( @@ -317,7 +227,13 @@ def run(self, manager, calculation, config: Configuration) -> bool: ) """ TS Optimization """ - tsguess = self.output("bspline")[0] - self._tsopt_hess_irc_ircopt_postprocessing(tsguess, settings_manager, program_helper) + tsguess_name = self.output("bspline")[0] + try: + self._tsopt_hess_irc_ircopt_postprocessing(tsguess_name, settings_manager, program_helper) + except BaseException: + _, tsguess_structure = self._store_ts_with_propensity_info(tsguess_name, program_helper, + db.Label.TS_GUESS) + self._calculation.set_restart_information("TS_GUESS", tsguess_structure.id()) + raise return self.postprocess_calculation_context() diff --git a/scine_puffin/jobs/scine_react_complex_nt.py b/scine_puffin/jobs/scine_react_complex_nt.py index ce478ff..096db58 100644 --- a/scine_puffin/jobs/scine_react_complex_nt.py +++ b/scine_puffin/jobs/scine_react_complex_nt.py @@ -1,17 +1,27 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING + from scine_puffin.config import Configuration from scine_puffin.utilities import scine_helper from .templates.job import breakable, calculation_context, job_configuration_wrapper from .templates.scine_react_job import ReactJob +from ..utilities.task_to_readuct_call import SubTaskToReaductCall +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class ScineReactComplexNt(ReactJob): - """ + __doc__ = (""" A job that tries to force a reaction, given a reactive complex and its parts. The reactive complex is expected to be generated as two structures placed next to one another. The job then forces groups of atoms onto or away from @@ -43,10 +53,10 @@ class ScineReactComplexNt(ReactJob): sites in the complex that shall be pressed onto one another need to be given using: - nt_nt_rhs_list :: int + nt_nt_rhs_list : int This specifies list of indices of atoms to be forced onto or away from those in the LHS list. - nt_nt_lhs_list :: int + nt_nt_lhs_list : int This specifies list of indices of atoms to be forced onto or away from those in the RHS list. @@ -55,7 +65,7 @@ class ScineReactComplexNt(ReactJob): any ``Calculation`` stored in a SCINE Database. All possible settings for this job are based on those available in SCINE Readuct. For a complete list see the - `ReaDuct manual `_ + `ReaDuct manual `_ Given that this job does more than one, in fact many separate calculations it is possible to target each individually with the settings. In order to @@ -75,116 +85,18 @@ class ScineReactComplexNt(ReactJob): 3. TS optimization: ``tsopt_*`` 4. Validation using an IRC scan: ``irc_*`` 5. Optimization of the structures obtained with the IRC scan : ``ircopt_*`` - 6. Optimization of new products: ``opt_*`` - 7. (Optional) optimization of the reactive complex: ``rcopt_*`` - - The following options are available for the reactive complex generation: - - rc_x_alignment_0 :: List[float], length=9 - In case of two structures building the reactive complex, this option - describes a rotation of the first structure (index 0) that aligns - the reaction coordinate along the x-axis (pointing towards +x). - The rotation assumes that the geometric mean position of all - atoms in the reactive site (``nt_lhs_list``) is shifted into the - origin. - rc_x_alignment_1 :: List[float], length=9 - In case of two structures building the reactive complex, this option - describes a rotation of the second structure (index 1) that aligns - the reaction coordinate along the x-axis (pointing towards -x). - The rotation assumes that the geometric mean position of all - atoms in the reactive site (``nt_rhs_list``) is shifted into the - origin. - rc_x_rotation :: float - In case of two structures building the reactive complex, this option - describes a rotation angle around the x-axis of one of the two - structures after ``rc_x_alignment_0`` and ``rc_x_alignment_1`` have - been applied. - rc_x_spread :: float - In case of two structures building the reactive complex, this option - gives the distance by which the two structures are moved apart along - the x-axis after ``rc_x_alignment_0``, ``rc_x_alignment_1``, and - ``rc_x_rotation`` have been applied. - rc_displacement :: float - In case of two structures building the reactive complex, this option - adds a random displacement to all atoms (random direction, random - length). The maximum length of this displacement (per atom) is set to - be the value of this option. - rc_spin_multiplicity :: int - This option sets the ``spin_multiplicity`` of the reactive complex. - In case this is not given the ``spin_multiplicity`` of the initial - structure or minimal possible spin of the two initial structures is - used. - rc_molecular_charge :: int - This option sets the ``molecular_charge`` of the reactive complex. - In case this is not given the ``molecular_charge`` of the initial - structure or sum of the charges of the initial structures is used. - Note: If you set the ``rc_molecular_charge`` to a value different - from the sum of the start structures charges the possibly resulting - elementary steps will never be balanced but include removal or - addition of electrons. - rc_minimal_spin_multiplicity :: bool - True: The total spin multiplicity in a bimolecular reaction is - based on the assumption of total spin recombination (s + t = s; t + t = s; d + s = d; d + t = d) - False: No spin recombination is assumed (s + t = t; t + t = quin; d + s = d; d + t = quar) - (default: False) - - The following settings are recognized without a prepending flag: - - add_based_on_distance_connectivity :: bool - Whether to add the connectivity (i.e. add bonds) as derived from - atomic distances when graphs are generated. (default: True) - sub_based_on_distance_connectivity :: bool - Whether to subtract the connectivity (i.e. remove bonds) as derived from - atomic distances when graphs are generated. (default: True) - only_distance_connectivity :: bool - Whether to impose the connectivity solely from distances. (default: False) - imaginary_wavenumber_threshold :: float - Threshold value in inverse centimeters below which a wavenumber - is considered as imaginary when the transition state is analyzed. - Negative numbers are interpreted as imaginary. (default: 0.0) - spin_propensity_check :: int - The range to check for possible multiplicities for products. A value - of 2 (default) will check triplet and quintet for a singlet - and will check singlet, quintet und septet for triplet. - - Additionally all settings that are recognized by the SCF program chosen. - are also available. These settings are not required to be prepended with - any flag. - - Common examples are: - - max_scf_iterations :: int - The number of allowed SCF cycles until convergence. - - **Required Packages** - - SCINE: Database (present by default) - - SCINE: molassembler (present by default) - - SCINE: Readuct (present by default) - - SCINE: Utils (present by default) - - A program implementing the SCINE Calculator interface, e.g. Sparrow - - **Generated Data** - If successful (technically and chemically) the following data will be - generated and added to the database: - - Elementary Steps - If found, a single new elementary step with the associated transition - state will be added to the database. - - Structures - The transition state (TS) and also the separated products will be added - to the database. - - Properties - The ``hessian`` (``DenseMatrixProperty``), ``frequencies`` - (``VectorProperty``), ``normal_modes`` (``DenseMatrixProperty``), - ``gibbs_energy_correction`` (``NumberProperty``) and - ``gibbs_free_energy`` (``NumberProperty``) of the TS will be - provided. The ``electronic_energy`` associated with the TS structure and - each of the products will be added to the database. - """ - - def __init__(self): + 6. Analyze supersystem, derive individual products and assign charges: ``sp_*`` + 7. Optimization of new products: ``opt_*`` + 8. (Optional) optimization of the reactive complex: ``rcopt_*`` + + """ + "\n" + + ReactJob.optional_settings_doc() + "\n" + + ReactJob.general_calculator_settings_docstring() + "\n" + + ReactJob.generated_data_docstring() + "\n" + + ReactJob.required_packages_docstring() + ) + + def __init__(self) -> None: super().__init__() self.name = "Scine React Job with Newton Trajectory" self.exploration_key = "nt" @@ -214,11 +126,11 @@ def __init__(self): "tsopt": tsopt_defaults, "irc": irc_defaults, "ircopt": ircopt_defaults, - "opt": opt_defaults, + self.opt_key: opt_defaults } @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: # Everything that calls SCINE is enclosed in a try/except block with breakable(calculation_context(self)): @@ -228,7 +140,7 @@ def run(self, manager, calculation, config: Configuration) -> bool: print("NT Settings:") print(self.settings["nt"], "\n") self.systems, success = self.observed_readuct_call( - 'run_nt_task', self.systems, [self.rc_key], **self.settings["nt"] + SubTaskToReaductCall.NT, self.systems, [self.rc_key], **self.settings["nt"] ) if not success: self.verify_connection() @@ -245,11 +157,17 @@ def run(self, manager, calculation, config: Configuration) -> bool: # update model because job will be marked complete # use start calculator because nt might have last failed calculation scine_helper.update_model( - self.systems[self.rc_key], calculation, self.config + self.get_system(self.rc_key), calculation, self.config ) raise breakable.Break - tsguess = self.output("nt")[0] - self._tsopt_hess_irc_ircopt_postprocessing(tsguess, settings_manager, program_helper) + tsguess_name = self.output("nt")[0] + try: + self._tsopt_hess_irc_ircopt_postprocessing(tsguess_name, settings_manager, program_helper) + except BaseException: + _, tsguess_structure = self._store_ts_with_propensity_info(tsguess_name, program_helper, + db.Label.TS_GUESS) + self._calculation.set_restart_information("TS_GUESS", tsguess_structure.id()) + raise return self.postprocess_calculation_context() diff --git a/scine_puffin/jobs/scine_react_complex_nt2.py b/scine_puffin/jobs/scine_react_complex_nt2.py index 36c355d..1c87c37 100644 --- a/scine_puffin/jobs/scine_react_complex_nt2.py +++ b/scine_puffin/jobs/scine_react_complex_nt2.py @@ -1,17 +1,27 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING + from scine_puffin.config import Configuration from scine_puffin.utilities import scine_helper from .templates.job import breakable, calculation_context, job_configuration_wrapper from .templates.scine_react_job import ReactJob +from ..utilities.task_to_readuct_call import SubTaskToReaductCall +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class ScineReactComplexNt2(ReactJob): - """ + __doc__ = (""" A job that tries to force a reaction, given a reactive complex and its parts. The reactive complex is expected to be generated as two structures placed next to one another. The job then forces pairs of atoms onto or away from @@ -43,11 +53,11 @@ class ScineReactComplexNt2(ReactJob): sites in the complex that shall be pressed onto one another need to be given using: - nt_nt_associations :: int + nt_nt_associations : int This specifies list of indices of atoms pairs to be forced onto another. Pairs are given in a flat list such that indices ``2i`` and ``2i+1`` form a pair. - nt_nt_dissociations :: int + nt_nt_dissociations : int This specifies list of indices of atoms pairs to be forced away from another. Pairs are given in a flat list such that indices ``2i`` and ``2i+1`` form a pair. @@ -57,7 +67,7 @@ class ScineReactComplexNt2(ReactJob): any ``Calculation`` stored in a SCINE Database. All possible settings for this job are based on those available in SCINE Readuct. For a complete list see the - `ReaDuct manual `_ + `ReaDuct manual `_ Given that this job does more than one, in fact many separate calculations it is possible to target each individually with the settings. In order to @@ -65,7 +75,7 @@ class ScineReactComplexNt2(ReactJob): has to be prepended with a tag, identifying which part of the job it is meant to impact. If the setting is meant to be added to the IRC scan at the end of this job ``irc_convergence_max_iterations`` should be used. - Note that this may include a doubling of this style of flags, as Readuct + Note that this may include a doubling of this style of flags, as ReaDuct uses a similar way of sorting options. Hence, choosing a none default ``irc_mode`` in this task has to be done using ``irc_irc_mode``. @@ -77,116 +87,18 @@ class ScineReactComplexNt2(ReactJob): 3. TS optimization: ``tsopt_*`` 4. Validation using an IRC scan: ``irc_*`` 5. Optimization of the structures obtained with the IRC scan : ``ircopt_*`` - 6. Optimization of new products: ``opt_*`` - 7. (Optional) optimization of the reactive complex: ``rcopt_*`` - - The following options are available for the reactive complex generation: - - rc_x_alignment_0 :: List[float], length=9 - In case of two structures building the reactive complex, this option - describes a rotation of the first structure (index 0) that aligns - the reaction coordinate along the x-axis (pointing towards +x). - The rotation assumes that the geometric mean position of all - atoms in the reactive site (``nt_lhs_list``) is shifted into the - origin. - rc_x_alignment_1 :: List[float], length=9 - In case of two structures building the reactive complex, this option - describes a rotation of the second structure (index 1) that aligns - the reaction coordinate along the x-axis (pointing towards -x). - The rotation assumes that the geometric mean position of all - atoms in the reactive site (``nt_rhs_list``) is shifted into the - origin. - rc_x_rotation :: float - In case of two structures building the reactive complex, this option - describes a rotation angle around the x-axis of one of the two - structures after ``rc_x_alignment_0`` and ``rc_x_alignment_1`` have - been applied. - rc_x_spread :: float - In case of two structures building the reactive complex, this option - gives the distance by which the two structures are moved apart along - the x-axis after ``rc_x_alignment_0``, ``rc_x_alignment_1``, and - ``rc_x_rotation`` have been applied. - rc_displacement :: float - In case of two structures building the reactive complex, this option - adds a random displacement to all atoms (random direction, random - length). The maximum length of this displacement (per atom) is set to - be the value of this option. - rc_spin_multiplicity :: int - This option sets the ``spin_multiplicity`` of the reactive complex. - In case this is not given the ``spin_multiplicity`` of the initial - structure or minimal possible spin of the two initial structures is - used. - rc_molecular_charge :: int - This option sets the ``molecular_charge`` of the reactive complex. - In case this is not given the ``molecular_charge`` of the initial - structure or sum of the charges of the initial structures is used. - Note: If you set the ``rc_molecular_charge`` to a value different - from the sum of the start structures charges the possibly resulting - elementary steps will never be balanced but include removal or - addition of electrons. - rc_minimal_spin_multiplicity :: bool - True: The total spin multiplicity in a bimolecular reaction is - based on the assumption of total spin recombination (s + t = s; t + t = s; d + s = d; d + t = d) - False: No spin recombination is assumed (s + t = t; t + t = quin; d + s = d; d + t = quar) - (default: False) - - The following settings are recognized without a prepending flag: - - add_based_on_distance_connectivity :: bool - Whether to add the connectivity (i.e. add bonds) as derived from - atomic distances when graphs are generated. (default: True) - sub_based_on_distance_connectivity :: bool - Whether to subtract the connectivity (i.e. remove bonds) as derived from - atomic distances when graphs are generated. (default: True) - only_distance_connectivity :: bool - Whether to impose the connectivity solely from distances. (default: False) - imaginary_wavenumber_threshold :: float - Threshold value in inverse centimeters below which a wavenumber - is considered as imaginary when the transition state is analyzed. - Negative numbers are interpreted as imaginary. (default: 0.0) - spin_propensity_check :: int - The range to check for possible multiplicities for products. A value - of 2 (default) will check triplet and quintet for a singlet - and will check singlet, quintet und septet for triplet. - - Additionally all settings that are recognized by the SCF program chosen. - are also available. These settings are not required to be prepended with - any flag. - - Common examples are: - - max_scf_iterations :: int - The number of allowed SCF cycles until convergence. - - **Required Packages** - - SCINE: Database (present by default) - - SCINE: molassembler (present by default) - - SCINE: Readuct (present by default) - - SCINE: Utils (present by default) - - A program implementing the SCINE Calculator interface, e.g. Sparrow - - **Generated Data** - If successful (technically and chemically) the following data will be - generated and added to the database: - - Elementary Steps - If found, a single new elementary step with the associated transition - state will be added to the database. - - Structures - The transition state (TS) and also the separated products will be added - to the database. - - Properties - The ``hessian`` (``DenseMatrixProperty``), ``frequencies`` - (``VectorProperty``), ``normal_modes`` (``DenseMatrixProperty``), - ``gibbs_energy_correction`` (``NumberProperty``) and - ``gibbs_free_energy`` (``NumberProperty``) of the TS will be - provided. The ``electronic_energy`` associated with the TS structure and - each of the products will be added to the database. - """ - - def __init__(self): + 6. Analyze supersystem, derive individual products and assign charges: ``sp_*`` + 7. Optimization of new products: ``opt_*`` + 8. (Optional) optimization of the reactive complex: ``rcopt_*`` + + """ + "\n" + + ReactJob.optional_settings_doc() + "\n" + + ReactJob.general_calculator_settings_docstring() + "\n" + + ReactJob.generated_data_docstring() + "\n" + + ReactJob.required_packages_docstring() + ) + + def __init__(self) -> None: super().__init__() self.name = "Scine React Job with Newton Trajectory 2" self.exploration_key = "nt" @@ -216,11 +128,11 @@ def __init__(self): "tsopt": tsopt_defaults, "irc": irc_defaults, "ircopt": ircopt_defaults, - "opt": opt_defaults + self.opt_key: opt_defaults } @job_configuration_wrapper - def run(self, _, calculation, config: Configuration) -> bool: + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: # Everything that calls SCINE is enclosed in a try/except block with breakable(calculation_context(self)): @@ -230,7 +142,7 @@ def run(self, _, calculation, config: Configuration) -> bool: print("NT Settings:") print(self.settings["nt"], "\n") self.systems, success = self.observed_readuct_call( - 'run_nt2_task', self.systems, [self.rc_key], **self.settings["nt"]) + SubTaskToReaductCall.NT2, self.systems, [self.rc_key], **self.settings["nt"]) if not success: self.verify_connection() """ Barrierless Reaction Check """ @@ -246,11 +158,17 @@ def run(self, _, calculation, config: Configuration) -> bool: # update model because job will be marked complete # use start calculator because nt might have last failed calculation scine_helper.update_model( - self.systems[self.rc_key], calculation, self.config + self.get_system(self.rc_key), calculation, self.config ) raise breakable.Break - tsguess = self.output("nt")[0] - self._tsopt_hess_irc_ircopt_postprocessing(tsguess, settings_manager, program_helper) + tsguess_name = self.output("nt")[0] + try: + self._tsopt_hess_irc_ircopt_postprocessing(tsguess_name, settings_manager, program_helper) + except BaseException: + _, tsguess_structure = self._store_ts_with_propensity_info(tsguess_name, program_helper, + db.Label.TS_GUESS) + self._calculation.set_restart_information("TS_GUESS", tsguess_structure.id()) + raise return self.postprocess_calculation_context() diff --git a/scine_puffin/jobs/scine_react_ts_guess.py b/scine_puffin/jobs/scine_react_ts_guess.py new file mode 100644 index 0000000..21aff87 --- /dev/null +++ b/scine_puffin/jobs/scine_react_ts_guess.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +import sys +from typing import TYPE_CHECKING + +from scine_puffin.config import Configuration +from scine_puffin.utilities.task_to_readuct_call import SubTaskToReaductCall +from .templates.job import breakable, calculation_context, job_configuration_wrapper +from .templates.scine_react_job import ReactJob +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") + + +class ScineReactTsGuess(ReactJob): + __doc__ = (""" + A job that tries to find an elementary step based on a transition state guess. + + The list of calculations/steps done is the following: + + 1. Optimization of the TS guess to the actual TS. + 2. Hessian calculation of the TS structure + 3. Validate the TS using a combination of IRC scan and optimization of the + resulting structures. + 4. Check if the IRC generated the input on one side and a new set of + structures on the other side of the TS. + 5. Optimize the products separately. + 6. Store the new elementary step in the database. + + **Order Name** + ``scine_react_ts_guess`` + + **Required Input** + A single structure that is the transition state guess. + + **Optional Settings** + Optional settings are read from the ``settings`` field, which is part of + any ``Calculation`` stored in a SCINE Database. + All possible settings for this job are based on those available in SCINE + Readuct. For a complete list see the + `ReaDuct manual `_ + + Given that this job does more than one, in fact many separate calculations + it is possible to target each individually with the settings. In order to + achieve this each regular setting, such as ``convergence_max_iterations`` + has to be prepended with a tag, identifying which part of the job it is + meant to impact. If the setting is meant to be added to the IRC scan at + the end of this job ``irc_convergence_max_iterations`` should be used. + Note that this may include a doubling of this style of flags, as Readuct + uses a similar way of sorting options. Hence, choosing a none default + ``irc_mode`` in this task has to be done using ``irc_irc_mode``. + + The complete list prefixes for specific settings for the steps listed at + the start of this section is: + + 1. TS optimization: ``tsopt_*`` + 2. Validation using an IRC scan: ``irc_*`` + 3. Optimization of the structures obtained with the IRC scan : ``ircopt_*`` + 4. Analyze supersystem, derive individual products and assign charges: ``sp_*`` + 5. Optimization of the products and reactants: ``opt_*`` + + """ + "\n" + + ReactJob.optional_settings_doc() + "\n" + + ReactJob.general_calculator_settings_docstring() + "\n" + + ReactJob.generated_data_docstring() + "\n" + + ReactJob.required_packages_docstring() + ) + + def __init__(self): + super().__init__() + self.name = "Scine React Job based on TS Guess" + self.exploration_key = "" + tsopt_defaults = { + "output": ["ts"], + "optimizer": "bofill", + "convergence_max_iterations": 200, + "automatic_mode_selection": [] + } + irc_defaults = { + "output": ["irc_forward", "irc_backward"], + "convergence_max_iterations": 50, + "irc_initial_step_size": 0.1, + "sd_factor": 1.0, + "stop_on_error": False, + } + ircopt_defaults = {"stop_on_error": True, "convergence_max_iterations": 200} + opt_defaults = { + "convergence_max_iterations": 500, + } + spin_propensity_defaults = { + **self.settings[self.propensity_key], + "ts_check": 2 + } + self.settings = { + **self.settings, + "tsopt": tsopt_defaults, + "irc": irc_defaults, + "ircopt": ircopt_defaults, + self.opt_key: opt_defaults, + self.propensity_key: spin_propensity_defaults + } + + @job_configuration_wrapper + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: + + # Everything that calls SCINE is enclosed in a try/except block + with breakable(calculation_context(self)): + """ Prepare data structures """ + structures = calculation.get_structures() + if len(structures) != 1: + raise RuntimeError("The calculation must have exactly one structure.") + ts_guess_structure = db.Structure(structures[0], self._structures) + self.ref_structure = ts_guess_structure + + settings_manager, program_helper = self.create_helpers(ts_guess_structure) + self.systems, keys = settings_manager.prepare_readuct_task(ts_guess_structure, + self._calculation, + self._calculation.get_settings(), + config["resources"]) + self.sort_settings(settings_manager.task_settings) + ts_guess_name = 'ts' + self.systems[ts_guess_name] = self.get_system(keys[0]) + + """ Propensity check for TS guess """ + # The setting does not make much sense here, because we don't know if the reaction is unimolecular + if self.settings[self.propensity_key]["check"] and \ + not self.settings[self.propensity_key]["check_for_unimolecular_reaction"]: + sys.stderr.write("Warning: The setting 'check_for_unimolecular_reaction' is set to False, " + "but the job does not have the information to decide if the reaction is unimolecular. " + "The setting will be ignored.\n") + self.settings[self.propensity_key]["check_for_unimolecular_reaction"] = True + self.setup_automatic_mode_selection("tsopt") + former_spin_propensity_range = self.settings[self.propensity_key]["check"] + self.settings[self.propensity_key]["check"] = self.settings[self.propensity_key]["ts_check"] + names, self.systems = self.optimize_structures( + ts_guess_name, + self.systems, + [ts_guess_structure.get_atoms()], + [ts_guess_structure.get_charge()], + [ts_guess_structure.get_multiplicity()], + settings_manager.calculator_settings, + False, + SubTaskToReaductCall.TSOPT, + "tsopt" + ) + if len(names) != 1: + self.raise_named_exception("Optimization of the TS guess yielded multiple structures, " + "which is not expected") + + lowest_name, names_within_range = self._get_propensity_names_within_range( + names[0], self.systems, self.settings[self.propensity_key]["energy_range_to_optimize"] + ) + if lowest_name is None: + self.raise_named_exception("No TS optimization was successful") + raise RuntimeError("Unreachable") + self.settings[self.propensity_key]["check"] = former_spin_propensity_range + db_results = calculation.get_results() + for name in [lowest_name] + names_within_range: + # make sure to overwrite the output names with the new one + self.settings["tsopt"]["output"] = [name] + """ TSOPT Hessian IRC IRCOPT and Postprocessing""" + try: + product_names, start_names = self._hess_irc_ircopt(name, settings_manager) + self._postprocessing_with_conformer_handling(product_names, start_names, program_helper) + # the postprocessing will remove previous results, so we have to keep track of them over the loop + db_results += calculation.get_results() + except (BaseException, breakable.Break) as e: + if isinstance(e, BaseException): + sys.stderr.write(f"Reaction trial for {name} did not succeed: {e}\n") + calculation.set_results(db_results) + + return self.postprocess_calculation_context() diff --git a/scine_puffin/jobs/scine_single_point.py b/scine_puffin/jobs/scine_single_point.py index c6e77ba..33e4fbc 100644 --- a/scine_puffin/jobs/scine_single_point.py +++ b/scine_puffin/jobs/scine_single_point.py @@ -1,12 +1,22 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING, List + from scine_puffin.config import Configuration -from .templates.job import calculation_context, job_configuration_wrapper -from .templates.scine_job import ScineJob +from scine_puffin.jobs.templates.job import calculation_context, job_configuration_wrapper +from scine_puffin.jobs.templates.scine_job import ScineJob +from scine_puffin.utilities.qm_mm_settings import prepare_optional_settings +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class ScineSinglePoint(ScineJob): @@ -14,6 +24,9 @@ class ScineSinglePoint(ScineJob): A job calculating the electronic energy for a given structure with a given model. + QM/MM and MM calculations expect the presence of bond orders ('bond_orders'), (optionally) atomic + charges ('atomic_charges'), and the QM-atom selection ('qm_atoms') as properties of the structure. + **Order Name** ``scine_single_point`` @@ -28,7 +41,7 @@ class ScineSinglePoint(ScineJob): Common examples are: - max_scf_iterations :: int + max_scf_iterations : int The number of allowed SCF cycles until convergence. **Required Packages** @@ -46,14 +59,13 @@ class ScineSinglePoint(ScineJob): The ``atomic_charges`` associated with the given structure (if available). """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.name = "Scine Single Point Calculation Job" @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: - import scine_database as db import scine_readuct as readuct # preprocessing of structure @@ -62,17 +74,17 @@ def run(self, manager, calculation, config: Configuration) -> bool: # actual calculation with calculation_context(self): + prepare_optional_settings(structure, calculation, settings_manager, self._properties) systems, keys = settings_manager.prepare_readuct_task( structure, calculation, calculation.get_settings(), config["resources"] ) if program_helper is not None: - program_helper.calculation_preprocessing(systems[keys[0]], calculation.get_settings()) + program_helper.calculation_preprocessing(self.get_calc(keys[0], systems), calculation.get_settings()) systems, success = readuct.run_sp_task(systems, keys, **settings_manager.task_settings) - self.sp_postprocessing(success, systems, keys, structure, program_helper) return self.postprocess_calculation_context() @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "readuct", "utils"] diff --git a/scine_puffin/jobs/scine_step_refinement.py b/scine_puffin/jobs/scine_step_refinement.py deleted file mode 100644 index dd79ff9..0000000 --- a/scine_puffin/jobs/scine_step_refinement.py +++ /dev/null @@ -1,277 +0,0 @@ -# -*- coding: utf-8 -*- -__copyright__ = """ This code is licensed under the 3-clause BSD license. -Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. -See LICENSE.txt for details. -""" - -from scine_puffin.config import Configuration -from .templates.job import breakable, calculation_context, job_configuration_wrapper -from .templates.scine_react_job import ReactJob -from scine_puffin.utilities import masm_helper - - -class ScineStepRefinement(ReactJob): - """ - A job that tries to refine an elementary step using a different electronic - structure method. The job is essentially a scine_react job that uses a - previously optimized transition state as an initial guess. - - The list of calculations/steps done is the following: - - 1. Optimization of the reactant structures using the refinement model. - 2. Optimization of the TS guess / old TS to the actual TS. - 3. Hessian calculation of the TS structure - 4. Validate the TS using a combination of IRC scan and optimization of the - resulting structures. - 5. Check if the IRC generated the input on one side and a new set of - structures on the other side of the TS. - 6. Optimize the products separately. - 7. Store the new elementary step in the database. - - **Order Name** - ``scine_step_refinement`` - - **Required Input** - The first up to two structures correspond to the reactants of the reaction. - The original transition state must be given through the auxiliaries of the calculation as - "transition-state-id": id-of-transition state. Furthermore, the reactive sites in the complex - that shall be pressed onto one another need to be given using: - - nt_nt_associations :: int - This specifies list of indices of atoms pairs to be forced onto - another. Pairs are given in a flat list such that indices ``2i`` - and ``2i+1`` form a pair. - nt_nt_dissociations :: int - This specifies list of indices of atoms pairs to be forced away from - another. Pairs are given in a flat list such that indices ``2i`` - and ``2i+1`` form a pair. - - **Optional Settings** - Optional settings are read from the ``settings`` field, which is part of - any ``Calculation`` stored in a SCINE Database. - All possible settings for this job are based on those available in SCINE - Readuct. For a complete list see the - `ReaDuct manual `_ - - Given that this job does more than one, in fact many separate calculations - it is possible to target each individually with the settings. In order to - achieve this each regular setting, such as ``convergence_max_iterations`` - has to be prepended with a tag, identifying which part of the job it is - meant to impact. If the setting is meant to be added to the IRC scan at - the end of this job ``irc_convergence_max_iterations`` should be used. - Note that this may include a doubling of this style of flags, as Readuct - uses a similar way of sorting options. Hence, choosing a none default - ``irc_mode`` in this task has to be done using ``irc_irc_mode``. - - The complete list prefixes for specific settings for the steps listed at - the start of this section is: - - 1. Newton trajectory scan: ``nt_*`` (used exclusively for the mode selection - in the transition state optimization) - 2. TS optimization: ``tsopt_*`` - 3. Validation using an IRC scan: ``irc_*`` - 4. Optimization of the structures obtained with the IRC scan : ``ircopt_*`` - 5. Optimization of the products and reactants: ``opt_*`` - - The following settings are recognized without a prepending flag: - - add_based_on_distance_connectivity :: bool - Whether to add the connectivity (i.e. add bonds) as derived from - atomic distances when graphs are generated. (default: True) - sub_based_on_distance_connectivity :: bool - Whether to subtract the connectivity (i.e. remove bonds) as derived from - atomic distances when graphs are generated. (default: True) - only_distance_connectivity :: bool - Whether to impose the connectivity solely from distances. (default: False) - imaginary_wavenumber_threshold :: float - Threshold value in inverse centimeters below which a wavenumber - is considered as imaginary when the transition state is analyzed. - Negative numbers are interpreted as imaginary. (default: 0.0) - spin_propensity_check :: int - The range to check for possible multiplicities for products. A value - of 2 (default) will check triplet and quintet for a singlet - and will check singlet, quintet und septet for triplet. - - Additionally, all settings that are recognized by the SCF program chosen. - are also available. These settings are not required to be prepended with - any flag. - - Common examples are: - - max_scf_iterations :: int - The number of allowed SCF cycles until convergence. - - **Required Packages** - - SCINE: Database (present by default) - - SCINE: molassembler (present by default) - - SCINE: Readuct (present by default) - - SCINE: Utils (present by default) - - A program implementing the SCINE Calculator interface, e.g. Sparrow - - **Generated Data** - If successful (technically and chemically) the following data will be - generated and added to the database: - - Elementary Steps - If found, a single new elementary step with the associated transition - state will be added to the database. - - Structures - The transition state (TS) and also the separated products and reactants - will be added to the database. - - Properties - The ``hessian`` (``DenseMatrixProperty``), ``frequencies`` - (``VectorProperty``), ``normal_modes`` (``DenseMatrixProperty``), - ``gibbs_energy_correction`` (``NumberProperty``) and - ``gibbs_free_energy`` (``NumberProperty``) of the TS will be - provided. The ``electronic_energy`` associated with the TS structure and - each of the products will be added to the database. - """ - - def __init__(self): - super().__init__() - self.name = "Scine React Job starting from previous transition state" - self.exploration_key = "nt" - nt_defaults = { - "output": ["nt"], - "stop_on_error": False, - } - tsopt_defaults = { - "output": ["ts"], - "optimizer": "bofill", - "convergence_max_iterations": 200, - } - irc_defaults = { - "output": ["irc_forward", "irc_backward"], - "convergence_max_iterations": 50, - "irc_initial_step_size": 0.1, - "sd_factor": 1.0, - "stop_on_error": False, - } - ircopt_defaults = {"stop_on_error": True, "convergence_max_iterations": 200} - opt_defaults = { - "convergence_max_iterations": 500, - } - self.settings = { - **self.settings, - "tsopt": tsopt_defaults, - "irc": irc_defaults, - "ircopt": ircopt_defaults, - "opt": opt_defaults, - "nt": nt_defaults, - } - - @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - - import scine_database as db - # Everything that calls SCINE is enclosed in a try/except block - with breakable(calculation_context(self)): - settings_manager, program_helper = self.reactive_complex_preparations() - - all_struc_ids = self._calculation.get_structures() - ts_struc = db.Structure(calculation.get_auxiliaries()["transition-state-id"], self._structures) - start_structures = [db.Structure(ident, self._structures) for ident in all_struc_ids] - settings_manager.separate_settings(self._calculation.get_settings()) - self.sort_settings(settings_manager.task_settings) - """ TSOPT Hessian IRC IRCOPT """ - ts_guess, keys = settings_manager.prepare_readuct_task(ts_struc, - self._calculation, - self._calculation.get_settings(), - config["resources"]) - self.systems[keys[0]] = ts_guess[keys[0]] - product_names, start_names = self._tsopt_hess_irc_ircopt(keys[0], settings_manager) - - """ Store new starting material conformer(s) """ - if start_names is not None: - start_structures = self.store_start_structures( - start_names, program_helper, "tsopt") - else: - start_names, start_structure_objects = self.optimize_reactants(start_structures, - settings_manager, - config) - start_structures = [o.id() for o in start_structure_objects] - - """ Save the elementary step, transition state, and product """ - self.react_postprocessing(product_names, program_helper, "tsopt", start_structures) - - return self.postprocess_calculation_context() - - def optimize_reactants(self, reactant_structures, settings_manager, config: Configuration): - """ - Optimize the reactant structures and saves them in the database. - - Notes - ----- - * writes reactant calculators to self.systems - * May throw exception. - * Requires run configuration - - Parameters - ---------- - reactant_structures :: List[scine_database.Structure] - The original structures of the elementary step to be optimized. - settings_manager :: SettingsManager - The settings_manager which is used to construct the reactant calculators. - config :: : scine_puffin.config.Configuration - The run configuration. - - Returns - ------- - reactant_names :: List[str] - The names of the reactant calculators in self.systems. - optimized_structures :: List[scine_database.Structure] - The optimized reactant structures. - """ - print("Reactant Opt Settings:") - print(self.settings["opt"], "\n") - - reactant_names = [] - optimized_structures = [] - # Optimize reactants, if they have more than one atom; otherwise just run a single point calculation - for i, structure in enumerate(reactant_structures): - # Build the initial calculator. - tmp_calculator_set, keys = settings_manager.prepare_readuct_task(structure, self._calculation, - self._calculation.get_settings(), - config["resources"]) - name = "reactant_opt_{:02d}".format(i) - reactant_names.append(name) - self.systems[name] = tmp_calculator_set[keys[0]] - print("Optimizing " + name + " :\n") - # Run the optimization if more than one atom is present. - if len(self.systems[name].structure) > 1: - self.systems, success = self.observed_readuct_call( - 'run_opt_task', self.systems, [name], **self.settings["opt"]) - self.throw_if_not_successful( - success, - self.systems, - [name], - ["energy"], - "Reactant optimization failed:\n", - ) - - bond_orders, self.systems = self.make_bond_orders_from_calc(self.systems, name) - cbor = masm_helper.get_cbor_graph( - self.systems[name].structure, - bond_orders, - self.connectivity_settings, - self._calculation.get_model().periodic_boundaries, - self.surface_indices(structure) - ) - structure_label = self._determine_new_label_based_on_graph(self.systems[name], cbor) - new_structure = self.create_new_structure(self.systems[name], structure_label) - self.transfer_properties(structure, new_structure) - self.store_energy(self.systems[name], new_structure) - self.store_property( - self._properties, - "bond_orders", - "SparseMatrixProperty", - bond_orders.matrix, - self._calculation.get_model(), - self._calculation, - new_structure, - ) - self.add_graph(new_structure, bond_orders) - optimized_structures.append(new_structure) - return reactant_names, optimized_structures diff --git a/scine_puffin/jobs/scine_ts_optimization.py b/scine_puffin/jobs/scine_ts_optimization.py index 4088885..6ab8791 100644 --- a/scine_puffin/jobs/scine_ts_optimization.py +++ b/scine_puffin/jobs/scine_ts_optimization.py @@ -1,12 +1,21 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING + from scine_puffin.config import Configuration from .templates.job import calculation_context, job_configuration_wrapper from .templates.scine_optimization_job import OptimizationJob +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class ScineTsOptimization(OptimizationJob): @@ -27,34 +36,34 @@ class ScineTsOptimization(OptimizationJob): Common examples are: - optimizer :: str + optimizer : str The name of the optimizer to be used, e.g. 'bofill' or 'evf' or 'dimer'. - convergence_max_iterations :: int + convergence_max_iterations : int The maximum number of geometry optimization cycles. - convergence_delta_value :: float + convergence_delta_value : float The convergence criterion for the electronic energy difference between two steps. - convergence_gradient_max_coefficient :: float + convergence_gradient_max_coefficient : float The convergence criterion for the maximum absolute gradient. contribution. - convergence_step_rms :: float + convergence_step_rms : float The convergence criterion for root mean square of the geometric gradient. - convergence_step_max_coefficient :: float + convergence_step_max_coefficient : float The convergence criterion for the maximum absolute coefficient in the last step taken in the geometry optimization. - convergence_gradient_rms :: float + convergence_gradient_rms : float The convergence criterion for root mean square of the last step taken in the geometry optimization. For a complete list see the - `ReaDuct manual `_ + `ReaDuct manual `_ All settings that are recognized by the SCF program chosen. Common examples are: - max_scf_iterations :: int + max_scf_iterations : int The number of allowed SCF cycles until convergence. **Required Packages** @@ -73,14 +82,12 @@ class ScineTsOptimization(OptimizationJob): The ``electronic_energy`` associated with the new structure. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.name = "Scine Transition State Optimization" @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - - import scine_database as db + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: import scine_readuct as readuct # preprocessing of structure @@ -93,7 +100,7 @@ def run(self, manager, calculation, config: Configuration) -> bool: structure, calculation, calculation.get_settings(), config["resources"] ) if program_helper is not None: - program_helper.calculation_preprocessing(systems[keys[0]], calculation.get_settings()) + program_helper.calculation_preprocessing(self.get_calc(keys[0], systems), calculation.get_settings()) systems, success = readuct.run_tsopt_task(systems, keys, **settings_manager.task_settings) self.optimization_postprocessing( diff --git a/scine_puffin/jobs/sleep.py b/scine_puffin/jobs/sleep.py index 3b5db02..2d1dd37 100644 --- a/scine_puffin/jobs/sleep.py +++ b/scine_puffin/jobs/sleep.py @@ -1,12 +1,21 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ from time import sleep +from typing import TYPE_CHECKING, List + from scine_puffin.config import Configuration from .templates.job import Job, job_configuration_wrapper +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class Sleep(Job): @@ -22,7 +31,7 @@ class Sleep(Job): any ``Calculation`` stored in a SCINE Database. Possible settings for this job are: - time :: int + time : int The time to sleep for in seconds. Default: 300. **Required Packages** @@ -32,17 +41,15 @@ class Sleep(Job): This dummy job does not generate new data. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.name = "Sleep Job" @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - - import scine_database as db + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: # Get the requested sleep time - sleeptime = int(calculation.get_settings().get("time", 300)) + sleeptime = int(calculation.get_settings().get("time", 300)) # type: ignore sleep(sleeptime) @@ -51,5 +58,5 @@ def run(self, manager, calculation, config: Configuration) -> bool: return True @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database"] diff --git a/scine_puffin/jobs/swoose_qmmm_forces.py b/scine_puffin/jobs/swoose_qmmm_forces.py index bed5cdf..a515845 100644 --- a/scine_puffin/jobs/swoose_qmmm_forces.py +++ b/scine_puffin/jobs/swoose_qmmm_forces.py @@ -1,15 +1,29 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ import os +from typing import TYPE_CHECKING, List, Dict, Optional, Tuple + from scine_puffin.config import Configuration -from .templates.job import Job, calculation_context, job_configuration_wrapper +from .templates.job import calculation_context, job_configuration_wrapper +from .templates.scine_job import ScineJob +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") -class SwooseQmmmForces(Job): +class SwooseQmmmForces(ScineJob): """ A job calculating the forces for a given structure with the QM/MM method provided by the Scine Swoose Module. @@ -27,7 +41,7 @@ class SwooseQmmmForces(Job): Common examples are: - max_scf_iterations :: int + max_scf_iterations : int The number of allowed SCF cycles until convergence. **Required Packages** @@ -45,28 +59,41 @@ class SwooseQmmmForces(Job): The ``atomic_forces`` associated with the given structure. """ - def write_parameter_and_connectivity_file(self, parameter_file: str, connectivity_file: str, - settings, structure, properties): - """ - This function needs to be called inside the calculation context, because it writes files to - strange places otherwise. - """ + @staticmethod + def get_qm_atoms(properties: db.Collection, structure: db.Structure) -> List[int]: + try: + qm_atoms = db.VectorProperty(structure.get_property('qm_atoms')) + except RuntimeError as e: + raise RuntimeError('QM atoms are not available as a property of the QM/MM structure.') from e + qm_atoms.link(properties) + return [int(i) for i in qm_atoms.get_data()] - import scine_database as db + @staticmethod + def write_partial_charge_file(charge_file_name: str, properties: db.Collection, structure: db.Structure) -> None: + try: + charges = db.VectorProperty(structure.get_property('atomic_charges')) + except RuntimeError as e: + raise RuntimeError('Atomic charges are not available as a property of the QM/MM structure.') from e + charges.link(properties) + charge_file_str = "" + for charge in charges.get_data(): + charge_file_str += str(charge) + "\n" + with open(charge_file_name, 'w') as p_file: + p_file.write(charge_file_str) + + @staticmethod + def write_connectivity_file(connectivity_file_name: str, properties: db.Collection, + structure: db.Structure) -> None: try: - parameters = db.StringProperty(structure.get_property('sfam_parameters')) bond_orders = db.SparseMatrixProperty(structure.get_property('bond_orders')) except RuntimeError as e: - raise RuntimeError('Parameters or bond orders are missing as properties of the structure.') from e - parameters.link(properties) + raise RuntimeError('Bond orders are missing as properties of the structure during QM/MM.') from e bond_orders.link(properties) - n_atoms = len(structure.get_atoms()) bo_matrix = bond_orders.get_data().toarray() + n_atoms = len(structure.get_atoms()) if bo_matrix.shape != (n_atoms, n_atoms): raise RuntimeError('The dimensions of the provided bond orders are incompatible with the structure.') - with open(parameter_file, 'w') as p_file: - p_file.write(parameters.get_data()) - with open(connectivity_file, 'w') as c_file: + with open(connectivity_file_name, 'w') as c_file: for i in range(n_atoms): neighbors = '' for j in range(n_atoms): @@ -74,10 +101,32 @@ def write_parameter_and_connectivity_file(self, parameter_file: str, connectivit continue neighbors += str(j) + ' ' c_file.write(neighbors + '\n') + + @staticmethod + def write_parameter_file(parameter_file_name: str, properties: db.Collection, structure: db.Structure) -> None: + try: + parameters = db.StringProperty(structure.get_property('sfam_parameters')) + except RuntimeError as e: + raise RuntimeError('SFAM-parameters are missing as properties of the structure in QM/MM.') from e + parameters.link(properties) + with open(parameter_file_name, 'w') as p_file: + p_file.write(parameters.get_data()) + + @staticmethod + def write_parameter_and_connectivity_file(parameter_file: str, connectivity_file: str, + settings: utils.ValueCollection, structure: db.Structure, + properties: db.Collection) -> utils.ValueCollection: + """ + This function needs to be called inside the calculation context, because it writes files to + strange places otherwise. + """ + SwooseQmmmForces.write_connectivity_file(connectivity_file, properties, structure) + SwooseQmmmForces.write_parameter_file(parameter_file, properties, structure) settings.update({'mm_parameter_file': parameter_file, 'mm_connectivity_file': connectivity_file}) return settings - def parse_energy(self, output: str): + @staticmethod + def parse_energy(output: str) -> Optional[float]: try: lines = output.split('\n') for line in lines: @@ -88,8 +137,8 @@ def parse_energy(self, output: str): except (IndexError, ValueError): return None - def parse_forces(self, output: str, n_atoms: int): - import scine_utilities as utils + @staticmethod + def parse_forces(output: str, n_atoms: int) -> Optional[List[List[float]]]: try: lines = output.split('\n') for i, line in enumerate(lines): @@ -103,9 +152,8 @@ def parse_forces(self, output: str, n_atoms: int): except (IndexError, ValueError): return None - def process_output(self, calculation, atoms): - import scine_database as db - + def process_output(self, calculation: db.Calculation, atoms: utils.AtomCollection) \ + -> Tuple[float, List[List[float]]]: # Capture raw output and parse the results from it stdout_path = os.path.join(self.work_dir, 'output') stderr_path = os.path.join(self.work_dir, 'errors') @@ -121,7 +169,8 @@ def process_output(self, calculation, atoms): # Check whether everything worked if energy is None or forces is None: calculation.set_status(db.Status.FAILED) - raise RuntimeError("Something went wrong while parsing energies or forces.") + self.raise_named_exception("Something went wrong while parsing energies or forces.") + raise RuntimeError # linter only dimension_is_correct = len(forces) == len(atoms) for force in forces: @@ -131,14 +180,20 @@ def process_output(self, calculation, atoms): if not dimension_is_correct: calculation.set_status(db.Status.FAILED) - raise RuntimeError("The obtained forces have wrong dimension.") + self.raise_named_exception("The obtained forces have wrong dimension.") + raise RuntimeError # linter only return energy, forces - def manage_settings(self, settings: dict, job, resources): - import scine_utilities as utils + def manage_settings(self, settings: utils.ValueCollection, job: db.Job, resources: Dict) -> None: program = settings['qm_module'] + if not isinstance(program, str): + self.raise_named_exception('The QM module is not a string.') + raise RuntimeError('Unreachable') # for mypy method_family = 'dft' if program.lower() == 'orca' or program.lower() == 'turbomole' else settings['qm_model'] + if not isinstance(method_family, str): + self.raise_named_exception('The QM model is not a string.') + raise RuntimeError('Unreachable') available_qm_settings = utils.core.get_available_settings(method_family, program) if job.cores > 1 and 'external_program_nprocs' in available_qm_settings: settings['external_program_nprocs'] = job.cores @@ -146,11 +201,8 @@ def manage_settings(self, settings: dict, job, resources): settings['external_program_memory'] = int(resources['memory'] * 1024) @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - import scine_database as db + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: import scine_swoose as swoose - import scine_utilities as utils - # Gather all required collections structures = manager.get_collection('structures') calculations = manager.get_collection('calculations') @@ -201,5 +253,5 @@ def run(self, manager, calculation, config: Configuration) -> bool: return True @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ['database', 'utils', 'swoose'] diff --git a/scine_puffin/jobs/templates/job.py b/scine_puffin/jobs/templates/job.py index ce0dea3..b38f0af 100644 --- a/scine_puffin/jobs/templates/job.py +++ b/scine_puffin/jobs/templates/job.py @@ -1,12 +1,16 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from abc import ABC, abstractmethod from contextlib import contextmanager from functools import wraps -from typing import Callable, List, Optional, Tuple +from typing import Union, Callable, List, Optional, Tuple, Iterator, Any, TYPE_CHECKING, Type +from typing_extensions import TypeGuard, TypeVar, ParamSpec, Concatenate + import shutil import tarfile import os @@ -14,14 +18,39 @@ import ctypes import io import time -import subprocess +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency from scine_puffin.config import Configuration +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") + libc = ctypes.CDLL(None) c_stdout = ctypes.c_void_p.in_dll(libc, "stdout") c_stderr = ctypes.c_void_p.in_dll(libc, "stderr") +T = TypeVar("T", bound='Job') +P = ParamSpec("P") +AnyReturnType = TypeVar("AnyReturnType") + + +def is_configured(run: Callable[Concatenate[T, P], AnyReturnType]) \ + -> Callable[Concatenate[T, P], AnyReturnType]: + """ + A decorator to check if the job has been configured before running a method + """ + + @wraps(run) + def _impl(self: T, *args: P.args, **kwargs: P.kwargs) -> AnyReturnType: + if self.check_configuration(self): + return run(self, *args, **kwargs) + else: + raise RuntimeError("Job has not been configured properly") + + return _impl + def job_configuration_wrapper(run: Callable): """ @@ -32,7 +61,7 @@ def job_configuration_wrapper(run: Callable): """ @wraps(run) - def _impl(self, manager, calculation, config): + def _impl(self, manager: db.Manager, calculation: db.Calculation, config: Configuration): self.configure_run(manager, calculation, config) try: success = run(self, manager, calculation, config) @@ -44,37 +73,35 @@ def _impl(self, manager, calculation, config): success = False if not success: # additional safety that every failed job gets also a failed status - import scine_database as db - calculation.set_status(db.Status.FAILED) return success return _impl -class Job: +class Job(ABC): """ A common interface for all jobs in/carried out by a Puffin """ - def __init__(self): - self.work_dir = None - self.stdout_path = None - self.stderr_path = None - self.config = None - self._id = None - self._calculation = None - self._manager = None - self._calculations = None - self._compounds = None - self._elementary_steps = None - self._properties = None - self._reactions = None - self._structures = None - self._flasks = None + work_dir: str + stdout_path: str = "stdout" + stderr_path: str = "stderr" + config: Configuration + _id: db.ID + _calculation: db.Calculation + _manager: db.Manager + _calculations: db.Collection + _compounds: db.Collection + _elementary_steps: db.Collection + _properties: db.Collection + _reactions: db.Collection + _structures: db.Collection + _flasks: db.Collection @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: + @abstractmethod + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: """ Runs the actual job. This function has to be implemented by any job that shall be added to @@ -82,16 +109,17 @@ def run(self, manager, calculation, config: Configuration) -> bool: Parameters ---------- - manager :: db.Manager (Scine::Database::Manager) + manager : db.Manager (Scine::Database::Manager) The manager/database to work on/with. - calculation :: db.Calculation (Scine::Database::Calculation) + calculation : db.Calculation (Scine::Database::Calculation) The calculation that triggered the execution of this job. - config :: scine_puffin.config.Configuration + config : scine_puffin.config.Configuration The configuration of Puffin. """ raise NotImplementedError @staticmethod + @abstractmethod def required_programs() -> List[str]: """ This function has to be implemented by any job that shall be added to @@ -99,13 +127,56 @@ def required_programs() -> List[str]: Returns ------- - requirements :: List[str] + requirements : List[str] A list of names of programs/packages that are required for the execution of the job. """ raise NotImplementedError - def prepare(self, job_dir: str, id) -> None: + @classmethod + def optional_settings_doc(cls) -> str: + """ + Returns a docstring description of the available optional settings of the job. + To be implemented by child classes if applicable + """ + return "" + + @classmethod + def generated_data_docstring(cls) -> str: + """ + Returns a docstring description of the generated data of the job. + """ + return "**Generated Data**\n" + + @staticmethod + def general_calculator_settings_docstring() -> str: + return """ + Additionally, all settings that are recognized by the SCF program chosen. + are also available. These settings are not required to be prepended with + any flag. + + Common examples are: + + max_scf_iterations : int + The number of allowed SCF cycles until convergence. + """ + + @classmethod + def required_packages_docstring(cls) -> str: + required = cls.required_programs() + docs = "\n**Required Packages**\n" + if "database" in required: + docs += " - SCINE: Database (present by default)\n" + if "molassembler" in required: + docs += " - SCINE: Molassembler (present by default)\n" + if "readuct" in required: + docs += " - SCINE: ReaDuct (present by default)\n" + if "utils" in required: + docs += " - SCINE: Utils (present by default)\n" + docs += " - A program implementing the SCINE Calculator interface, e.g. Sparrow\n" + return docs + + def prepare(self, job_dir: str, id: db.ID) -> None: """ Prepares the actual job. This function has to be implemented by any job that shall be added to @@ -113,9 +184,9 @@ def prepare(self, job_dir: str, id) -> None: Parameters ---------- - job_dir :: str + job_dir : str The path to the directory in which all jobs are executed. - id :: db.ID (Scine::Database::ID) + id : db.ID (Scine::Database::ID) The calculation that triggered the execution of this job. """ self._id = id @@ -130,12 +201,15 @@ def archive(self, archive: str) -> None: Parameters ---------- - archive :: str + archive : str The path to move the resulting tarball to. """ - if not os.path.exists(self.work_dir): + if not self.work_dir or not os.path.exists(self.work_dir): sys.stderr.write(f"The job directory {self.work_dir} does not exist, cannot archive.\n") return + if not self._id: + sys.stderr.write(f"The job {self.__class__.__name__} has no ID, cannot archive.\n") + return basedir = os.path.dirname(self.work_dir) # Tar the folder tar_gen_path = os.path.join(basedir, self._id.string() + ".tar.gz") @@ -151,12 +225,13 @@ def clear(self) -> None: """ Clears the directory in which the job was run. """ - if not os.path.exists(self.work_dir): + if not self.work_dir or not os.path.exists(self.work_dir): sys.stderr.write(f"The job directory {self.work_dir} does not exist, cannot remove anything.\n") return shutil.rmtree(self.work_dir) - def verify_connection(self): + @is_configured + def verify_connection(self) -> None: """ Verifies the connection to the database. Returns only if a connection is established, if it is not, the function @@ -170,60 +245,59 @@ def verify_connection(self): while not self._manager.is_connected(): time.sleep(10) + @requires("database") def store_property( self, - properties, + properties: db.Collection, property_name: str, property_type: str, - data, - model, - calculation, - structure, - replace=True, - ) -> object: + data: Any, + model: db.Model, + calculation: db.Calculation, + structure: db.Structure, + replace: bool = True, + ) -> Optional[db.Property]: """ Adds a single property into the database, connecting it with a given structure and calculation (it's results section) and also Parameters ---------- - properties :: db.Collection (Scine::Database::Collection) + properties : db.Collection (Scine::Database::Collection) The collection housing all properties. - property_name :: str + property_name : str The name (key) of the new property, e.g. ``electronic_energy``. - property_type :: str + property_type : str The type of property to be added, e.g. ``NumberProperty``. - data :: object (According to 'property_type') + data : Any (According to 'property_type') The data to be stored in the property, the type of this object is dependent on the type of property requested. A ``NumberProperty`` will require a ``float``, a ``VectorProperty`` will require a ``List[float]``, etc. - model :: db.Model (Scine::Database::Model) + model : db.Model (Scine::Database::Model) The model used in the calculation that resulted in this property. - calculation :: db.Calculation (Scine::Database::Calculation) + calculation : db.Calculation (Scine::Database::Calculation) The calculation that resulted in this property. The calculation has to be linked to its collection. - structure :: db.Structure (Scine::Database::Structure) + structure : db.Structure (Scine::Database::Structure) The structure for which the property is to be added. The properties field of the structure will receive an additional entry, or have an entry replaced, based on the options given to this function. The structure has to be linked to its collection. - replace :: bool + replace : bool If true, replaces an existing property (identical name and model) with the new one. This option is true by default. If false, doesnothing in the previous case, and returns ``None`` Returns ------- - property :: Derived of db.Property (Scine::Database::Property) + property : Derived of db.Property (Scine::Database::Property) The property, a derived class of db.Property, linked to the properties' collection, or ``None`` if no property was generated due to duplication. """ existing = self.check_duplicate_property(structure, properties, property_name, model) if existing and replace: - import scine_database as db - class_ = getattr(db, property_type) db_property = class_(existing) db_property.link(properties) @@ -235,8 +309,6 @@ def store_property( elif existing: return None else: - import scine_database as db - class_ = getattr(db, property_type) db_property = class_() db_property.link(properties) @@ -247,26 +319,28 @@ def store_property( calculation.set_results(results) return db_property - def check_duplicate_property(self, structure, properties, property_name, model) -> object: + @staticmethod + def check_duplicate_property(structure: db.Structure, properties: db.Collection, property_name: str, + model: db.Model) -> Union[db.ID, bool]: """ Checks for a property that is an exact match for the one queried here. Exact match meaning that key and model both are matches. Parameters ---------- - properties :: db.Collection (Scine::Database::Collection) + properties : db.Collection (Scine::Database::Collection) The collection housing all properties. - property_name :: str + property_name : str The name (key) of the queried property, e.g. ``electronic_energy``. - model :: db.Model (Scine::Database::Model) + model : db.Model (Scine::Database::Model) The model used in the calculation that resulted in this property. - structure :: db.Structure (Scine::Database::Structure) + structure : db.Structure (Scine::Database::Structure) The structure to be checked in. The structure has to be linked to its collection. Returns ------- - ID :: db.ID (Scine::Database::ID) + ID : db.ID (Scine::Database::ID) Returns ``False`` if there is no existing property like the one queried or the ID of the first duplicate. """ @@ -275,32 +349,19 @@ def check_duplicate_property(self, structure, properties, property_name, model) return hits[0] return False - def configure_run(self, manager, calculation, config: Configuration): + def configure_run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> None: """ Configures a job for a given Calculation to do tasks in the run function Parameters ---------- - manager :: db.Manager (Scine::Database::Manager) + manager : db.Manager (Scine::Database::Manager) The manager of the database holding all collections - calculation :: db.Calculation (Scine::Database::Calculation) + calculation : db.Calculation (Scine::Database::Calculation) The calculation to be performed - config :: Configuration + config : Configuration The configuration of the Puffin doing the job """ - self.get_collections(manager) - self.set_calculation(calculation) - self.config = config - - def get_collections(self, manager) -> None: - """ - Saves Scine Database collections as class variables - - Parameters - ---------- - manager :: db.Manager (Scine::Database::Manager) - The manager of the database holding all collections - """ self._manager = manager self._calculations = manager.get_collection("calculations") self._compounds = manager.get_collection("compounds") @@ -309,20 +370,29 @@ def get_collections(self, manager) -> None: self._reactions = manager.get_collection("reactions") self._structures = manager.get_collection("structures") self._flasks = manager.get_collection("flasks") - - def set_calculation(self, calculation) -> None: - """ - Sets the current Calculation for this job and ensures connection - - Parameters - ---------- - calculation :: db.Calculation (Scine::Database::Calculation) - The calculation to be carried out - """ self._calculation = calculation if not self._calculation.has_link(): self._calculation.link(self._calculations) + self.config = config + @classmethod + def check_configuration(cls: Type[T], instance: T) -> TypeGuard[T]: + return hasattr(instance, "work_dir") and \ + hasattr(instance, "stdout_path") and \ + hasattr(instance, "stderr_path") and \ + hasattr(instance, "config") and \ + hasattr(instance, "_id") and \ + hasattr(instance, "_calculation") and \ + hasattr(instance, "_manager") and \ + hasattr(instance, "_calculations") and \ + hasattr(instance, "_compounds") and \ + hasattr(instance, "_elementary_steps") and \ + hasattr(instance, "_properties") and \ + hasattr(instance, "_reactions") and \ + hasattr(instance, "_structures") and \ + hasattr(instance, "_flasks") + + @is_configured def capture_raw_output(self) -> Tuple[str, str]: """ Tries to capture the raw output of the calculation context and save it in the raw_output field of the @@ -351,22 +421,22 @@ def capture_raw_output(self) -> Tuple[str, str]: return "", raw_err return raw_out, raw_err + @requires('database') + @is_configured def complete_job(self) -> None: """ Saves the executing Puffin, changes status to db.Status.COMPLETE. """ - import scine_database as db - self.capture_raw_output() self._calculation.set_executor(self.config["daemon"]["uuid"]) self._calculation.set_status(db.Status.COMPLETE) + @is_configured + @requires('database') def fail_job(self) -> None: """ Saves the executing Puffin, changes status to db.Status.FAILED. """ - import scine_database as db - self._calculation.set_executor(self.config["daemon"]["uuid"]) self._calculation.set_status(db.Status.FAILED) _, error = self.capture_raw_output() @@ -404,68 +474,9 @@ def success_file(self) -> str: return os.path.join(self.work_dir, "success") -class TurbomoleJob(Job): - """ - A common interface for all jobs in Puffin that use Turbomole. - """ - - def __init__(self): - super().__init__() - self.input_structure = "system.xyz" - - env = os.environ.copy() - - self.turboexe = "" - self.turboscripts = "" - self.smp_turboexe = "" - - if "TURBODIR" in env.keys(): - if env["TURBODIR"]: - if os.environ.get("PARA_ARCH") is not None: - del os.environ["PARA_ARCH"] - if os.path.exists(os.path.join(env["TURBODIR"], "scripts", "sysname")): - self.sysname = ( - subprocess.check_output(os.path.join(env["TURBODIR"], "scripts", "sysname")) - .decode("utf-8", errors='replace') - .rstrip() - ) - self.sysname_parallel = self.sysname + "_smp" - self.turboexe = os.path.join(env["TURBODIR"], "bin", self.sysname) - self.smp_turboexe = os.path.join(env["TURBODIR"], "bin", self.sysname_parallel) - self.turboscripts = os.path.join(env["TURBODIR"], "scripts") - else: - raise RuntimeError("TURBODIR not assigned correctly. Check spelling or empty the env variable.") - - def prepare_calculation(self, structure, calculation_settings, model, job): - - import scine_utilities as utils - from scine_puffin.utilities.turbomole_helper import TurbomoleHelper - - tm_helper = TurbomoleHelper() - - # Write xyz file - utils.io.write(self.input_structure, structure.get_atoms()) - # Write coord file - tm_helper.write_coord_file(calculation_settings) - # Check if settings are available - tm_helper.check_settings_availability(job, calculation_settings) - # Generate input file for preprocessing tool 'define' - tm_helper.prepare_define_session(structure, model, calculation_settings, job) - # Initialize via define - tm_helper.initialize(model, calculation_settings) - - def run(self, manager, calculation, config: Configuration) -> bool: - """See Job.run()""" - raise NotImplementedError - - @staticmethod - def required_programs() -> List[str]: - """See Job.required_programs()""" - raise NotImplementedError - - @contextmanager -def calculation_context(job: Job, stdout_name="output", stderr_name="errors", debug: Optional[bool] = None): +def calculation_context(job, stdout_name: str = "output", stderr_name: str = "errors", + debug: Optional[bool] = None) -> Iterator: """ A context manager for a types of calculations that are run externally and may fail, dump large amounts of files or do other nasty things. @@ -482,15 +493,15 @@ def calculation_context(job: Job, stdout_name="output", stderr_name="errors", de Parameters ---------- - job :: Job + job : Job The job holding the working directory and receiving the output and error paths - stdout_name :: str + stdout_name : str Name of the file that the stdout stream should be redirected to. The file will be generated in the given scratch directory. - stderr_name :: str + stderr_name : str Name of the file that the stderr stream should be redirected to. The file will be generated in the given scratch directory. - debug :: bool + debug : bool If not given, will be taken from Job Configuration (config['daemon']['mode']) If true, runs in debug mode, disabling all redirections. @@ -601,7 +612,7 @@ class breakable(object): class Break(Exception): """Break out of the with statement""" - def __init__(self, value): + def __init__(self, value) -> None: self.value = value def __enter__(self): diff --git a/scine_puffin/jobs/templates/kinetic_modeling_jobs.py b/scine_puffin/jobs/templates/kinetic_modeling_jobs.py index 1e6016d..9bcc679 100644 --- a/scine_puffin/jobs/templates/kinetic_modeling_jobs.py +++ b/scine_puffin/jobs/templates/kinetic_modeling_jobs.py @@ -1,24 +1,31 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -import numpy as np -from typing import List +from abc import ABC +from typing import List, TYPE_CHECKING -import scine_database as db +import numpy as np -from ..templates.job import Job, job_configuration_wrapper +from ..templates.job import Job from ...utilities.compound_and_flask_helpers import get_compound_or_flask -from scine_puffin.config import Configuration +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") -class KineticModelingJob(Job): + +class KineticModelingJob(Job, ABC): """ Abstract base class for the RMS kinetic modeling and KiNetX kinetic modeling jobs. """ - def __init__(self): + + def __init__(self) -> None: super().__init__() self.name = "KineticModelingJob" self.model: db.Model = db.Model("PM6", "PM6", "") @@ -29,33 +36,15 @@ def __init__(self): self.r_forward_label = "_forward_edge_flux" self.r_backward_label = "_backward_edge_flux" - @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - """See Job.run()""" - raise NotImplementedError - - @staticmethod - def required_programs(): - raise NotImplementedError - def _write_concentrations_to_centroids(self, aggregate_ids: List[db.ID], aggregate_types: List[db.CompoundOrFlask], reaction_ids: List[db.ID], aggregate_wise_concentrations: List[np.ndarray], reaction_wise_concentrations: List[np.ndarray], aggregate_wise_labels: List[str], reaction_wise_labels: List[str], results: db.Results, - post_fix: str = "", - add_flask_result_to_compounds: bool = False): + post_fix: str = ""): assert len(aggregate_wise_concentrations) == len(aggregate_wise_labels) assert len(reaction_wise_concentrations) == len(reaction_wise_labels) - if add_flask_result_to_compounds: - original_concentration_data = np.zeros((len(aggregate_ids), len(aggregate_wise_concentrations))) - for i, concentrations in enumerate(aggregate_wise_concentrations): - original_concentration_data[:, 0] = concentrations - concentration_data = self._resolve_flask_to_compound_mapping(original_concentration_data, aggregate_ids, - aggregate_types) - for i, concentrations in enumerate(aggregate_wise_concentrations): - concentrations = concentration_data[:, i] print("Concentration Properties") for i, (a_id, a_type) in enumerate(zip(aggregate_ids, aggregate_types)): @@ -75,29 +64,16 @@ def _write_concentrations_to_centroids(self, aggregate_ids: List[db.ID], aggrega label = r_id.string() + concentration_label + post_fix self._write_concentration_property(centroid, label, c, results) - def _write_concentration_property(self, centroid: db.Structure, label: str, value: float, results: db.Results): + @requires("database") + def _write_concentration_property(self, centroid: db.Structure, label: str, value: float, results: db.Results) \ + -> None: prop = db.NumberProperty.make(label, self.model, value, self._properties) results.add_property(prop.id()) centroid.add_property(label, prop.id()) prop.set_structure(centroid.id()) print("struc", centroid.id().string(), " prop ", prop.id().string(), " ", label, " ", value) - def _resolve_flask_to_compound_mapping(self, concentration_data, aggregate_id_list, - aggregate_type_list): - i = 0 - new_concentration_data = np.copy(concentration_data) - for a_id, a_type in zip(aggregate_id_list, aggregate_type_list): - if a_type == db.CompoundOrFlask.FLASK: - flask = db.Flask(a_id, self._flasks) - compounds_in_flask = flask.get_compounds() - for c_id in compounds_in_flask: - if c_id in aggregate_id_list: - j = aggregate_id_list.index(c_id) - new_concentration_data[j, :] += concentration_data[i, :] - i += 1 - return new_concentration_data - - def _disable_all_aggregates(self): + def _disable_all_aggregates(self) -> None: """ Disable the exploration of all aggregates. """ @@ -108,12 +84,14 @@ def _disable_all_aggregates(self): flask.link(self._flasks) flask.disable_exploration() - def _get_reaction_centroid(self, r_id): + @requires("database") + def _get_reaction_centroid(self, r_id: db.ID) -> db.Structure: a_id = db.Reaction(r_id, self._reactions).get_reactants(db.Side.LHS)[0][0] a_type = db.Reaction(r_id, self._reactions).get_reactant_types(db.Side.LHS)[0][0] aggregate = get_compound_or_flask(a_id, a_type, self._compounds, self._flasks) return db.Structure(aggregate.get_centroid(), self._structures) - def _get_aggregate_centroid(self, a_id, a_type): + @requires("database") + def _get_aggregate_centroid(self, a_id: db.ID, a_type: db.CompoundOrFlask) -> db.Structure: aggregate = get_compound_or_flask(a_id, a_type, self._compounds, self._flasks) return db.Structure(aggregate.get_centroid(), self._structures) diff --git a/scine_puffin/jobs/templates/scine_connectivity_job.py b/scine_puffin/jobs/templates/scine_connectivity_job.py index d5d2b71..b2c4815 100644 --- a/scine_puffin/jobs/templates/scine_connectivity_job.py +++ b/scine_puffin/jobs/templates/scine_connectivity_job.py @@ -1,49 +1,84 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -from typing import Dict, Set, Tuple, Optional, Union, List, Any +from abc import ABC +from typing import Dict, Set, Tuple, Optional, Union, List, Any, TYPE_CHECKING import sys -import scine_database as db -import scine_utilities as utils - -from .job import job_configuration_wrapper +from .job import is_configured from .scine_job import ScineJob -from scine_puffin.config import Configuration from scine_puffin.utilities import masm_helper - - -class ConnectivityJob(ScineJob): +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") +if module_exists("scine_readuct") or TYPE_CHECKING: + import scine_readuct as readuct +else: + readuct = MissingDependency("scine_readuct") +if module_exists("scine_molassembler") or TYPE_CHECKING: + import scine_molassembler as masm +else: + masm = MissingDependency("scine_molassembler") + + +class ConnectivityJob(ScineJob, ABC): """ A common interface for all jobs in Puffin that use the Scine::Core::Calculator interface and aim at deriving some form of bonding information within a job. This can be simple bond orders and/or a full Molassembler graph. """ - def __init__(self): + own_expected_results = ["energy", "bond_orders"] + + def __init__(self) -> None: super().__init__() self.name = "ConnectivityJob" - self.own_expected_results = ["energy", "bond_orders"] - self.connectivity_settings = { + self.connectivity_settings: Dict[str, Any] = { "only_distance_connectivity": False, # determine connectivity solely based on distances "sub_based_on_distance_connectivity": True, # remove connectivity based on distances "add_based_on_distance_connectivity": True, # add connectivity based on distances "enforce_bond_order_model": True, # Whether bond orders must have an identical model "dihedral_retries": 100, # Number of attempts to generate the dihedral decision during conformer generation + "n_surface_atom_threshold": 1, # required surface atoms for a structure to be labelled as a surface } - @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - """See Job.run()""" - raise NotImplementedError + @classmethod + def optional_settings_doc(cls) -> str: + return super().optional_settings_doc() + """ + These connectivity-based settings are recognized: + + add_based_on_distance_connectivity : bool + Whether to add the connectivity (i.e. add bonds) as derived from + atomic distances when graphs are generated. (default: True) + sub_based_on_distance_connectivity : bool + Whether to subtract the connectivity (i.e. remove bonds) as derived from + atomic distances when graphs are generated. (default: True) + only_distance_connectivity : bool + Whether to impose the connectivity solely from distances. (default: False) + enforce_bond_order_model : bool + Whether bond orders must have an identical model + dihedral_retries : int + Number of attempts to generate the dihedral decision during conformer generation + n_surface_atom_threshold : int + required surface atoms for a structure to be labelled as a surface + """ @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "molassembler", "readuct", "utils"] + @is_configured def connectivity_settings_from_only_connectivity_settings(self) -> None: """ Overwrite default connectivity settings based on settings of configured Calculation and expect no other @@ -63,7 +98,7 @@ def connectivity_settings_from_only_connectivity_settings(self) -> None: + " was/were not recognized." ) - def extract_connectivity_settings_from_dict(self, dictionary: Dict[str, bool]) -> None: + def extract_connectivity_settings_from_dict(self, dictionary: Dict[str, Any]) -> None: """ Overwrite default connectivity settings based on given dictionary and removes those from the dictionary. @@ -71,9 +106,12 @@ def extract_connectivity_settings_from_dict(self, dictionary: Dict[str, bool]) - for key, value in self.connectivity_settings.items(): self.connectivity_settings[key] = dictionary.pop(key, value) - def make_bond_orders_from_calc(self, systems: dict, key: str, + @is_configured + @requires("database") + def make_bond_orders_from_calc(self, + systems: Dict[str, Optional[utils.core.Calculator]], key: str, surface_indices: Optional[Union[List[int], Set[int]]] = None) \ - -> Tuple[utils.BondOrderCollection, Dict[str, utils.core.Calculator]]: + -> Tuple[utils.BondOrderCollection, Dict[str, Optional[utils.core.Calculator]]]: """ Gives bond orders for the specified system based on the connectivity settings of this class. @@ -84,25 +122,24 @@ def make_bond_orders_from_calc(self, systems: dict, key: str, Parameters ---------- - systems :: Dict[str, utils.core.Calculator] + systems : Dict[str, Optional[utils.core.Calculator]] Dictionary of system names to calculators representing them - key :: str + key : str Index into systems dictionary to get bond orders for - surface_indices :: Optional[Union[List[int], Set[int]]] + surface_indices : Optional[Union[List[int], Set[int]]] The indices of the atoms for which the rules of solid state atoms shall be applied. Returns ------- - bond_orders :: utils.BondOrderCollection (Scine::Utilties::BondOrderCollection) + bond_orders : utils.BondOrderCollection (Scine::Utilties::BondOrderCollection) The bond orders of the system - systems :: Dict[str, utils.core.Calculator] + systems : Dict[str, Optional[utils.core.Calculator]] Dictionary of system names to calculators representing them, updated with the results of the single point calculation requesting bond orders. """ - import scine_readuct as readuct # Distance based bond orders if self.connectivity_settings["only_distance_connectivity"]: - bond_orders = self.distance_bond_orders(systems[key].structure, surface_indices) + bond_orders = self.distance_bond_orders(self.get_calc(key, systems).structure, surface_indices) # Bond order calculation with readuct else: if not self.expected_results_check(systems, [key], ["energy", "bond_orders", "atomic_charges"])[0]: @@ -113,13 +150,14 @@ def make_bond_orders_from_calc(self, systems: dict, key: str, self.throw_if_not_successful( success, systems, [key], ["energy", "bond_orders", "atomic_charges"] ) - bond_orders = systems[key].get_results().bond_orders + bond_orders = self.get_calc(key, systems).get_results().bond_orders # type: ignore return bond_orders, systems - def make_graph_from_calc(self, systems: dict, key: str, + @is_configured + def make_graph_from_calc(self, systems: Dict[str, Optional[utils.core.Calculator]], key: str, surface_indices: Optional[Union[List[int], Set[int]]] = None) \ - -> Tuple[str, Dict[str, utils.core.Calculator]]: + -> Tuple[str, Dict[str, Optional[utils.core.Calculator]]]: """ Runs bond orders for the specified name in the dictionary of systems if not present already and return cbor graph for based on them. @@ -131,20 +169,19 @@ def make_graph_from_calc(self, systems: dict, key: str, Parameters ---------- - systems :: Dict[str, utils.core.Calculator] + systems : Dict[str, Optional[utils.core.Calculator]] Dictionary of system names to calculators representing them - key :: str + key : str Index into systems dictionary to get bond orders for - surface_indices :: Optional[Union[List[int], Set[int]]] + surface_indices : Optional[Union[List[int], Set[int]]] The indices of the atoms for which the rules of solid state atoms shall be applied. Returns ------- - graph_cbor :: str + graph_cbor : str Serialized representation of interpreted molassembler molecule. - systems :: Dict[str, utils.core.Calculator] + systems : Dict[str, Optional[utils.core.Calculator]] Dictionary of system names to calculators representing them, - """ if surface_indices is None: @@ -154,7 +191,7 @@ def make_graph_from_calc(self, systems: dict, key: str, start_structures = [db.Structure(s, self._structures) for s in self._calculation.get_structures()] n_start_atoms = sum(len(s.get_atoms()) for s in start_structures if s.get_label() != db.Label.SURFACE_ADSORPTION_GUESS) - n_system_atoms = len(systems[key].structure) + n_system_atoms = len(self.get_calc(key, systems).structure) if n_system_atoms == n_start_atoms: surface_indices = all_indices else: @@ -168,23 +205,27 @@ def make_graph_from_calc(self, systems: dict, key: str, self.raise_named_exception(f"Start structures of calculation includes surface indices, " f"but these could not propagated to the given system {key}") if self.connectivity_settings["only_distance_connectivity"]: - bond_orders = self.distance_bond_orders(systems[key].structure, surface_indices) + bond_orders = self.distance_bond_orders(self.get_calc(key, systems).structure, surface_indices) else: - bond_orders = systems[key].get_results().bond_orders + bond_orders = self.get_calc(key, systems).get_results().bond_orders # type: ignore if bond_orders is None: bond_orders, systems = self.make_bond_orders_from_calc(systems, key, surface_indices) - pbc_string = systems[key].settings.get(utils.settings_names.periodic_boundaries, "") + pbc_string = self.get_calc(key, systems).settings.get(utils.settings_names.periodic_boundaries, "") + if not isinstance(pbc_string, str): + self.raise_named_exception("Periodic boundaries setting is not a string.") + raise RuntimeError("Unreachable") # just for linters return masm_helper.get_cbor_graph( - systems[key].structure, + self.get_calc(key, systems).structure, bond_orders, self.connectivity_settings, pbc_string, surface_indices ), systems - def make_masm_result_from_calc(self, systems: dict, key: str, + @is_configured + def make_masm_result_from_calc(self, systems: Dict[str, Optional[utils.core.Calculator]], key: str, unimportant_atoms: Optional[Union[List[int], Set[int]]]) \ - -> Tuple[Any, Dict[str, utils.core.Calculator]]: + -> Tuple[masm.interpret.MoleculesResult, Dict[str, Optional[utils.core.Calculator]]]: """ Gives Molassembler interpret result for the specified system based on the connectivity settings of this class. @@ -196,34 +237,38 @@ def make_masm_result_from_calc(self, systems: dict, key: str, Parameters ---------- - systems :: Dict[str, utils.core.Calculator] + systems : Dict[str, Optional[utils.core.Calculator]] Dictionary of system names to calculators representing them - key :: str + key : str Index into systems dictionary to get bond orders for - unimportant_atoms :: Optional[Union[List[int], Set[int]]] + unimportant_atoms : Optional[Union[List[int], Set[int]]] The indices of atoms for which no stereopermutators shall be determined. Returns ------- - masm_result :: masm.interpret.MoleculesResult (Scine::Molassembler::interpret::MoleculesResult) + masm_result : masm.interpret.MoleculesResult (Scine::Molassembler::interpret::MoleculesResult) The interpretation result - systems :: Dict[str, utils.core.Calculator] + systems : Dict[str, Optional[utils.core.Calculator]] Dictionary of system names to calculators representing them, updated with the results of the single point calculation requesting bond orders. """ bond_orders, systems = self.make_bond_orders_from_calc(systems, key, unimportant_atoms) - pbc_string = systems[key].settings.get(utils.settings_names.periodic_boundaries, "") + pbc_string = self.get_calc(key, systems).settings.get(utils.settings_names.periodic_boundaries, "") + if not isinstance(pbc_string, str): + self.raise_named_exception("Periodic boundaries setting is not a string.") + raise RuntimeError("Unreachable") # just for linters return masm_helper.get_molecules_result( - systems[key].structure, + self.get_calc(key, systems).structure, bond_orders, self.connectivity_settings, pbc_string, unimportant_atoms=unimportant_atoms ), systems - def make_decision_lists_from_calc(self, systems: dict, key: str, - surface_indices: Optional[Union[List[int], Set[int]]] = None) \ - -> Tuple[List[str], Dict[str, utils.core.Calculator]]: + @is_configured + def make_decision_lists_from_calc(self, systems: Dict[str, Optional[utils.core.Calculator]], + key: str, surface_indices: Optional[Union[List[int], Set[int]]] = None) \ + -> Tuple[List[str], Dict[str, Optional[utils.core.Calculator]]]: """ Calculates bond orders for the specified name in the dictionary of systems if not present already. @@ -236,38 +281,42 @@ def make_decision_lists_from_calc(self, systems: dict, key: str, Parameters ---------- - systems :: Dict[str, utils.core.Calculator] + systems : Dict[str, Optional[utils.core.Calculator]] Dictionary of system names to calculators representing them - key :: str + key : str Index into systems dictionary to get bond orders for - surface_indices :: Optional[Union[List[int], Set[int]]] + surface_indices : Optional[Union[List[int], Set[int]]] The indices of the atoms for which the rules of solid state atoms shall be applied. Returns ------- - decision_lists :: List[str] + decision_lists : List[str] Decision lists per molecule in structure. - systems :: Dict[str, utils.core.Calculator] + systems : Dict[str, Optional[utils.core.Calculator]] Dictionary of system names to calculators representing them, updated with the results of a possible single point calculation requesting bond orders. """ if self.connectivity_settings["only_distance_connectivity"]: - bond_orders = self.distance_bond_orders(systems[key].structure, surface_indices) + bond_orders = self.distance_bond_orders(self.get_calc(key, systems).structure, surface_indices) else: - bond_orders = systems[key].get_results().bond_orders + bond_orders = self.get_calc(key, systems).get_results().bond_orders # type: ignore if bond_orders is None: bond_orders, systems = self.make_bond_orders_from_calc(systems, key, surface_indices) - pbc_string = systems[key].settings.get(utils.settings_names.periodic_boundaries, "") + pbc_string = self.get_calc(key, systems).settings.get(utils.settings_names.periodic_boundaries, "") + if not isinstance(pbc_string, str): + self.raise_named_exception("Periodic boundaries setting is not a string.") + raise RuntimeError("Unreachable") # just for linters return masm_helper.get_decision_lists( - systems[key].structure, + self.get_calc(key, systems).structure, bond_orders, self.connectivity_settings, pbc_string, surface_indices ), systems + @is_configured def surface_indices_all_structures(self, start_structures: Optional[List[db.ID]] = None) -> Set[int]: """ Get the combined surface indices of all structures of the configured calculation except a @@ -281,13 +330,13 @@ def surface_indices_all_structures(self, start_structures: Optional[List[db.ID]] Parameters ---------- - start_structures :: Optional[List[db.ID]] + start_structures : Optional[List[db.ID]] Optional list of the starting structure ids. If no list is given. The input structures of the calculation are used. Returns ------- - surface_indices :: set + surface_indices : set A set of all surface indices over all structures combined assuming an atom ordering identical to the addition of all structures in their order within the calculation. """ @@ -318,7 +367,8 @@ def surface_indices(self, structure: db.Structure) -> Set[int]: surface_indices = set() return surface_indices - def distance_bond_orders(self, structure: db.Structure, + @is_configured + def distance_bond_orders(self, structure: Union[db.Structure, utils.AtomCollection], surface_indices: Optional[Union[List[int], Set[int]]] = None) -> utils.BondOrderCollection: """ Construct bond order solely based on distance for either an AtomCollection or a Database Structure. @@ -330,14 +380,14 @@ def distance_bond_orders(self, structure: db.Structure, Parameters ---------- - structure :: Union[utils.AtomCollection, db.Structure] + structure : Union[utils.AtomCollection, db.Structure] Either an AtomCollection or a structure for which distance based bond orders are constructed. - surface_indices :: Optional[Union[List[int], Set[int]]] + surface_indices : Optional[Union[List[int], Set[int]]] The indices of the atoms for which the rules of solid state atoms shall be applied. Returns ------- - bond_orders :: utils.BondOrderCollection (Scine::Utilties::BondOrderCollection) + bond_orders : utils.BondOrderCollection (Scine::Utilties::BondOrderCollection) The bond orders of the structure. """ if isinstance(structure, db.Structure): @@ -352,7 +402,7 @@ def distance_bond_orders(self, structure: db.Structure, self.raise_named_exception( "Unknown type of provided structure for distance bond orders." ) - return # actually unreached, just avoid lint errors + return utils.BondOrderCollection(0) # actually unreached, just avoid lint errors model = self._calculation.get_model() # generate bond orders depending on model and surface atoms @@ -363,7 +413,7 @@ def distance_bond_orders(self, structure: db.Structure, bond_orders = ps.construct_bond_orders() elif surface_indices: # Use mixture of Nearest Neighbors and BondDetector - bond_orders = utils.SolidStateBondDetector.detect_bonds(atoms, surface_indices) + bond_orders = utils.SolidStateBondDetector.detect_bonds(atoms, set(surface_indices)) else: bond_orders = utils.BondDetector.detect_bonds(atoms) return bond_orders @@ -375,11 +425,11 @@ def add_graph(self, structure: db.Structure, bond_orders: utils.BondOrderCollect Parameters ---------- - structure :: Union[utils.AtomCollection, db.Structure] + structure : Union[utils.AtomCollection, db.Structure] Either an AtomCollection or a structure for which distance based bond orders are constructed. - bond_orders :: utils.BondOrderCollection (Scine::Utilties::BondOrderCollection) + bond_orders : utils.BondOrderCollection (Scine::Utilities::BondOrderCollection) The bond orders of the structure. - surface_indices :: Optional[Union[List[int], Set[int]]] + surface_indices : Optional[Union[List[int], Set[int]]] The indices of the atoms for which the rules of solid state atoms shall be applied. """ print("\nGenerating Molassembler graphs") @@ -402,6 +452,8 @@ def add_graph(self, structure: db.Structure, bond_orders: utils.BondOrderCollect if structure.has_graph("masm_decision_list"): print("masm_decision_list: " + structure.get_graph("masm_decision_list")) + @is_configured + @requires("database") def query_bond_orders(self, structure: db.Structure) -> db.SparseMatrixProperty: """ Query the given Database structure for bond orders based on the model of the configured calculation @@ -413,11 +465,12 @@ def query_bond_orders(self, structure: db.Structure) -> db.SparseMatrixProperty: Parameters ---------- - structure :: db.Structure (Scine::Database::Structure) + structure : db.Structure (Scine::Database::Structure) A database structure to query. + Returns ------- - db_bond_orders :: db.SparseMatrixProperty (Scine::Database::SparseMatrixProperty) + db_bond_orders : db.SparseMatrixProperty (Scine::Database::SparseMatrixProperty) A database property holding bond orders. """ # db bond orders @@ -434,16 +487,46 @@ def query_bond_orders(self, structure: db.Structure) -> db.SparseMatrixProperty: return db_bond_orders @staticmethod + @requires("utilities") def bond_orders_from_db_bond_orders(structure: db.Structure, db_bond_orders: db.SparseMatrixProperty) \ -> utils.BondOrderCollection: """ A shortcut to construct a BondOrderCollection from a Database Property holding bond orders. + Returns ------- - bond_orders :: utils.BondOrderCollection (Scine::Utilties::BondOrderCollection) + bond_orders : utils.BondOrderCollection (Scine::Utilities::BondOrderCollection) The bond orders of the structure. """ atoms = structure.get_atoms() bond_orders = utils.BondOrderCollection(len(atoms)) bond_orders.matrix = db_bond_orders.get_data() return bond_orders + + def _cbor_graph_from_structure(self, structure: db.Structure) -> str: + """ + Retrieve masm_cbor_graph from a database structure and throws error if none present. + + Parameters + ---------- + structure : db.Structure + + Returns + ------- + masm_cbor_graph : str + """ + if not structure.has_graph("masm_cbor_graph"): + self.raise_named_exception(f"Missing graph in structure {str(structure.id())}.") + return structure.get_graph("masm_cbor_graph") + + @requires("database") + def _determine_new_label_based_on_graph_and_surface_indices(self, graph_str: str, + surface_indices: Union[List[int], Set[int], None]) \ + -> db.Label: + graph_is_split = ";" in graph_str + no_surf_split_decision_label = db.Label.COMPLEX_OPTIMIZED if graph_is_split else db.Label.MINIMUM_OPTIMIZED + surf_split_decision_label = db.Label.SURFACE_COMPLEX_OPTIMIZED if graph_is_split else db.Label.SURFACE_OPTIMIZED + thresh = self.connectivity_settings["n_surface_atom_threshold"] + if surface_indices is not None and len(surface_indices) > thresh: + return surf_split_decision_label + return no_surf_split_decision_label diff --git a/scine_puffin/jobs/templates/scine_hessian_job.py b/scine_puffin/jobs/templates/scine_hessian_job.py index 13de77f..2846bc4 100644 --- a/scine_puffin/jobs/templates/scine_hessian_job.py +++ b/scine_puffin/jobs/templates/scine_hessian_job.py @@ -1,40 +1,47 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -import numpy as np - -import scine_database as db -import scine_utilities as utils +from abc import ABC +from typing import TYPE_CHECKING, List +import numpy as np -from .job import job_configuration_wrapper from .scine_job import ScineJob -from scine_puffin.config import Configuration +from .job import is_configured +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") -class HessianJob(ScineJob): +class HessianJob(ScineJob, ABC): """ A common interface for all jobs in Puffin that use the Scine::Core::Calculator interface to calculate a Hessian and carry out a Thermochemistry analysis. """ - def __init__(self): + own_expected_results = ["energy", "hessian", "thermochemistry"] + + def __init__(self) -> None: super().__init__() self.name = "HessianJob" - self.own_expected_results = ["energy", "hessian", "thermochemistry"] - - @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - """See Job.run()""" - raise NotImplementedError @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "readuct", "utils"] + @is_configured + @requires("utilities") def store_hessian_data(self, system: utils.core.Calculator, structure: db.Structure) -> None: """ Stores results from a Hessian calculation and Thermochemistry for the specified structure based on the given @@ -46,9 +53,9 @@ def store_hessian_data(self, system: utils.core.Calculator, structure: db.Struct Parameters ---------- - system :: core.calculator (Scine::Core::Calculator) + system : utils.core.Calculator (Scine::Core::Calculator) A Scine calculator holding a results object with energy, Hessian, and Thermochemistry properties. - structure :: db.Structure (Scine::Database::Structure) + structure : db.Structure (Scine::Database::Structure) A structure for which the property is saved. """ results = system.get_results() @@ -108,7 +115,6 @@ def store_hessian_data(self, system: utils.core.Calculator, structure: db.Struct thermo_calculator.set_temperature(float(model.temperature)) thermo_calculator.set_pressure(float(model.pressure)) thermo_container = thermo_calculator.calculate() - self.store_property( self._properties, "gibbs_free_energy", diff --git a/scine_puffin/jobs/templates/scine_job.py b/scine_puffin/jobs/templates/scine_job.py index 5e7a1d3..0034b58 100644 --- a/scine_puffin/jobs/templates/scine_job.py +++ b/scine_puffin/jobs/templates/scine_job.py @@ -1,22 +1,36 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -from typing import List, Tuple, Optional +from abc import ABC +from typing import Dict, List, Tuple, Optional, TYPE_CHECKING, Type -import scine_database as db -import scine_utilities as utils - -from .job import Job, job_configuration_wrapper -from scine_puffin.config import Configuration +from .job import Job, is_configured from scine_puffin.utilities.scine_helper import SettingsManager, update_model from scine_puffin.utilities.program_helper import ProgramHelper from scine_puffin.utilities.transfer_helper import TransferHelper +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") + + +class CalculatorNotPresentException(Exception): + """ + Exception to be raised when a calculator is not present in a system dictionary. + """ -class ScineJob(Job): +class ScineJob(Job, ABC): """ A common interface for all jobs in Puffin that use the Scine::Core::Calculator interface. @@ -24,10 +38,11 @@ class ScineJob(Job): This interface also holds subclasses that include elementary workflows, which can be combined to build complex jobs """ - def __init__(self): + own_expected_results: List[str] = [] # to be overwritten by child class + + def __init__(self) -> None: super().__init__() self.name = self.__class__.__name__ - self.own_expected_results = [] # to be overwritten by child class # to be added by child class: self.properties_to_transfer = [ "surface_atom_indices", @@ -35,33 +50,28 @@ def __init__(self): ] self._fallback_error = "Error: " + self.name + " failed with an unspecified error." - @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - """See Job.run()""" - raise NotImplementedError + @staticmethod + def required_programs() -> List[str]: + return ["database", "readuct", "utils"] - def expected_results(self) -> List[str]: + @classmethod + def expected_results(cls) -> List[str]: """ Gives a list of str specifying the results expected to be present for a system within a job based on the class member and all its parents. Returns ------- - expected_results :: List[str] + expected_results : List[str] The results to be expected as str corresponding to the members of the Scine::Utils::Results class. """ parent_expects = [] - for Parent in self.__class__.__bases__: - if "expected_results" in dir(Parent): - parent = Parent() - parent_expects += parent.expected_results() # pylint: disable=no-member - return list(set(parent_expects + self.own_expected_results)) - - @staticmethod - def required_programs() -> List[str]: - """See Job.required_programs()""" - raise NotImplementedError + for Parent in cls.__bases__: + if hasattr(Parent, "expected_results"): + parent_expects += Parent.expected_results() # pylint: disable=no-member + return list(set(parent_expects + cls.own_expected_results)) + @is_configured def create_helpers(self, structure: db.Structure) -> Tuple[SettingsManager, Optional[ProgramHelper]]: """ Creates a Scine SettingsManager and ProgramHelper based on the configured job and the given structure. @@ -74,12 +84,12 @@ def create_helpers(self, structure: db.Structure) -> Tuple[SettingsManager, Opti Parameters ---------- - structure :: db.Structure (Scine::Database::Structure) + structure : db.Structure (Scine::Database::Structure) The structure on which a Calculation is performed. Returns ------- - helper_tuple :: Tuple[SettingsManager, Optional[ProgramHelper] + helper_tuple : Tuple[SettingsManager, Optional[ProgramHelper] A tuple of the SettingsManager for Scine Calculators and ProgramHelper if available. """ model = self._calculation.get_model() @@ -88,17 +98,18 @@ def create_helpers(self, structure: db.Structure) -> Tuple[SettingsManager, Opti program_helper = ProgramHelper.get_correct_helper(program, self._manager, structure, self._calculation) return settings_manager, program_helper - def raise_named_exception(self, error_message: str) -> None: + def raise_named_exception(self, error_message: str, exception_type: Type[BaseException] = BaseException) -> None: """ Raises an error including the name/description of the current job. """ error_begin = "Error: " + self.name + " failed with message:\n" - raise BaseException(error_begin + error_message) + raise exception_type(error_begin + error_message) + @is_configured def throw_if_not_successful( self, success: bool, - systems: dict, + systems: Dict[str, Optional[utils.core.Calculator]], keys: List[str], expected_results: Optional[List[str]] = None, sub_task_error_line: str = "", @@ -113,16 +124,16 @@ def throw_if_not_successful( Parameters ---------- - success :: bool + success : bool Whether the calculation was successful (either forwarded from readuct task or specified if not relevant). - systems :: Dict[str, core.calculator] (Scine::Core::Calculator) + systems : Dict[str, Optional[utils.core.Calculator]] (Scine::Core::Calculator) The dictionary holding calculators. - keys :: List[str] + keys : List[str] The list of keys of the systems dict to be checked. - expected_results :: Optional[List[str]] + expected_results : Optional[List[str]] The results to be required to be present in systems to qualify as successful calculations. If None is given, this defaults to the expected results of the class, see expected_results(). - sub_task_error_line :: str + sub_task_error_line : str An additional line for the error message to specify in which subtask the calculation crashed. """ self.verify_connection() @@ -136,13 +147,14 @@ def throw_if_not_successful( error_message = error_begin + error_message raise BaseException(error_message) + @is_configured def calculation_postprocessing( self, success: bool, - systems: dict, + systems: Dict[str, Optional[utils.core.Calculator]], keys: List[str], expected_results: Optional[List[str]] = None, - ): + ) -> db.Results: """ Performs a verification protocol that a Scine Calculation was successful. If not throws an exception, if yes, the model is updated and the cleared db.Results object of the configured calculation is returned to @@ -155,13 +167,13 @@ def calculation_postprocessing( Parameters ---------- - success :: bool + success : bool Whether the calculation was successful (either forwarded from readuct task or specified if not relevant). - systems :: Dict[str, core.calculator] (Scine::Core::Calculator) + systems : Dict[str, utils.core.Calculator] (Scine::Core::Calculator) The dictionary holding calculators. - keys :: List[str] + keys : List[str] The list of keys of the systems dict to be checked. - expected_results :: Optional[List[str]] + expected_results : Optional[List[str]] The results to be required to be present in systems to qualify as successful calculations. If None is given, this defaults to the expected results of the class, see expected_results(). """ @@ -174,31 +186,34 @@ def calculation_postprocessing( if not success: self.raise_named_exception(self._fallback_error) update_model( - systems[keys[0]], self._calculation, self.config + self.get_calc(keys[0], systems), self._calculation, self.config ) # calculation is safe -> update model db_results = self._calculation.get_results() db_results.clear() self._calculation.set_results(db_results) return db_results - def prepend_to_comment(self, message: str): + @is_configured + def prepend_to_comment(self, message: str) -> None: """ Prepends given message to the comment field of the currently configured calculation. Notes ----- * Requires run configuration + Parameters ---------- - message :: str + message : str The message to be prepended. """ comment = self._calculation.get_comment() self._calculation.set_comment(message + comment) + @is_configured def expected_results_check( self, - systems: dict, + systems: Dict[str, Optional[utils.core.Calculator]], keys: List[str], expected_results: Optional[List[str]] = None, ) -> Tuple[bool, str]: @@ -214,18 +229,18 @@ def expected_results_check( Parameters ---------- - systems :: Dict[str, core.calculator] (Scine::Core::Calculator) + systems : Dict[str, Optional[utils.core.Calculator]] (Scine::Core::Calculator) The dictionary holding calculators. - keys :: List[str] + keys : List[str] The list of keys of the systems dict to be checked. - expected_results :: Optional[List[str]] + expected_results : Optional[List[str]] The results to be required to be present in systems to qualify as successful calculations. If None is given, this defaults to the expected results of the class, see expected_results(). Returns ------- - error_message :: str - A string containing the error message, describing failure in expected results. + Tuple[bool, str] + Whether the results are correct and an error message, describing failure in expected results. """ if expected_results is None: expected_results = self.expected_results() @@ -236,14 +251,16 @@ def expected_results_check( if systems[key] is None: return False, "" # check if desired results are present - if not systems[key].has_results(): + calc = self.get_calc(key, systems) + if not calc.has_results(): return False, ("System '" + key + "' is missing results!") - results = systems[key].get_results() + results = calc.get_results() for expected in expected_results: if getattr(results, expected) is None: return False, (expected + " is missing in results!") return True, "" + @is_configured def store_energy(self, system: utils.core.Calculator, structure: db.Structure) -> None: """ Stores an 'electronic_energy' property for the given structure based on the energy in the results of the given @@ -255,9 +272,9 @@ def store_energy(self, system: utils.core.Calculator, structure: db.Structure) - Parameters ---------- - system :: core.calculator (Scine::Core::Calculator) + system : utils.core.Calculator (Scine::Core::Calculator) A Scine calculator holding a results object with the energy property. - structure :: db.Structure (Scine::Database::Structure) + structure : db.Structure (Scine::Database::Structure) A structure for which the property is saved. """ self.store_property( @@ -270,23 +287,31 @@ def store_energy(self, system: utils.core.Calculator, structure: db.Structure) - structure, ) + @is_configured + def store_bond_orders(self, bond_orders: utils.BondOrderCollection, structure: db.Structure) -> None: + self.store_property( + self._properties, + "bond_orders", + "SparseMatrixProperty", + bond_orders.matrix, + self._calculation.get_model(), + self._calculation, + structure, + ) + def transfer_properties(self, old_structure: db.Structure, new_structure: db.Structure, transfer_helper: Optional[TransferHelper] = None) -> None: """ Copies property IDs from one structure to another one based on the specified properties in the class member. - Notes - ----- - * Requires run configuration - Parameters ---------- - old_structure :: db.Structure (Scine::Database::Structure) + old_structure : db.Structure (Scine::Database::Structure) The structure holding the properties. If a specified property is not present for the structure, no error is given. - new_structure :: db.Structure (Scine::Database::Structure) + new_structure : db.Structure (Scine::Database::Structure) The structure for which the properties are to be added. - transfer_helper :: Optional[TransferHelper] + transfer_helper : Optional[TransferHelper] An optional helper for more difficult transfer task. Otherwise, the specified properties are just copied. """ properties_to_transfer = list(set(self.properties_to_transfer)) # make sure no duplicates @@ -296,14 +321,15 @@ def transfer_properties(self, old_structure: db.Structure, new_structure: db.Str else: transfer_helper.transfer_properties(old_structure, new_structure, properties_to_transfer) + @is_configured def sp_postprocessing( self, success: bool, - systems: dict, + systems: Dict[str, Optional[utils.core.Calculator]], keys: List[str], structure: db.Structure, program_helper: Optional[ProgramHelper], - ): + ) -> None: """ Performs a verification and results saving protocol for a Scine Single Point Calculation. @@ -314,26 +340,27 @@ def sp_postprocessing( Parameters ---------- - success :: bool + success : bool Whether the calculation was successful (either forwarded from readuct task or specified if not relevant). - systems :: Dict[str, core.calculator] (Scine::Core::Calculator) + systems : Dict[str, utils.core.Calculator] (Scine::Core::Calculator) The dictionary holding calculators. - keys :: List[str] + keys : List[str] The list of keys of the systems dict to be checked. - structure :: db.Structure (Scine::Database::Structure) + structure : db.Structure (Scine::Database::Structure) The structure on which the calculation was performed. - program_helper :: ProgramHelper + program_helper : ProgramHelper The possible ProgramHelper that may also want to do some postprocessing after a calculation. """ # postprocessing of results with sanity checks - _ = self.calculation_postprocessing(success, systems, keys) + self.calculation_postprocessing(success, systems, keys) # handle properties - self.store_energy(systems[keys[0]], structure) - results = systems[keys[0]].get_results() + calc = self.get_calc(keys[0], systems) + self.store_energy(calc, structure) + results = calc.get_results() # Store atomic charges if available if results.atomic_charges is not None: self.store_property( @@ -374,6 +401,7 @@ def sp_postprocessing( if program_helper is not None: program_helper.calculation_postprocessing(self._calculation, structure) + @is_configured def get_calculation(self) -> db.Calculation: """ Getter for the current calculation. Throws if not configured. @@ -385,9 +413,129 @@ def get_calculation(self) -> db.Calculation: Returns ------- - calculation :: db.Calculation (Scine::Database::Calculation) + calculation : db.Calculation (Scine::Database::Calculation) The current calculation being carried out. """ if self._calculation is None: self.raise_named_exception("Job is not configured and does not hold a calculation right now") return self._calculation + + @requires("database") + def create_new_structure(self, calculator: utils.core.Calculator, label: db.Label) -> db.Structure: + """ + Add a new structure to the database based on the given calculator and label. + + Notes + ----- + * Requires run configuration + + Parameters + ---------- + calculator : utils.core.Calculator + The calculator holding the optimized structure + label : db.Label + The label of the new structure + + Returns + ------- + db.Structure + The new structure + """ + # New structure + new_structure = db.Structure() + new_structure.link(self._structures) + new_structure.create( + calculator.structure, + self.get_charge(calculator), + self.get_multiplicity(calculator), + self._calculation.get_model(), + label, + ) + return new_structure + + def get_calc(self, name: str, systems: Dict[str, Optional[utils.core.Calculator]]) -> utils.core.Calculator: + """ + Get a calculator from the given map and ensures the system is present + + Notes + ----- + * May throw exception + + Parameters + ---------- + name : str + The name of the system to get + systems : Dict[str, Optional[utils.core.Calculator]] + The map of systems + + Returns + ------- + utils.core.Calculator + The calculator + """ + calc = systems.get(name) + if calc is None: + self.raise_named_exception(f"Could not find system {name}", CalculatorNotPresentException) + raise RuntimeError("Unreachable") + return calc + + @staticmethod + def get_charge(calculator: utils.core.Calculator) -> int: + """ + Get the molecular charge of a calculator's settings. + + Parameters + ---------- + calculator : utils.core.Calculator + The calculator + + Returns + ------- + int + The molecular charge + """ + charge = calculator.settings[utils.settings_names.molecular_charge] + assert isinstance(charge, int) + return charge + + @staticmethod + def get_multiplicity(calculator: utils.core.Calculator) -> int: + """ + Get the spin multiplicity of a calculator's settings. Return 0 if the setting is not present. + + Parameters + ---------- + calculator : utils.core.Calculator + The calculator + + Returns + ------- + int + The molecular charge + """ + multiplicity = calculator.settings.get(utils.settings_names.spin_multiplicity, 0) + assert isinstance(multiplicity, int) + return multiplicity + + def get_energy(self, calculator: utils.core.Calculator) -> float: + """ + Get the energy of a calculator's results. + + Parameters + ---------- + calculator : utils.core.Calculator + The calculator + + Returns + ------- + float + The energy + """ + if not calculator.has_results(): + self.raise_named_exception("Calculator has no results") + raise RuntimeError("Unreachable") # just for linters + energy = calculator.get_results().energy + if energy is None: + self.raise_named_exception("Calculator has no energy") + raise RuntimeError("Unreachable") + return energy diff --git a/scine_puffin/jobs/templates/scine_job_with_observers.py b/scine_puffin/jobs/templates/scine_job_with_observers.py new file mode 100644 index 0000000..5f52fa4 --- /dev/null +++ b/scine_puffin/jobs/templates/scine_job_with_observers.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +from abc import ABC +from typing import Any, Dict, List, Tuple, TYPE_CHECKING, Optional + +from .job import is_configured +from .scine_observers import StoreEverythingObserver, StoreWithFrequencyObserver, StoreWithFractionObserver +from .sub_settings_job import SubSettingsJob +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency +from scine_puffin.utilities.task_to_readuct_call import SubTaskToReaductCall + +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") +if module_exists("scine_readuct") or TYPE_CHECKING: + import scine_readuct as readuct +else: + readuct = MissingDependency("scine_readuct") + + +class ScineJobWithObservers(SubSettingsJob, ABC): + """ + A common interface for all jobs in Puffin that want to have an observer for individual calculations. + """ + + def __init__(self) -> None: + super().__init__() + self.name = "ScineJobWithObservers" # to be overwritten by child + # to be extended by child: + self.settings: Dict[str, Dict[str, Any]] = { + **self.settings, + self.job_key: { + **self.settings[self.job_key], + "store_all_structures": False, + "store_structures_with_frequency": { + task: 0 for task in SubTaskToReaductCall.__members__ + }, + "store_structures_with_fraction": { + task: 0.0 for task in SubTaskToReaductCall.__members__ + }, + } + } + + @classmethod + def optional_settings_doc(cls) -> str: + return super().optional_settings_doc() + """ + + These settings are recognized related to storing individual structures encountered during the job: + + store_all_structures : bool + If all structures encountered during the elementary step search should be stored in the database. + store_structures_with_frequency : Dict[str, int] + Determine for each subtask, such as 'opt' or 'tsopt', a frequency, such as every third, to store the + structures encountered in the subtask in the database. + store_structures_with_fraction : Dict[str, float] + Determine for each subtask, such as 'opt' or 'tsopt', a fraction of structures, such as a third, + to store the structures encountered in the subtask in the database. The saved structure will not be spaced + evenly but the fraction determines the random chance for each structure to be stored. + """ + + @staticmethod + def required_programs() -> List[str]: + return ["database", "molassembler", "readuct", "utils"] + + @is_configured + @requires("readuct") + def observed_readuct_call(self, task: SubTaskToReaductCall, systems: Dict[str, Optional[utils.core.Calculator]], + input_names: List[str], **kwargs) \ + -> Tuple[Dict[str, Optional[utils.core.Calculator]], bool]: + from scine_utilities.settings_names import molecular_charge, spin_multiplicity + for name in input_names: + if systems.get(name) is None: + self.raise_named_exception(f"System {name} is not in systems: {systems}") + raise RuntimeError("Unreachable") # just for linters + observers = [] + observer_functions = [] + model = self._calculation.get_model() + model.complete_model(self.get_calc(input_names[0], systems).settings) + if self.settings[self.job_key]["store_all_structures"]: + observers.append(StoreEverythingObserver(self._calculation.get_id(), model)) + observer_functions = [observers[-1].gather] + elif (self.settings[self.job_key]["store_structures_with_frequency"][task.name] and + self.settings[self.job_key]["store_structures_with_fraction"][task.name]): + raise NotImplementedError("Non-zero values in a single task for both store_structures_with_frequency ", + "and store_structures_with_fraction are not allowed.") + elif self.settings[self.job_key]["store_structures_with_frequency"].get(task.name): + observers.append(StoreWithFrequencyObserver( + self._calculation.get_id(), model, + self.settings[self.job_key]["store_structures_with_frequency"][task.name]) + ) + observer_functions = [observers[-1].gather] + elif self.settings[self.job_key]["store_structures_with_fraction"].get(task.name): + observers.append(StoreWithFractionObserver( + self._calculation.get_id(), model, + self.settings[self.job_key]["store_structures_with_fraction"][task.name]) + ) + observer_functions = [observers[-1].gather] + # carry out ReaDuct task + result = getattr(readuct, task.value)(systems, input_names, observers=observer_functions, **kwargs) + # TODO this may need to be redone for multi input calls + # i.e. tasks with more than one input structure. + # But it would only be a problem if they have a different charge or multiplicity + # currently no such task exists in ReaDuct + calc = self.get_calc(input_names[0], systems) + charge = calc.settings[molecular_charge] + multiplicity = calc.settings[spin_multiplicity] + for observer in observers: + observer.finalize(self._manager, charge, multiplicity) + return result + + @is_configured + def observed_readuct_call_with_throw(self, subtask: SubTaskToReaductCall, + systems: Dict[str, Optional[utils.core.Calculator]], input_names: List[str], + expected_results: List[str], error_msg: str, **kwargs) \ + -> Dict[str, Optional[utils.core.Calculator]]: + systems, success = self.observed_readuct_call(subtask, systems, input_names, **kwargs) + names_to_check = [kwargs['output'] if 'output' in kwargs else input_names] + self.throw_if_not_successful(success, systems, names_to_check, expected_results, error_msg) + return systems diff --git a/scine_puffin/jobs/templates/scine_observers.py b/scine_puffin/jobs/templates/scine_observers.py index d258b6c..cca931d 100644 --- a/scine_puffin/jobs/templates/scine_observers.py +++ b/scine_puffin/jobs/templates/scine_observers.py @@ -1,31 +1,56 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ from abc import ABC, abstractmethod +from typing import Any, Dict, TYPE_CHECKING, List + +from numpy.random import default_rng + +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") class Observer(ABC): + """ + Abstract base class that defines an observer pattern in a Puffin job + in order to observe each calculation. + """ @abstractmethod - def gather(self, cycle: int, atoms, results, tag: str): + def gather(self, cycle: int, atoms, results, tag: str) -> None: raise NotImplementedError @abstractmethod - def finalize(self, db_manager, charge: int, multiplicity: int): + def finalize(self, db_manager: db.Manager, charge: int, multiplicity: int) -> None: raise NotImplementedError class StoreEverythingObserver(Observer): - def __init__(self, calculation_id, model): - self.data = [] + """ + Observer implementation that stores every structure + and calculated properties in the database. + """ + + def __init__(self, calculation_id: db.ID, model: db.Model) -> None: + super().__init__() + self.data: List[Dict[str, Any]] = [] self.white_list = ['energy', 'gradients'] self.calculation_id = calculation_id self.model = model - def gather(self, _: int, atoms, results, tag: str): + def gather(self, cycle: int, atoms: utils.AtomCollection, results: utils.Results, tag: str) -> None: tmp = { 'atoms': atoms, @@ -38,8 +63,8 @@ def gather(self, _: int, atoms, results, tag: str): self.data.append(tmp) @staticmethod - def tag_to_label(tag: str): - import scine_database as db + @requires('database') + def tag_to_label(tag: str) -> db.Label: mapping = { 'geometry_optimization': db.Label.MINIMUM_GUESS, 'ts_optimization': db.Label.TS_GUESS, @@ -51,8 +76,8 @@ def tag_to_label(tag: str): } return mapping[tag] - def finalize(self, db_manager, charge: int, multiplicity: int): - import scine_database as db + @requires('database') + def finalize(self, db_manager: db.Manager, charge: int, multiplicity: int) -> None: has_white_list: bool = (len(self.white_list) > 0) structures = db_manager.get_collection('structures') properties = db_manager.get_collection('properties') @@ -62,13 +87,14 @@ def finalize(self, db_manager, charge: int, multiplicity: int): label = StoreEverythingObserver.tag_to_label(result['tag']) structure.set_label(label) for property_name in result: - if property_name in ['atoms', 'tag', 'successfull_calculation']: + if property_name in ['atoms', 'tag', 'successful_calculation']: continue if (has_white_list and property_name in self.white_list) or not has_white_list: db_name = property_name if property_name == 'energy': db_name = 'electronic_energy' - new_prop = db.NumberProperty.make(db_name, self.model, result[property_name], properties) + new_prop: db.Property = db.NumberProperty.make(db_name, self.model, result[property_name], + properties) elif property_name == 'gradients': new_prop = db.DenseMatrixProperty.make( @@ -78,3 +104,37 @@ def finalize(self, db_manager, charge: int, multiplicity: int): new_prop.set_structure(structure.get_id()) new_prop.set_calculation(self.calculation_id) structure.add_property(db_name, new_prop.get_id()) + + +class StoreWithFrequencyObserver(StoreEverythingObserver): + """ + Observer implementation that stores every nth structure + and calculated properties in the database. + """ + + def __init__(self, calculation_id: db.ID, model: db.Model, frequency: float) -> None: + super().__init__(calculation_id, model) + self.frequency = frequency + + def gather(self, cycle: int, atoms: utils.AtomCollection, results: utils.Results, tag: str) -> None: + if self.frequency == 0: + return + if cycle % self.frequency == 0: + super().gather(cycle, atoms, results, tag) + + +class StoreWithFractionObserver(StoreEverythingObserver): + """ + Observer implementation that stores a given fraction of structures + and their properties in the database. + Which structures are stored is determined at random. + """ + + def __init__(self, calculation_id: db.ID, model: db.Model, fraction: float) -> None: + super().__init__(calculation_id, model) + self.fraction = fraction + self.rng = default_rng() + + def gather(self, cycle: int, atoms: utils.AtomCollection, results: utils.Results, tag: str) -> None: + if self.rng.random() < self.fraction: + super().gather(cycle, atoms, results, tag) diff --git a/scine_puffin/jobs/templates/scine_optimization_job.py b/scine_puffin/jobs/templates/scine_optimization_job.py index 9e32b6b..24b2d72 100644 --- a/scine_puffin/jobs/templates/scine_optimization_job.py +++ b/scine_puffin/jobs/templates/scine_optimization_job.py @@ -1,102 +1,112 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -from typing import List, Union +from abc import ABC +from typing import List, Union, Dict, TYPE_CHECKING, Optional -import scine_database as db -import scine_utilities as utils - - -from .job import job_configuration_wrapper from .scine_job import ScineJob -from scine_puffin.config import Configuration +from .job import is_configured from scine_puffin.utilities.program_helper import ProgramHelper +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") -class OptimizationJob(ScineJob): +class OptimizationJob(ScineJob, ABC): """ A common interface for all jobs in Puffin that use the Scine::Core::Calculator interface to optimize a structure in the Scine database. """ - def __init__(self): + own_expected_results = ["energy"] + + def __init__(self) -> None: super().__init__() self.name = "OptimizationJob" - self.own_expected_results = ["energy"] - - @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - """See Job.run()""" - raise NotImplementedError @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "readuct", "utils"] - def determine_new_label(self, structure: db.Structure, graph: str, ignore_user_label: bool = False) -> db.Label: + @requires("database") + def determine_new_label( + self, + old_label: db.Label, + graph: str, + is_surface: bool, + ignore_user_label: bool = False + ) -> db.Label: """ - Derive the label of the optimized structure based on the given structure and its Molassembler graph. + Derive the label of an optimized structure based on the previous label and the Molassembler graph. Notes ----- - * Requires run configuration * May throw exception Parameters ---------- - structure :: db.Structure - The structure to be optimized - graph :: str + old_label : db.Label + The label which the structure had previous to optimization + graph : str The graph of the structure - ignore_user_label :: bool + is_surface : bool + Whether the structure is a surface + ignore_user_label : bool Whether the user label of the given structure shall be ignored. If True, an input structure 'user_guess' will get an optimized structure with 'minimum_optimized' Returns ------- - new_label :: db.Label + new_label : db.Label The label of the optimized structure """ - label = structure.get_label() graph_is_split = ";" in graph - if label == db.Label.MINIMUM_GUESS or label == db.Label.MINIMUM_OPTIMIZED: + if old_label == db.Label.MINIMUM_GUESS or old_label == db.Label.MINIMUM_OPTIMIZED: if graph_is_split: new_label = db.Label.COMPLEX_OPTIMIZED else: new_label = db.Label.MINIMUM_OPTIMIZED - elif label == db.Label.SURFACE_GUESS or label == db.Label.SURFACE_OPTIMIZED: + elif old_label == db.Label.SURFACE_GUESS or old_label == db.Label.SURFACE_OPTIMIZED: if graph_is_split: new_label = db.Label.SURFACE_COMPLEX_OPTIMIZED else: new_label = db.Label.SURFACE_OPTIMIZED - elif label == db.Label.COMPLEX_GUESS or label == db.Label.COMPLEX_OPTIMIZED: + elif old_label == db.Label.COMPLEX_GUESS or old_label == db.Label.COMPLEX_OPTIMIZED: if graph_is_split: new_label = db.Label.COMPLEX_OPTIMIZED else: new_label = db.Label.MINIMUM_OPTIMIZED - elif label == db.Label.USER_OPTIMIZED: + elif old_label == db.Label.USER_OPTIMIZED: if graph_is_split: new_label = db.Label.USER_COMPLEX_OPTIMIZED else: new_label = db.Label.USER_OPTIMIZED - elif label == db.Label.USER_GUESS: + elif old_label == db.Label.USER_GUESS: if graph_is_split: - if structure.has_property("surface_atom_indices"): + if is_surface: new_label = db.Label.USER_SURFACE_COMPLEX_OPTIMIZED else: new_label = db.Label.USER_COMPLEX_OPTIMIZED else: - if structure.has_property("surface_atom_indices"): + if is_surface: new_label = db.Label.USER_SURFACE_OPTIMIZED else: new_label = db.Label.USER_OPTIMIZED else: - error = f"Unknown label '{str(label)}' of input structure: '{str(structure.id())}'\n" + error = f"Unknown label '{str(old_label)}'\n" self.raise_named_exception(error) - return # for type checking + raise RuntimeError("Unreachable") # for type checking if ignore_user_label and new_label == db.Label.USER_OPTIMIZED: if graph_is_split: new_label = db.Label.COMPLEX_OPTIMIZED @@ -104,37 +114,11 @@ def determine_new_label(self, structure: db.Structure, graph: str, ignore_user_l new_label = db.Label.MINIMUM_OPTIMIZED return new_label - def create_new_structure(self, calculator: utils.core.Calculator, label: db.Label) -> db.Structure: - """ - Add a new structure to the database based on the given calculator and label. - - Notes - ----- - * Requires run configuration - - Parameters - ---------- - calculator :: utils.core.Calculator - The calculator holding the optimized structure - label :: db.Label - The label of the new structure - """ - # New structure - new_structure = db.Structure() - new_structure.link(self._structures) - new_structure.create( - calculator.structure, - calculator.settings[utils.settings_names.molecular_charge], - calculator.settings.get(utils.settings_names.spin_multiplicity, 0), - self._calculation.get_model(), - label, - ) - return new_structure - + @is_configured def optimization_postprocessing( self, success: bool, - systems: dict, + systems: Dict[str, Optional[utils.core.Calculator]], keys: List[str], old_structure: db.Structure, new_label: db.Label, @@ -151,34 +135,40 @@ def optimization_postprocessing( Parameters ---------- - success :: bool + success : bool The boolean signalling whether the task was successful (forwarded from ReaDuct) - systems :: dict + systems : dict Dictionary holding the calculators (forwarded from ReaDuct) - keys :: List[str] + keys : List[str] The keys specifying the checked calculator - old_structure :: db.Structure + old_structure : db.Structure The structure which was optimized - new_label :: db.Label + new_label : db.Label The label of the new structure - program_helper :: Union[ProgramHelper, None] + program_helper : Union[ProgramHelper, None] The optional helper of the employed program for postprocessing - program_helper :: Union[List[str], None] + program_helper : Union[List[str], None] The expected results for the calculators, if not given, assumed from invoking Job class - expected_results :: Union[List[str], None] + expected_results : Union[List[str], None] The expected results for the calculators, if not given, assumed from invoking Job class + + Returns + ------- + db.Structure + The new structure """ # postprocessing of results with sanity checks db_results = self.calculation_postprocessing(success, systems, keys, expected_results) # New structure - new_structure = self.create_new_structure(systems[keys[0]], new_label) + calc = self.get_calc(keys[0], systems) + new_structure = self.create_new_structure(calc, new_label) db_results.add_structure(new_structure.id()) self._calculation.set_results(db_results) # handle properties - self.store_energy(systems[keys[0]], new_structure) + self.store_energy(calc, new_structure) self.transfer_properties(old_structure, new_structure) if program_helper is not None: diff --git a/scine_puffin/jobs/templates/scine_propensity_job.py b/scine_puffin/jobs/templates/scine_propensity_job.py new file mode 100644 index 0000000..102f77f --- /dev/null +++ b/scine_puffin/jobs/templates/scine_propensity_job.py @@ -0,0 +1,646 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +from abc import ABC +from typing import Any, Dict, List, Tuple, Union, Optional, Iterator, Set, TYPE_CHECKING +import sys +from copy import deepcopy + +from .job import is_configured +from .scine_job import CalculatorNotPresentException +from .scine_job_with_observers import ScineJobWithObservers +from scine_puffin.utilities.task_to_readuct_call import SubTaskToReaductCall +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utilities = MissingDependency("scine_utilities") +if module_exists("scine_readuct") or TYPE_CHECKING: + import scine_readuct as readuct +else: + readuct = MissingDependency("scine_readuct") + + +class ScinePropensityJob(ScineJobWithObservers, ABC): + """ + A common interface for all jobs in Puffin that carry out calculations + with different spin multiplicities for spin propensity checks. + """ + + def __init__(self) -> None: + super().__init__() + self.name = "PropensityJob" # to be overwritten by child + self.propensity_key = "spin_propensity" + self.opt_key = "opt" + # to be extended by child: + self.settings: Dict[str, Dict[str, Any]] = { + **self.settings, + self.propensity_key: { + "check_for_unimolecular_reaction": True, + "energy_range_to_save": 200.0, + "optimize_all": True, + "energy_range_to_optimize": 500.0, + "check": 2, + }, + self.opt_key: {} + } + + @classmethod + def optional_settings_doc(cls) -> str: + return super().optional_settings_doc() + """ + + These settings are recognized for calculations checking for spin propensities: + + spin_propensity_check : int + The range to check for possible multiplicities for products. A value + of 2 (default) will check triplet and quintet for a singlet + and will check singlet, quintet und septet for triplet. + spin_propensity_energy_range_to_save : float + The energy range in kJ/mol to save structures with different spin multiplicities. + spin_propensity_energy_range_to_optimize : float + The energy range in kJ/mol to optimize structures with different spin multiplicities. + spin_propensity_optimize_all : bool + If set to True, all spin states will be optimized regardless of their energy. + spin_propensity_check_for_unimolecular_reaction : bool + Applies to jobs searching for elementary steps. Determine if spin propensities should be checked even if + the elementary step is purely unimolecular (reactant and product a single continuous graph. + """ + + @staticmethod + def required_programs() -> List[str]: + return ["database", "molassembler", "readuct", "utils"] + + @requires("utilities") + @is_configured + def optimize_structures( + self, + name_stub: str, + systems: Dict[str, Optional[utils.core.Calculator]], + structures: List[utils.AtomCollection], + structure_charges: List[int], + structure_multiplicities: List[int], + calculator_settings: utils.Settings, + stop_on_error: bool = True, + readuct_task: SubTaskToReaductCall = SubTaskToReaductCall.OPT, + task_settings_key: Optional[str] = None, + ) -> Tuple[List[str], Dict[str, Optional[utils.core.Calculator]]]: + """ + For each given product AtomCollection: + First, construct a Scine Calculator and save in class member map. + Second, perform a Single Point with the given charge and spin multiplicity including spin propensity check + Last, optimize the product if more than one atom and perform spin propensity check again to be sure. + + Notes + ----- + * Requires run configuration + * May throw exception + + Parameters + ---------- + name_stub : str + The stub for naming of the structures, example: `start` will generate + systems `start_00`, `start_01`, and so on. + systems : Dict[str, Optional[utils.core.Calculator]] + The map of systems + structures : List[utils.AtomCollection] + The atoms of the structures in a list. + structure_charges : List[int] + The charges of the structures. + structure_multiplicities : List[int] + The spin multiplicities of the structures. + calculator_settings : utils.Settings + The general settings for the Scine calculator. Charge and spin multiplicity will be overwritten. + stop_on_error : bool + If set to False, skip unsuccessful calculations and replace calculator with None + readuct_task : SubTaskToReaductCall + The task to perform with readuct, by default SubTaskToReaductCall.OPT + task_settings_key : Optional[str] + The key in the settings dictionary to use for the task settings, by default None which will be the opt_key + + Returns + ------- + product_names : List[str] + A list of the access keys to the structures in the system map. + systems : Dict[str, Optional[utils.core.Calculator]] + The updated map of systems. + """ + if task_settings_key is None: + task_settings_key = self.opt_key + structure_names: List[str] = [] + method_family = self._calculation.get_model().method_family + # Generate structure systems + for i, atoms in enumerate(structures): + name = f"{name_stub}_{i:02d}" + structure_names.append(name) + utils.io.write(name + ".xyz", atoms) + try: + # correct PES + structure_calculator_settings = deepcopy(calculator_settings) + structure_calculator_settings[utils.settings_names.molecular_charge] = structure_charges[i] + structure_calculator_settings[utils.settings_names.spin_multiplicity] = structure_multiplicities[i] + # generate calculator + new = utils.core.load_system_into_calculator( + name + ".xyz", + method_family, + **structure_calculator_settings, + ) + systems[name] = new + systems = self._add_propensity_systems(name, systems) + except RuntimeError as e: + if stop_on_error: + raise e + sys.stderr.write(f"{name} cannot be calculated because: {str(e)}") + systems[name] = None + + print("Product Opt Settings:") + print(self.settings[self.opt_key], "\n") + required_properties = ["energy"] + if not self.connectivity_settings['only_distance_connectivity']: + required_properties.append("bond_orders") + # Optimize structures, if they have more than one atom; otherwise run a single point calculation + for structure_name in structure_names: + if systems[structure_name] is None: + continue + try: + if not self.settings[self.propensity_key]["check"]: + systems, success = readuct.run_single_point_task( + systems, + [structure_name], + require_bond_orders=not self.connectivity_settings['only_distance_connectivity'], + ) + self.throw_if_not_successful(success, systems, [structure_name], required_properties, + f"{name_stub.capitalize()} single point failed:\n") + else: + systems = self._spin_propensity_single_points(structure_name, systems, + f"{name_stub.capitalize()} single point failed:\n") + if len(self.get_calc(structure_name, systems).structure) > 1: + if len(structure_names) == 1 and len(self._calculation.get_structures()) == 1 and \ + not self.settings[self.propensity_key]["check_for_unimolecular_reaction"]: + # optimize only base multiplicity + systems = self.observed_readuct_call_with_throw( + readuct_task, systems, [structure_name], required_properties, + f"{name_stub.capitalize()} optimization failed:\n", + **self.settings[task_settings_key] + ) + # still do propensity SP to store close energy multiplicities in DB + systems = self._spin_propensity_single_points( + structure_name, systems, f"{name_stub.capitalize()} optimization failed:\n") + elif self.settings[self.propensity_key]["optimize_all"]: + systems = self._spin_propensity_optimizations( + structure_name, systems, f"{name_stub.capitalize()} optimization failed:\n", + readuct_task, task_settings_key + ) + else: + systems = self._limited_spin_propensity_optimization(structure_name, systems, name_stub, + required_properties, + readuct_task, task_settings_key) + except RuntimeError as e: + if stop_on_error: + raise e + sys.stderr.write(f"{structure_name} cannot be calculated because: {str(e)}") + systems[structure_name] = None + return structure_names, systems + + @requires("utilities") + def _add_propensity_systems(self, name: str, systems: Dict[str, Optional[utils.core.Calculator]]) \ + -> Dict[str, Optional[utils.core.Calculator]]: + """ + Adds clone systems of the given name to the given systems map that have different spin multiplicities. + + Parameters + ---------- + name : str + The name of the system to add the propensity systems for. + systems : Dict[str, Optional[utils.core.Calculator]] + The map of systems. + + Returns + ------- + Dict[str, Optional[utils.core.Calculator]] + The updated systems map. + + Raises + ------ + RuntimeError + If the settings object of a calculator was replaced with a broken object + """ + + for shift_name, multiplicity in self._propensity_iterator(name, systems): + if shift_name == name: + continue + shifted_calc = self.get_calc(name, systems).clone() + systems[shift_name] = shifted_calc + shifted_calc.delete_results() # make sure results of clone are empty + if utils.settings_names.spin_mode in shifted_calc.settings: + dc = shifted_calc.settings.descriptor_collection + if utils.settings_names.spin_mode not in dc: + self.raise_named_exception(f"{utils.settings_names.spin_mode} not in descriptor collection " + f"of {name} system") + raise RuntimeError("Unreachable") # just for linters + descriptor = dc[utils.settings_names.spin_mode] + if isinstance(descriptor, utils.OptionListDescriptor): + for suitable in ["unrestricted", "restricted_open_shell", "any"]: + if suitable in descriptor.options: + shifted_calc.settings[utils.settings_names.spin_mode] = suitable + break + else: + shifted_calc.settings[utils.settings_names.spin_mode] = "any" + shifted_calc.settings[utils.settings_names.spin_multiplicity] = multiplicity + return systems + + @requires("utilities") + def _propensity_iterator(self, name: str, systems: Dict[str, Optional[utils.core.Calculator]]) \ + -> Iterator[Tuple[str, int]]: + """ + Allows one to iterate over the names and spin multiplicities of the propensity systems of a given name. + + Parameters + ---------- + name : str + The base name of the system to iterate over; must not be a propensity system name. + systems : Dict[str, Optional[utils.core.Calculator]] + The map of systems. + + Yields + ------ + Iterator[Tuple[str, int]] + The system name and spin multiplicity of the propensity systems. + """ + propensity_limit = self.settings[self.propensity_key]["check"] + for shift in range(-propensity_limit, propensity_limit + 1): + try: + multiplicity = self.get_multiplicity(self.get_calc(name, systems)) + shift * 2 + except CalculatorNotPresentException: + continue + if multiplicity > 0: + shift_name = f"{name}_multiplicity_shift_{shift}" if shift else name + yield shift_name, multiplicity + + def _spin_propensity_single_points(self, name: str, systems: Dict[str, Optional[utils.core.Calculator]], + error_msg: str) -> Dict[str, Optional[utils.core.Calculator]]: + """ + Carry out single point energy calculations for the spin propensities of a given name. + + Parameters + ---------- + name : str + The base name of the system + systems : Dict[str, Optional[utils.core.Calculator]] + The systems map + error_msg : str + The error message to give, should all calculations fail + + Returns + ------- + Dict[str, Optional[utils.core.Calculator]] + The updated systems map + """ + + info = f"Single point calculations of {name}" + if self.settings[self.propensity_key]["check"]: + info += " with potential spin propensities" + info += ":\n" + print(info) + total_success = 0 + for shift_name, multiplicity in self._propensity_iterator(name, systems): + if self.get_calc(shift_name, systems).get_results().energy is not None: + # we already have energy for this system + total_success += 1 + continue + print(f"Carrying out single point calculation for the spin multiplicity of '{multiplicity}':") + try: + systems, success = readuct.run_single_point_task( + systems, + [shift_name], + require_bond_orders=not self.connectivity_settings['only_distance_connectivity'], + stop_on_error=False + ) + except RuntimeError as e: + sys.stderr.write(f"{shift_name} cannot be calculated because: {str(e)}\n") + systems[shift_name] = None + success = False + if success: + total_success += 1 + else: + systems[shift_name] = None + if not total_success: + self.throw_if_not_successful(False, systems, [name], ["energy"], error_msg) + return systems + + @is_configured + def _spin_propensity_optimizations(self, name: str, systems: Dict[str, Optional[utils.core.Calculator]], + error_msg: str, + readuct_task: SubTaskToReaductCall = SubTaskToReaductCall.OPT, + task_settings_key: Optional[str] = None) \ + -> Dict[str, Optional[utils.core.Calculator]]: + """ + Carry out structure optimizations for the spin propensities of a given name. + + Parameters + ---------- + name : str + The base name of the system + systems : Dict[str, Optional[utils.core.Calculator]] + The systems map + error_msg : str + The error message to give, should all calculations fail + readuct_task : SubTaskToReaductCall + The task to perform with readuct, by default SubTaskToReaductCall.OPT + task_settings_key : Optional[str] + The key in the settings dictionary to use for the task settings, by default None which will be the opt_key + + Returns + ------- + Dict[str, Optional[utils.core.Calculator]] + The updated systems map + + Raises + ------ + RuntimeError + If all calculations fail + """ + info = f"Optimizing {name}" + if self.settings[self.propensity_key]["check"]: + info += " with potential spin propensities" + info += ":\n" + print(info) + total_success = 0 + lowest_name, allowed_names = self._get_propensity_names_within_range( + name, systems, + self.settings[self.propensity_key]["energy_range_to_optimize"] + ) + all_names = [lowest_name] + allowed_names + if task_settings_key is None: + task_settings_key = self.opt_key + task_settings = deepcopy(self.settings[task_settings_key]) + wanted_output_name: Optional[List[str]] = task_settings.get("output") + if wanted_output_name is not None and len(wanted_output_name) > 1: + self.raise_named_exception("More than one output name is not allowed") + if wanted_output_name is not None: + del task_settings["output"] + for shift_name, multiplicity in self._propensity_iterator(name, systems): + if shift_name not in all_names: + continue + print(f"Carrying out structure optimization for the spin multiplicity of '{multiplicity}':") + try: + systems, success = self.observed_readuct_call( + readuct_task, systems, [shift_name], stop_on_error=False, **task_settings + ) + except RuntimeError as e: + sys.stderr.write(f"{shift_name} cannot be calculated because: {str(e)}\n") + success = False + if success: + total_success += 1 + else: + systems[shift_name] = None + if not total_success: + self.throw_if_not_successful(False, systems, [name], ["energy"], error_msg) + if wanted_output_name is not None: + lowest_name, _ = self._get_propensity_names_within_range( + name, systems, + self.settings[self.propensity_key]["energy_range_to_optimize"] + ) + if lowest_name is None: + self.raise_named_exception("No optimization was successful.") + raise RuntimeError("Unreachable") + systems[wanted_output_name[0]] = self.get_calc(lowest_name, systems) + return systems + + @requires("readuct") + @is_configured + def _limited_spin_propensity_optimization(self, structure_name: str, + systems: Dict[str, Optional[utils.core.Calculator]], + name_stub: str, required_properties: List[str], + readuct_task: SubTaskToReaductCall = SubTaskToReaductCall.OPT, + task_settings_key: Optional[str] = None) \ + -> Dict[str, Optional[utils.core.Calculator]]: + """ + Carries out structure optimizations for the spin propensities of a given name, but only for the lowest energy + spin state and the base spin state. + + Parameters + ---------- + structure_name : str + The base name of the system + systems : Dict[str, Optional[utils.core.Calculator]] + The systems map + name_stub : str + The stub for naming of the structures + required_properties : List[str] + The properites expected to be present after a calculation + readuct_task : SubTaskToReaductCall + The task to perform with readuct, by default SubTaskToReaductCall.OPT + task_settings_key : Optional[str] + The key in the settings dictionary to use for the task settings, by default None which will be the opt_key + + Returns + ------- + Dict[str, Optional[utils.core.Calculator]] + The updated systems map + + Raises + ------ + RuntimeError + No single point calculation was previously successful + RuntimeError + All calculations fail + """ + prev_lowest = None + lowest_name, _ = self._get_propensity_names_within_range( + structure_name, systems, self.settings[self.propensity_key]["energy_range_to_optimize"] + ) + if lowest_name is None: + self.raise_named_exception(f"No calculation was successful for {structure_name}.") + raise RuntimeError("Unreachable") + if task_settings_key is None: + task_settings_key = self.opt_key + task_settings = deepcopy(self.settings[task_settings_key]) + wanted_output_name: Optional[List[str]] = task_settings.get("output") + if wanted_output_name is not None: + del task_settings["output"] + if wanted_output_name is not None and len(wanted_output_name) > 1: + self.raise_named_exception("More than one output name is not allowed") + while lowest_name != prev_lowest: + prev_lowest = lowest_name + multiplicity = self._multiplicity_from_name(structure_name, lowest_name, systems) + print(f"Optimizing {lowest_name}' with spin multiplicity {multiplicity}:") + try: + systems = self.observed_readuct_call_with_throw( + readuct_task, systems, [lowest_name], required_properties, + f"{name_stub.capitalize()} optimization failed:\n", **task_settings + ) + except RuntimeError as e: + sys.stderr.write(f"{lowest_name} cannot be calculated because: {str(e)}\n") + systems[lowest_name] = None + break + systems = self._spin_propensity_single_points( + structure_name, systems, f"{name_stub.capitalize()} optimization failed:\n") + lowest_name, _ = self._get_propensity_names_within_range( + structure_name, systems, self.settings[self.propensity_key]["energy_range_to_optimize"] + ) + if wanted_output_name is not None: + lowest_name, _ = self._get_propensity_names_within_range( + structure_name, systems, + self.settings[self.propensity_key]["energy_range_to_optimize"] + ) + if lowest_name is None: + self.raise_named_exception("No optimization was successful.") + raise RuntimeError("Unreachable") + systems[wanted_output_name[0]] = self.get_calc(lowest_name, systems) + return systems + + def _multiplicity_from_name(self, base_name: str, propensity_system_name: str, + systems: Dict[str, Optional[utils.core.Calculator]]) -> int: + for shift_name, multiplicity in self._propensity_iterator(base_name, systems): + if shift_name == propensity_system_name: + return multiplicity + self.raise_named_exception(f"Could not find multiplicity for {propensity_system_name}") + raise RuntimeError("Unreachable") + + @requires("database") + @is_configured + def _store_structure_with_propensity_check(self, name: str, systems: Dict[str, Optional[utils.core.Calculator]], + label: db.Label, enforce_to_save_base_name: bool, + surface_indices: Optional[Union[List[int], Set[int]]] = None) \ + -> db.Structure: + """ + Stores the structure with the given name in the database, but checks for spin propensities and stores the + structures with a relevant energy difference to the lowest energy structure as well. + + Parameters + ---------- + name : str + The name of the structure to store. + systems : Dict[str, Optional[utils.core.Calculator]] + The map of systems. + label : db.Label + The label to store the structure with. + enforce_to_save_base_name : bool + If set to True, the base name will be saved as well, even if it has a higher energy. + surface_indices : Optional[Union[List[int], Set[int]]] + The indices of the surface atoms in the structure. + + Returns + ------- + db.Structure + The structure that was stored in the database. + """ + from scine_utilities import settings_names as sn + + def create_impl(structure_name: str, system_map: Dict[str, Optional[utils.core.Calculator]]) -> db.Structure: + bond_orders, system_map = self.make_bond_orders_from_calc(system_map, structure_name, surface_indices) + calc = self.get_calc(structure_name, system_map) + new_structure = self.create_new_structure(calc, label) + self.store_energy(calc, new_structure) + self.store_bond_orders(bond_orders, new_structure) + self.add_graph(new_structure, bond_orders, surface_indices) + # Label can change based on graph after optimization + if label not in [db.Label.TS_OPTIMIZED, db.Label.TS_GUESS]: + new_graph = self._cbor_graph_from_structure(new_structure) + new_label = self._determine_new_label_based_on_graph_and_surface_indices(new_graph, surface_indices) + if label != new_label: + print("Propensity check led to new label of " + structure_name + ". Relabeling it.") + new_structure.set_label(new_label) + results = self._calculation.get_results() + results.add_structure(new_structure.id()) + self._calculation.set_results(results) + return new_structure + + lowest_name, names_to_save = self._get_propensity_names_within_range( + name, systems, self.settings[self.propensity_key]["energy_range_to_save"] + ) + if lowest_name is None: + self.raise_named_exception(f"No successful calculation available for {name}") + raise RuntimeError("Unreachable") + spin_propensity_hit = lowest_name != name + # Printing information + if spin_propensity_hit: + print(f"Noticed spin propensity. Lowest energy spin multiplicity of {name} is " + f"{self.get_calc(lowest_name, systems).settings[sn.spin_multiplicity]}") + if names_to_save: + print("Spin states with rel. energies to lowest state in kJ/mol which are also saved to the database:") + print("name | multiplicity | rel. energy") + base_energy = self.get_calc(lowest_name, systems).get_results().energy + if base_energy is None: + self.raise_named_exception(f"No energy calculated for {lowest_name}") + raise RuntimeError("Unreachable") # just for linters + for n in names_to_save: + system = self.get_calc(n, systems) + multiplicity = system.settings[sn.spin_multiplicity] + energy = system.get_results().energy + if energy is None: + self.raise_named_exception(f"No energy calculated for {n}") + raise RuntimeError("Unreachable") # just for linters + rel_energy = (energy - base_energy) * utils.KJPERMOL_PER_HARTREE + print(f" {n} | {multiplicity} | {rel_energy}") + if enforce_to_save_base_name: + print(f"Still saving the base multiplicity of " + f"{self.get_calc(name, systems).settings[sn.spin_multiplicity]} in the elementary step") + # overwrite names to simply safe and write as product of elementary step + names_to_save += [lowest_name] + if name in names_to_save: + names_to_save.remove(name) + lowest_name = name + + # Saving information + name_to_structure_and_label_map: Dict[str, Tuple[db.Structure, db.Label]] = {} + for n in names_to_save + [lowest_name]: + # Store as Tuple[db.Structure, db.Label] + structure = create_impl(n, systems) + name_to_structure_and_label_map[n] = (structure, structure.get_label()) + + # Decide which structure to return + # Lowest name if no better spin state was found or if the lower spin state still has the same label as name + if not spin_propensity_hit or \ + name_to_structure_and_label_map[lowest_name][1] == label or \ + enforce_to_save_base_name: + return name_to_structure_and_label_map[lowest_name][0] + return name_to_structure_and_label_map[name][0] + + @requires("utilities") + def _get_propensity_names_within_range(self, name: str, systems: Dict[str, Optional[utils.core.Calculator]], + allowed_energy_range: float) -> Tuple[Optional[str], List[str]]: + """ + Gives the lowest-energy name and names within a given energy range of the lowest energy name. + + Parameters + ---------- + name : str + The base name of the system. + systems : Dict[str, Optional[utils.core.Calculator]] + The systems map. + allowed_energy_range : float + The allowed energy range in kJ/mol. + + Returns + ------- + Tuple[Optional[str], List[str]] + The lowest name and the names within the energy range. + """ + energies: Dict[str, Optional[float]] = {} + for shift_name, _ in self._propensity_iterator(name, systems): + calc = systems.get(shift_name, None) + energies[shift_name] = calc.get_results().energy if calc is not None else None + cleared_energies = {k: v for k, v in energies.items() if v is not None} + if not cleared_energies: + sys.stderr.write(f"No energy calculated for any spin state of {name}\n") + return None, [] + # get name with the lowest energy to save as product + lowest_name = min(cleared_energies, key=cleared_energies.get) # type: ignore + lowest_energy = cleared_energies[lowest_name] + names_within_range: List[str] = [] + for k, v in energies.items(): + if v is not None and k != lowest_name and \ + abs(v - lowest_energy) * utils.KJPERMOL_PER_HARTREE < allowed_energy_range: + names_within_range.append(k) + return lowest_name, names_within_range diff --git a/scine_puffin/jobs/templates/scine_react_job.py b/scine_puffin/jobs/templates/scine_react_job.py index 3acd3a5..debae48 100644 --- a/scine_puffin/jobs/templates/scine_react_job.py +++ b/scine_puffin/jobs/templates/scine_react_job.py @@ -1,57 +1,76 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from abc import ABC from math import ceil -from typing import Any, Dict, List, Tuple, Union, Optional, Iterator, Set -import numpy as np +from typing import Any, Dict, List, Tuple, Union, Optional, Set, TYPE_CHECKING import sys import os from copy import deepcopy -import scine_database as db -import scine_utilities as utils +import numpy as np -from .job import job_configuration_wrapper, breakable -from .scine_connectivity_job import ConnectivityJob +from .job import breakable, is_configured from .scine_hessian_job import HessianJob from .scine_optimization_job import OptimizationJob -from .scine_observers import StoreEverythingObserver -from scine_puffin.config import Configuration +from .scine_propensity_job import ScinePropensityJob from scine_puffin.utilities.scine_helper import SettingsManager, update_model from scine_puffin.utilities.program_helper import ProgramHelper from scine_puffin.utilities import masm_helper - - -class ReactJob(OptimizationJob, HessianJob, ConnectivityJob): +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency +from scine_puffin.utilities.task_to_readuct_call import SubTaskToReaductCall + +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_readuct") or TYPE_CHECKING: + import scine_readuct as readuct +else: + readuct = MissingDependency("scine_readuct") +if module_exists("scine_molassembler") or TYPE_CHECKING: + import scine_molassembler as masm +else: + masm = MissingDependency("scine_molassembler") + + +class ReactJob(ScinePropensityJob, OptimizationJob, HessianJob, ABC): """ A common interface for all jobs in Puffin that use the Scine::Core::Calculator interface to find new reactions. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.name = "ReactJob" # to be overwritten by child self.exploration_key = "" # to be overwritten by child - self.own_expected_results = [] self.rc_key = "rc" - self.job_key = "job" self.rc_opt_system_name = "rcopt" self.single_point_key = "sp" self.no_irc_structure_matches_start = False # to be extended by child: self.settings: Dict[str, Dict[str, Any]] = { + **self.settings, self.job_key: { + **self.settings[self.job_key], "imaginary_wavenumber_threshold": 0.0, - "spin_propensity_check_for_unimolecular_reaction": True, - "spin_propensity_energy_range_to_save": 200.0, - "spin_propensity_optimize_all": True, - "spin_propensity_energy_range_to_optimize": 500.0, - "spin_propensity_check": 2, "store_full_mep": False, "store_all_structures": False, - "n_surface_atom_threshold": 1, + "store_structures_with_frequency": { + task: 0 for task in SubTaskToReaductCall.__members__ + }, + "store_structures_with_fraction": { + task: 0.0 for task in SubTaskToReaductCall.__members__ + }, + "always_add_barrierless_step_for_reactive_complex": False, + "allow_exhaustive_product_decomposition": False, }, self.rc_key: { "minimal_spin_multiplicity": False, @@ -72,69 +91,142 @@ def __init__(self): "charge_separation_threshold": 0.4 } } - """ - expect_charge_separation : If true, fragment charges are no longer determined by rounding, i.e, if a product - consists of multiple molecules (according to its graph), the charges are determined initially by rounding. - However, then the residual (the difference of the integrated charge to the rounded one) is checked against - . If this residual exceeds the charge separation threshold, the charge is - increased/lowered by one according to its sign. This is especially useful if a clear charge separation only - occurs upon separation of the molecules which is often the case for DFT-based descriptions of the electronic - structure. - charge_separation_threshold : The threshold for the charge separation (vide supra). - """ self.start_graph = "" self.end_graph = "" - self.start_charges = [] - self.start_multiplicities = [] - self.start_decision_lists = [] - self.ref_structure = None - self.step_direction = None + self.start_charges: List[int] = [] + self.start_multiplicities: List[int] = [] + self.start_decision_lists: List[str] = [] + self.ref_structure: Optional[db.Structure] = None + self.step_direction: Optional[str] = None self.lhs_barrierless_reaction = False self.lhs_complexation = False self.rhs_complexation = False self.complexation_criterion = -12.0 / 2625.5 # kj/mol self.check_charges = True - self.systems = {} + self.systems: Dict[str, Optional[utils.core.Calculator]] = {} self._component_maps: Dict[str, List[int]] = {} self.products_component_map: Optional[List[int]] = None - @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - """See Job.run()""" - raise NotImplementedError + @classmethod + def optional_settings_doc(cls) -> str: + return super().optional_settings_doc() + """\n + The following options are available for the reactive complex generation: + + rc_x_alignment_0 : List[float], length=9 + In case of two structures building the reactive complex, this option + describes a rotation of the first structure (index 0) that aligns + the reaction coordinate along the x-axis (pointing towards +x). + The rotation assumes that the geometric mean position of all + atoms in the reactive site (``nt_lhs_list``) is shifted into the + origin. + rc_x_alignment_1 : List[float], length=9 + In case of two structures building the reactive complex, this option + describes a rotation of the second structure (index 1) that aligns + the reaction coordinate along the x-axis (pointing towards -x). + The rotation assumes that the geometric mean position of all + atoms in the reactive site (``nt_rhs_list``) is shifted into the + origin. + rc_x_rotation : float + In case of two structures building the reactive complex, this option + describes a rotation angle around the x-axis of one of the two + structures after ``rc_x_alignment_0`` and ``rc_x_alignment_1`` have + been applied. + rc_x_spread : float + In case of two structures building the reactive complex, this option + gives the distance by which the two structures are moved apart along + the x-axis after ``rc_x_alignment_0``, ``rc_x_alignment_1``, and + ``rc_x_rotation`` have been applied. + rc_displacement : float + In case of two structures building the reactive complex, this option + adds a random displacement to all atoms (random direction, random + length). The maximum length of this displacement (per atom) is set to + be the value of this option. + rc_spin_multiplicity : int + This option sets the ``spin_multiplicity`` of the reactive complex. + In case this is not given the ``spin_multiplicity`` of the initial + structure or minimal possible spin of the two initial structures is + used. + rc_molecular_charge : int + This option sets the ``molecular_charge`` of the reactive complex. + In case this is not given the ``molecular_charge`` of the initial + structure or sum of the charges of the initial structures is used. + Note: If you set the ``rc_molecular_charge`` to a value different + from the sum of the start structures charges the possibly resulting + elementary steps will never be balanced but include removal or + addition of electrons. + rc_minimal_spin_multiplicity : bool + True: The total spin multiplicity in a bimolecular reaction is + based on the assumption of total spin recombination (s + t = s; t + t = s; d + s = d; d + t = d) + False: No spin recombination is assumed (s + t = t; t + t = quin; d + s = d; d + t = quar) + (default: False) + + The following options are available for the analysis of the single points of the optimized supersystems: + + expect_charge_separation : bool + If true, fragment charges are no longer determined by rounding, i.e, if a product + consists of multiple molecules (according to its graph), the charges are determined initially by rounding. + However, then the residual (the difference of the integrated charge to the rounded one) is checked against + . If this residual exceeds the charge separation threshold, the charge is + increased/lowered by one according to its sign. This is especially useful if a clear charge separation only + occurs upon separation of the molecules which is often the case for DFT-based descriptions of the electronic + structure. + (default: False) + charge_separation_threshold : float + The threshold for the charge separation (vide supra). + (default: 0.4) + + These additional settings are recognized: + + imaginary_wavenumber_threshold : float + Threshold value in inverse centimeters below which a wavenumber + is considered as imaginary when the transition state is analyzed. + Negative numbers are interpreted as imaginary. (default: 0.0) + allow_exhaustive_product_decomposition : bool + Whether to allow the decomposition of the new products of a complex into further sub-products, + e.g. of the complex A+B, the product B further decomposes during the optimization of new products + to C and D. + Might ignore possible elementary steps (the formation of B from C and D), + hence the option should be activated with care. + (default: False) + store_full_mep : bool + Whether all individual structures of the IRC and IRCOPT should be saved and attached to the ElementaryStep. + always_add_barrierless_step_for_reactive_complex : bool + Add a barrierless reaction for the flask formation of two compounds regardless of their complexation energy. + """ + + @classmethod + def generated_data_docstring(cls) -> str: + return super().generated_data_docstring() + """ + If successful (technically and chemically) the following data will be + generated and added to the database: + + Elementary Steps + If found, a single new elementary step with the associated transition + state will be added to the database. + + Structures + The transition state (TS) and also the separated products will be added + to the database. + + Properties + The ``hessian`` (``DenseMatrixProperty``), ``frequencies`` + (``VectorProperty``), ``normal_modes`` (``DenseMatrixProperty``), + ``gibbs_energy_correction`` (``NumberProperty``) and + ``gibbs_free_energy`` (``NumberProperty``) of the TS will be + provided. The ``electronic_energy`` associated with the TS structure and + each of the products will be added to the database.\n + """ @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "molassembler", "readuct", "utils"] def clear(self) -> None: self.systems = {} super().clear() - def observed_readuct_call(self, call_str: str, systems: dict, input_names: List[str], **kwargs) \ - -> Tuple[dict, bool]: - import scine_readuct as readuct - observers = [] - observer_functions = [] - model = self._calculation.get_model() - model.complete_model(systems[input_names[0]].settings) - if self.settings[self.job_key]["store_all_structures"]: - observers.append(StoreEverythingObserver(self._calculation.get_id(), model)) - observer_functions = [observers[-1].gather] - ret = getattr(readuct, call_str)(systems, input_names, observers=observer_functions, **kwargs) - # TODO this may need to be redone for multi input calls - charge = systems[input_names[0]].settings["molecular_charge"] - multiplicity = systems[input_names[0]].settings["spin_multiplicity"] - for observer in observers: - observer.finalize(self._manager, charge, multiplicity) - return ret - - def observed_readuct_call_with_throw(self, call_str: str, systems: dict, input_names: List[str], - expected_results: List[str], error_msg: str, **kwargs) -> dict: - systems, success = self.observed_readuct_call(call_str, systems, input_names, **kwargs) - self.throw_if_not_successful(success, systems, input_names, expected_results, error_msg) - return systems - + @is_configured + @requires("database") def reactive_complex_preparations(self) -> Tuple[SettingsManager, Union[ProgramHelper, None]]: """ Determine settings for this task based on settings of configured calculation, construct a reactive complex @@ -148,13 +240,12 @@ def reactive_complex_preparations(self) -> Tuple[SettingsManager, Union[ProgramH Returns ------- - settings_manager, program_helper :: Tuple[SettingsManager, Union[ProgramHelper, None]] + settings_manager, program_helper : Tuple[SettingsManager, Union[ProgramHelper, None]] A database property holding bond orders. """ - import scine_molassembler as masm # preprocessing of structure self.ref_structure = self.check_structures() - settings_manager, program_helper = self.create_helpers(self.ref_structure) + settings_manager, program_helper = self.create_helpers(self.ref_structure) # type: ignore # Separate the calculation settings from the database into the task and calculator settings # This overwrites any default settings by user settings @@ -201,7 +292,7 @@ def reactive_complex_preparations(self) -> Tuple[SettingsManager, Union[ProgramH self.systems[self.rc_key] = reactive_complex if program_helper is not None: program_helper.calculation_preprocessing( - self.systems[self.rc_key], self._calculation.get_settings()) + self.get_system(self.rc_key), self._calculation.get_settings()) # Calculate bond orders and graph of reactive complex and compare to database graph of start structures reactive_complex_graph, self.systems = self.make_graph_from_calc(self.systems, self.rc_key) @@ -210,6 +301,8 @@ def reactive_complex_preparations(self) -> Tuple[SettingsManager, Union[ProgramH self.start_graph = reactive_complex_graph return settings_manager, program_helper + @is_configured + @requires("database") def check_structures(self, start_structures: Union[List[db.ID], None] = None) -> db.Structure: """ Perform sanity check whether we only have 1 or 2 structures in the configured calculation. Return a possible @@ -222,14 +315,15 @@ def check_structures(self, start_structures: Union[List[db.ID], None] = None) -> Parameters ---------- - start_structures :: List[db.ID] + start_structures : List[db.ID] If given, this structure id list is used instead of the list given in self._calculation.get_structures(). Returns ------- - ref_structure :: db.Structure (Scine::Database::Structure) + ref_structure : db.Structure (Scine::Database::Structure) The largest structure of the calculation. """ + ref_id = None if start_structures is None: start_structures = self._calculation.get_structures() if len(start_structures) == 0: @@ -248,49 +342,24 @@ def check_structures(self, start_structures: Union[List[db.ID], None] = None) -> structure = db.Structure(s, self._structures) if structure.get_label() == db.Label.SURFACE_ADSORPTION_GUESS: ref_id = s + break + else: + self.raise_named_exception( + "Could not identify adsorption structure in calculation with more than 2 structures." + ) else: self.raise_named_exception( "Reactive complexes built from more than 2 structures are not supported." ) - return db.Structure(ref_id, self._structures) - - def sort_settings(self, task_settings: dict) -> None: - """ - Take settings of configured calculation and save them in class member. Throw exception for unknown settings. - - Notes - ----- - * Requires run configuration - * May throw exception - - Parameters - ---------- - task_settings :: dict - A dictionary from which the settings are taken - """ - self.extract_connectivity_settings_from_dict(task_settings) - # Dissect settings into individual user task_settings - for key, value in task_settings.items(): - for task in self.settings.keys(): - if task == self.job_key: - if key in self.settings[task].keys(): - self.settings[task][key] = value - break # found right task, leave inner loop - else: - indicator_length = len(task) + 1 # underscore to avoid ambiguities - if key[:indicator_length] == task + "_": - self.settings[task][key[indicator_length:]] = value - break # found right task, leave inner loop - else: - self.raise_named_exception( - f"The key '{key}' was not recognized." - ) - - if "ircopt" in self.settings.keys() and "output" in self.settings["ircopt"]: + if ref_id is None: self.raise_named_exception( - "Cannot specify a separate output system for the optimization of the IRC end points" + "Could not identify a reference structure in the calculation." ) + raise RuntimeError("Unreachable") # for type checking + + return db.Structure(ref_id, self._structures) + @requires("database") def save_initial_graphs_and_charges(self, settings_manager: SettingsManager, structures: List[db.Structure]) \ -> None: """ @@ -302,9 +371,9 @@ def save_initial_graphs_and_charges(self, settings_manager: SettingsManager, str Parameters ---------- - settings_manager :: SettingsManager + settings_manager : SettingsManager The settings manager for the calculation. - structures :: List[scine_database.Structure] + structures : List[scine_database.Structure] The reactant structures. """ graphs = [] @@ -354,22 +423,6 @@ def save_initial_graphs_and_charges(self, settings_manager: SettingsManager, str "Reactive complexes built from more than 2 structures are not supported." ) - def _cbor_graph_from_structure(self, structure: db.Structure) -> str: - """ - Retrieve masm_cbor_graph from a database structure and throws error if none present. - - Parameters - ---------- - structure :: db.Structure - - Returns - ------- - masm_cbor_graph :: str - """ - if not structure.has_graph("masm_cbor_graph"): - self.raise_named_exception(f"Missing graph in structure {str(structure.id())}.") - return structure.get_graph("masm_cbor_graph") - @staticmethod def _decision_list_from_structure(structure: db.Structure) -> Optional[str]: """ @@ -378,16 +431,18 @@ def _decision_list_from_structure(structure: db.Structure) -> Optional[str]: Parameters ---------- - structure :: db.Structure + structure : db.Structure Returns ------- - masm_decision_list :: Optional[str] + masm_decision_list : Optional[str] """ if not structure.has_graph("masm_decision_list"): return None return structure.get_graph("masm_decision_list") + @is_configured + @requires("database") def build_reactive_complex(self, settings_manager: SettingsManager) -> utils.AtomCollection: """ Aligns the structure(s) to form a reactive complex and returns the AtomCollection. In case of multiple @@ -401,12 +456,12 @@ def build_reactive_complex(self, settings_manager: SettingsManager) -> utils.Ato Parameters ---------- - settings_manager :: SettingsManager + settings_manager : SettingsManager The settings_manager in which the charge and multiplicity of the new atoms are set. Returns ------- - reactive_complex :: utils.AtomCollection (Scine::Utilities::AtomCollection) + reactive_complex : utils.AtomCollection (Scine::Utilities::AtomCollection) The atoms of the reactive complex """ start_structure_ids = self._calculation.get_structures() @@ -476,7 +531,10 @@ def build_reactive_complex(self, settings_manager: SettingsManager) -> utils.Ato self.raise_named_exception( "Reactive complexes built from more than 2 structures are not supported." ) + raise RuntimeError("Unreachable") # for type checking + @requires("utilities") + @is_configured def determine_pes_of_rc(self, settings_manager: SettingsManager, s0: db.Structure, s1: Optional[db.Structure] = None) -> None: """ @@ -488,12 +546,12 @@ def determine_pes_of_rc(self, settings_manager: SettingsManager, s0: db.Structur * Requires run configuration Parameters - ------- - settings_manager :: SettingsManager + ---------- + settings_manager : SettingsManager The settings_manager in which the charge and multiplicity of the new atoms are set. - s0 :: db.Structure (Scine::Database::Structure) + s0 : db.Structure (Scine::Database::Structure) A structure of the configured calculation - s1 :: Union[db.Structure, None] + s1 : Union[db.Structure, None] A potential second structure for bimolecular reactions """ from scine_utilities.settings_names import molecular_charge, spin_multiplicity @@ -529,14 +587,14 @@ def _orient_coordinates(self, coord1: np.ndarray, coord2: np.ndarray) -> np.ndar Parameters ------- - coord1 :: np.ndarray of shape (n,3) + coord1 : np.ndarray of shape (n,3) The coordinates of the first molecule - coord2 :: np.ndarray of shape (m,3) + coord2 : np.ndarray of shape (m,3) The coordinates of the second molecule Returns ------- - coord :: np.ndarray of shape (n+m, 3) + coord : np.ndarray of shape (n+m, 3) The combined and aligned coordinates of both molecules """ rc_settings = self.settings[self.rc_key] @@ -576,8 +634,8 @@ def setup_automatic_mode_selection(self, name: str) -> None: user settings. Parameters - ------- - name :: str + ---------- + name : str The name of the subtask for which the automatic mode selection is added. """ if "automatic_mode_selection" not in self.settings[name] and all( @@ -594,24 +652,39 @@ def setup_automatic_mode_selection(self, name: str) -> None: + self.settings[self.exploration_key]["nt_dissociations"] ) + @requires("utilities") def n_imag_frequencies(self, name: str) -> int: """ A helper function to count the number of imaginary frequencies based on the threshold in the settings. Does not carry out safety checks. + Notes + ----- + * May throw exception (system not present) + Parameters - ------- - name :: str + ---------- + name : str The name of the system which holds Hessian results. """ - atoms = self.systems[name].structure - modes_container = utils.normal_modes.calculate(self.systems[name].get_results().hessian, atoms) + calc = self.systems.get(name) + if calc is None: + self.raise_named_exception(f"System '{name}' not found in systems.") + return -1 # only for linter + atoms = calc.structure + hessian = calc.get_results().hessian + if hessian is None: + self.raise_named_exception(f"No Hessian found for system '{name}'.") + return -1 # only for linter + modes_container = utils.normal_modes.calculate(hessian, atoms) wavenumbers = modes_container.get_wave_numbers() return np.count_nonzero(np.array(wavenumbers) < self.settings[self.job_key]["imaginary_wavenumber_threshold"]) - def get_graph_charges_multiplicities(self, name: str, total_charge: int, total_system_name: Optional[str] = None, - split_index: Optional[int] = None) \ + @requires("utilities") + @is_configured + def get_graph_charges_multiplicities(self, name: str, total_charge: int, + total_system_name: Optional[str] = None, split_index: Optional[int] = None) \ -> Tuple[List[utils.AtomCollection], str, List[int], List[int], List[str]]: """ Runs bond orders for the specified name in the dictionary of @@ -628,33 +701,32 @@ def get_graph_charges_multiplicities(self, name: str, total_charge: int, total_s Parameters ---------- - name :: str + name : str Index into systems dictionary to calculate bond orders for - total_charge :: str + total_charge : str The charge of the system - total_system_name :: str + total_system_name : str The name of the total system which can be specified in case this method is called for a partial system. This can enable to assign the indices of the total system to the indices of the partial system. - split_index :: int + split_index : int The index of the system in the total system which is split. This is used to assign the indices of the total system to the indices of the partial system. Both total_system_name and split_index must be specified or neither must be specified. Returns ------- - ordered_structures :: List[utils.AtomCollection] + ordered_structures : List[utils.AtomCollection] List of atom collections corresponding to the split molecules. - graph_string :: str + graph_string : str Sorted molassembler cbor graphs separated by semicolons. - charges :: List[int] + charges : List[int] Charges of the molecules. - multiplicities :: List[int] + multiplicities : List[int] Multiplicities of the molecules, total multiplicity before split influences these returned values based on a buff spread over all split structures, these values have to be checked with spin propensity checks - decision_lists :: List[str] + decision_lists : List[str] Molassembler decision lists for free dihedrals """ - import scine_readuct as readuct from scine_puffin.utilities.reaction_transfer_helper import ReactionTransferHelper all_surface_indices = self.surface_indices_all_structures() @@ -662,11 +734,11 @@ def get_graph_charges_multiplicities(self, name: str, total_charge: int, total_s surface_indices: Union[Set[int], List[int]] = all_surface_indices elif total_system_name not in self._component_maps: self.raise_named_exception(f"Total system name '{total_system_name}' not found in component maps") - return utils.AtomCollection(), "", [], [], [] # For type checking + return [utils.AtomCollection()], "", [], [], [] # For type checking elif split_index is None: self.raise_named_exception(f"Split index must be given, " f"if total system name '{total_system_name}' is specified") - return utils.AtomCollection(), "", [], [], [] # For type checking + return [utils.AtomCollection()], "", [], [], [] # For type checking else: split_surfaces_indices = \ ReactionTransferHelper.map_total_indices_to_split_structure_indices( @@ -675,7 +747,7 @@ def get_graph_charges_multiplicities(self, name: str, total_charge: int, total_s masm_results, self.systems = self.make_masm_result_from_calc(self.systems, name, surface_indices) - split_structures = masm_results.component_map.apply(self.systems[name].structure) + split_structures = masm_results.component_map.apply(self.get_system(name).structure) decision_lists = [masm_helper.get_decision_list_from_molecule(m, a) for m, a in zip(masm_results.molecules, split_structures)] @@ -686,7 +758,8 @@ def get_graph_charges_multiplicities(self, name: str, total_charge: int, total_s # Determine partial charges, charges per molecules and number of electrons per molecule bond_orders, self.systems = self.make_bond_orders_from_calc(self.systems, name, surface_indices) - partial_charges = self.systems[name].get_results().atomic_charges + calc = self.get_system(name) + partial_charges = calc.get_results().atomic_charges if partial_charges is None: self.systems, success = readuct.run_single_point_task( self.systems, [name], require_charges=True @@ -694,9 +767,9 @@ def get_graph_charges_multiplicities(self, name: str, total_charge: int, total_s self.throw_if_not_successful( success, self.systems, [name], ["energy", "atomic_charges"] ) - partial_charges = self.systems[name].get_results().atomic_charges - # TODO replace with propert setter if we have on in utils, this does not work - self.systems[name].get_results().bond_orders = bond_orders + partial_charges = calc.get_results().atomic_charges + # TODO replace with property setter if we have one in utils, this does not work + calc.get_results().bond_orders = bond_orders charges, n_electrons, _ = self._integrate_charges(masm_results.component_map, partial_charges, split_structures, total_charge) @@ -706,7 +779,7 @@ def get_graph_charges_multiplicities(self, name: str, total_charge: int, total_s # --> if before 3 -> give one structure (largest) triplet, before 5 --> give each a triplet # this ensures that the spin propensity checks later can cover as much as possible # this should work with any multiplicity and any number of split structures - multiplicity_before = self.systems[name].settings[utils.settings_names.spin_multiplicity] + multiplicity_before = self.get_multiplicity(self.get_system(name)) total_electrons_were_even = multiplicity_before % 2 != 0 min_multiplicity = 1 if total_electrons_were_even else 2 buff = (multiplicity_before - min_multiplicity) / 2.0 @@ -751,7 +824,7 @@ def get_graph_charges_multiplicities(self, name: str, total_charge: int, total_s return ordered_structures, graph_string, charges, multiplicities, decision_lists @staticmethod - def _custom_round(number: float, threshold: float = 0.5) -> float: + def _custom_round(number: float, threshold: float = 0.5) -> Tuple[float, bool]: """ Rounding number up or down depending on the threshold. To round down, delta must be smaller than the threshold. @@ -767,14 +840,16 @@ def _custom_round(number: float, threshold: float = 0.5) -> float: ------- float Number rounded according to threshold. + bool + True if the number was rounded up, False if it was rounded down. """ sign = np.copysign(1.0, number) number = abs(number) delta = number - np.trunc(number) if delta < threshold: - return np.trunc(number) * sign + return np.trunc(number) * sign, False else: - return (np.trunc(number) + 1) * sign + return (np.trunc(number) + 1) * sign, True @staticmethod def _calculate_residual(original_values: List[Any], new_values: List[Any]) -> List[float]: @@ -802,7 +877,8 @@ def _distribute_charge( self, total_charge: float, charge_guess, - summed_partial_charges: List[float]) -> List[int]: + summed_partial_charges: List[float], + changed_charge_indices: Union[List[int], None] = None) -> List[int]: """ Check if the sum of the charges of the non-bonded molecules equals the total charge of the supersystem. If this should not be the case, add or remove one charge, depending on the difference between the total charge @@ -819,13 +895,19 @@ def _distribute_charge( List of guessed charges for each molecule in the supersystem. summed_partial_charges : List[float] List of the sum over the partial charges of the non-bonded molecules in the supersystem. + changed_charge_indices : List[int] + List of atom indices where the charge was changed. Returns ------- charge_guess : List[float] The updated list of guessed charges where the sum equals the total charge of the supersystem. """ - residual = self._calculate_residual(summed_partial_charges, charge_guess) + residual = np.ma.array(self._calculate_residual(summed_partial_charges, charge_guess), mask=False) + + if self.settings[self.single_point_key]["expect_charge_separation"] and changed_charge_indices is not None: + for i in changed_charge_indices: + residual.mask[i] = True while sum(charge_guess) != total_charge: charge_diff = sum(charge_guess) - total_charge # too many electrons, add a charge @@ -834,13 +916,17 @@ def _distribute_charge( charge_guess[np.argmax(residual)] += 1 # too little electrons, remove a charge else: - # Substract one charge from selection + # Subtract one charge from selection charge_guess[np.argmin(residual)] -= 1 # Update residual - residual = self._calculate_residual(summed_partial_charges, charge_guess) + residual = np.ma.array(self._calculate_residual(summed_partial_charges, charge_guess), mask=False) + if self.settings[self.single_point_key]["expect_charge_separation"] and changed_charge_indices is not None: + for i in changed_charge_indices: + residual.mask[i] = True # return updated charge guess return charge_guess + @requires("utilities") def _integrate_charges(self, component_map: List[int], partial_charges: List[float], split_structures, total_charge: float) -> Tuple[List[int], List[int], List[float]]: """ @@ -876,17 +962,21 @@ def _integrate_charges(self, component_map: List[int], partial_charges: List[flo for i, c in zip(component_map, partial_charges): charges[i] += c summed_partial_charges = deepcopy(charges) - print("Charge separation check " + str(self.settings[self.single_point_key]["expect_charge_separation"])) # Update charges to charge guess, only containing ints + changed_charge_indices: List[int] = [] for i in range(len(split_structures)): if not self.settings[self.single_point_key]["expect_charge_separation"]: - charges[i] = int(self._custom_round(charges[i], 0.5)) + charges[i], round_up = self._custom_round(charges[i], 0.5) + charges[i] = int(charges[i]) else: - charges[i] = int(self._custom_round(charges[i], - self.settings[self.single_point_key]["charge_separation_threshold"])) + charges[i], round_up = self._custom_round( + charges[i], self.settings[self.single_point_key]["charge_separation_threshold"]) + charges[i] = int(charges[i]) + if round_up: + changed_charge_indices.append(i) # Check and re-distribute if necessary - updated_charges = self._distribute_charge(total_charge, charges, summed_partial_charges) + updated_charges = self._distribute_charge(total_charge, charges, summed_partial_charges, changed_charge_indices) # Update number of electrons for i in range(len(split_structures)): electrons = 0 @@ -904,21 +994,20 @@ def check_for_barrierless_reaction(self) -> Union[Tuple[str, List[str]], Tuple[N Returns ------- - rc_opt_graph :: Optional[str] + rc_opt_graph : Optional[str] Sorted molassembler cbor graphs separated by semicolons of the reaction product if there was any. - rc_opt_decision_lists :: Optional[List[str]] + rc_opt_decision_lists : Optional[List[str]] Molassembler decision lists for free dihedrals of the reaction product if there was any. """ - import scine_molassembler as masm # Check for barrierless reaction leading to new graphs if self.rc_opt_system_name not in self.systems: # Skip if already done print("Running Reactive Complex Optimization") print("Settings:") print(self.settings[self.rc_opt_system_name], "\n") self.systems, success = self.observed_readuct_call( - 'run_opt_task', self.systems, [self.rc_key], **self.settings[self.rc_opt_system_name] + SubTaskToReaductCall.RCOPT, self.systems, [self.rc_key], **self.settings[self.rc_opt_system_name] ) self.throw_if_not_successful( success, @@ -948,12 +1037,12 @@ def output(self, name: str) -> List[str]: Parameters ---------- - name :: str + name : str Index into systems dictionary to retrieve output for Returns ------- - outputs :: List[str] + outputs : List[str] A list of output system names """ if name not in self.settings: @@ -973,12 +1062,161 @@ def output(self, name: str) -> List[str]: ) return self.settings[name]["output"] + def analyze_side(self, input_name: str, initial_charge: int, opt_name: str, + calculator_settings: utils.Settings) -> Union[Tuple[str, List[int], List[str], List[str]], + Tuple[None, None, None, None]]: + print(opt_name.capitalize() + " Bond Orders") + ( + structures, + full_graph, + original_charges, + original_multiplicities, + original_decision_lists, + ) = self.get_graph_charges_multiplicities(input_name, initial_charge) + # Compress info and find unique structures + full_info = [(graph, charge, multiplicity, decision_list) + for graph, charge, multiplicity, decision_list in zip(full_graph.split(';'), + original_charges, + original_multiplicities, + original_decision_lists)] + + # Map unique info to indices + unique_map: Dict[Tuple[str, int, int, str], List[int]] = {} + min_indices: List[int] = [] + # Loop over molecule infos and collect indices of identical structures + for i, info in enumerate(full_info): + if info not in unique_map: + unique_map[info] = [] + unique_map[info].append(i) + # Extract min indices of each unique structure + min_indices = [min(indices) for indices in unique_map.values()] + + # Optimize unique structures + unique_names, self.systems = self.optimize_structures(opt_name, self.systems, + [structures[i] for i in min_indices], + [original_charges[i] for i in min_indices], + [original_multiplicities[i] for i in min_indices], + calculator_settings) + # Map back to initial structures + output_names = [""] * len(structures) + unique_result: Dict[Tuple[str, int, int, str], str] = {} + for min_index, name in zip(min_indices, unique_names): + unique_result[full_info[min_index]] = name + # Get indices + for index in unique_map[full_info[min_index]]: + output_names[index] = name + + output_graphs = [] + output_decision_lists = [] + split_molecules = [] + # Analyze separated molecules + for i, (name, charge) in enumerate(zip(output_names, original_charges)): + (tmp_structure, tmp_graph, tmp_charge, + tmp_multiplicity, tmp_decision_list + ) = self.get_graph_charges_multiplicities(name, charge, total_system_name=input_name, split_index=i) + if len(tmp_structure) > 1: + if not self.settings[self.job_key]['allow_exhaustive_product_decomposition']: + self._calculation.set_comment(self.name + ": " + opt_name.capitalize() + + ": IRC results keep decomposing (more than once).") + return None, None, None, None + split_molecules.append((name, i, + tmp_structure, tmp_graph.split(";"), + tmp_charge, tmp_multiplicity, tmp_decision_list)) + continue + + output_graphs += tmp_graph.split(';') + output_decision_lists += tmp_decision_list + + # Re-optimize split molecules, until they are no longer split + # NOTE: If 'allow_exhaustive_product_decomposition' is enabled and a split product + # decomposes during optimization, the new products are re-optimized until they are no longer decomposing. + # The resulting barrierless elementary step will only connect the IRC end of the regular elementary step + # with the final products and not save the intermediate sub-products. + # E.g. for a complex A+B, the product B further decomposes during the optimization of new products to C and D. + # The possible elementary step of C + D <-> B will not be checked for or captured with this option. + # Instead the barrierless elementary step A+B <-> C + D is written as a result. + if self.settings[self.job_key]["allow_exhaustive_product_decomposition"] and\ + len(split_molecules) > 0: + indices_to_remove: List[int] = [] + while (len(split_molecules) > 0): + # Get first entry + (org_name, org_index, + structures, graphs, split_charges, multiplicities, decision_lists) = split_molecules[0] + # Look if we already have optimized this structure + new_structure_indices: List[int] = [] + stored_names: List[str] = [] + for i, molecule_key in enumerate(zip(graphs, split_charges, multiplicities, decision_lists)): + if molecule_key in unique_result.keys(): + stored_names.append(unique_result[molecule_key]) + else: + new_structure_indices.append(i) + # Optimize unknown structures + new_names, self.systems = self.optimize_structures(org_name, self.systems, + [structures[i] for i in new_structure_indices], + [split_charges[i] for i in new_structure_indices], + [multiplicities[i] for i in new_structure_indices], + calculator_settings) + # Combine new and stored names + new_names += stored_names + # Remove entry due to split + indices_to_remove.append(org_index) + # Check new structures + for i, (name, charge) in enumerate(zip(new_names, split_charges)): + (tmp_structure, tmp_graph, tmp_charge, + tmp_multiplicity, tmp_decision_list + ) = self.get_graph_charges_multiplicities(name, charge, total_system_name=input_name, + split_index=org_index) + if len(tmp_structure) > 1: + # Repeat re-optimization + split_molecules.append((name, org_index, + tmp_structure, tmp_graph.split(";"), + tmp_charge, tmp_multiplicity, tmp_decision_list)) + else: + # Append new entries at the end + output_names.append(name) + original_charges.append(charge) + original_multiplicities += tmp_multiplicity + output_graphs += tmp_graph.split(';') + output_decision_lists += tmp_decision_list + # Remove from split molecules + split_molecules.pop(0) + + # Remove the name, the charge and multiplicity of original molecule + output_names = [name for i, name in enumerate(output_names) if i not in indices_to_remove] + original_charges = [charge for i, charge in enumerate(original_charges) if i not in indices_to_remove] + original_multiplicities = [multiplicity for i, multiplicity in enumerate(original_multiplicities) + if i not in indices_to_remove] + + # Sort everything again, based on graphs + output_graphs, original_charges, original_multiplicities, output_decision_lists, \ + output_names = ( + list(start_val) for start_val in zip(*sorted(zip( + output_graphs, + original_charges, + original_multiplicities, + output_decision_lists, + output_names))) + ) + output_graph = ';'.join(output_graphs) + + return output_graph, original_charges, output_decision_lists, output_names + + def _determine_complexation_energy(self, input_name: str, molecule_names: List[str]) -> float: + complexation_energy = 0.0 + for name in molecule_names: + complexation_energy -= self.get_energy(self.get_system(name)) + complexation_energy += self.get_energy(self.get_system(input_name)) + print("Complexation Energy:", complexation_energy * utils.KJPERMOL_PER_HARTREE, "kJ/mol") + return complexation_energy + + @requires("database") + @is_configured def irc_sanity_checks_and_analyze_sides( self, initial_charge: int, check_charges: bool, inputs: List[str], - calculator_settings: dict) -> Union[Tuple[List[str], Optional[List[str]]], Tuple[None, None]]: + calculator_settings: utils.Settings) -> Union[Tuple[List[str], Optional[List[str]]], Tuple[None, None]]: """ Check whether we found a new structure, whether our IRC matches the start (and end in case of double ended). This decision is made based on optimized @@ -993,104 +1231,56 @@ def irc_sanity_checks_and_analyze_sides( Parameters ---------- - initial_charge :: int + initial_charge : int The charge of the reactive complex - check_charges :: bool + check_charges : bool Whether the charges must be checked - inputs :: List[str] + inputs : List[str] The name of the IRC outputs to use as inputs - calculator_settings :: dict + calculator_settings : utils.Settings The general settings for the Scine calculator. Charge and spin multiplicity will be overwritten. Returns ------- - product_names :: Optional[List[str]] + product_names : Optional[List[str]] A list of the access keys to the products in the system map. - start_names :: Optional[List[str]] + start_names : Optional[List[str]] A list of the access keys to the starting materials in the system map. """ - import scine_molassembler as masm if len(inputs) != 2: self.raise_named_exception( "Requires to pass 2 systems to the IRC sanity check" ) # All lists ordered according to graph - charges - multiplicities with decreasing priority # Get graphs, charges and minimal multiplicities of split forward and backward structures - print("Forward Bond Orders") ( - forward_structures, forward_graph, forward_charges, - forward_multiplicities, forward_decision_lists, - ) = self.get_graph_charges_multiplicities(inputs[0], initial_charge) - print("Backward Bond Orders") + forward_names + ) = self.analyze_side(inputs[0], initial_charge, "forward", + calculator_settings) + if any(f_info is None for f_info in [forward_graph, forward_charges, forward_decision_lists, forward_names]): + # NOTE: Maybe still save TS for restart here + return None, None + assert forward_graph + assert forward_names ( - backward_structures, backward_graph, backward_charges, - backward_multiplicities, backward_decision_lists, - ) = self.get_graph_charges_multiplicities(inputs[1], initial_charge) - + backward_names + ) = self.analyze_side(inputs[1], initial_charge, "backward", + calculator_settings) + if any(b_info is None for b_info in [backward_graph, backward_charges, + backward_decision_lists, backward_names]): + # NOTE: Maybe still save TS for restart here + return None, None + assert backward_graph + assert backward_names print("Forward charges: " + str(forward_charges)) print("Backward charges: " + str(backward_charges)) - # Optimize separated forward molecules - forward_names = self.optimize_structures("forward", forward_structures, - forward_charges, - forward_multiplicities, - calculator_settings) - # Optimize separated backward molecules - backward_names = self.optimize_structures("backward", backward_structures, - backward_charges, - backward_multiplicities, - calculator_settings) - # Analyze separated forward molecules - forward_graphs = [] - forward_decision_lists = [] - for i, (name, charge) in enumerate(zip(forward_names, forward_charges)): - s, g, _, _, d = self.get_graph_charges_multiplicities(name, charge, - total_system_name=inputs[0], split_index=i) - if len(s) > 1: - self._calculation.set_comment(self.name + ": IRC results keep decomposing (more than once).") - return None, None - forward_graphs += g.split(';') - forward_decision_lists += d - # Sort everything again - forward_graphs, forward_charges, forward_multiplicities, forward_decision_lists, \ - forward_names = ( - list(start_val) for start_val in zip(*sorted(zip( - forward_graphs, - forward_charges, - forward_multiplicities, - forward_decision_lists, - forward_names))) - ) - forward_graph = ';'.join(forward_graphs) - # Analyze separated backward molecules - backward_graphs = [] - backward_decision_lists = [] - for i, (name, charge) in enumerate(zip(backward_names, backward_charges)): - s, g, _, _, d = self.get_graph_charges_multiplicities(name, charge, - total_system_name=inputs[1], split_index=i) - if len(s) > 1: - self._calculation.set_comment(self.name + ": IRC results keep decomposing (more than once).") - return None, None - backward_graphs += g.split(';') - backward_decision_lists += d - # Sort everything again - backward_graphs, backward_charges, backward_multiplicities, backward_decision_lists, \ - backward_names = ( - list(start_val) for start_val in zip(*sorted(zip( - backward_graphs, - backward_charges, - backward_multiplicities, - backward_decision_lists, - backward_names))) - ) - backward_graph = ';'.join(backward_graphs) - # Check for new structures and compare IRC to Start print("Start Graph:") print(self.start_graph) # Equals reactive complex graph @@ -1110,9 +1300,20 @@ def irc_sanity_checks_and_analyze_sides( self._save_ts_for_restart(db.Label.TS_OPTIMIZED) return None, None + def no_match() -> Tuple[List[str], bool]: + print(self.name + ": No IRC structure matches starting structure.") + # Step direction must be forward to guarantee working logic downstream + self.step_direction = "forward" + self.products_component_map = self._component_maps[inputs[0]] + # Trigger to set 'start_names' as 'backward_names' + self.no_irc_structure_matches_start = True + return forward_names, False + compare_decision_lists = True + if not self.start_graph: + product_names, compare_decision_lists = no_match() # Do not expect matching charges if reactive complex charge differs from sum of start structure charges - if masm.JsonSerialization.equal_molecules(forward_graph, self.start_graph) \ + elif masm.JsonSerialization.equal_molecules(forward_graph, self.start_graph) \ and (not check_charges or forward_charges == self.start_charges): product_names = backward_names self.step_direction = "backward" @@ -1148,23 +1349,9 @@ def irc_sanity_checks_and_analyze_sides( self.products_component_map = self._component_maps[inputs[0]] self.lhs_barrierless_reaction = True else: - print(self.name + ": No IRC structure matches starting structure.") - product_names = forward_names - # Step direction must be forward to guarantee working logic downstream - self.step_direction = "forward" - self.products_component_map = self._component_maps[inputs[0]] - # Trigger to set 'start_names' as 'backward_names' - compare_decision_lists = False - self.no_irc_structure_matches_start = True + product_names, compare_decision_lists = no_match() else: - print(self.name + ": No IRC structure matches starting structure.") - product_names = forward_names - # Step direction must be forward to guarantee working logic downstream - self.step_direction = "forward" - self.products_component_map = self._component_maps[inputs[0]] - # Trigger to set 'start_names' as 'backward_names' - compare_decision_lists = False - self.no_irc_structure_matches_start = True + product_names, compare_decision_lists = no_match() if not compare_decision_lists: # ensures that we save the start structures @@ -1177,6 +1364,7 @@ def irc_sanity_checks_and_analyze_sides( else: new_decision_lists = backward_decision_lists decision_lists_match = True + assert new_decision_lists for new, orig in zip(new_decision_lists, original_decision_lists): if not masm.JsonSerialization.equal_decision_lists(new, orig): decision_lists_match = False @@ -1211,20 +1399,24 @@ def irc_sanity_checks_and_analyze_sides( start_names = forward_names self.step_direction = "backward" # Check if complexations need to be tracked - forward_complexation_energy = 0.0 - for name in forward_names: - forward_complexation_energy -= self.systems[name].get_results().energy - forward_complexation_energy += self.systems[inputs[0]].get_results().energy - if forward_complexation_energy < self.complexation_criterion: + if self.settings[self.job_key]["always_add_barrierless_step_for_reactive_complex"]: + add_forward_step = len(forward_names) > 1 + add_backward_step = len(backward_names) > 1 + else: + # # # Forward complexation + forward_complexation_energy = self._determine_complexation_energy(inputs[0], forward_names) + add_forward_step = bool(forward_complexation_energy < self.complexation_criterion) + + # # # Back complexation + backward_complexation_energy = self._determine_complexation_energy(inputs[1], backward_names) + add_backward_step = bool(backward_complexation_energy < self.complexation_criterion) + # Decide whether to add complexation steps + if add_forward_step: if self.step_direction == "backward": self.lhs_complexation = True else: self.rhs_complexation = True - backward_complexation_energy = 0.0 - for name in backward_names: - backward_complexation_energy -= self.systems[name].get_results().energy - backward_complexation_energy += self.systems[inputs[1]].get_results().energy - if backward_complexation_energy < self.complexation_criterion: + if add_backward_step: if self.step_direction == "backward": self.rhs_complexation = True else: @@ -1232,212 +1424,7 @@ def irc_sanity_checks_and_analyze_sides( return product_names, start_names - def optimize_structures( - self, - name_stub: str, - structures: List[utils.AtomCollection], - structure_charges: List[int], - structure_multiplicities: List[int], - calculator_settings: dict, - stop_on_error: bool = True - ) -> List[str]: - """ - For each given product AtomCollection: - First, construct a Scine Calculator and save in class member map. - Second, perform a Single Point with the given charge and spin multiplicity including spin propensity check - Last, optimize the product if more than one atom and perform spin propensity check again to be sure. - - Notes - ----- - * Requires run configuration - * May throw exception - - Parameters - ---------- - name_stub :: str - The stub for naming of the structures, example: `start` will generate - systems `start_00`, `start_01`, and so on. - structures :: List[utils.AtomCollection] - The atoms of the structures in a list. - structure_charges :: List[int] - The charges of the structures. - structure_multiplicities :: List[int] - The spin multiplicities of the structures. - calculator_settings :: dict - The general settings for the Scine calculator. Charge and spin multiplicity will be overwritten. - stop_on_error :: bool - If set to False, skip unsuccessful calculations and replace calculator with None - - Returns - ------- - product_names :: List[str] - A list of the access keys to the structures in the system map. - """ - import scine_readuct as readuct - structure_names = [] - method_family = self._calculation.get_model().method_family - # Generate structure systems - for i, structure in enumerate(structures): - name = f"{name_stub}_{i:02d}" - structure_names.append(name) - utils.io.write(name + ".xyz", structure) - try: - # correct PES - structure_calculator_settings = deepcopy(calculator_settings) - structure_calculator_settings[utils.settings_names.molecular_charge] = structure_charges[i] - structure_calculator_settings[utils.settings_names.spin_multiplicity] = structure_multiplicities[i] - # generate calculator - new = utils.core.load_system_into_calculator( - name + ".xyz", - method_family, - **structure_calculator_settings, - ) - self.systems[name] = new - self._add_propensity_systems(name) - except RuntimeError as e: - if stop_on_error: - raise e - sys.stderr.write(f"{name} cannot be calculated because: {str(e)}") - self.systems[name] = None - - print("Product Opt Settings:") - print(self.settings["opt"], "\n") - required_properties = ["energy"] - if not self.connectivity_settings['only_distance_connectivity']: - required_properties.append("bond_orders") - # Optimize structures, if they have more than one atom; otherwise just run a single point calculation - for structure in structure_names: - if self.systems[structure] is None: - continue - try: - if not self.settings[self.job_key]["spin_propensity_check"]: - self.systems, success = readuct.run_single_point_task( - self.systems, - [structure], - require_bond_orders=not self.connectivity_settings['only_distance_connectivity'], - ) - self.throw_if_not_successful(success, self.systems, [structure], required_properties, - f"{name_stub.capitalize()} single point failed:\n") - else: - self._spin_propensity_single_points(structure, f"{name_stub.capitalize()} single point failed:\n") - if len(self.systems[structure].structure) > 1: - if len(structure_names) == 1 and len(self._calculation.get_structures()) == 1 and \ - not self.settings[self.job_key]["spin_propensity_check_for_unimolecular_reaction"]: - # optimize only base multiplicity - self.systems = self.observed_readuct_call_with_throw( - 'run_opt_task', self.systems, [structure], required_properties, - f"{name_stub.capitalize()} optimization failed:\n", **self.settings["opt"] - ) - # still do propensity SP to store close energy multiplicities in DB - self._spin_propensity_single_points(structure, - f"{name_stub.capitalize()} optimization failed:\n") - elif not self.settings[self.job_key]["spin_propensity_optimize_all"]: - prev_lowest = None - lowest_name, _ = self._get_propensity_names_within_range( - structure, self.settings[self.job_key]["spin_propensity_energy_range_to_optimize"] - ) - while lowest_name != prev_lowest: - print("Optimizing " + lowest_name + ":\n") - self.systems = self.observed_readuct_call_with_throw( - 'run_opt_task', self.systems, [lowest_name], required_properties, - f"{name_stub.capitalize()} optimization failed:\n", **self.settings["opt"] - ) - self._spin_propensity_single_points(structure, - f"{name_stub.capitalize()} optimization failed:\n") - lowest_name, _ = self._get_propensity_names_within_range( - structure, self.settings[self.job_key]["spin_propensity_energy_range_to_optimize"] - ) - else: - self._spin_propensity_optimizations(structure, - f"{name_stub.capitalize()} optimization failed:\n") - except RuntimeError as e: - if stop_on_error: - raise e - sys.stderr.write(f"{structure} cannot be calculated because: {str(e)}") - self.systems[structure] = None - return structure_names - - def _add_propensity_systems(self, name: str) -> None: - for shift_name, multiplicity in self._propensity_iterator(name): - if shift_name == name: - continue - self.systems[shift_name] = self.systems[name].clone() - self.systems[shift_name].delete_results() # make sure results of clone are empty - if utils.settings_names.spin_mode in self.systems[shift_name].settings: - dc = self.systems[shift_name].settings.descriptor_collection - if isinstance(dc[utils.settings_names.spin_mode], - utils.OptionListDescriptor): - for suitable in ["unrestricted", "restricted_open_shell", "any"]: - if suitable in dc[utils.settings_names.spin_mode].options: - self.systems[shift_name].settings[utils.settings_names.spin_mode] = suitable - break - else: - self.systems[shift_name].settings[utils.settings_names.spin_mode] = "any" - self.systems[shift_name].settings[utils.settings_names.spin_multiplicity] = multiplicity - - def _propensity_iterator(self, name: str) -> Iterator[Tuple[str, int]]: - from scine_utilities import settings_names - - propensity_limit = self.settings[self.job_key]["spin_propensity_check"] - for shift in range(-propensity_limit, propensity_limit + 1): - multiplicity = self.systems[name].settings[settings_names.spin_multiplicity] + shift * 2 - if multiplicity > 0: - shift_name = f"{name}_multiplicity_shift_{shift}" if shift else name - yield shift_name, multiplicity - - def _spin_propensity_single_points(self, name: str, error_msg: str) -> None: - import scine_readuct as readuct - info = f"Single point calculations of {name}" - if self.settings[self.job_key]["spin_propensity_check"]: - info += " with potential spin propensities" - info += ":\n" - print(info) - total_success = 0 - for shift_name, _ in self._propensity_iterator(name): - if self.systems.get(shift_name) is None: - continue - if self.systems[shift_name].get_results().energy is not None: - # we already have an energy for this system - total_success += 1 - continue - self.systems, success = readuct.run_single_point_task( - self.systems, - [shift_name], - require_bond_orders=not self.connectivity_settings['only_distance_connectivity'], - stop_on_error=False - ) - if success: - total_success += 1 - else: - self.systems[shift_name] = None - if not total_success: - self.throw_if_not_successful(False, self.systems, [name], ["energy"], error_msg) - - def _spin_propensity_optimizations(self, name: str, error_msg: str) -> None: - info = f"Optimizing {name}" - if self.settings[self.job_key]["spin_propensity_check"]: - info += " with potential spin propensities" - info += ":\n" - print(info) - total_success = 0 - lowest_name, allowed_names = self._get_propensity_names_within_range( - name, - self.settings[self.job_key]["spin_propensity_energy_range_to_optimize"] - ) - all_names = [lowest_name] + allowed_names - for shift_name, _ in self._propensity_iterator(name): - if self.systems.get(shift_name) is None or shift_name not in all_names: - continue - self.systems, success = self.observed_readuct_call( - 'run_opt_task', self.systems, [shift_name], stop_on_error=False, **self.settings["opt"] - ) - if success: - total_success += 1 - else: - self.systems[shift_name] = None - if not total_success: - self.throw_if_not_successful(False, self.systems, [name], ["energy"], error_msg) - + @is_configured def _save_ts_for_restart(self, ts_label: db.Label) -> None: """ Saves the output system of 'tsopt' (hence must already be finished) @@ -1452,117 +1439,141 @@ def _save_ts_for_restart(self, ts_label: db.Label) -> None: _, ts = self._store_ts_with_propensity_info(ts_name, None, ts_label) self._calculation.set_restart_information("TS", ts.id()) - def generate_spline( - self, tsopt_task_name: str, n_fit_points: int = 23, degree: int = 3 - ): + def read_irc_and_irc_opt_trajectories(self, tsopt_task_name: str, irc_task_name: str) \ + -> Tuple[utils.MolecularTrajectory, int]: """ - Using the transition state, IRC and IRC optimization outputs generates - a spline that describes the trajectory of the elementary step, fitting - both atom positions and energy. - - Notes - ----- - * May throw exception + Reads the IRC and IRC optimization trajectories in the correct order to get a cohesive minimum energy path. Parameters ---------- - tsopt_task_name :: str - Name of the transition state task. - n_fit_points :: str - Number of fit points to use in the spline compression. - degree :: str - Fit degree to use in the spline generation. + tsopt_task_name : str + The name of the transition state optimization task. + irc_task_name : str + The name of the IRC task. + + Raises + ------ + RuntimeError + If the IRC or IRC optimization trajectory files are missing. + RuntimeError + If the step_direction is not "forward" or "backward". Returns ------- - spline :: utils.bsplines.TrajectorySpline - The fitted spline of the elementary step trajectory. + Tuple[utils.MolecularTrajectory, int] + The IRC trajectory and the index of the transition state in the IRC trajectory. """ - rpi = utils.bsplines.ReactionProfileInterpolation() - - def read_trj(fname): - trj = utils.io.read_trajectory(utils.io.TrajectoryFormat.Xyz, fname) - energies = [] - with open(fname, "r") as f: - lines = f.readlines() - nAtoms = int(lines[0].strip()) - i = 0 - while i < len(lines): - energies.append(float(lines[i + 1].strip())) - i += nAtoms + 2 - return trj, energies - if self.step_direction == "forward": - dir = "forward" - rev_dir = "backward" + forward_dir = self.output(irc_task_name)[0] + backward_dir = self.output(irc_task_name)[1] elif self.step_direction == "backward": - dir = "backward" - rev_dir = "forward" + forward_dir = self.output(irc_task_name)[1] + backward_dir = self.output(irc_task_name)[0] else: self.raise_named_exception("Could not determine elementary step direction.") + raise RuntimeError("Unreachable") # just for linter - ts_calc = self.systems[self.output(tsopt_task_name)[0]] + ts_calc = self.get_system(self.output(tsopt_task_name)[0]) ts_energy = ts_calc.get_results().energy + if ts_energy is None: + self.raise_named_exception("Missing energy for transition state to construct spline.") + raise RuntimeError("Unreachable") # just for linter + + mep = utils.MolecularTrajectory(ts_calc.structure.elements, 0.0) + + def add_file_to_mep(file_name: str, read_in_reversed: bool = False) -> None: + trj = utils.io.read_trajectory(utils.io.TrajectoryFormat.Xyz, file_name) + energies = trj.get_energies() + if read_in_reversed: + for pos, e in zip(reversed(trj), reversed(energies)): + mep.push_back(pos, e) + else: + for pos, e in zip(trj, energies): + mep.push_back(pos, e) - fpath = os.path.join( - self.work_dir, f"irc_{rev_dir}", f"irc_{rev_dir}.opt.trj.xyz" - ) - if os.path.isfile(fpath): - trj, energies = read_trj(fpath) - for pos, e in zip(reversed(trj), reversed(energies)): - if e > ts_energy: - continue - rpi.append_structure(utils.AtomCollection(trj.elements, pos), e) + def file_name_from_dir_name(dir_name: str, is_irc: bool) -> str: + if is_irc: + return os.path.join( + self.work_dir, f"{dir_name}", f"{dir_name}.irc.{dir_name.split('_')[-1]}.trj.xyz" + ) + return os.path.join( + self.work_dir, f"{dir_name}", f"{dir_name}.opt.trj.xyz" + ) - fpath = os.path.join( - self.work_dir, f"irc_{rev_dir}", f"irc_{rev_dir}.irc.{rev_dir}.trj.xyz" - ) + # we now combine ircopt backward - irc backward - ts - irc forward - ircopt forward + # and we reverse the backward trajectories + fpath = file_name_from_dir_name(backward_dir, is_irc=False) if os.path.isfile(fpath): - trj, energies = read_trj(fpath) - for pos, e in zip(reversed(trj), reversed(energies)): - if e > ts_energy: - continue - rpi.append_structure(utils.AtomCollection(trj.elements, pos), e) - else: - raise RuntimeError( - f"Missing IRC trajectory file: irc_{rev_dir}/irc_{rev_dir}.irc.{rev_dir}.trj.xyz" - ) + add_file_to_mep(fpath, read_in_reversed=True) - fpath = os.path.join(self.work_dir, "ts", "ts.xyz") + fpath = file_name_from_dir_name(backward_dir, is_irc=True) if os.path.isfile(fpath): - ts_calc = self.systems[self.output(tsopt_task_name)[0]] - results = ts_calc.get_results() - ts_xyz, _ = utils.io.read(fpath) - rpi.append_structure(ts_xyz, results.energy, True) + add_file_to_mep(fpath, read_in_reversed=True) else: - raise RuntimeError("Missing TS structure file: ts/ts.xyz") + self.raise_named_exception(f"Missing IRC trajectory file: {fpath}") - fpath = os.path.join( - self.work_dir, f"irc_{dir}", f"irc_{dir}.irc.{dir}.trj.xyz" - ) + ts_index = mep.size() + mep.push_back(ts_calc.structure.positions, ts_energy) + + fpath = file_name_from_dir_name(forward_dir, is_irc=True) if os.path.isfile(fpath): - trj, energies = read_trj(fpath) - for pos, e in zip(trj, energies): - if e > ts_energy: - continue - rpi.append_structure(utils.AtomCollection(trj.elements, pos), e) + add_file_to_mep(fpath) else: - raise RuntimeError( - f"Missing IRC trajectory file: irc_{dir}/irc_{dir}.irc.{dir}.trj.xyz" - ) + self.raise_named_exception(f"Missing IRC trajectory file: {fpath}") - fpath = os.path.join(self.work_dir, f"irc_{dir}", f"irc_{dir}.opt.trj.xyz") + fpath = file_name_from_dir_name(forward_dir, is_irc=False) if os.path.isfile(fpath): - trj, energies = read_trj(fpath) - for pos, e in zip(trj, energies): - if e > ts_energy: - continue - rpi.append_structure(utils.AtomCollection(trj.elements, pos), e) + add_file_to_mep(fpath) - # Get spline - spline = rpi.spline(n_fit_points, degree) - return spline + return mep, ts_index + @requires("utilities") + def generate_spline( + self, trajectory: utils.MolecularTrajectory, ts_index: int, n_fit_points: int = 23, degree: int = 3 + ) -> utils.bsplines.TrajectorySpline: + """ + Using the transition state, IRC and IRC optimization outputs generates + a spline that describes the trajectory of the elementary step, fitting + both atom positions and energy. + Removes all structures that have a higher energy than the transition state. + + Notes + ----- + * May throw exception + + Parameters + ---------- + trajectory : utils.MolecularTrajectory + The trajectory of the elementary step. + ts_index : int + The index of the transition state in the trajectory. + n_fit_points : str + Number of fit points to use in the spline compression. + degree : str + Fit degree to use in the spline generation. + + Returns + ------- + spline : utils.bsplines.TrajectorySpline + The fitted spline of the elementary step trajectory. + """ + + if ts_index >= trajectory.size(): + self.raise_named_exception(f"Transition state index {ts_index} is out of bounds " + f"for trajectory of size {trajectory.size()}.") + + energies = trajectory.get_energies() + ts_energy = energies[ts_index] + elements = trajectory.elements + rpi = utils.bsplines.ReactionProfileInterpolation() + for index, (pos, e) in enumerate(zip(trajectory, energies)): + if index != ts_index and e > ts_energy: + continue + rpi.append_structure(utils.AtomCollection(elements, pos), e, is_the_transition_state=(index == ts_index)) + + return rpi.spline(n_fit_points, degree) + + @requires("database") def store_start_structures( self, start_structure_names: List[str], @@ -1579,22 +1590,21 @@ def store_start_structures( Parameters ---------- - start_structure_names :: List[str] + start_structure_names : List[str] The names of the start structure names in the system map. - program_helper :: Union[ProgramHelper, None] + program_helper : Union[ProgramHelper, None] The ProgramHelper which might also want to do postprocessing - tsopt_task_name :: str + tsopt_task_name : str The name of the task where the TS was output - start_structures :: Optional[List[db.ID]] + start_structures : Optional[List[db.ID]] Optional list of the starting structure ids. If no list is given. The input structures of the calculation are used. Returns ------- - start_structure_ids :: List[scine_database.ID] + start_structure_ids : List[scine_database.ID] A list of the database IDs of the start structures. """ - import scine_molassembler as masm from scine_puffin.utilities.reaction_transfer_helper import ReactionTransferHelper if start_structures is None: @@ -1619,12 +1629,12 @@ def store_start_structures( models = [db.Structure(sid, self._structures).get_model() for sid in start_structures] start_model = models[0] - if not all(model == start_model for model in models): + if not all(model.equal_without_periodic_boundary_check(start_model) for model in models): self.raise_named_exception("React job with mixed model input structures") # Update model to make sure there are no 'any' values left update_model( - self.systems[self.output(tsopt_task_name)[0]], + self.get_system(self.output(tsopt_task_name)[0]), self._calculation, self.config, ) @@ -1644,12 +1654,11 @@ def store_start_structures( if not masm.JsonSerialization.equal_molecules(initial_graph, graph): continue aggregate_id = initial_structure.get_aggregate() + aggregate: Union[db.Flask, db.Compound] if ';' in initial_graph: - aggregate = db.Flask(aggregate_id) - aggregate.link(self._flasks) + aggregate = db.Flask(aggregate_id, self._flasks) else: - aggregate = db.Compound(aggregate_id) - aggregate.link(self._compounds) + aggregate = db.Compound(aggregate_id, self._compounds) existing_structures = aggregate.get_structures() for existing_structure_id in existing_structures: existing_structure = db.Structure(existing_structure_id, self._structures) @@ -1657,7 +1666,7 @@ def store_start_structures( [db.Label.DUPLICATE, db.Label.MINIMUM_GUESS, db.Label.USER_GUESS, db.Label.SURFACE_GUESS, db.Label.SURFACE_ADSORPTION_GUESS]: continue - if existing_structure.get_model() != start_model: + if not start_model.equal_without_periodic_boundary_check(existing_structure.get_model()): continue existing_structure_dl = existing_structure.get_graph("masm_decision_list") if masm.JsonSerialization.equal_decision_lists(dl, existing_structure_dl): @@ -1683,12 +1692,13 @@ def store_start_structures( if program_helper is not None: program_helper.calculation_postprocessing(self._calculation, initial_structure, new_structure) bond_orders, self.systems = self.make_bond_orders_from_calc(self.systems, name, surface_indices) - self.store_energy(self.systems[name], new_structure) + self.store_energy(self.get_system(name), new_structure) self.store_bond_orders(bond_orders, new_structure) self.add_graph(new_structure, bond_orders, surface_indices) start_structure_ids.append(new_structure.id()) return start_structure_ids + @requires("database") def save_barrierless_reaction_from_rcopt(self, product_graph: str, program_helper: Optional[ProgramHelper]) -> None: self.lhs_barrierless_reaction = True print("Barrierless product Graph:") @@ -1724,13 +1734,13 @@ def _save_complex_to_db(self, complex_name: str, program_helper: Optional[Progra Parameters ---------- - complex_name :: str + complex_name : str The name of the complex system in the systems map - program_helper :: Union[ProgramHelper, None] + program_helper : Union[ProgramHelper, None] The ProgramHelper which might also want to do postprocessing Returns ------- - complex_structure_id :: db.ID + complex_structure_id : db.ID The id of the added structure """ complex_system = self.systems[complex_name] @@ -1738,14 +1748,16 @@ def _save_complex_to_db(self, complex_name: str, program_helper: Optional[Progra structure_label = self._determine_new_label_based_on_graph(complex_system, complex_graph) complex_structure = self.create_new_structure(complex_system, structure_label) bond_orders, self.systems = self.make_bond_orders_from_calc(self.systems, complex_name) - self.transfer_properties(self.ref_structure, complex_structure) - self.store_energy(self.systems[complex_name], complex_structure) + if self.ref_structure is not None: + self.transfer_properties(self.ref_structure, complex_structure) + self.store_energy(self.get_system(complex_name), complex_structure) self.store_bond_orders(bond_orders, complex_structure) self.add_graph(complex_structure, bond_orders) - if program_helper is not None: + if program_helper is not None and self.ref_structure is not None: program_helper.calculation_postprocessing(self._calculation, self.ref_structure, complex_structure) return complex_structure.id() + @requires("database") def react_postprocessing( self, product_names: List[str], @@ -1765,13 +1777,13 @@ def react_postprocessing( Parameters ---------- - product_names :: List[str] + product_names : List[str] A list of the access keys to the products in the system map. - program_helper :: Union[ProgramHelper, None] + program_helper : Union[ProgramHelper, None] The ProgramHelper which might also want to do postprocessing - tsopt_task_name :: str + tsopt_task_name : str The name of the task where the TS was output - reactant_structure_ids :: List[scine_database.ID] + reactant_structure_ids : List[scine_database.ID] A list of all structure IDs for the reactants. """ from scine_puffin.utilities.reaction_transfer_helper import ReactionTransferHelper @@ -1789,7 +1801,7 @@ def react_postprocessing( # do this with TS system, because we want a calculator that captures the whole system # and is safe to have a successful last calculation update_model( - self.systems[self.output(tsopt_task_name)[0]], + self.get_system(self.output(tsopt_task_name)[0]), self._calculation, self.config, ) @@ -1805,13 +1817,13 @@ def react_postprocessing( end_structures = [] single_molecule_mode: bool = len(product_names) == 1 and len(self._calculation.get_structures()) == 1 and \ - not self.settings[self.job_key]["spin_propensity_check_for_unimolecular_reaction"] + not self.settings[self.propensity_key]["check_for_unimolecular_reaction"] for i, (label, product) in enumerate(zip(new_labels, product_names)): surface_indices = split_surfaces_indices[i] - new_structure = self._store_structure_with_propensity_check(product, label, + new_structure = self._store_structure_with_propensity_check(product, self.systems, label, enforce_to_save_base_name=single_molecule_mode, surface_indices=surface_indices) - if program_helper is not None: + if program_helper is not None and self.ref_structure is not None: program_helper.calculation_postprocessing(self._calculation, self.ref_structure, new_structure) end_structures.append(new_structure.id()) """ transfer properties to products which requires to pass all structures""" @@ -1867,14 +1879,22 @@ def react_postprocessing( db_results.add_elementary_step(new_step.id()) """ Save Reaction Path as a Spline""" - spline = self.generate_spline(tsopt_task_name) - new_step.set_spline(spline) - """ Save Reaction Path """ - charge = ts_calc.settings[utils.settings_names.molecular_charge] - multiplicity = ts_calc.settings[utils.settings_names.spin_multiplicity] - model = self._calculation.get_model() - if self.settings[self.job_key]["store_full_mep"]: - _ = self.save_mep_in_db(new_step, charge, multiplicity, model) + try: + trajectory, ts_index = self.read_irc_and_irc_opt_trajectories(tsopt_task_name, "irc") + spline = self.generate_spline(trajectory, ts_index) + new_step.set_spline(spline) + """ Save Reaction Path """ + if self.settings[self.job_key]["store_full_mep"]: + charge = self.get_charge(ts_calc) + multiplicity = self.get_multiplicity(ts_calc) + model = self._calculation.get_model() + self.save_mep_in_db(new_step, trajectory, ts_index, charge, multiplicity, model) + except BaseException as e: + # If the spline generation crashes we need to continue, + # otherwise the database is in a broken state. + # For now, just do not add a spline. + print("Failed to generate spline interpolation for the reaction, continuing without adding the spline to" + " the database. The error was:\n", e) """ Save new starting materials if there are any""" original_start_structures = self._calculation.get_structures() for rid in reactant_structure_ids: @@ -1889,117 +1909,28 @@ def react_postprocessing( def _store_ts_with_propensity_info(self, ts_name: str, program_helper: Optional[ProgramHelper], ts_label: db.Label) -> Tuple[utils.core.Calculator, db.Structure]: # do propensity single_points for TS - self._add_propensity_systems(ts_name) - self._spin_propensity_single_points(ts_name, "Failed all spin propensity single points for TS, " - "which means we could not recalculate the TS system. " - "This points to a SCINE calculator error.") - new_ts = self._store_structure_with_propensity_check(ts_name, ts_label, + self.systems = self._add_propensity_systems(ts_name, self.systems) + self.systems = self._spin_propensity_single_points( + ts_name, + self.systems, + "Failed all spin propensity single points for TS, " + "which means we could not recalculate the TS system. " + "This points to a SCINE calculator error." + ) + new_ts = self._store_structure_with_propensity_check(ts_name, self.systems, ts_label, enforce_to_save_base_name=True) - self.transfer_properties(self.ref_structure, new_ts) - ts_calc = self.systems[ts_name] - self.store_hessian_data(ts_calc, new_ts) - if program_helper is not None: + if self.ref_structure is not None: + self.transfer_properties(self.ref_structure, new_ts) + ts_calc = self.get_system(ts_name) + if ts_label == db.Label.TS_OPTIMIZED or ts_calc.get_results().hessian is not None: + self.store_hessian_data(ts_calc, new_ts) + if program_helper is not None and self.ref_structure is not None: program_helper.calculation_postprocessing(self._calculation, self.ref_structure, new_ts) return ts_calc, new_ts - def _store_structure_with_propensity_check(self, name: str, label: db.Label, enforce_to_save_base_name: bool, - surface_indices: Optional[Union[List[int], Set[int]]] = None) \ - -> db.Structure: - from scine_utilities import settings_names as sn - from scine_utilities import KJPERMOL_PER_HARTREE - - def create_impl(structure_name: str) -> db.Structure: - bond_orders, self.systems = self.make_bond_orders_from_calc(self.systems, structure_name, surface_indices) - new_structure = self.create_new_structure(self.systems[structure_name], label) - self.store_energy(self.systems[structure_name], new_structure) - self.store_bond_orders(bond_orders, new_structure) - self.add_graph(new_structure, bond_orders, surface_indices) - # Label can change based on graph after optimization - if label not in [db.Label.TS_OPTIMIZED, db.Label.TS_GUESS]: - new_graph = self._cbor_graph_from_structure(new_structure) - new_label = self._determine_new_label_based_on_graph_and_surface_indices(new_graph, surface_indices) - if label != new_label: - print("Propensity check led to new label of " + structure_name + ". Relabeling it.") - new_structure.set_label(new_label) - results = self._calculation.get_results() - results.add_structure(new_structure.id()) - self._calculation.set_results(results) - return new_structure - - lowest_name, names_to_save = self._get_propensity_names_within_range( - name, self.settings[self.job_key]["spin_propensity_energy_range_to_save"] - ) - spin_propensity_hit = lowest_name != name - # Printing information - if spin_propensity_hit: - print(f"Noticed spin propensity. Lowest energy spin multiplicity of {name} is " - f"{self.systems[lowest_name].settings[sn.spin_multiplicity]}") - if names_to_save: - print("Spin states with rel. energies to lowest state in kJ/mol which are also saved to the database:") - print("name | multiplicity | rel. energy") - base_energy = self.systems[lowest_name].get_results().energy - for n in names_to_save: - multiplicity = self.systems[n].settings[sn.spin_multiplicity] - energy = self.systems[n].get_results().energy - rel_energy = (energy - base_energy) * KJPERMOL_PER_HARTREE - print(f" {n} | {multiplicity} | {rel_energy}") - if enforce_to_save_base_name: - print(f"Still saving the base multiplicity of {self.systems[name].settings[sn.spin_multiplicity]} " - f"in the elementary step") - # overwrite names to simply safe and write as product of elementary step - names_to_save += [lowest_name] - if name in names_to_save: - names_to_save.remove(name) - lowest_name = name - - # Saving information - name_to_structure_and_label_map = {} - for n in names_to_save: - # Store as Tuple[db.Sturcture, db.Label] - name_to_structure_and_label_map[n] = [create_impl(n)] - name_to_structure_and_label_map[n] += [name_to_structure_and_label_map[n][0].get_label()] - - name_to_structure_and_label_map[lowest_name] = [create_impl(lowest_name)] - name_to_structure_and_label_map[lowest_name] += [name_to_structure_and_label_map[lowest_name][0].get_label()] - - # Decide which structure to return - # Lowest name if no better spin state was found or if the lower spin state still has the same label as name - if not spin_propensity_hit or \ - name_to_structure_and_label_map[lowest_name][1] == label or \ - enforce_to_save_base_name: - return name_to_structure_and_label_map[lowest_name][0] - else: - return name_to_structure_and_label_map[name][0] - - def store_bond_orders(self, bond_orders: utils.BondOrderCollection, structure: db.Structure) -> None: - self.store_property( - self._properties, - "bond_orders", - "SparseMatrixProperty", - bond_orders.matrix, - self._calculation.get_model(), - self._calculation, - structure, - ) - - def _get_propensity_names_within_range(self, name: str, allowed_energy_range: float) -> Tuple[str, List[str]]: - energies: Dict[str, Optional[float]] = {} - for shift_name, _ in self._propensity_iterator(name): - calc = self.systems[shift_name] - energy = calc.get_results().energy if calc is not None else None - energies[shift_name] = energy - # get name with the lowest energy to save as product - lowest_name = min({k: v for k, v in energies.items() if v is not None}, key=energies.get) # type: ignore - lowest_energy = energies[lowest_name] - assert lowest_energy is not None - names_within_range: List[str] = [] - for k, v in energies.items(): - if v is not None and k != lowest_name and \ - abs(v - lowest_energy) * utils.KJPERMOL_PER_HARTREE < allowed_energy_range: - names_within_range.append(k) - return lowest_name, names_within_range - - def save_mep_in_db(self, elementary_step: db.ElementaryStep, charge: int, multiplicity: int, model: db.Model) \ + @requires("database") + def save_mep_in_db(self, elementary_step: db.ElementaryStep, trajectory: utils.MolecularTrajectory, ts_index: int, + charge: int, multiplicity: int, model: db.Model) \ -> List[db.ID]: """ Store each point on the MEP as a structure in the database. @@ -2011,29 +1942,22 @@ def save_mep_in_db(self, elementary_step: db.ElementaryStep, charge: int, multip Parameters ---------- - elementary_step :: scine_database.ElementaryStep + elementary_step : scine_database.ElementaryStep The elementary step of which to store the MEP. - charge :: int + trajectory : utils.MolecularTrajectory + The trajectory of the elementary step. + ts_index : int + The index of the transition state in the trajectory. + charge : int The total charge of the system. - multiplicity :: int + multiplicity : int The spin multiplicity of the system. - model :: scine_database.Model + model : scine_database.Model The model with which all energies in the elementary Step were calculated. """ - def read_trj(fname): - trj = utils.io.read_trajectory(utils.io.TrajectoryFormat.Xyz, fname) - energies = [] - with open(fname, "r") as f: - lines = f.readlines() - nAtoms = int(lines[0].strip()) - i = 0 - while i < len(lines): - energies.append(float(lines[i + 1].strip())) - i += nAtoms + 2 - return trj, energies - - def generate_structure(atoms, charge, multiplicity, model): + + def generate_structure(atoms: utils.AtomCollection): # New structure new_structure = db.Structure() new_structure.link(self._structures) @@ -2046,59 +1970,13 @@ def generate_structure(atoms, charge, multiplicity, model): ) return new_structure.get_id() - if self.step_direction == "forward": - dir = "forward" - rev_dir = "backward" - elif self.step_direction == "backward": - dir = "backward" - rev_dir = "forward" - else: - self.raise_named_exception("Could not determine elementary step direction.") - structure_ids = [] - fpath = os.path.join( - self.work_dir, f"irc_{rev_dir}", f"irc_{rev_dir}.opt.trj.xyz" - ) - if os.path.isfile(fpath): - trj, _ = read_trj(fpath) - for pos in reversed(trj): - sid = generate_structure(utils.AtomCollection(trj.elements, pos), charge, multiplicity, model) - structure_ids.append(sid) - - fpath = os.path.join( - self.work_dir, f"irc_{rev_dir}", f"irc_{rev_dir}.irc.{rev_dir}.trj.xyz" - ) - if os.path.isfile(fpath): - trj, _ = read_trj(fpath) - for pos in reversed(trj): - sid = generate_structure(utils.AtomCollection(trj.elements, pos), charge, multiplicity, model) - structure_ids.append(sid) - else: - raise RuntimeError( - f"Missing IRC trajectory file: irc_{rev_dir}/irc_{rev_dir}.irc.{rev_dir}.trj.xyz" - ) - - structure_ids.append(elementary_step.get_transition_state()) - - fpath = os.path.join( - self.work_dir, f"irc_{dir}", f"irc_{dir}.irc.{dir}.trj.xyz" - ) - if os.path.isfile(fpath): - trj, _ = read_trj(fpath) - for pos in trj: - sid = generate_structure(utils.AtomCollection(trj.elements, pos), charge, multiplicity, model) - structure_ids.append(sid) - else: - raise RuntimeError( - f"Missing IRC trajectory file: irc_{dir}/irc_{dir}.irc.{dir}.trj.xyz" - ) - - fpath = os.path.join(self.work_dir, f"irc_{dir}", f"irc_{dir}.opt.trj.xyz") - if os.path.isfile(fpath): - trj, _ = read_trj(fpath) - for pos in trj: - sid = generate_structure(utils.AtomCollection(trj.elements, pos), charge, multiplicity, model) - structure_ids.append(sid) + elements = trajectory.elements + for i, pos in enumerate(trajectory): + if i == ts_index: + structure_ids.append(elementary_step.get_transition_state()) + else: + structure_ids.append(generate_structure(utils.AtomCollection(elements, pos))) elementary_step.set_path(structure_ids) return structure_ids @@ -2114,13 +1992,15 @@ def _includes_label(self, structure_id_list: List[db.ID], labels: List[db.Label] Parameters ---------- - structure_id_list :: List[db.ID] + structure_id_list : List[db.ID] A list structure ids - labels :: List[db.Label] + labels : List[db.Label] The required labels """ return self._label_locations(structure_id_list, labels)[0] is not None + @requires("database") + @is_configured def _label_locations(self, structure_id_list: List[db.ID], labels: List[db.Label]) \ -> Union[Tuple[int, int], Tuple[None, None]]: """ @@ -2135,29 +2015,20 @@ def _label_locations(self, structure_id_list: List[db.ID], labels: List[db.Label Parameters ---------- - structure_id_list :: List[db.ID] + structure_id_list : List[db.ID] A list structure ids - labels :: List[db.Label] + labels : List[db.Label] The required labels """ for i, sid in enumerate(structure_id_list): - structure = db.Structure(sid, self._structures) + structure_label = db.Structure(sid, self._structures).get_label() for j, label in enumerate(labels): - if structure.get_label() == label: + if structure_label == label: return i, j return None, None - def _determine_new_label_based_on_graph_and_surface_indices(self, graph_str: str, - surface_indices: Union[List[int], Set[int], None]) \ - -> db.Label: - graph_is_split = ";" in graph_str - no_surf_split_decision_label = db.Label.COMPLEX_OPTIMIZED if graph_is_split else db.Label.MINIMUM_OPTIMIZED - surf_split_decision_label = db.Label.SURFACE_COMPLEX_OPTIMIZED if graph_is_split else db.Label.SURFACE_OPTIMIZED - thresh = self.settings[self.job_key]["n_surface_atom_threshold"] - if surface_indices is not None and len(surface_indices) > thresh: - return surf_split_decision_label - return no_surf_split_decision_label - + @requires("database") + @is_configured def _determine_new_label_based_on_graph(self, calculator: utils.core.Calculator, graph_str: str) -> db.Label: """ Determines label for a product structure of the given react job based on the given graph and the labels @@ -2175,13 +2046,13 @@ def _determine_new_label_based_on_graph(self, calculator: utils.core.Calculator, Parameters ---------- - calculator :: Core::Calculator + calculator : Core::Calculator The calculator holding the structure - graph_str :: str + graph_str : str The cbor graph of one or more molecules (separated by ';') Returns ------- - label :: db.Label + label : db.Label The correct label for the new structure corresponding to the given graph """ graph_is_split = ";" in graph_str @@ -2207,6 +2078,8 @@ def _determine_new_label_based_on_graph(self, calculator: utils.core.Calculator, raise RuntimeError(f"Could not deduced the label for the new structure {graph_str} " f"based on start structures {[str(s) for s in start_structure_ids]}") + @requires("database") + @is_configured def _determine_product_labels_of_single_compounds(self, names: List[str], component_map: Optional[List[int]] = None) -> List[db.Label]: """ @@ -2227,14 +2100,14 @@ def _determine_product_labels_of_single_compounds(self, names: List[str], Parameters ---------- - names :: List[str] + names : List[str] The list of system names of the products in the systems map - component_map :: Optional[List[int]] + component_map : Optional[List[int]] The component map of the given systems, take product_component_map if None Returns ------- - labels :: List[db.Label] + labels : List[db.Label] The correct labels for the new structures """ if self.products_component_map is None and component_map is None: @@ -2247,7 +2120,7 @@ def _determine_product_labels_of_single_compounds(self, names: List[str], # we don't have a surface --> all compounds and no user input because products return [db.Label.MINIMUM_OPTIMIZED] * len(names) # sanity checks - n_product_atoms = sum(len(self.systems[name].structure) for name in names) + n_product_atoms = sum(len(self.get_system(name).structure) for name in names) if any(index >= n_product_atoms for index in surface_indices): self.raise_named_exception("Surface indices include invalid numbers for the given products") if len(component_map) != n_product_atoms: @@ -2256,14 +2129,15 @@ def _determine_product_labels_of_single_compounds(self, names: List[str], for index in surface_indices: product_surface_atoms[component_map[index]] += 1 # do not categorize if only single surface atom, but assume this is a transfer from the surface to the product - thresh = self.settings[self.job_key]["n_surface_atom_threshold"] + thresh = self.connectivity_settings["n_surface_atom_threshold"] return [db.Label.SURFACE_OPTIMIZED if n > thresh else db.Label.MINIMUM_OPTIMIZED for n in product_surface_atoms] - def _tsopt_hess_irc_ircopt(self, tsguess_system_name: str, settings_manager: SettingsManager) \ + @requires("database") + @is_configured + def _hess_irc_ircopt(self, ts_system_name: str, settings_manager: SettingsManager) \ -> Tuple[List[str], Optional[List[str]]]: """ - Takes a TS guess and carries out: - * TS optimization + Takes an optimized TS and carries out: * Hessian calculation and check for valid TS * IRC calculation * random displacement of IRC points @@ -2271,29 +2145,20 @@ def _tsopt_hess_irc_ircopt(self, tsguess_system_name: str, settings_manager: Set Parameters ---------- - tsguess_system_name : str - The name of the system holding the TS guess + ts_system_name : str + The name of the system holding the optimized TS settings_manager : SettingsManager The settings manager - """ - import scine_readuct as readuct - inputs = [tsguess_system_name] - """ TSOPT JOB """ - self.setup_automatic_mode_selection("tsopt") - print("TSOpt Settings:") - print(self.settings["tsopt"], "\n") - self.systems, success = self.observed_readuct_call( - 'run_tsopt_task', self.systems, inputs, **self.settings["tsopt"]) - self.throw_if_not_successful( - success, - self.systems, - self.output("tsopt"), - ["energy"], - "TS optimization failed:\n", - ) + Returns + ------- + product_names : List[str] + The names of the products + start_names : Optional[List[str]] + The names of the start structures, if different to the structures of the react job + """ + inputs = [ts_system_name] """ TS HESSIAN """ - inputs = self.output("tsopt") self.systems, success = readuct.run_hessian_task(self.systems, inputs) self.throw_if_not_successful( success, @@ -2313,7 +2178,7 @@ def _tsopt_hess_irc_ircopt(self, tsguess_system_name: str, settings_manager: Set print("IRC Settings:") print(self.settings["irc"], "\n") self.systems, success = self.observed_readuct_call( - 'run_irc_task', self.systems, inputs, **self.settings["irc"]) + SubTaskToReaductCall.IRC, self.systems, inputs, **self.settings["irc"]) """ IRC OPT JOB """ # Run a small energy minimization after initial IRC @@ -2321,13 +2186,14 @@ def _tsopt_hess_irc_ircopt(self, tsguess_system_name: str, settings_manager: Set print("IRC Optimization Settings:") print(self.settings["ircopt"], "\n") for i in inputs: - atoms = self.systems[i].structure + calc = self.get_system(i) + atoms = calc.structure self.random_displace_atoms(atoms) - self.systems[i].positions = atoms.positions + calc.positions = atoms.positions self.systems, success = self.observed_readuct_call( - 'run_opt_task', self.systems, [inputs[0]], **self.settings["ircopt"]) + SubTaskToReaductCall.IRCOPT, self.systems, [inputs[0]], **self.settings["ircopt"]) self.systems, success = self.observed_readuct_call( - 'run_opt_task', self.systems, [inputs[1]], **self.settings["ircopt"]) + SubTaskToReaductCall.IRCOPT, self.systems, [inputs[1]], **self.settings["ircopt"]) """ Check whether we have a valid IRC """ initial_charge = settings_manager.calculator_settings[utils.settings_names.molecular_charge] @@ -2337,13 +2203,57 @@ def _tsopt_hess_irc_ircopt(self, tsguess_system_name: str, settings_manager: Set self.verify_connection() self.capture_raw_output() update_model( - self.systems[self.output("tsopt")[0]], + self.get_system(self.output("tsopt")[0]), self._calculation, self.config, ) raise breakable.Break return product_names, start_names + @requires("database") + @is_configured + def _tsopt_hess_irc_ircopt(self, tsguess_system_name: str, settings_manager: SettingsManager) \ + -> Tuple[List[str], Optional[List[str]]]: + """ + Takes a TS guess and carries out: + * TS optimization + * Hessian calculation and check for valid TS + * IRC calculation + * random displacement of IRC points + * Optimization with faster converging optimizer than Steepest Descent to arrive at true minima (default: BFGS), + however, one can select in principle any optimizer from those available in SCINE, + which could also be a Steepest Descent optimizer to calculate an IRC exactly as defined + (although at much increased computational costs due to its slow convergence) + + Parameters + ---------- + tsguess_system_name : str + The name of the system holding the TS guess + settings_manager : SettingsManager + The settings manager + + Returns + ------- + product_names : List[str] + The names of the products + start_names : Optional[List[str]] + The names of the start structures, if different to the structures of the react job + """ + inputs = [tsguess_system_name] + self.setup_automatic_mode_selection("tsopt") + print("TSOpt Settings:") + print(self.settings["tsopt"], "\n") + self.systems, success = self.observed_readuct_call( + SubTaskToReaductCall.TSOPT, self.systems, inputs, **self.settings["tsopt"]) + self.throw_if_not_successful( + success, + self.systems, + self.output("tsopt"), + ["energy"], + "TS optimization failed:\n", + ) + return self._hess_irc_ircopt(self.output("tsopt")[0], settings_manager) + def _tsopt_hess_irc_ircopt_postprocessing(self, tsguess_system_name: str, settings_manager: SettingsManager, program_helper: Optional[ProgramHelper]) -> None: """ @@ -2369,6 +2279,24 @@ def _tsopt_hess_irc_ircopt_postprocessing(self, tsguess_system_name: str, settin The program helper """ product_names, start_names = self._tsopt_hess_irc_ircopt(tsguess_system_name, settings_manager) + self._postprocessing_with_conformer_handling(product_names, start_names, program_helper) + + def _postprocessing_with_conformer_handling(self, product_names: List[str], start_names: Optional[List], + program_helper: Optional[ProgramHelper]) -> None: + """ + Stores the new start structures if given otherwise takes the input structures of the calculation and + then carries out the postprocessing of the reaction. + + Parameters + ---------- + product_names : List[str] + The names of the products + start_names : Optional[List[str]] + The names of the start structures, if different to the structures of the react job + program_helper : Optional[ProgramHelper] + The program helper + """ + """ Store new starting material conformer(s) """ if start_names is not None: start_structures = self.store_start_structures( @@ -2377,3 +2305,18 @@ def _tsopt_hess_irc_ircopt_postprocessing(self, tsguess_system_name: str, settin start_structures = self._calculation.get_structures() self.react_postprocessing(product_names, program_helper, "tsopt", start_structures) + + def get_system(self, name: str) -> utils.core.Calculator: + """ + Get a calculator from the system map by name and ensures the system is present + + Notes + ----- + * May throw exception + + Parameters + ---------- + name : str + The name of the system to get + """ + return self.get_calc(name, self.systems) diff --git a/scine_puffin/jobs/templates/sub_settings_job.py b/scine_puffin/jobs/templates/sub_settings_job.py new file mode 100644 index 0000000..833a6b2 --- /dev/null +++ b/scine_puffin/jobs/templates/sub_settings_job.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +from abc import ABC +from typing import Any, Dict + +from .job import is_configured +from .scine_connectivity_job import ConnectivityJob + + +class SubSettingsJob(ConnectivityJob, ABC): + """ + """ + + def __init__(self) -> None: + super().__init__() + self.name = "SubSettingsJob" # to be overwritten by child + self.job_key = "job" + # to be extended by child: + self.settings: Dict[str, Dict[str, Any]] = { + self.job_key: {} + } + + @is_configured + def sort_settings(self, task_settings: Dict[str, Any]) -> None: + """ + Take settings of configured calculation and save them in class member. Throw exception for unknown settings. + + Notes + ----- + * Requires run configuration + * May throw exception + + Parameters + ---------- + task_settings : dict + A dictionary from which the settings are taken + """ + self.extract_connectivity_settings_from_dict(task_settings) + # Dissect settings into individual user task_settings + for key, value in task_settings.items(): + for task in self.settings.keys(): + if task == self.job_key: + if key in self.settings[task].keys(): + self.settings[task][key] = value + break # found right task, leave inner loop + else: + indicator_length = len(task) + 1 # underscore to avoid ambiguities + if key[:indicator_length] == task + "_": + self.settings[task][key[indicator_length:]] = value + break # found right task, leave inner loop + else: + self.raise_named_exception( + f"The key '{key}' was not recognized." + ) + + if "ircopt" in self.settings.keys() and "output" in self.settings["ircopt"]: + self.raise_named_exception( + "Cannot specify a separate output system for the optimization of the IRC end points" + ) diff --git a/scine_puffin/jobs/templates/turbomole_job.py b/scine_puffin/jobs/templates/turbomole_job.py new file mode 100644 index 0000000..3668435 --- /dev/null +++ b/scine_puffin/jobs/templates/turbomole_job.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +import os +import subprocess +from typing import List, TYPE_CHECKING + +from scine_puffin.config import Configuration +from scine_puffin.jobs.templates.job import Job +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") + + +class TurbomoleJob(Job): + """ + A common interface for all jobs in Puffin that use Turbomole. + """ + + def __init__(self) -> None: + super().__init__() + self.input_structure = "system.xyz" + + env = os.environ.copy() + + self.turboexe = "" + self.turboscripts = "" + self.smp_turboexe = "" + + if "TURBODIR" in env.keys(): + if env["TURBODIR"]: + if os.environ.get("PARA_ARCH") is not None: + del os.environ["PARA_ARCH"] + if os.path.exists(os.path.join(env["TURBODIR"], "scripts", "sysname")): + self.sysname = ( + subprocess.check_output(os.path.join(env["TURBODIR"], "scripts", "sysname")) + .decode("utf-8", errors='replace') + .rstrip() + ) + self.sysname_parallel = self.sysname + "_smp" + self.turboexe = os.path.join(env["TURBODIR"], "bin", self.sysname) + self.smp_turboexe = os.path.join(env["TURBODIR"], "bin", self.sysname_parallel) + self.turboscripts = os.path.join(env["TURBODIR"], "scripts") + else: + raise RuntimeError("TURBODIR not assigned correctly. Check spelling or empty the env variable.") + + @requires("utilities") + def prepare_calculation(self, structure: db.Structure, calculation_settings: utils.ValueCollection, + model: db.Model, job: db.Job) -> None: + from scine_puffin.utilities.turbomole_helper import TurbomoleHelper + + tm_helper = TurbomoleHelper() + # Write xyz file + utils.io.write(self.input_structure, structure.get_atoms()) + # Write coord file + tm_helper.write_coord_file(calculation_settings) + # Check if settings are available + tm_helper.check_settings_availability(job, calculation_settings) + # Generate input file for preprocessing tool 'define' + tm_helper.prepare_define_session(structure, model, calculation_settings, job) + # Initialize via define + tm_helper.initialize(model, calculation_settings) + + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: + """See Job.run()""" + raise NotImplementedError + + @staticmethod + def required_programs() -> List[str]: + """See Job.required_programs()""" + raise NotImplementedError diff --git a/scine_puffin/jobs/turbomole_bond_orders.py b/scine_puffin/jobs/turbomole_bond_orders.py index 9b4b6c9..c9a947f 100644 --- a/scine_puffin/jobs/turbomole_bond_orders.py +++ b/scine_puffin/jobs/turbomole_bond_orders.py @@ -1,15 +1,26 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING, List import os + import numpy as np + from scine_puffin.config import Configuration -from .templates.job import calculation_context, TurbomoleJob, job_configuration_wrapper +from .templates.job import calculation_context, job_configuration_wrapper +from .templates.turbomole_job import TurbomoleJob from .turbomole_single_point import TurbomoleSinglePoint from ..utilities.turbomole_helper import TurbomoleHelper +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class TurbomoleBondOrders(TurbomoleJob): @@ -40,7 +51,7 @@ class TurbomoleBondOrders(TurbomoleJob): The ``bond_orders`` and the ``electronic_energy`` associated with the structure. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.input_structure = "system.xyz" self.bond_orders_input_file = "bond_orders.inp" @@ -48,8 +59,11 @@ def __init__(self): self.tm_helper = TurbomoleHelper() self.tm_single_point = TurbomoleSinglePoint() - # Read Wiberg bond orders from file generated by the 'proper' postprocessing tool - def get_bond_orders(self, natoms): + def get_bond_orders(self, natoms: int) -> np.ndarray: + """ + Read Wiberg bond orders from file generated by the 'proper' postprocessing tool + """ + bond_orders = np.zeros((natoms, natoms)) with open(self.bond_orders_input_file, "a") as bond_orders_file: bond_orders_file.write("pop\nwiberg\nend\nend\n") @@ -86,9 +100,7 @@ def get_bond_orders(self, natoms): return bond_orders @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - - import scine_database as db + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: # Gather all required collections structures = manager.get_collection("structures") @@ -182,5 +194,5 @@ def run(self, manager, calculation, config: Configuration) -> bool: return True @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "utils", "turbomole"] diff --git a/scine_puffin/jobs/turbomole_geometry_optimization.py b/scine_puffin/jobs/turbomole_geometry_optimization.py index 7509fd0..a4fd381 100644 --- a/scine_puffin/jobs/turbomole_geometry_optimization.py +++ b/scine_puffin/jobs/turbomole_geometry_optimization.py @@ -1,14 +1,26 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ import os -from typing import Any, Tuple +from typing import Any, Tuple, TYPE_CHECKING, List from scine_puffin.config import Configuration -from .templates.job import calculation_context, TurbomoleJob, job_configuration_wrapper +from .templates.job import calculation_context, job_configuration_wrapper +from .templates.turbomole_job import TurbomoleJob from ..utilities.turbomole_helper import TurbomoleHelper +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_database") class TurbomoleGeometryOptimization(TurbomoleJob): @@ -27,35 +39,35 @@ class TurbomoleGeometryOptimization(TurbomoleJob): any ``Calculation`` stored in a SCINE Database. Possible settings for this job are: - cartesian_constraints :: List[int] + cartesian_constraints : List[int] A list of atom indices of the atoms which positions will be constrained during the optimization. - max_scf_iterations :: int + max_scf_iterations : int The number of allowed SCF cycles until convergence. Default value is 30. - transform_coordinates :: bool + transform_coordinates : bool Switch to transform the input coordinates from redundant internal to cartesian coordinates. Setting this value to True and hence performing the calculation in Cartesian coordinates is helpful in rare occasions where the calculation with redundant internal coordinates fails. The optimization will take more time but is more likely to end successfully. The default is False. - convergence_max_iterations :: int + convergence_max_iterations : int The maximum number of geometry optimization cycles. - scf_damping :: bool + scf_damping : bool Increases damping during the SCF by modifying the $scfdamp parameter in the control file. The default is False. - scf_orbitalshift :: float + scf_orbitalshift : float Shifts virtual orbital energies upwards. Default value is 0.1. - convergence_delta_value :: int + convergence_delta_value : int The convergence criterion for the electronic energy difference between two steps. The default value is 1E-6. - calculate_loewdin_charges :: bool + calculate_loewdin_charges : bool Calculates the Loewdin partial charges. The default is False. - self_consistence_criterion :: float + self_consistence_criterion : float The self consistence criterion corresponding to the maximum energy change between two SCF cycles resulting in convergence. Default value ist 1E-6. - spin_mode :: string + spin_mode : string Sets the spin mode. If no spin mode is set, Turbomole's default for the corresponding system is chosen. Options are: restricted, unrestricted. @@ -74,7 +86,7 @@ class TurbomoleGeometryOptimization(TurbomoleJob): The ``electronic_energy`` associated with the new structure and ``atomic_charges`` for all atoms, if requested. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.input_structure = "system.xyz" self.converged_file = "GEO_OPT_CONVERGED" @@ -85,7 +97,7 @@ def __init__(self): self.tm_helper = TurbomoleHelper() # Executes structure optimization using the jobex script - def execute_optimization(self, settings: dict, job): + def execute_optimization(self, settings: utils.ValueCollection, job: db.Job) -> None: os.environ["PARA_ARCH"] = "SMP" os.environ["PARNODES"] = str(job.cores) @@ -107,10 +119,11 @@ def execute_optimization(self, settings: dict, job): if not os.path.exists(self.converged_file): raise RuntimeError("Structure optimization failed.") - # Parse energy and extract output structure from coord file + @requires("utilities") def parse_results(self) -> Tuple[Any, float]: - - import scine_utilities as utils + """ + Parse energy and extract output structure from coord file + """ successful = False @@ -136,9 +149,7 @@ def parse_results(self) -> Tuple[Any, float]: raise RuntimeError @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - - import scine_database as db + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: # Gather all required collections structures = manager.get_collection("structures") @@ -161,8 +172,7 @@ def run(self, manager, calculation, config: Configuration) -> bool: job = calculation.get_job() # New label - # TODO: These labels are not necessarily correct; during the optimization, a - # complex coul be created + # TODO: These labels are not necessarily correct; during the optimization, a complex could be created label = structure.get_label() if label == db.Label.MINIMUM_GUESS or label == db.Label.MINIMUM_OPTIMIZED: new_label = db.Label.MINIMUM_OPTIMIZED @@ -281,5 +291,5 @@ def run(self, manager, calculation, config: Configuration) -> bool: return True @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "utils", "turbomole"] diff --git a/scine_puffin/jobs/turbomole_hessian.py b/scine_puffin/jobs/turbomole_hessian.py index 703eec6..e67e6b9 100644 --- a/scine_puffin/jobs/turbomole_hessian.py +++ b/scine_puffin/jobs/turbomole_hessian.py @@ -1,14 +1,26 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING, List import os + +import numpy as np + from scine_puffin.config import Configuration -from .templates.job import calculation_context, TurbomoleJob, job_configuration_wrapper +from .templates.job import calculation_context, job_configuration_wrapper +from .templates.turbomole_job import TurbomoleJob from .turbomole_single_point import TurbomoleSinglePoint from ..utilities.turbomole_helper import TurbomoleHelper +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class TurbomoleHessian(TurbomoleJob): @@ -24,30 +36,30 @@ class TurbomoleHessian(TurbomoleJob): any ``Calculation`` stored in a SCINE Database. Possible settings for this job are: - self_consistence_criterion :: float + self_consistence_criterion : float The self consistence criterion corresponding to the maximum energy change between two SCF cycles resulting in convergence. Default value ist 1E-6. - cartesian_constraints :: List[int] + cartesian_constraints : List[int] A list of atom indices of the atoms which positions will be constrained during the optimization. - max_scf_iterations :: int + max_scf_iterations : int The number of allowed SCF cycles until convergence. Default value is 30. - transform_coordinates :: bool + transform_coordinates : bool Switch to transform the input coordinates from redundant internal to cartesian coordinates. Setting this value to True and hence performing the calculation in Cartesian coordinates is helpful in rare occasions where the calculation with redundant internal coordinates fails. The optimization will take more time but is more likely to end successfully. The default is True. - scf_damping :: bool + scf_damping : bool Increases damping during the SCF by modifying the $scfdamp parameter in the control file. The default is False. - scf_orbitalshift :: float + scf_orbitalshift : float Shifts virtual orbital energies upwards. Default value is 0.1. - calculate_loewdin_charges :: bool + calculate_loewdin_charges : bool Calculates the Loewdin partial charges. The default is False. - spin_mode :: str + spin_mode : str Sets the spin mode. If no spin mode is set, Turbomole's default for the corresponding system is chosen. Options are: restricted, unrestricted. @@ -65,7 +77,7 @@ class TurbomoleHessian(TurbomoleJob): ``atomic_charges`` for all atoms, if requested """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.input_structure = "system.xyz" self.hessian_file = "hessian" @@ -75,7 +87,7 @@ def __init__(self): self.tm_single_point = TurbomoleSinglePoint() # Run hessian calculation - def execute_hessian_calculation(self, job): + def execute_hessian_calculation(self, job: db.Job) -> None: if job.cores > 1: os.environ["PARA_ARCH"] = "SMP" os.environ["PARNODES"] = str(job.cores) @@ -84,11 +96,9 @@ def execute_hessian_calculation(self, job): self.tm_helper.execute(os.path.join(self.turboexe, "aoforce")) # Read hessian matrix from file "hessian" - def read_hessian_matrix(self, natoms): + def read_hessian_matrix(self, natoms: int) -> np.ndarray: - import numpy - - hessian = [] + hessian_list = [] with open(self.hessian_file, "r") as file: lines = [line.strip() for line in file.readlines()] hess_lines = [line.split() for line in lines[1:-1]] @@ -99,19 +109,18 @@ def read_hessian_matrix(self, natoms): int(item) except ValueError: hess_row.append(float(item)) - hessian.append(hess_row) - hessian = [item for sublist in hessian for item in sublist] - hessian = numpy.array([hessian[i: i + 3 * natoms] for i in range(0, len(hessian), 3 * natoms)]) + hessian_list.append(hess_row) + flat_hessian_list = [item for sublist in hessian_list for item in sublist] + hessian = np.array([flat_hessian_list[i: i + 3 * natoms] + for i in range(0, len(flat_hessian_list), 3 * natoms)]) # Check if Hessian has correct dimension and is symmetric - if (len(hessian) == 3 * natoms) and (numpy.allclose(numpy.transpose(hessian), hessian)): + if (len(hessian) == 3 * natoms) and (np.allclose(np.transpose(hessian), hessian)): return hessian else: raise RuntimeError("Something went wrong while parsing the Hessian matrix.") @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - - import scine_database as db + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: # Gather all required collections structures = manager.get_collection("structures") @@ -218,5 +227,5 @@ def run(self, manager, calculation, config: Configuration) -> bool: return True @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "utils", "turbomole"] diff --git a/scine_puffin/jobs/turbomole_single_point.py b/scine_puffin/jobs/turbomole_single_point.py index 4aa9ca2..517e37c 100644 --- a/scine_puffin/jobs/turbomole_single_point.py +++ b/scine_puffin/jobs/turbomole_single_point.py @@ -1,13 +1,22 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING, List import os from scine_puffin.config import Configuration -from .templates.job import calculation_context, TurbomoleJob, job_configuration_wrapper +from .templates.job import calculation_context, job_configuration_wrapper +from .templates.turbomole_job import TurbomoleJob from ..utilities.turbomole_helper import TurbomoleHelper +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class TurbomoleSinglePoint(TurbomoleJob): @@ -24,30 +33,30 @@ class TurbomoleSinglePoint(TurbomoleJob): any ``Calculation`` stored in a SCINE Database. Possible settings for this job are: - self_consistence_criterion :: float + self_consistence_criterion : float The self consistence criterion corresponding to the maximum energy change between two SCF cycles resulting in convergence. Default value ist 1E-6. - cartesian_constraints :: List[int] + cartesian_constraints : List[int] A list of atom indices of the atoms which positions will be constrained during the optimization. - max_scf_iterations :: int + max_scf_iterations : int The number of allowed SCF cycles until convergence. Default value is 30. - transform_coordinates :: bool + transform_coordinates : bool Switch to transform the input coordinates from redundant internal to cartesian coordinates. Setting this value to True and hence performing the calculation in Cartesian coordinates is helpful in rare occasions where the calculation with redundant internal coordinates fails. The optimization will take more time but is more likely to end successfully. The default is True. - scf_damping :: bool + scf_damping : bool Increases damping during the SCF by modifying the $scfdamp parameter in the control file. The default is False. - scf_orbitalshift :: float + scf_orbitalshift : float Shifts virtual orbital energies upwards. Default value is 0.1. - calculate_loewdin_charges :: bool + calculate_loewdin_charges : bool Calculates the Loewdin partial charges. The default is False. - spin_mode :: string + spin_mode : string Sets the spin mode. If no spin mode is set, Turbomole's default for the corresponding system is chosen. Options are: restricted, unrestricted. @@ -66,13 +75,13 @@ class TurbomoleSinglePoint(TurbomoleJob): """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.input_structure = "system.xyz" self.tm_helper = TurbomoleHelper() # Executes a single point calculation using the dscf script - def execute_single_point_calculation(self, job): + def execute_single_point_calculation(self, job: db.Job) -> None: if job.cores > 1: os.environ["PARA_ARCH"] = "SMP" os.environ["PARNODES"] = str(job.cores) @@ -81,9 +90,7 @@ def execute_single_point_calculation(self, job): self.tm_helper.execute("{}".format(os.path.join(self.turboexe, "ridft"))) @job_configuration_wrapper - def run(self, manager, calculation, config: Configuration) -> bool: - - import scine_database as db + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: # Gather all required collections structures = manager.get_collection("structures") @@ -180,5 +187,5 @@ def run(self, manager, calculation, config: Configuration) -> bool: return True @staticmethod - def required_programs(): + def required_programs() -> List[str]: return ["database", "utils", "turbomole"] diff --git a/scine_puffin/programs/program.py b/scine_puffin/programs/program.py index 3ec9fff..ea7534a 100644 --- a/scine_puffin/programs/program.py +++ b/scine_puffin/programs/program.py @@ -21,12 +21,12 @@ class Program: Parameters ---------- - settings :: dict + settings : dict The settings for the particular program. This dictionary should be the given program's block in the ``Configuration``. """ - def __init__(self, settings: dict): + def __init__(self, settings: dict) -> None: self.version = settings["version"] self.root = settings["root"] self.source = settings["source"] @@ -41,13 +41,13 @@ def install(self, repo_dir: str, install_dir: str, ncores: int): Parameters ---------- - repo_dir :: str + repo_dir : str The folder for all repositories, if a clone or download is required for the installation, this folder will be used. - install_dir :: str + install_dir : str If the program is actually installed and not just loaded, this folder will be used as target directory for the install process. - ncores :: int + ncores : int The number of cores/threads to be used when compiling/installing the program. """ @@ -73,13 +73,13 @@ def setup_environment(self, config: Configuration, env_paths: dict, env_vars: di Parameters ---------- - config :: scine_puffin.config.Configuration + config : scine_puffin.config.Configuration The current global configuration. - env_paths :: dict + env_paths : dict A dictionary for all the environment paths, such as ``PATH`` and ``LD_LIBRARY_PATH``. The added settings will be appended to the existing paths, using ``export PATH=$PATH:...``. - env_vars :: dict + env_vars : dict A dictionary for all fixed environment variables. All settings will replace existing variables such as ``export OMP_NUM_THREADS=1`` """ @@ -92,7 +92,7 @@ def available_models(self) -> List[str]: Returns ------- - models :: List[str] + models : List[str] A list of names of models that are available if the program is available. """ diff --git a/scine_puffin/programs/rms.py b/scine_puffin/programs/rms.py index 9e3aec0..f696c46 100644 --- a/scine_puffin/programs/rms.py +++ b/scine_puffin/programs/rms.py @@ -4,6 +4,7 @@ See LICENSE.txt for details. """ +import os from typing import List, Optional from .program import Program @@ -76,7 +77,6 @@ def set_root(self, root: str): # pylint: enable=attribute-defined-outside-init def compile_julia(self): - import os # Try to load the system image if the file already exists. if self.root: if ".so" not in self.root or not os.path.exists(self.root): diff --git a/scine_puffin/tests/conftest.py b/scine_puffin/tests/conftest.py deleted file mode 100644 index 9799b52..0000000 --- a/scine_puffin/tests/conftest.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -__copyright__ = """ This code is licensed under the 3-clause BSD license. -Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. -See LICENSE.txt for details. -""" - - -import pytest -from .db_setup import get_clean_db - - -@pytest.fixture(scope='session', autouse=True) -def precondition(): - try: - _ = get_clean_db() - except RuntimeError as e: - pytest.exit(f'{str(e)}\nFirst start database before running unittests.') diff --git a/scine_puffin/tests/cp2k/cp2k_test.py b/scine_puffin/tests/cp2k/cp2k_test.py index 13f8d4a..ab91f39 100644 --- a/scine_puffin/tests/cp2k/cp2k_test.py +++ b/scine_puffin/tests/cp2k/cp2k_test.py @@ -14,7 +14,7 @@ class Cp2kTests(unittest.TestCase): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super(Cp2kTests, self).__init__(*args, **kwargs) self.resources_directory = os.path.join(pathlib.Path(__file__).parent.resolve(), "files") diff --git a/scine_puffin/tests/daemon_test.py b/scine_puffin/tests/daemon_test.py index 82cc6f3..9f818dc 100644 --- a/scine_puffin/tests/daemon_test.py +++ b/scine_puffin/tests/daemon_test.py @@ -24,7 +24,7 @@ def test_database_connection(self): class ValidJobClasses(unittest.TestCase): - @skip_without('database') + def test_job_folder(self): config = Configuration() config.load() diff --git a/scine_puffin/tests/db_setup.py b/scine_puffin/tests/db_setup.py index 779bd5d..6aac738 100644 --- a/scine_puffin/tests/db_setup.py +++ b/scine_puffin/tests/db_setup.py @@ -19,12 +19,12 @@ def get_test_db_credentials(name: str = "puffin_unittests"): Parameters ---------- - name :: str + name : str The name of the database to connect to. Returns ------- - result :: db.Credentials + result : db.Credentials The credentials to access the test database. """ import scine_database as db @@ -40,12 +40,12 @@ def get_clean_db(name: str = "puffin_unittests"): Parameters ---------- - name :: str + name : str The name of the database to connect to. Returns ------- - result :: db.Manager + result : db.Manager The database manager, connected to the requested server and named database. """ @@ -70,23 +70,23 @@ def add_structure(manager, xyz_path, label, charge: int = 0, multiplicity: int = Parameters ---------- - manager :: db.Manager + manager : db.Manager The manager of the database to create data in. - xyz_path :: str + xyz_path : str Path to the xyz file containing the structures coordinates. - label :: db.Label + label : db.Label The label of the structure to be generated. - charge :: int + charge : int The charge of the structure - multiplicity :: int + multiplicity : int The multiplicity of the structure - model :: db.Model, optional + model : db.Model, optional The model of the structure to be generated. Take db.Model("dftb3", "dftb3", "") with program "Sparrow" as default. Returns ------- - structure :: db.Structure + structure : db.Structure The generated Structure linked to its collection """ import scine_database as db @@ -105,21 +105,21 @@ def add_calculation(manager, model, job, structures, settings: Optional[dict] = Parameters ---------- - manager :: db.Manager + manager : db.Manager The manager of the database to create data in. - model :: db.Model + model : db.Model The Model of the calculation. - job :: db.Job + job : db.Job The Job of the calculation. - structures :: List[db.ID] + structures : List[db.ID] List of structure IDs set as structures used as input for the calculation - settings :: dict + settings : dict Settings to be set in the Calculation. Returns ------- - calculation :: db.Calculation + calculation : db.Calculation The generated Calculation linked to its collection """ import scine_database as db @@ -143,22 +143,22 @@ def add_compound_and_structure(manager, xyz_file: str = "proline_acid.xyz", char Parameters ---------- - manager :: db.Manager + manager : db.Manager The manager of the database to create data in. - xyz_file :: str + xyz_file : str The xyz file name for the structure that is added - charge :: int + charge : int The charge of the structure - multiplicity :: int + multiplicity : int The spin multiplicity of the structure - label :: db.Label, optional + label : db.Label, optional The label of the structure to be generated. - model :: db.Model, optional + model : db.Model, optional The model of the structure to be generated. Take db.Model("dftb3", "dftb3", "") with program "Sparrow" as default. Returns ------- - compound :: db.Compound + compound : db.Compound The Compound. """ import scine_database as db @@ -179,13 +179,13 @@ def add_flask_and_structure(manager, xyz_file: str = "proline_acid.xyz", model: Parameters ---------- - manager :: db.Manager + manager : db.Manager The manager of the database to create data in. - xyz_file :: str + xyz_file : str The xyz file name for the structure that is added Returns ------- - flask :: db.Flask + flask : db.Flask The Flask. """ import scine_database as db @@ -204,15 +204,15 @@ def add_reaction(manager, lhs_compound_ids, rhs_compound_ids): Parameters ---------- - manager :: db.Manager + manager : db.Manager The manager of the database to create data in. - lhs_compound_ids :: List[db.ID] + lhs_compound_ids : List[db.ID] The left-hand side of the reaction. - rhs_compound_ids :: List[db.ID] + rhs_compound_ids : List[db.ID] The right-hand side of the reaction. Returns ------- - compound :: db.Compound + compound : db.Compound The Reaction. """ import scine_database as db diff --git a/scine_puffin/tests/jobs/test_kinetx_kinetic_modeling_job.py b/scine_puffin/tests/jobs/test_kinetx_kinetic_modeling_job.py index b63ca10..f316941 100644 --- a/scine_puffin/tests/jobs/test_kinetx_kinetic_modeling_job.py +++ b/scine_puffin/tests/jobs/test_kinetx_kinetic_modeling_job.py @@ -18,7 +18,6 @@ add_reaction, add_flask_and_structure ) -from ...utilities.compound_and_flask_helpers import get_compound_or_flask class KinetxKineticModelingJobTest(JobTestCase): @@ -27,6 +26,7 @@ class KinetxKineticModelingJobTest(JobTestCase): def test_concentrations(self): # import Job from scine_puffin.jobs.kinetx_kinetic_modeling import KinetxKineticModeling + from scine_puffin.utilities.compound_and_flask_helpers import get_compound_or_flask import scine_database as db # This reaction network is made up and converges fairly quickly. diff --git a/scine_puffin/tests/jobs/test_rms_input_file_creator.py b/scine_puffin/tests/jobs/test_rms_input_file_creator.py index 9efc16a..8daa2b1 100644 --- a/scine_puffin/tests/jobs/test_rms_input_file_creator.py +++ b/scine_puffin/tests/jobs/test_rms_input_file_creator.py @@ -1,12 +1,13 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +from typing import TYPE_CHECKING import os -import scine_database as db - from ..testcases import ( JobTestCase, ) @@ -14,9 +15,18 @@ add_compound_and_structure, add_reaction, ) +from scine_puffin.tests.testcases import skip_without + +from scine_puffin.utilities.imports import module_exists, MissingDependency +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class RMSInputFileCreatorTest(JobTestCase): + + @skip_without('utilities') def test_phase_entry(self): """ Idea of the test: Check if the created dictionaries have the expected format. @@ -34,7 +44,7 @@ def test_phase_entry(self): 'polys': [{ 'Tmax': 5000.0, 'Tmin': 1.0, - 'coefs': [0.0, 0.0, 0.0, 0.0, 0.0, 3.0/r, 0.0/r], + 'coefs': [0.0, 0.0, 0.0, 0.0, 0.0, 3.0 / r, 0.0 / r], 'type': 'NASApolynomial'}], 'type': 'NASA'}, 'type': 'Species'}, @@ -43,7 +53,7 @@ def test_phase_entry(self): 'thermo': { 'polys': [{'Tmax': 5000.0, 'Tmin': 1.0, - 'coefs': [0.0, 0.0, 0.0, 0.0, 0.0, 2.0/r, 1.0/r], + 'coefs': [0.0, 0.0, 0.0, 0.0, 0.0, 2.0 / r, 1.0 / r], 'type': 'NASApolynomial'}], 'type': 'NASA'}, 'type': 'Species'}, {'name': 'Some-Solvent', @@ -57,6 +67,7 @@ def test_phase_entry(self): 'name': 'phase'}] assert reference == phase_list + @skip_without('database') def test_create_rms_reaction_entry(self): """ Idea of the test: Check if the created dictionaries have the expected format. @@ -90,6 +101,7 @@ def test_create_rms_reaction_entry(self): 'type': 'ElementaryReaction'}] assert reaction_list == reference + @skip_without('database') def test_create_rms_yml_file(self): """ Idea of the test: Check if the rms yaml input file can be created without problems. diff --git a/scine_puffin/tests/jobs/test_rms_kinetic_modeling_job.py b/scine_puffin/tests/jobs/test_rms_kinetic_modeling_job.py index 3870e40..8b1daad 100644 --- a/scine_puffin/tests/jobs/test_rms_kinetic_modeling_job.py +++ b/scine_puffin/tests/jobs/test_rms_kinetic_modeling_job.py @@ -115,8 +115,8 @@ def test_concentrations_ideal_dilute_solution(self): enthalpies = [-2e+2, -1.98e+2, -1.97e+2, -1.99e+2, -2.1e+2] n = [0, 0, 0, 0] start_concentrations = [0.5, 0.4, 0.0, 0.0, 0.0] - reference_data = [0.20531956, 0.15521179, 0.20514741, 0.15525519, 0.17906605, 14.3] - reference_max = [0.5, 0.4, 0.22863946, 0.22209669, 0.17906605, 14.3] + reference_data = [0.20531956, 0.15521179, 0.20514741, 0.15525519, 0.17906605, 14.3] + reference_max = [0.5, 0.4, 0.22863946, 0.22209669, 0.17906605, 14.3] reference_flux = [0.38552628, 0.37854594, 0.29599325, 0.28901291, 0.17906606] model = db.Model('FAKE', '', '') @@ -184,7 +184,7 @@ def test_sensitivity_analysis(self): all_reaction_ids = [ add_reaction(self.manager, [c_ids[8]], [c_ids[11], c_ids[12]]).id(), add_reaction(self.manager, [c_ids[5]], [c_ids[8]]).id(), - add_reaction(self.manager, [c_ids[1], c_ids[1]], [c_ids[5]]).id(), + add_reaction(self.manager, [c_ids[1], c_ids[1]], [c_ids[5]]).id(), add_reaction(self.manager, [c_ids[9]], [c_ids[13], c_ids[14], c_ids[1]]).id(), add_reaction(self.manager, [c_ids[4]], [c_ids[2]]).id(), add_reaction(self.manager, [c_ids[4]], [c_ids[9]]).id(), diff --git a/scine_puffin/tests/jobs/test_scine_afir.py b/scine_puffin/tests/jobs/test_scine_afir.py index 8380fb3..35e8a2d 100644 --- a/scine_puffin/tests/jobs/test_scine_afir.py +++ b/scine_puffin/tests/jobs/test_scine_afir.py @@ -40,7 +40,8 @@ def run_by_label(self, input_label, expected_label): settings = { "afir_afir_lhs_list": [3, 16], "afir_afir_rhs_list": [17, 20], - "afir_convergence_max_iterations": 800 + "afir_convergence_max_iterations": 800, + "spin_propensity_check": 0, } calculation = add_calculation(self.manager, model, job, [structure.id()], settings) @@ -77,14 +78,14 @@ def run_by_label(self, input_label, expected_label): @skip_without('database', 'readuct', 'molassembler') def test_user_guess(self): import scine_database as db - self.run_by_label(db.Label.USER_GUESS, db.Label.USER_COMPLEX_OPTIMIZED) + self.run_by_label(db.Label.USER_GUESS, db.Label.USER_OPTIMIZED) @skip_without('database', 'readuct', 'molassembler') def test_minimum_guess(self): import scine_database as db - self.run_by_label(db.Label.MINIMUM_GUESS, db.Label.COMPLEX_OPTIMIZED) + self.run_by_label(db.Label.MINIMUM_GUESS, db.Label.MINIMUM_OPTIMIZED) @skip_without('database', 'molassembler', 'readuct') def test_surface_guess(self): import scine_database as db - self.run_by_label(db.Label.SURFACE_GUESS, db.Label.SURFACE_COMPLEX_OPTIMIZED) + self.run_by_label(db.Label.SURFACE_GUESS, db.Label.SURFACE_OPTIMIZED) diff --git a/scine_puffin/tests/jobs/test_scine_dissociation_cut_job.py b/scine_puffin/tests/jobs/test_scine_dissociation_cut_job.py index 46a9a0a..e2e3bf1 100644 --- a/scine_puffin/tests/jobs/test_scine_dissociation_cut_job.py +++ b/scine_puffin/tests/jobs/test_scine_dissociation_cut_job.py @@ -37,18 +37,26 @@ def _e_help(self, sid) -> float: properties).get_data() def _setup_and_execute(self, dissociations: List[int], charge_propensity_check: int, - charge: int = 0, multiplicity: int = 1): + charge: int = 0, multiplicity: int = 1, with_opt: bool = False, + system_name: str = "butane"): from scine_puffin.jobs.scine_dissociation_cut import ScineDissociationCut + from scine_puffin.jobs.scine_dissociation_cut_with_optimization import ScineDissociationCutWithOptimization import scine_database as db - reactant_path = os.path.join(resource_path(), "butane.mol") + + reactant_path = os.path.join(resource_path(), f"{system_name}.mol") + if not os.path.exists(reactant_path): + reactant_path = os.path.join(resource_path(), f"{system_name}.xyz") + if not os.path.exists(reactant_path): + raise FileNotFoundError(f"Could not find {system_name}.mol or {system_name}.xyz in {resource_path()}") reactant_guess = add_structure(self.manager, reactant_path, db.Label.MINIMUM_OPTIMIZED, charge=charge, multiplicity=multiplicity) - graph = json.load(open(os.path.join(resource_path(), "butane.json"), "r")) + graph = json.load(open(os.path.join(resource_path(), f"{system_name}.json"), "r")) for key, value in graph.items(): reactant_guess.set_graph(key, value) - model = db.Model('dftb3', 'dftb3', '') - db_job = db.Job('scine_dissociation_cut') + model = db.Model('dftb3', 'dftb3', '') if not with_opt else db.Model('pm6', 'pm6', '') + db_job = db.Job('scine_dissociation_cut') if not with_opt \ + else db.Job('scine_dissociation_cut_with_optimization') settings = { "dissociations": dissociations, "charge_propensity_check": charge_propensity_check, @@ -59,7 +67,7 @@ def _setup_and_execute(self, dissociations: List[int], charge_propensity_check: # Run calculation/job config = self.get_configuration() - job = ScineDissociationCut() + job = ScineDissociationCut() if not with_opt else ScineDissociationCutWithOptimization() job.prepare(config["daemon"]["job_dir"], calculation.id()) self.run_job(job, calculation, config) @@ -71,49 +79,110 @@ def test_single_butane_cut(self): import scine_utilities as utils import scine_molassembler as masm - calculation = self._setup_and_execute([1, 2], 1) - - # Check results - structures = self.manager.get_collection("structures") - elementary_steps = self.manager.get_collection("elementary_steps") - - assert calculation.get_status() == db.Status.COMPLETE - results = calculation.get_results() - expected_structures = 6 # -1 0 +1 charge for two structures from single split - assert len(results.structure_ids) == expected_structures - # bo + energy for products + reactant + lowest dissociated structures property - assert len(results.property_ids) == (expected_structures + 1) * 2 + 1 - assert len(results.elementary_step_ids) == 1 + for with_opt in [False, True]: + calculation = self._setup_and_execute([1, 2], 1, with_opt=with_opt) + + # Check results + structures = self.manager.get_collection("structures") + elementary_steps = self.manager.get_collection("elementary_steps") + + assert calculation.get_status() == db.Status.COMPLETE + results = calculation.get_results() + expected_structures = 6 + int(with_opt) # -1 0 +1 charge for two structures from single split + assert len(results.structure_ids) == expected_structures + # bo + energy for products + reactant + lowest dissociated structures property + assert len(results.property_ids) == (expected_structures - int(with_opt) + 1) * 2 + 1 + assert len(results.elementary_step_ids) == 1 + + step = db.ElementaryStep(results.elementary_step_ids[0], elementary_steps) + assert step.get_type() == db.ElementaryStepType.BARRIERLESS + + reactants = step.get_reactants(db.Side.BOTH) + assert len(reactants[0]) == 1 + assert len(reactants[1]) == 2 + for product in reactants[1]: + assert db.Structure(product, structures).get_label() == db.Label.MINIMUM_OPTIMIZED + assert db.Structure(product, structures).has_property('bond_orders') + assert db.Structure(product, structures).has_property('electronic_energy') + assert db.Structure(product, structures).has_graph('masm_cbor_graph') + + assert masm.JsonSerialization.equal_molecules(self._g_help(reactants[1][0]), self._g_help(reactants[1][1])) + assert with_opt or masm.JsonSerialization.equal_molecules( + self._g_help( + reactants[0][0]), self._g_help( + calculation.get_structures()[0])) + + reaction_energy = (sum(self._e_help(p) + for p in reactants[1]) - self._e_help(reactants[0][0])) * utils.KJPERMOL_PER_HARTREE + ref = 240.8673484190177 if with_opt else 426.1682851069426 + self.assertAlmostEqual(reaction_energy, ref, delta=1) - step = db.ElementaryStep(results.elementary_step_ids[0], elementary_steps) - assert step.get_type() == db.ElementaryStepType.BARRIERLESS - - reactants = step.get_reactants(db.Side.BOTH) - assert len(reactants[0]) == 1 - assert len(reactants[1]) == 2 - for product in reactants[1]: - assert db.Structure(product, structures).get_label() == db.Label.MINIMUM_OPTIMIZED - assert db.Structure(product, structures).has_property('bond_orders') - assert db.Structure(product, structures).has_property('electronic_energy') - assert db.Structure(product, structures).has_graph('masm_cbor_graph') - - assert masm.JsonSerialization.equal_molecules(self._g_help(reactants[1][0]), self._g_help(reactants[1][1])) - assert masm.JsonSerialization.equal_molecules( - self._g_help( - reactants[0][0]), self._g_help( - calculation.get_structures()[0])) + @skip_without('database', 'readuct', 'molassembler') + def test_double_butane_cut(self): + import scine_database as db + import scine_utilities as utils + import scine_molassembler as masm - reaction_energy = (sum(self._e_help(p) - for p in reactants[1]) - self._e_help(reactants[0][0])) * utils.KJPERMOL_PER_HARTREE - self.assertAlmostEqual(reaction_energy, 426.1682851069426, delta=1) + for with_opt in [False, True]: + calculation = self._setup_and_execute([0, 1, 1, 2], 1, with_opt=with_opt) + + # Check results + structures = self.manager.get_collection("structures") + elementary_steps = self.manager.get_collection("elementary_steps") + + assert calculation.get_status() == db.Status.COMPLETE + results = calculation.get_results() + expected_structures = 9 + int(with_opt) # -1 0 +1 charge for three structures from double split + assert len(results.structure_ids) == expected_structures + # bo + energy for products + reactant + lowest dissociated structures property + assert len(results.property_ids) == (expected_structures - int(with_opt) + 1) * 2 + 1 + assert len(results.elementary_step_ids) == 1 + + step = db.ElementaryStep(results.elementary_step_ids[0], elementary_steps) + assert step.get_type() == db.ElementaryStepType.BARRIERLESS + + reactants = step.get_reactants(db.Side.BOTH) + assert len(reactants[0]) == 1 + assert len(reactants[1]) == 3 + for product in reactants[1]: + assert db.Structure(product, structures).get_label() == db.Label.MINIMUM_OPTIMIZED + assert db.Structure(product, structures).has_property('bond_orders') + assert db.Structure(product, structures).has_property('electronic_energy') + assert db.Structure(product, structures).has_graph('masm_cbor_graph') + + # two doublet and one triplet + assert sum([db.Structure(product, structures).get_multiplicity() == 2 for product in reactants[1]]) == 2 + if with_opt: + assert sum([db.Structure(product, structures).get_multiplicity() == 3 for product in reactants[1]]) == 1 + assert sum([db.Structure(product, structures).get_multiplicity() == 1 for product in reactants[1]]) == 0 + else: + assert sum([db.Structure(product, structures).get_multiplicity() == 3 for product in reactants[1]]) == 0 + assert sum([db.Structure(product, structures).get_multiplicity() == 1 for product in reactants[1]]) == 1 + + assert with_opt or masm.JsonSerialization.equal_molecules( + self._g_help( + reactants[0][0]), self._g_help( + calculation.get_structures()[0])) + assert not masm.JsonSerialization.equal_molecules( + self._g_help(reactants[1][0]), self._g_help(reactants[1][1])) + assert not masm.JsonSerialization.equal_molecules( + self._g_help(reactants[1][0]), self._g_help(reactants[1][2])) + assert not masm.JsonSerialization.equal_molecules( + self._g_help(reactants[1][1]), self._g_help(reactants[1][2])) + + reaction_energy = (sum(self._e_help(p) + for p in reactants[1]) - self._e_help(reactants[0][0])) * utils.KJPERMOL_PER_HARTREE + ref = 602.2781779325359 if with_opt else 895.4669538980517 + self.assertAlmostEqual(reaction_energy, ref, delta=1) @skip_without('database', 'readuct', 'molassembler') - def test_double_butane_cut(self): + def test_single_butane_charged_cut(self): import scine_database as db import scine_utilities as utils import scine_molassembler as masm - calculation = self._setup_and_execute([0, 1, 1, 2], 1) + charge = 1 + calculation = self._setup_and_execute([1, 2], 1, charge=charge, multiplicity=2) # Check results structures = self.manager.get_collection("structures") @@ -121,7 +190,7 @@ def test_double_butane_cut(self): assert calculation.get_status() == db.Status.COMPLETE results = calculation.get_results() - expected_structures = 9 # -1 0 +1 charge for three structures from double split + expected_structures = 6 # -1 0 +1 charge for two structures from single split assert len(results.structure_ids) == expected_structures # bo + energy for products + reactant + lowest dissociated structures property assert len(results.property_ids) == (expected_structures + 1) * 2 + 1 @@ -132,32 +201,31 @@ def test_double_butane_cut(self): reactants = step.get_reactants(db.Side.BOTH) assert len(reactants[0]) == 1 - assert len(reactants[1]) == 3 + assert db.Structure(reactants[0][0], structures).get_charge() == charge + assert len(reactants[1]) == 2 for product in reactants[1]: assert db.Structure(product, structures).get_label() == db.Label.MINIMUM_OPTIMIZED assert db.Structure(product, structures).has_property('bond_orders') assert db.Structure(product, structures).has_property('electronic_energy') assert db.Structure(product, structures).has_graph('masm_cbor_graph') + assert masm.JsonSerialization.equal_molecules(self._g_help(reactants[1][0]), self._g_help(reactants[1][1])) assert masm.JsonSerialization.equal_molecules( self._g_help( reactants[0][0]), self._g_help( calculation.get_structures()[0])) - assert not masm.JsonSerialization.equal_molecules(self._g_help(reactants[1][0]), self._g_help(reactants[1][1])) - assert not masm.JsonSerialization.equal_molecules(self._g_help(reactants[1][0]), self._g_help(reactants[1][2])) - assert not masm.JsonSerialization.equal_molecules(self._g_help(reactants[1][1]), self._g_help(reactants[1][2])) reaction_energy = (sum(self._e_help(p) for p in reactants[1]) - self._e_help(reactants[0][0])) * utils.KJPERMOL_PER_HARTREE - self.assertAlmostEqual(reaction_energy, 895.4669538980517, delta=1) + self.assertAlmostEqual(reaction_energy, 255.10683618838843, delta=1) @skip_without('database', 'readuct', 'molassembler') - def test_single_butane_charged_cut(self): + def test_single_co2_cut(self): import scine_database as db import scine_utilities as utils - import scine_molassembler as masm - calculation = self._setup_and_execute([1, 2], 1, charge=1, multiplicity=2) + calculation = self._setup_and_execute([1, 2], 1, with_opt=False, + system_name="co2") # Check results structures = self.manager.get_collection("structures") @@ -182,13 +250,10 @@ def test_single_butane_charged_cut(self): assert db.Structure(product, structures).has_property('bond_orders') assert db.Structure(product, structures).has_property('electronic_energy') assert db.Structure(product, structures).has_graph('masm_cbor_graph') - - assert masm.JsonSerialization.equal_molecules(self._g_help(reactants[1][0]), self._g_help(reactants[1][1])) - assert masm.JsonSerialization.equal_molecules( - self._g_help( - reactants[0][0]), self._g_help( - calculation.get_structures()[0])) + assert any(len(db.Structure(product, structures).get_atoms()) == 1 for product in reactants[1]) + assert any(db.Structure(product, structures).get_multiplicity() == 3 for product in reactants[1]) reaction_energy = (sum(self._e_help(p) - for p in reactants[1]) - self._e_help(reactants[0][0])) * utils.KJPERMOL_PER_HARTREE - self.assertAlmostEqual(reaction_energy, 255.10683618838843, delta=1) + for p in reactants[1]) - self._e_help(reactants[0][0])) * utils.KJPERMOL_PER_HARTREE + ref = 542.5066448245391 + self.assertAlmostEqual(reaction_energy, ref, delta=1) diff --git a/scine_puffin/tests/jobs/test_scine_geometry_optimization_job.py b/scine_puffin/tests/jobs/test_scine_geometry_optimization_job.py index b8908d4..8922576 100644 --- a/scine_puffin/tests/jobs/test_scine_geometry_optimization_job.py +++ b/scine_puffin/tests/jobs/test_scine_geometry_optimization_job.py @@ -21,22 +21,51 @@ class ScineGeometryOptimizationJobTest(JobTestCase): + def calculate_graph(self, structure_file): + from scine_puffin.jobs.graph import Graph + import scine_utilities as utils + import scine_database as db + + properties = self.manager.get_collection("properties") + + structure_file_path = os.path.join(resource_path(), structure_file) + structure = add_structure(self.manager, structure_file_path, db.Label.IRRELEVANT) + model = db.Model('dftb3', 'dftb3', '') + + bos = utils.io.read(structure_file_path)[1] + bo_prop = db.SparseMatrixProperty.make("bond_orders", model, bos.matrix, properties) + bo_prop.set_structure(structure.id()) + structure.add_property("bond_orders", bo_prop.id()) + + job = db.Job('graph') + calculation = add_calculation(self.manager, model, job, [structure.id()]) + + # Test successful run + config = self.get_configuration() + job = Graph() + job.prepare(config["daemon"]["job_dir"], calculation.id()) + self.run_job(job, calculation, config) + assert calculation.get_status() == db.Status.COMPLETE + graph = structure.get_graph("masm_cbor_graph") - def run_by_label(self, input_label, expected_label): + return graph + + def run_by_label(self, input_label, expected_label, structure_file="water.xyz"): + import scine_database as db # import Job from scine_puffin.jobs.scine_geometry_optimization import ScineGeometryOptimization - import scine_database as db # Setup DB for calculation - water = os.path.join(resource_path(), "water.xyz") - structure = add_structure(self.manager, water, input_label) + structure_file_path = os.path.join(resource_path(), structure_file) + input_structure = add_structure(self.manager, structure_file_path, input_label) model = db.Model('dftb3', 'dftb3', '') if input_label == db.Label.SURFACE_GUESS: prop = db.VectorProperty.make("surface_atom_indices", model, [0, 1], self.manager.get_collection("properties")) - structure.add_property("surface_atom_indices", prop.id()) + input_structure.add_property("surface_atom_indices", prop.id()) job = db.Job('scine_geometry_optimization') - calculation = add_calculation(self.manager, model, job, [structure.id()]) + calculation = add_calculation(self.manager, model, job, [input_structure.id()], + {'opt_convergence_max_iterations': 500}) # Run calculation/job config = self.get_configuration() @@ -50,7 +79,7 @@ def run_by_label(self, input_label, expected_label): assert len(results.structure_ids) == 1 structure = db.Structure(results.structure_ids[0]) structures = self.manager.get_collection("structures") - assert structures.count("{}") == 2 + assert structures.count("{}") >= 2 structure.link(structures) assert expected_label == structure.get_label() assert structure.has_property("electronic_energy") @@ -61,7 +90,9 @@ def run_by_label(self, input_label, expected_label): energy = db.NumberProperty(energy_props[0]) properties = self.manager.get_collection("properties") energy.link(properties) - self.assertAlmostEqual(energy.get_data(), -4.071575644, delta=1e-1) + if structure_file == "water.xyz": + self.assertAlmostEqual(energy.get_data(), -4.071575644, delta=1e-1) + return input_structure, structure @skip_without('database', 'readuct') def test_user_guess(self): @@ -77,3 +108,14 @@ def test_minimum_guess(self): def test_surface_guess(self): import scine_database as db self.run_by_label(db.Label.SURFACE_GUESS, db.Label.SURFACE_OPTIMIZED) + + @skip_without('database', 'readuct') + def test_user_guess_optimized_to_complex(self): + import scine_database as db + graph = self.calculate_graph("c5of8.mol") + assert ';' not in graph + input_structure, output_structure = self.run_by_label(db.Label.USER_GUESS, db.Label.USER_COMPLEX_OPTIMIZED, + "c5of8.mol") + assert output_structure.has_graph("masm_cbor_graph") + assert ';' in output_structure.get_graph("masm_cbor_graph") + assert not input_structure.has_graph("masm_cbor_graph") diff --git a/scine_puffin/tests/jobs/test_scine_geometry_validation_job.py b/scine_puffin/tests/jobs/test_scine_geometry_validation_job.py index cfc6834..f8fc029 100644 --- a/scine_puffin/tests/jobs/test_scine_geometry_validation_job.py +++ b/scine_puffin/tests/jobs/test_scine_geometry_validation_job.py @@ -118,15 +118,15 @@ def test_non_valid_minimum(self): assert np.allclose(ref_hessian, hessian, atol=1e-1) # Normal modes ref_normal_modes = np.array([ - [-6.57555073e-02, 1.26620092e-02, 4.40344617e-02, ], - [1.75882705e-03, -4.50720615e-03, 4.28132636e-02, ], - [6.25238992e-18, -4.82124061e-18, 2.08537396e-18, ], - [6.80766002e-01, -6.84858319e-01, -6.00657576e-02, ], - [3.90929440e-01, 4.12321086e-01, 4.06809503e-02, ], - [2.59753169e-16, -1.21379778e-17, -1.53607653e-17, ], - [3.63010525e-01, 4.83866667e-01, -6.38919610e-01, ], - [-4.18848354e-01, -3.40775505e-01, -7.20281511e-01, ], - [-4.33840517e-16, 1.20815437e-17, 2.94634947e-17, ], + [-6.82647619e-02, 1.27298723e-02, 4.55149739e-02, ], + [1.82667950e-03, -4.53157634e-03, 4.42513393e-02, ], + [6.49103407e-18, -4.84694439e-18, 2.15525997e-18, ], + [7.06741648e-01, -6.88502360e-01, -6.21030118e-02, ], + [4.05861756e-01, 4.14501053e-01, 4.20445836e-02, ], + [2.69669044e-16, -1.22067159e-17, -1.58816392e-17, ], + [3.76865779e-01, 4.86433477e-01, -6.60383413e-01, ], + [-4.34857733e-01, -3.42568629e-01, -7.44472580e-01, ], + [-4.50402118e-16, 1.21528609e-17, 3.04612875e-17, ], ]) normal_mode_prop = db.DenseMatrixProperty(normal_modes_props[0]) normal_mode_prop.link(properties) @@ -261,17 +261,17 @@ def test_valid_minimum(self): assert hessian.shape == (9, 9) assert np.allclose(ref_hessian, hessian, atol=1e-1) # Normal modes - ref_normal_modes = np.array( - [[-4.47497118e-02, -3.85208122e-02, -4.31593322e-02], - [-4.47122166e-02, -3.88386563e-02, 4.29126332e-02], - [-8.49613600e-18, 5.24295770e-20, 2.59912639e-19], - [2.95571230e-02, 6.81190413e-01, 6.85647964e-01], - [6.80781838e-01, -6.97265040e-02, -5.54050376e-04], - [1.70377797e-16, 1.60989274e-16, 1.17463267e-16], - [6.80781838e-01, -6.97265040e-02, -5.54050376e-04], - [2.89619394e-02, 6.86235743e-01, -6.80623863e-01], - [-2.83048332e-16, -1.53196255e-16, -2.05063421e-16]] - ) + ref_normal_modes = np.array([ + [-4.63375323e-02, -3.95703688e-02, -4.45850571e-02, ], + [-4.62987096e-02, -3.98968446e-02, 4.43302315e-02, ], + [-8.79759548e-18, 5.38614204e-20, 2.68498367e-19, ], + [3.06061064e-02, 6.99750720e-01, 7.08297607e-01, ], + [7.04937280e-01, -7.16265701e-02, -5.72307806e-04, ], + [1.76423192e-16, 1.65375651e-16, 1.21343530e-16, ], + [7.04937280e-01, -7.16265701e-02, -5.72307806e-04, ], + [2.99898507e-02, 7.04933066e-01, -7.03107992e-01, ], + [-2.93091491e-16, -1.57370285e-16, -2.11837496e-16, ], + ]) normal_mode_prop = db.DenseMatrixProperty(normal_modes_props[0]) normal_mode_prop.link(properties) normal_modes = normal_mode_prop.get_data() @@ -300,11 +300,12 @@ def test_valid_minimum(self): gibbs_energy_correction = gibbs_energy_correction_prop.get_data() self.assertAlmostEqual(gibbs_energy_correction, 0.0005468527260594769, delta=1e-5) - @skip_without('database', 'readuct') + @skip_without('database', 'readuct', 'molassembler') def test_fail_to_optimize_non_valid_minimum(self): # fails because of different graph from scine_puffin.jobs.scine_geometry_validation import ScineGeometryValidation import scine_database as db + import scine_molassembler as masm # Setup DB for calculation h2o2 = os.path.join(resource_path(), "h2o2_distorted.xyz") @@ -316,9 +317,10 @@ def test_fail_to_optimize_non_valid_minimum(self): model = db.Model('dftb3', 'dftb3', '') model.program = "sparrow" + model.spin_mode = "restricted" job = db.Job('scine_geometry_validation') settings = { - "optimization_attempts": 2 + "val_optimization_attempts": 2 } calculation = add_calculation(self.manager, model, job, [structure.id()], settings) @@ -330,9 +332,9 @@ def test_fail_to_optimize_non_valid_minimum(self): success = job.run(self.manager, calculation, config) assert not success # Check comment of calculation - ref_comment = "Scine Geometry Validation Job: End structure does not match starting structure." + \ - "\nError: Scine Geometry Validation Job failed with message:" + \ - "\nStructure could not be validated to be a minimum. Hessian information is stored anyway." + ref_comment = "\nError: Scine Geometry Validation Job failed with message:" + \ + "\nFinal structure does not match starting structure. " + \ + "Structure could not be validated to be a minimum. Hessian information is stored anyway." assert calculation.get_comment() == ref_comment # Check results @@ -373,18 +375,18 @@ def test_fail_to_optimize_non_valid_minimum(self): self.assertAlmostEqual(energy.get_data(), -7.168684600560611, delta=1e-1) # Normal modes ref_normal_modes = np.array([ - [-4.31688799e-02, -6.91523883e-03, -1.82492631e-02, -3.86771134e-02, -3.45645862e-02, 3.81068441e-03, ], - [-1.65111999e-03, 2.83228734e-03, 2.10319980e-02, -4.31722218e-02, 1.36502269e-03, -5.94798269e-02, ], - [-2.99343119e-02, 1.78068975e-01, -2.59408246e-04, -1.58605276e-02, -1.23374087e-02, 2.33946275e-03, ], - [4.03868933e-02, -1.78581009e-03, -4.13741993e-03, -6.89669850e-03, -1.88649585e-02, 9.27050089e-04, ], - [-3.48727166e-03, -6.65257470e-03, -4.16577386e-02, 5.81676804e-03, 2.25155821e-03, -2.87158874e-04, ], - [-2.68061138e-02, -1.69761922e-01, 5.77588045e-02, -5.30648270e-03, 1.05427368e-02, -9.30432318e-04, ], - [-2.90044217e-02, 7.59482476e-02, 5.02213061e-02, 1.25262116e-01, 9.65015716e-01, -5.88417133e-02, ], - [7.31645599e-02, 6.21687101e-02, 3.05135914e-01, 5.98158280e-01, -1.16896572e-01, -1.63631824e-02, ], - [9.47125787e-01, -4.54672494e-03, -7.34029615e-02, -1.01582077e-01, 3.33256908e-02, -7.34742518e-03, ], - [7.31645599e-02, 6.21687101e-02, 3.05135914e-01, 5.98158280e-01, -1.16896572e-01, -1.63631824e-02, ], - [8.40020643e-03, -1.52700207e-03, 2.22688135e-02, -5.19280259e-03, 5.94884190e-02, 9.65080322e-01, ], - [-4.64510457e-02, -1.27316095e-01, -8.39319373e-01, 4.37578701e-01, -4.83778945e-03, -1.50189582e-02, ], + [-4.51056224e-02, -2.30053324e-02, -1.91838298e-02, -3.99609071e-02, -3.51840370e-02, 3.93200055e-03, ], + [-1.72509150e-03, 9.42119128e-03, 2.21094036e-02, -4.46041374e-02, 1.39010219e-03, -6.13743055e-02, ], + [-3.12783219e-02, 5.92381295e-01, -2.71106437e-04, -1.63870079e-02, -1.25590502e-02, 2.41365634e-03, ], + [4.21994858e-02, -5.94026789e-03, -4.34931029e-03, -7.12559616e-03, -1.92028040e-02, 9.56529286e-04, ], + [-3.64372163e-03, -2.21299691e-02, -4.37910660e-02, 6.00944990e-03, 2.29206345e-03, -2.96324400e-04, ], + [-2.80083927e-02, -5.64748740e-01, 6.07151008e-02, -5.48202434e-03, 1.07317405e-02, -9.59698746e-04, ], + [-3.03145349e-02, 2.52662209e-01, 5.27982951e-02, 1.29431627e-01, 9.82317479e-01, -6.07141499e-02, ], + [7.64453798e-02, 2.06808631e-01, 3.20757324e-01, 6.18000623e-01, -1.19002597e-01, -1.68844103e-02, ], + [9.89629279e-01, -1.51090374e-02, -7.71596608e-02, -1.04947466e-01, 3.39323358e-02, -7.58173211e-03, ], + [7.64453798e-02, 2.06808631e-01, 3.20757324e-01, 6.18000623e-01, -1.19002597e-01, -1.68844103e-02, ], + [8.77700467e-03, -5.07459285e-03, 2.34086814e-02, -5.36404689e-03, 6.05533777e-02, 9.95819233e-01, ], + [-4.85357718e-02, -4.23519054e-01, -8.82304042e-01, 4.52087705e-01, -4.92635570e-03, -1.54978070e-02, ], ]) normal_mode_prop = db.DenseMatrixProperty(normal_modes_props[0]) @@ -415,7 +417,7 @@ def test_fail_to_optimize_non_valid_minimum(self): self.assertAlmostEqual(gibbs_energy_correction, -0.001757357127836201, delta=1e-5) # Graph - assert structure.get_graph("masm_cbor_graph") == ref_graph + assert masm.JsonSerialization.equal_molecules(structure.get_graph("masm_cbor_graph"), ref_graph) @skip_without('database', 'readuct') @pytest.mark.filterwarnings("ignore:.+The structure had a graph already") @@ -434,9 +436,10 @@ def test_optimize_non_valid_minimum(self): model = db.Model('dftb3', 'dftb3', '') model.program = "sparrow" + model.spin_mode = "restricted" job = db.Job('scine_geometry_validation') settings = { - "optimization_attempts": 2 + "val_optimization_attempts": 2 } calculation = add_calculation(self.manager, model, job, [structure.id()], settings) @@ -534,15 +537,15 @@ def test_optimize_non_valid_minimum(self): assert np.allclose(ref_hessian, hessian, atol=1e-1) # Normal modes ref_normal_modes = np.array([ - [-2.89597029e-02, -3.15134055e-02, 6.80792263e-02, ], - [-6.12657343e-02, -3.78163196e-02, -3.58186536e-02, ], - [4.20375520e-18, 2.34897827e-18, 1.00104275e-17, ], - [-1.79731098e-01, 7.85076713e-01, -5.36167737e-01, ], - [5.37311156e-01, 1.05145171e-01, 8.02545212e-03, ], - [-1.04237003e-16, -1.01785409e-16, 7.12699448e-17, ], - [6.39425796e-01, -2.84845564e-01, -5.44494573e-01, ], - [4.35196516e-01, 4.95135906e-01, 5.60545477e-01, ], - [-1.32320360e-16, 7.19185833e-17, -4.43320067e-16, ], + [-3.00191566e-02, -3.23938595e-02, 7.15634599e-02, ], + [-6.37968518e-02, -3.85915640e-02, -3.76619702e-02, ], + [4.38956694e-18, 2.37844772e-18, 1.05262604e-17, ], + [-1.87877770e-01, 8.04193332e-01, -5.61620285e-01, ], + [5.59025142e-01, 1.07533595e-01, 7.82721537e-03, ], + [-1.08329864e-16, -1.04272618e-16, 7.48675733e-17, ], + [6.64389811e-01, -2.89986210e-01, -5.74349350e-01, ], + [4.53660473e-01, 5.05053400e-01, 5.90003781e-01, ], + [-1.38379756e-16, 7.46124958e-17, -4.65817026e-16, ], ]) normal_mode_prop = db.DenseMatrixProperty(normal_modes_props[0]) normal_mode_prop.link(properties) diff --git a/scine_puffin/tests/jobs/test_scine_observers.py b/scine_puffin/tests/jobs/test_scine_observers.py new file mode 100644 index 0000000..9be8f92 --- /dev/null +++ b/scine_puffin/tests/jobs/test_scine_observers.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +import os +import json +from typing import Dict + +from ..testcases import ( + JobTestCase, + skip_without +) + +from ..db_setup import ( + add_calculation, + add_compound_and_structure, + add_structure +) + +from ..resources import resource_path + + +class ScineReactComplexNt2ObserverTests(JobTestCase): + + # all subject to numerics and changes in optimization algorithms + # lower and upper bound + n_opt = (520, 1560) + n_nt = (50, 60) + n_tsopt = (20, 30) + n_irc = (150, 150) + + def execute_job(self, additional_settings) -> tuple: + from scine_puffin.jobs.scine_react_complex_nt2 import ScineReactComplexNt2 + import scine_database as db + + reactant_one_path = os.path.join(resource_path(), "proline_acid.xyz") + reactant_two_path = os.path.join(resource_path(), "propanal.xyz") + compound_one = add_compound_and_structure(self.manager, reactant_one_path) + compound_two = add_compound_and_structure(self.manager, reactant_two_path) + reactant_one_guess = db.Structure(compound_one.get_centroid()) + reactant_two_guess = db.Structure(compound_two.get_centroid()) + reactant_one_guess.link(self.manager.get_collection('structures')) + reactant_two_guess.link(self.manager.get_collection('structures')) + graph_one = json.load(open(os.path.join(resource_path(), "proline_acid.json"), "r")) + graph_two = json.load(open(os.path.join(resource_path(), "propanal.json"), "r")) + reactant_one_guess.set_graph("masm_cbor_graph", graph_one["masm_cbor_graph"]) + reactant_one_guess.set_graph("masm_idx_map", graph_one["masm_idx_map"]) + reactant_one_guess.set_graph("masm_decision_list", graph_one["masm_decision_list"]) + reactant_two_guess.set_graph("masm_cbor_graph", graph_two["masm_cbor_graph"]) + reactant_two_guess.set_graph("masm_idx_map", graph_two["masm_idx_map"]) + reactant_two_guess.set_graph("masm_decision_list", graph_two["masm_decision_list"]) + + model = db.Model('dftb3', 'dftb3', '') + db_job = db.Job('scine_react_complex_nt2') + settings = { + "nt_convergence_max_iterations": 600, + "nt_nt_total_force_norm": 0.1, + "nt_sd_factor": 1.0, + "nt_nt_use_micro_cycles": True, + "nt_nt_fixed_number_of_micro_cycles": True, + "nt_nt_number_of_micro_cycles": 10, + "nt_nt_filter_passes": 10, + "tsopt_convergence_max_iterations": 800, + "tsopt_convergence_step_max_coefficient": 0.002, + "tsopt_convergence_step_rms": 0.001, + "tsopt_convergence_gradient_max_coefficient": 0.0002, + "tsopt_convergence_gradient_rms": 0.0001, + "tsopt_convergence_requirement": 3, + "tsopt_convergence_delta_value": 1e-06, + "tsopt_optimizer": "bofill", + "tsopt_geoopt_coordinate_system": "cartesianWithoutRotTrans", + "tsopt_bofill_trust_radius": 0.2, + "tsopt_bofill_follow_mode": 0, + "irc_convergence_max_iterations": 75, + "irc_sd_factor": 2.0, + "irc_irc_initial_step_size": 0.3, + "irc_stop_on_error": False, + "irc_convergence_step_max_coefficient": 0.002, + "irc_convergence_step_rms": 0.001, + "irc_convergence_gradient_max_coefficient": 0.0002, + "irc_convergence_gradient_rms": 0.0001, + "irc_convergence_delta_value": 1e-06, + "irc_irc_coordinate_system": "cartesianWithoutRotTrans", + "ircopt_convergence_max_iterations": 800, + "ircopt_convergence_step_max_coefficient": 0.002, + "ircopt_convergence_step_rms": 0.001, + "ircopt_convergence_gradient_max_coefficient": 0.0002, + "ircopt_convergence_gradient_rms": 0.0001, + "ircopt_convergence_requirement": 3, + "ircopt_convergence_delta_value": 1e-06, + "ircopt_geoopt_coordinate_system": "cartesianWithoutRotTrans", + "ircopt_bfgs_use_trust_radius": True, + "ircopt_bfgs_trust_radius": 0.2, + "opt_convergence_max_iterations": 800, + "opt_convergence_step_max_coefficient": 0.002, + "opt_convergence_step_rms": 0.001, + "opt_convergence_gradient_max_coefficient": 0.0002, + "opt_convergence_gradient_rms": 0.0001, + "opt_convergence_requirement": 3, + "opt_convergence_delta_value": 1e-06, + "opt_geoopt_coordinate_system": "cartesianWithoutRotTrans", + "opt_bfgs_use_trust_radius": True, + "opt_bfgs_trust_radius": 0.4, + "imaginary_wavenumber_threshold": -30, + "nt_nt_associations": [ + 3, + 17, + 16, + 20 + ], + "nt_nt_dissociations": [], + "rc_x_alignment_0": [ + -0.405162890256947, + 0.382026105406949, + 0.830601641670804, + -0.382026105406949, + 0.754648889886101, + -0.533442693999341, + -0.830601641670804, + -0.533442693999341, + -0.159811780143048 + ], + "rc_x_alignment_1": [ + 0.881555658365378, + -0.439248859713409, + -0.172974161204658, + -0.311954059087669, + -0.817035288655183, + 0.484910303160151, + -0.354322291456116, + -0.373515429845424, + -0.857287546535394 + ], + "rc_x_rotation": 0.0, + "rc_x_spread": 3.48715302740618, + "rc_displacement": 0.0, + "rc_minimal_spin_multiplicity": False + } + for k, v in additional_settings.items(): + settings[k] = v + + calculation = add_calculation(self.manager, model, db_job, + [reactant_one_guess.id(), reactant_two_guess.id()], + settings) + config = self.get_configuration() + job = ScineReactComplexNt2() + job.prepare(config["daemon"]["job_dir"], calculation.id()) + self.run_job(job, calculation, config) + return calculation, job + + def standard_check(self, calculation, connectivity_settings) -> None: + import scine_database as db + import scine_utilities as utils + from scine_puffin.utilities.masm_helper import get_molecules_result + + ts_reference_path = os.path.join(resource_path(), "ts_proline_acid_propanal.xyz") + ts_reference = add_structure(self.manager, ts_reference_path, db.Label.TS_OPTIMIZED) + product_reference_path = os.path.join(resource_path(), "proline_acid_propanal_product.xyz") + product_reference = utils.io.read(str(product_reference_path))[0] + + structures = self.manager.get_collection("structures") + properties = self.manager.get_collection("properties") + elementary_steps = self.manager.get_collection("elementary_steps") + assert calculation.get_status() == db.Status.COMPLETE + results = calculation.get_results() + assert len(results.property_ids) == 11 + # Structure counts: (complex + TS + product) + re-optimized reactants (x2) + assert len(results.structure_ids) == 3 + 2 + assert len(results.elementary_step_ids) == 2 + new_elementary_step = db.ElementaryStep(results.elementary_step_ids[-1], elementary_steps) + assert len(new_elementary_step.get_reactants(db.Side.RHS)[1]) == 1 + product = db.Structure(new_elementary_step.get_reactants(db.Side.RHS)[1][0], structures) + assert product.has_property('bond_orders') + assert product.has_graph('masm_cbor_graph') + bonds = utils.BondOrderCollection() + bonds.matrix = db.SparseMatrixProperty(product.get_property('bond_orders'), properties).get_data() + assert len(get_molecules_result(product.get_atoms(), bonds, connectivity_settings).molecules) == 1 + new_ts = db.Structure(new_elementary_step.get_transition_state(), structures) + assert new_ts.has_property('electronic_energy') + energy_props = new_ts.get_properties("electronic_energy") + assert energy_props[0] in results.property_ids + energy = db.NumberProperty(energy_props[0], properties) + self.assertAlmostEqual(energy.get_data(), -31.6595724342182, delta=1e-1) + fit = utils.QuaternionFit(ts_reference.get_atoms().positions, new_ts.get_atoms().positions) + assert fit.get_rmsd() < 1e-2 + fit = utils.QuaternionFit(product_reference.positions, product.get_atoms().positions) + assert fit.get_rmsd() < 1e-2 + + def observer_check(self, factors: Dict[str, float], more_relaxed: bool = False) -> None: + from ...utilities.task_to_readuct_call import SubTaskToReaductCall + + lee_way = 10 if more_relaxed else 0 + + structures = self.manager.get_collection("structures") + selection = {"label": "complex_optimized"} + assert structures.count(json.dumps(selection)) == 1 + selection = {"label": "minimum_guess"} + n_opt_structures = structures.count(json.dumps(selection)) + assert (self.n_opt[0] * factors[SubTaskToReaductCall.OPT.name] - lee_way <= n_opt_structures <= + self.n_opt[1] * factors[SubTaskToReaductCall.OPT.name] + lee_way) + selection = {"label": "reactive_complex_scanned"} + n_nt_structures = structures.count(json.dumps(selection)) + assert (self.n_nt[0] * factors[SubTaskToReaductCall.NT2.name] - lee_way <= n_nt_structures <= + self.n_nt[1] * factors[SubTaskToReaductCall.NT2.name] + lee_way) + selection = {"label": "ts_guess"} + n_tsopt_structures = structures.count(json.dumps(selection)) + assert (self.n_tsopt[0] * factors[SubTaskToReaductCall.TSOPT.name] - lee_way <= n_tsopt_structures <= + self.n_tsopt[1] * factors[SubTaskToReaductCall.TSOPT.name] + lee_way) + selection = {"label": "elementary_step_optimized"} + n_irc_structures = structures.count(json.dumps(selection)) + if factors[SubTaskToReaductCall.IRC.name] == 1: + assert n_irc_structures == self.n_irc[0] # IRCScan algorithm does not converge + else: + # relax a bit due to numerics of fractions + assert (self.n_irc[0] * factors[SubTaskToReaductCall.IRC.name] - 10 <= n_irc_structures <= + self.n_irc[1] * factors[SubTaskToReaductCall.IRC.name] + 10) + total = 3 + 3 + 2 + n_opt_structures + n_nt_structures + n_tsopt_structures + n_irc_structures + assert structures.count("{}") == total + + @skip_without('database', 'readuct', 'molassembler') + def test_full_storage(self): + from ...utilities.task_to_readuct_call import SubTaskToReaductCall + + calculation, job = self.execute_job({"store_all_structures": True}) + self.standard_check(calculation, job.connectivity_settings) + self.observer_check( + { + SubTaskToReaductCall.OPT.name: 1, + SubTaskToReaductCall.NT2.name: 1, + SubTaskToReaductCall.TSOPT.name: 1, + SubTaskToReaductCall.IRC.name: 1 + } + ) + + @skip_without('database', 'readuct', 'molassembler') + def test_fraction_storage(self): + from ...utilities.task_to_readuct_call import SubTaskToReaductCall + import scine_utilities as utils + fractions = utils.ValueCollection({ + SubTaskToReaductCall.OPT.name: 0.1, + SubTaskToReaductCall.NT2.name: 0.5, + SubTaskToReaductCall.TSOPT.name: 0.2, + SubTaskToReaductCall.IRC.name: 0.2 + }) + calculation, job = self.execute_job( + {"store_structures_with_fraction": fractions} + ) + self.standard_check(calculation, job.connectivity_settings) + self.observer_check(fractions.as_dict(), True) # more relaxed check due to randomness + + @skip_without('database', 'readuct', 'molassembler') + def test_frequency_storage(self): + from ...utilities.task_to_readuct_call import SubTaskToReaductCall + import scine_utilities as utils + frequencies = utils.ValueCollection({ + SubTaskToReaductCall.OPT.name: 10, + SubTaskToReaductCall.NT2.name: 2, + SubTaskToReaductCall.TSOPT.name: 5, + SubTaskToReaductCall.IRC.name: 5 + }) + calculation, job = self.execute_job( + {"store_structures_with_frequency": frequencies} + ) + self.standard_check(calculation, job.connectivity_settings) + fractions = { + k: 1 / v for k, v in frequencies.items() + } + self.observer_check(fractions) diff --git a/scine_puffin/tests/jobs/test_scine_qm_region_selection.py b/scine_puffin/tests/jobs/test_scine_qm_region_selection.py new file mode 100644 index 0000000..b2aeeee --- /dev/null +++ b/scine_puffin/tests/jobs/test_scine_qm_region_selection.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +import os + +from ..testcases import ( + JobTestCase, + skip_without +) + +from ..db_setup import ( + add_calculation, + add_structure +) + +from ..resources import resource_path + + +class ScineQmRegionSelectionTest(JobTestCase): + + @skip_without('database', 'swoose', 'readuct', 'xtb_wrapper') + def test_selection_full_qm_reference(self): + from scine_puffin.jobs.scine_qm_region_selection import ScineQmRegionSelection + from scine_puffin.jobs.scine_bond_orders import ScineBondOrders + import scine_database as db + + # Setup DB for calculation + water = os.path.join(resource_path(), "8-gly-chain-opt.xyz") + structure = add_structure(self.manager, water, db.Label.USER_GUESS) + + # Calculate bond orders + bond_order_model = db.Model('dftb3', 'dftb3', '') + bond_order_job = db.Job('scine_bond_orders') + bond_order_calculation = add_calculation(self.manager, bond_order_model, bond_order_job, [structure.id()]) + config = self.get_configuration() + job = ScineBondOrders() + job.prepare(config["daemon"]["job_dir"], bond_order_calculation.id()) + self.run_job(job, bond_order_calculation, config) + + # Set QM atoms + properties = self.manager.get_collection("properties") + + # Set up + Run QM/MM + qmmm_model = db.Model('gfn2/gaff', 'gfn2', '') + qmmm_model.program = "xtb/swoose" + job = db.Job('scine_qm_region_selection') + qmmm_calculation = add_calculation(self.manager, qmmm_model, job, [structure.id()]) + settings = qmmm_calculation.get_settings() + settings["electrostatic_embedding"] = True + settings["qm_region_center_atoms"] = [0] + settings["initial_radius"] = 5.1 + settings["cutting_probability"] = 0.7 + settings["tol_percentage_error"] = 20.0 + settings["qm_region_max_size"] = 50 + settings["qm_region_min_size"] = 20 + settings["ref_max_size"] = 100 + settings["tol_percentage_sym_score"] = float("inf") + + qmmm_calculation.set_settings(settings) + + config = self.get_configuration() + job = ScineQmRegionSelection() + job.prepare(config["daemon"]["job_dir"], qmmm_calculation.id()) + self.run_job(job, qmmm_calculation, config) + + assert structure.has_property("qm_atoms") + qm_atoms = db.VectorProperty(structure.get_property("qm_atoms"), properties).get_data() + assert len(qm_atoms) == 27 + reference_selection = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 25, 26, + 27, 71] + assert list(qm_atoms) == reference_selection + + @skip_without('database', 'swoose', 'readuct', 'xtb_wrapper') + def test_selection_qm_mm_reference(self): + from scine_puffin.jobs.scine_qm_region_selection import ScineQmRegionSelection + from scine_puffin.jobs.scine_bond_orders import ScineBondOrders + import scine_database as db + + # Setup DB for calculation + water = os.path.join(resource_path(), "8-gly-chain-opt.xyz") + structure = add_structure(self.manager, water, db.Label.USER_GUESS) + + # Calculate bond orders + bond_order_model = db.Model('dftb3', 'dftb3', '') + bond_order_job = db.Job('scine_bond_orders') + bond_order_calculation = add_calculation(self.manager, bond_order_model, bond_order_job, [structure.id()]) + config = self.get_configuration() + job = ScineBondOrders() + job.prepare(config["daemon"]["job_dir"], bond_order_calculation.id()) + self.run_job(job, bond_order_calculation, config) + + # Set QM atoms + properties = self.manager.get_collection("properties") + + # Set up + Run QM/MM + qmmm_model = db.Model('gfn2/gaff', 'gfn2', '') + qmmm_model.program = "xtb/swoose" + job = db.Job('scine_qm_region_selection') + qmmm_calculation = add_calculation(self.manager, qmmm_model, job, [structure.id()]) + settings = qmmm_calculation.get_settings() + settings["electrostatic_embedding"] = True + settings["qm_region_center_atoms"] = [0] + settings["initial_radius"] = 2.0 + settings["cutting_probability"] = 0.7 + settings["tol_percentage_error"] = 20.0 + settings["qm_region_max_size"] = 40 + settings["qm_region_min_size"] = 10 + settings["ref_max_size"] = 70 + settings["tol_percentage_sym_score"] = float("inf") + + qmmm_calculation.set_settings(settings) + + config = self.get_configuration() + job = ScineQmRegionSelection() + job.prepare(config["daemon"]["job_dir"], qmmm_calculation.id()) + self.run_job(job, qmmm_calculation, config) + + assert structure.has_property("qm_atoms") + qm_atoms = db.VectorProperty(structure.get_property("qm_atoms"), properties).get_data() + assert len(qm_atoms) == 27 + reference_selection = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 25, 26, + 27, 71] + assert list(qm_atoms) == reference_selection diff --git a/scine_puffin/tests/jobs/test_scine_react_complex_afir_job.py b/scine_puffin/tests/jobs/test_scine_react_complex_afir_job.py index 0d92d34..2bc88a2 100644 --- a/scine_puffin/tests/jobs/test_scine_react_complex_afir_job.py +++ b/scine_puffin/tests/jobs/test_scine_react_complex_afir_job.py @@ -155,6 +155,7 @@ def test_energy_and_structure(self): assert new_elementary_step_one.get_type() == db.ElementaryStepType.BARRIERLESS new_elementary_step_two = db.ElementaryStep(results.elementary_step_ids[1], elementary_steps) assert new_elementary_step_two.get_type() == db.ElementaryStepType.REGULAR + assert new_elementary_step_two.has_spline() assert new_elementary_step_one.get_reactants(db.Side.RHS)[1][0] == \ new_elementary_step_two.get_reactants(db.Side.LHS)[0][0] s_complex = db.Structure(new_elementary_step_two.get_reactants(db.Side.LHS)[0][0]) diff --git a/scine_puffin/tests/jobs/test_scine_react_complex_nt2_job.py b/scine_puffin/tests/jobs/test_scine_react_complex_nt2_job.py index fb11c34..d0091bc 100644 --- a/scine_puffin/tests/jobs/test_scine_react_complex_nt2_job.py +++ b/scine_puffin/tests/jobs/test_scine_react_complex_nt2_job.py @@ -167,6 +167,7 @@ def test_energy_and_structure(self): bonds = utils.BondOrderCollection() bonds.matrix = db.SparseMatrixProperty(product.get_property('bond_orders'), properties).get_data() assert len(get_molecules_result(product.get_atoms(), bonds, job.connectivity_settings).molecules) == 1 + assert new_elementary_step.has_spline() new_ts = db.Structure(new_elementary_step.get_transition_state(), structures) assert new_ts.has_property('electronic_energy') energy_props = new_ts.get_properties("electronic_energy") @@ -527,183 +528,6 @@ def test_mep_storage(self): fit = utils.QuaternionFit(product_reference.positions, product.get_atoms().positions) assert fit.get_rmsd() < 1e-2 - @skip_without('database', 'readuct', 'molassembler') - def test_full_storage(self): - # import Job - from scine_puffin.jobs.scine_react_complex_nt2 import ScineReactComplexNt2 - from scine_puffin.utilities.masm_helper import get_molecules_result - import scine_database as db - import scine_utilities as utils - - reactant_one_path = os.path.join(resource_path(), "proline_acid.xyz") - reactant_two_path = os.path.join(resource_path(), "propanal.xyz") - ts_reference_path = os.path.join(resource_path(), "ts_proline_acid_propanal.xyz") - product_reference_path = os.path.join(resource_path(), "proline_acid_propanal_product.xyz") - compound_one = add_compound_and_structure(self.manager, reactant_one_path) - compound_two = add_compound_and_structure(self.manager, reactant_two_path) - reactant_one_guess = db.Structure(compound_one.get_centroid()) - reactant_two_guess = db.Structure(compound_two.get_centroid()) - reactant_one_guess.link(self.manager.get_collection('structures')) - reactant_two_guess.link(self.manager.get_collection('structures')) - ts_reference = add_structure(self.manager, ts_reference_path, db.Label.TS_OPTIMIZED) - product_reference = utils.io.read(str(product_reference_path))[0] - graph_one = json.load(open(os.path.join(resource_path(), "proline_acid.json"), "r")) - graph_two = json.load(open(os.path.join(resource_path(), "propanal.json"), "r")) - reactant_one_guess.set_graph("masm_cbor_graph", graph_one["masm_cbor_graph"]) - reactant_one_guess.set_graph("masm_idx_map", graph_one["masm_idx_map"]) - reactant_one_guess.set_graph("masm_decision_list", graph_one["masm_decision_list"]) - reactant_two_guess.set_graph("masm_cbor_graph", graph_two["masm_cbor_graph"]) - reactant_two_guess.set_graph("masm_idx_map", graph_two["masm_idx_map"]) - reactant_two_guess.set_graph("masm_decision_list", graph_two["masm_decision_list"]) - - model = db.Model('dftb3', 'dftb3', '') - job = db.Job('scine_react_complex_nt2') - settings = { - "nt_convergence_max_iterations": 600, - "nt_nt_total_force_norm": 0.1, - "nt_sd_factor": 1.0, - "nt_nt_use_micro_cycles": True, - "nt_nt_fixed_number_of_micro_cycles": True, - "nt_nt_number_of_micro_cycles": 10, - "nt_nt_filter_passes": 10, - "tsopt_convergence_max_iterations": 800, - "tsopt_convergence_step_max_coefficient": 0.002, - "tsopt_convergence_step_rms": 0.001, - "tsopt_convergence_gradient_max_coefficient": 0.0002, - "tsopt_convergence_gradient_rms": 0.0001, - "tsopt_convergence_requirement": 3, - "tsopt_convergence_delta_value": 1e-06, - "tsopt_optimizer": "bofill", - "tsopt_geoopt_coordinate_system": "cartesianWithoutRotTrans", - "tsopt_bofill_trust_radius": 0.2, - "tsopt_bofill_follow_mode": 0, - "irc_convergence_max_iterations": 75, - "irc_sd_factor": 2.0, - "irc_irc_initial_step_size": 0.3, - "irc_stop_on_error": False, - "irc_convergence_step_max_coefficient": 0.002, - "irc_convergence_step_rms": 0.001, - "irc_convergence_gradient_max_coefficient": 0.0002, - "irc_convergence_gradient_rms": 0.0001, - "irc_convergence_delta_value": 1e-06, - "irc_irc_coordinate_system": "cartesianWithoutRotTrans", - "ircopt_convergence_max_iterations": 800, - "ircopt_convergence_step_max_coefficient": 0.002, - "ircopt_convergence_step_rms": 0.001, - "ircopt_convergence_gradient_max_coefficient": 0.0002, - "ircopt_convergence_gradient_rms": 0.0001, - "ircopt_convergence_requirement": 3, - "ircopt_convergence_delta_value": 1e-06, - "ircopt_geoopt_coordinate_system": "cartesianWithoutRotTrans", - "ircopt_bfgs_use_trust_radius": True, - "ircopt_bfgs_trust_radius": 0.2, - "opt_convergence_max_iterations": 800, - "opt_convergence_step_max_coefficient": 0.002, - "opt_convergence_step_rms": 0.001, - "opt_convergence_gradient_max_coefficient": 0.0002, - "opt_convergence_gradient_rms": 0.0001, - "opt_convergence_requirement": 3, - "opt_convergence_delta_value": 1e-06, - "opt_geoopt_coordinate_system": "cartesianWithoutRotTrans", - "opt_bfgs_use_trust_radius": True, - "opt_bfgs_trust_radius": 0.4, - "imaginary_wavenumber_threshold": -30, - "store_all_structures": True, - "nt_nt_associations": [ - 3, - 17, - 16, - 20 - ], - "nt_nt_dissociations": [], - "rc_x_alignment_0": [ - -0.405162890256947, - 0.382026105406949, - 0.830601641670804, - -0.382026105406949, - 0.754648889886101, - -0.533442693999341, - -0.830601641670804, - -0.533442693999341, - -0.159811780143048 - ], - "rc_x_alignment_1": [ - 0.881555658365378, - -0.439248859713409, - -0.172974161204658, - -0.311954059087669, - -0.817035288655183, - 0.484910303160151, - -0.354322291456116, - -0.373515429845424, - -0.857287546535394 - ], - "rc_x_rotation": 0.0, - "rc_x_spread": 3.48715302740618, - "rc_displacement": 0.0, - "rc_minimal_spin_multiplicity": False - } - - calculation = add_calculation(self.manager, model, job, [reactant_one_guess.id(), reactant_two_guess.id()], - settings) - - # Run calculation/job - config = self.get_configuration() - job = ScineReactComplexNt2() - job.prepare(config["daemon"]["job_dir"], calculation.id()) - self.run_job(job, calculation, config) - - # Check results - structures = self.manager.get_collection("structures") - properties = self.manager.get_collection("properties") - elementary_steps = self.manager.get_collection("elementary_steps") - assert calculation.get_status() == db.Status.COMPLETE - results = calculation.get_results() - assert len(results.property_ids) == 11 - # Structure counts: (complex + TS + product) + re-optimized reactants (x2) - assert len(results.structure_ids) == 3 + 2 - assert len(results.elementary_step_ids) == 2 - new_elementary_step = db.ElementaryStep(results.elementary_step_ids[-1], elementary_steps) - assert len(new_elementary_step.get_reactants(db.Side.RHS)[1]) == 1 - product = db.Structure(new_elementary_step.get_reactants(db.Side.RHS)[1][0], structures) - assert product.has_property('bond_orders') - assert product.has_graph('masm_cbor_graph') - bonds = utils.BondOrderCollection() - bonds.matrix = db.SparseMatrixProperty(product.get_property('bond_orders'), properties).get_data() - assert len(get_molecules_result(product.get_atoms(), bonds, job.connectivity_settings).molecules) == 1 - new_ts = db.Structure(new_elementary_step.get_transition_state(), structures) - assert new_ts.has_property('electronic_energy') - energy_props = new_ts.get_properties("electronic_energy") - assert energy_props[0] in results.property_ids - energy = db.NumberProperty(energy_props[0], properties) - self.assertAlmostEqual(energy.get_data(), -31.6595724342182, delta=1e-1) - - selection = {"label": "complex_optimized"} - assert structures.count(json.dumps(selection)) == 1 - selection = {"label": "minimum_guess"} - n_opt_structures = structures.count(json.dumps(selection)) - assert n_opt_structures >= 520 # Subject to numerics and changes in GeoOpt algorithm - assert n_opt_structures <= 1560 # Subject to numerics and changes in GeoOpt algorithm - selection = {"label": "reactive_complex_scanned"} - n_nt_structures = structures.count(json.dumps(selection)) - assert n_nt_structures >= 50 # Subject to numerics and changes in NT2 algorithm - assert n_nt_structures <= 60 # Subject to numerics and changes in NT2 algorithm - selection = {"label": "ts_guess"} - n_tsopt_structures = structures.count(json.dumps(selection)) - assert n_tsopt_structures >= 20 # Subject to numerics and changes in TSOpt algorithm - assert n_tsopt_structures <= 30 # Subject to numerics and changes in TSOpt algorithm - selection = {"label": "elementary_step_optimized"} - n_irc_structures = structures.count(json.dumps(selection)) - assert n_irc_structures == 150 # Subject to numerics and changes in IRCScan algorithm but does not converge - total = 3 + 3 + 2 + n_opt_structures + n_nt_structures + n_tsopt_structures + n_irc_structures - assert structures.count("{}") == total - - fit = utils.QuaternionFit(ts_reference.get_atoms().positions, new_ts.get_atoms().positions) - assert fit.get_rmsd() < 1e-2 - - fit = utils.QuaternionFit(product_reference.positions, product.get_atoms().positions) - assert fit.get_rmsd() < 1e-2 - @skip_without('database', 'readuct', 'molassembler') def test_charged_reactants(self): # import Job @@ -964,3 +788,166 @@ def test_elementary_step_not_from_starting_structures(self): assert energy_props[0] in results.property_ids energy = db.NumberProperty(energy_props[0], properties) self.assertAlmostEqual(energy.get_data(), -25.38936573516693, delta=1e-4) + + @skip_without('database', 'readuct', 'molassembler') + def test_elementary_step_with_always_complexation(self): + # import Job + from scine_puffin.jobs.scine_react_complex_nt2 import ScineReactComplexNt2 + from scine_puffin.utilities.masm_helper import get_molecules_result + import scine_database as db + import scine_utilities as utils + + reactant_one_path = os.path.join(resource_path(), "proline_acid.xyz") + reactant_two_path = os.path.join(resource_path(), "propanal.xyz") + ts_reference_path = os.path.join(resource_path(), "ts_proline_acid_propanal.xyz") + product_reference_path = os.path.join(resource_path(), "proline_acid_propanal_product.xyz") + compound_one = add_compound_and_structure(self.manager, reactant_one_path) + compound_two = add_compound_and_structure(self.manager, reactant_two_path) + reactant_one_guess = db.Structure(compound_one.get_centroid()) + reactant_two_guess = db.Structure(compound_two.get_centroid()) + reactant_one_guess.link(self.manager.get_collection('structures')) + reactant_two_guess.link(self.manager.get_collection('structures')) + ts_reference = add_structure(self.manager, ts_reference_path, db.Label.TS_OPTIMIZED) + product_reference = utils.io.read(str(product_reference_path))[0] + graph_one = json.load(open(os.path.join(resource_path(), "proline_acid.json"), "r")) + graph_two = json.load(open(os.path.join(resource_path(), "propanal.json"), "r")) + reactant_one_guess.set_graph("masm_cbor_graph", graph_one["masm_cbor_graph"]) + reactant_one_guess.set_graph("masm_idx_map", graph_one["masm_idx_map"]) + reactant_one_guess.set_graph("masm_decision_list", graph_one["masm_decision_list"]) + reactant_two_guess.set_graph("masm_cbor_graph", graph_two["masm_cbor_graph"]) + reactant_two_guess.set_graph("masm_idx_map", graph_two["masm_idx_map"]) + reactant_two_guess.set_graph("masm_decision_list", graph_two["masm_decision_list"]) + + model = db.Model('dftb3', 'dftb3', '') + job = db.Job('scine_react_complex_nt2') + settings = { + "nt_convergence_max_iterations": 600, + "nt_nt_total_force_norm": 0.1, + "nt_sd_factor": 1.0, + "nt_nt_use_micro_cycles": True, + "nt_nt_fixed_number_of_micro_cycles": True, + "nt_nt_number_of_micro_cycles": 10, + "nt_nt_filter_passes": 10, + "tsopt_convergence_max_iterations": 800, + "tsopt_convergence_step_max_coefficient": 0.002, + "tsopt_convergence_step_rms": 0.001, + "tsopt_convergence_gradient_max_coefficient": 0.0002, + "tsopt_convergence_gradient_rms": 0.0001, + "tsopt_convergence_requirement": 3, + "tsopt_convergence_delta_value": 1e-06, + "tsopt_optimizer": "bofill", + "tsopt_geoopt_coordinate_system": "cartesianWithoutRotTrans", + "tsopt_bofill_trust_radius": 0.2, + "tsopt_bofill_follow_mode": 0, + "irc_convergence_max_iterations": 75, + "irc_sd_factor": 2.0, + "irc_irc_initial_step_size": 0.3, + "irc_stop_on_error": False, + "irc_convergence_step_max_coefficient": 0.002, + "irc_convergence_step_rms": 0.001, + "irc_convergence_gradient_max_coefficient": 0.0002, + "irc_convergence_gradient_rms": 0.0001, + "irc_convergence_delta_value": 1e-06, + "irc_irc_coordinate_system": "cartesianWithoutRotTrans", + "ircopt_convergence_max_iterations": 800, + "ircopt_convergence_step_max_coefficient": 0.002, + "ircopt_convergence_step_rms": 0.001, + "ircopt_convergence_gradient_max_coefficient": 0.0002, + "ircopt_convergence_gradient_rms": 0.0001, + "ircopt_convergence_requirement": 3, + "ircopt_convergence_delta_value": 1e-06, + "ircopt_geoopt_coordinate_system": "cartesianWithoutRotTrans", + "ircopt_bfgs_use_trust_radius": True, + "ircopt_bfgs_trust_radius": 0.2, + "opt_convergence_max_iterations": 800, + "opt_convergence_step_max_coefficient": 0.002, + "opt_convergence_step_rms": 0.001, + "opt_convergence_gradient_max_coefficient": 0.0002, + "opt_convergence_gradient_rms": 0.0001, + "opt_convergence_requirement": 3, + "opt_convergence_delta_value": 1e-06, + "opt_geoopt_coordinate_system": "cartesianWithoutRotTrans", + "opt_bfgs_use_trust_radius": True, + "opt_bfgs_trust_radius": 0.4, + "imaginary_wavenumber_threshold": -30, + "nt_nt_associations": [ + 3, + 17, + 16, + 20 + ], + "nt_nt_dissociations": [], + "rc_x_alignment_0": [ + -0.405162890256947, + 0.382026105406949, + 0.830601641670804, + -0.382026105406949, + 0.754648889886101, + -0.533442693999341, + -0.830601641670804, + -0.533442693999341, + -0.159811780143048 + ], + "rc_x_alignment_1": [ + 0.881555658365378, + -0.439248859713409, + -0.172974161204658, + -0.311954059087669, + -0.817035288655183, + 0.484910303160151, + -0.354322291456116, + -0.373515429845424, + -0.857287546535394 + ], + "rc_x_rotation": 0.0, + "rc_x_spread": 3.48715302740618, + "rc_displacement": 0.0, + "rc_minimal_spin_multiplicity": False, + "always_add_barrierless_step_for_reactive_complex": True + } + + calculation = add_calculation(self.manager, model, job, [reactant_one_guess.id(), reactant_two_guess.id()], + settings) + + # Run calculation/job + config = self.get_configuration() + job = ScineReactComplexNt2() + job.complexation_criterion = -float('inf') + job.prepare(config["daemon"]["job_dir"], calculation.id()) + self.run_job(job, calculation, config) + + # Check results + structures = self.manager.get_collection("structures") + properties = self.manager.get_collection("properties") + elementary_steps = self.manager.get_collection("elementary_steps") + assert calculation.get_status() == db.Status.COMPLETE + results = calculation.get_results() + assert len(results.property_ids) == 11 + assert len(results.structure_ids) == 3 + 2 # re-optimized reactants (x2) + complex + TS + product + assert len(results.elementary_step_ids) == 2 + assert structures.count("{}") == 3 + 3 + 2 + new_elementary_step = db.ElementaryStep(results.elementary_step_ids[0], elementary_steps) + assert new_elementary_step.get_type() == db.ElementaryStepType.BARRIERLESS + new_elementary_step = db.ElementaryStep(results.elementary_step_ids[-1], elementary_steps) + assert len(new_elementary_step.get_reactants(db.Side.RHS)[1]) == 1 + product = db.Structure(new_elementary_step.get_reactants(db.Side.RHS)[1][0], structures) + assert product.has_property('bond_orders') + assert product.has_graph('masm_cbor_graph') + bonds = utils.BondOrderCollection() + bonds.matrix = db.SparseMatrixProperty(product.get_property('bond_orders'), properties).get_data() + assert len(get_molecules_result(product.get_atoms(), bonds, job.connectivity_settings).molecules) == 1 + new_ts = db.Structure(new_elementary_step.get_transition_state(), structures) + assert new_ts.has_property('electronic_energy') + energy_props = new_ts.get_properties("electronic_energy") + assert energy_props[0] in results.property_ids + energy = db.NumberProperty(energy_props[0], properties) + self.assertAlmostEqual(energy.get_data(), -31.6595724342182, delta=1e-1) + + selection = {"label": "complex_optimized"} + assert structures.count(json.dumps(selection)) == 1 + + fit = utils.QuaternionFit(ts_reference.get_atoms().positions, new_ts.get_atoms().positions) + assert fit.get_rmsd() < 1e-2 + + fit = utils.QuaternionFit(product_reference.positions, product.get_atoms().positions) + assert fit.get_rmsd() < 1e-2 diff --git a/scine_puffin/tests/jobs/test_scine_react_complex_nt2_propensity_job.py b/scine_puffin/tests/jobs/test_scine_react_complex_nt2_propensity_job.py index bc05048..48591b9 100644 --- a/scine_puffin/tests/jobs/test_scine_react_complex_nt2_propensity_job.py +++ b/scine_puffin/tests/jobs/test_scine_react_complex_nt2_propensity_job.py @@ -114,8 +114,12 @@ def test_propensity(self): assert any(structure.get_multiplicity() == 4 for structure in structures.query_structures("{}")) assert any(structure.get_multiplicity() == 6 for structure in structures.query_structures("{}")) - os.environ["OMP_NUM_THREADS"] = omp + if omp is not None: + os.environ["OMP_NUM_THREADS"] = omp + else: + del os.environ['OMP_NUM_THREADS'] + @skip_without('database', 'readuct', 'molassembler') def test_propensity_hit(self): # import Job from scine_puffin.jobs.scine_react_complex_nt2 import ScineReactComplexNt2 @@ -208,8 +212,12 @@ def test_propensity_hit(self): assert new_ts.has_property('electronic_energy') energy_props = new_ts.get_properties("electronic_energy") assert energy_props[0] in results.property_ids + assert new_elementary_step.has_spline() assert any(structure.get_multiplicity() == 1 for structure in structures.query_structures("{}")) assert any(structure.get_multiplicity() == 3 for structure in structures.query_structures("{}")) - os.environ["OMP_NUM_THREADS"] = omp + if omp is not None: + os.environ["OMP_NUM_THREADS"] = omp + else: + del os.environ['OMP_NUM_THREADS'] diff --git a/scine_puffin/tests/jobs/test_scine_react_complex_nt_job.py b/scine_puffin/tests/jobs/test_scine_react_complex_nt_job.py index c4c3792..aeddb41 100644 --- a/scine_puffin/tests/jobs/test_scine_react_complex_nt_job.py +++ b/scine_puffin/tests/jobs/test_scine_react_complex_nt_job.py @@ -161,6 +161,7 @@ def test_energy_and_structure(self): assert new_elementary_step_one.get_type() == db.ElementaryStepType.BARRIERLESS new_elementary_step_two = db.ElementaryStep(results.elementary_step_ids[1], elementary_steps) assert new_elementary_step_two.get_type() == db.ElementaryStepType.REGULAR + assert new_elementary_step_two.has_spline() assert new_elementary_step_one.get_reactants(db.Side.RHS)[1][0] == \ new_elementary_step_two.get_reactants(db.Side.LHS)[0][0] s_complex = db.Structure(new_elementary_step_two.get_reactants(db.Side.LHS)[0][0]) @@ -496,7 +497,6 @@ def test_surface_reaction(self): # Check results assert calculation.get_status() == db.Status.COMPLETE results = calculation.get_results() - print(calculation.get_comment()) assert len(results.property_ids) > 5 assert len(results.structure_ids) == 2 # TS + product assert len(results.elementary_step_ids) == 1 diff --git a/scine_puffin/tests/jobs/test_scine_react_job.py b/scine_puffin/tests/jobs/test_scine_react_job.py index dceed54..1627674 100644 --- a/scine_puffin/tests/jobs/test_scine_react_job.py +++ b/scine_puffin/tests/jobs/test_scine_react_job.py @@ -1,14 +1,38 @@ #!/usr/bin/env python3 +from __future__ import annotations # -*- coding: utf-8 -*- __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -import numpy as np + +from typing import TYPE_CHECKING, List import unittest -class ScineReactJobTest(unittest.TestCase): +import os +import numpy as np + +from scine_puffin.config import Configuration +from scine_puffin.jobs.templates.job import calculation_context +from scine_puffin.jobs.templates.scine_react_job import ReactJob +from scine_puffin.utilities.imports import module_exists +from scine_puffin.tests.testcases import skip_without, JobTestCase + +from ..db_setup import add_calculation, add_structure +from ..resources import resource_path + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db + + +class _Dummy(ReactJob): + + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: + return True + + +class ScineReactTest(unittest.TestCase): def _split_atoms(self, atoms, compound_map): import scine_utilities as utils @@ -23,18 +47,16 @@ def _split_atoms(self, atoms, compound_map): return split_structures def test_custom_round(self): - from scine_puffin.jobs.templates.scine_react_job import ReactJob - react_job = ReactJob() - assert (react_job._custom_round(2.45) == 2.0) - assert (react_job._custom_round(2.45, 0.4) == 3.0) - assert (react_job._custom_round(-2.45) == -2.0) - assert (react_job._custom_round(-2.45, 0.45) == -3.0) - assert (react_job._custom_round(-2.45, 0.46) == -2.0) + react_job = _Dummy() + assert (react_job._custom_round(2.45) == (2.0, False)) + assert (react_job._custom_round(2.45, 0.4) == (3.0, True)) + assert (react_job._custom_round(-2.45) == (-2.0, False)) + assert (react_job._custom_round(-2.45, 0.45) == (-3.0, True)) + assert (react_job._custom_round(-2.45, 0.46) == (-2.0, False)) def test_distribute_charge(self): # import Job - from scine_puffin.jobs.templates.scine_react_job import ReactJob - react_job = ReactJob() + react_job = _Dummy() assert (react_job._distribute_charge(0, [-1, 0], [-0.42, 0.2]) == [0, 0]) # Remove electron assert (react_job._distribute_charge(0, [-1, 1], [-0.42, 0.43]) == [-1, 1]) # Pass without change assert (react_job._distribute_charge(1, [-1, 1], [-0.42, 0.43]) == [0, 1]) # Remove electron @@ -53,9 +75,9 @@ def test_distribute_charge(self): assert (react_job._distribute_charge(-2, [-1, 0, 0, 0], [-1.3, 0.05, 0.1, 0.45]) == [-2, 0, 0, 0]) # Add electron at 0 + @skip_without("utilities") def test_two_molecules_in_complex(self): import scine_utilities as utils - from scine_puffin.jobs.templates.scine_react_job import ReactJob # Load test case total_charge = 0 positions = [[-2.091417412302329, -1.404388718237768, 0.6029258660694673, ], @@ -87,7 +109,7 @@ def test_two_molecules_in_complex(self): atoms = utils.AtomCollection(elements, positions) split_structures = self._split_atoms(atoms, compound_map) - react_job = ReactJob() + react_job = _Dummy() react_job.settings['sp']['expect_charge_separation'] = True react_job.settings['sp']['charge_separation_threshold'] = 0.6 # Test: no charge assigned @@ -106,9 +128,9 @@ def test_two_molecules_in_complex(self): for i in range(len(r2)): self.assertAlmostEqual(r2[i], ref_r2[i], delta=1e-6) + @skip_without("utilities") def test_three_molecules_in_complex(self): import scine_utilities as utils - from scine_puffin.jobs.templates.scine_react_job import ReactJob # Load test case total_charge = 0 @@ -145,9 +167,9 @@ def test_three_molecules_in_complex(self): atoms = utils.AtomCollection(elements, positions) split_structures = self._split_atoms(atoms, compound_map) - react_job = ReactJob() + react_job = _Dummy() react_job.settings['sp']['expect_charge_separation'] = True - react_job.settings['sp']['charge_separation_threshold'] = 0.4 + react_job.settings['sp']['charge_separation_threshold'] = 0.6 # Test: no charge assigned c, e, r = react_job._integrate_charges(compound_map, partial_charges, split_structures, total_charge) assert (c == [0, 0, 0]) @@ -156,7 +178,7 @@ def test_three_molecules_in_complex(self): for i in range(len(r)): self.assertAlmostEqual(r[i], ref_r[i], delta=1e-6) - react_job.settings['sp']['charge_separation_threshold'] = 0.3 + react_job.settings['sp']['charge_separation_threshold'] = 0.5 # Test: charge assigend c2, e2, r2 = react_job._integrate_charges(compound_map, partial_charges, split_structures, total_charge) assert (c2 == [1, 0, -1]) @@ -164,3 +186,145 @@ def test_three_molecules_in_complex(self): ref_r2 = [-0.69547, 0.2727, 0.42277] for i in range(len(r2)): self.assertAlmostEqual(r2[i], ref_r2[i], delta=1e-6) + + +class _DummyJob(ReactJob): + + def run(self, manager: db.Manager, calculation: db.Calculation, config: Configuration) -> bool: + return True + + def __init__(self) -> None: + super().__init__() + self.name = "Scine Test React Job" + self.exploration_key = "nt" + opt_defaults = { + "convergence_max_iterations": 500, + } + self.settings = { + **self.settings, + self.opt_key: opt_defaults + } + + self.settings[self.propensity_key]['check'] = 1 + self.settings[self.single_point_key]['expect_charge_separation'] = True + self.settings[self.single_point_key]['charge_separation_threshold'] = 0.5 + + @staticmethod + def required_programs() -> List[str]: + return ["database", "readuct", "utils"] + + +class ScineReactJobTest(JobTestCase): + + @skip_without("utilities", "readuct", "xtb_wrapper") + def test_analyze_side_reject_exhaustive_product_decomposition(self): + import scine_utilities as utils + + # # # Load system into calculator + xyz_name = os.path.join(resource_path(), "iodine_in_water.xyz") + test_model_method = "gfn2" + calculator_settings = utils.ValueCollection({ + 'program': 'xtb', + 'method': test_model_method, + 'spin_multiplicity': 1, + 'molecular_charge': 0, + 'spin_mode': 'restricted_open_shell', + 'self_consistence_criterion': 1e-07, + 'electronic_temperature': 300.0, + 'max_scf_iterations': 100, + 'solvent': 'water', + 'solvation': 'gbsa', + }) + test_calc = utils.core.load_system_into_calculator( + xyz_name, + test_model_method, + **calculator_settings) + + test_model = db.Model(test_model_method, test_model_method, "") + test_model.solvation = "gbsa" + test_model.solvent = "water" + + dummy_structure = add_structure(self.manager, xyz_name, db.Label.MINIMUM_OPTIMIZED, 0, 1, test_model) + dummy_calculation = add_calculation(self.manager, model=test_model, job=db.Job("dummy_test"), + structures=[dummy_structure.id()]) + + config = self.get_configuration() + react_job = _DummyJob() + react_job.configure_run(self.manager, dummy_calculation, config) + react_job.prepare(config["daemon"]["job_dir"], dummy_calculation.id()) + react_job.systems["test_irc"] = test_calc + + react_job.settings[react_job.opt_key]['geoopt_coordinate_system'] = "cartesianWithoutRotTrans" + + with calculation_context(react_job): + ( + test_graph, + test_charges, + test_decision_lists, + test_names + ) = react_job.analyze_side("test_irc", 0, "test_forward", calculator_settings) + + assert test_graph is None + assert test_charges is None + assert test_decision_lists is None + assert test_names is None + + @skip_without("utilities", "readuct", "molassembler", "xtb_wrapper") + def test_analyze_side_allow_exhaustive_product_decomposition(self): + import scine_utilities as utils + import scine_molassembler as masm + + # # # Load system into calculator + xyz_name = os.path.join(resource_path(), "iodine_in_water.xyz") + test_model_method = "gfn2" + calculator_settings = utils.ValueCollection({ + 'program': 'xtb', + 'method': test_model_method, + 'spin_multiplicity': 1, + 'molecular_charge': 0, + 'spin_mode': 'restricted_open_shell', + 'self_consistence_criterion': 1e-07, + 'electronic_temperature': 300.0, + 'max_scf_iterations': 100, + 'solvent': 'water', + 'solvation': 'gbsa', + }) + test_calc = utils.core.load_system_into_calculator( + xyz_name, + test_model_method, + **calculator_settings) + + test_model = db.Model(test_model_method, test_model_method, "") + test_model.solvation = "gbsa" + test_model.solvent = "water" + + dummy_structure = add_structure(self.manager, xyz_name, db.Label.MINIMUM_OPTIMIZED, 0, 1, test_model) + dummy_calculation = add_calculation(self.manager, model=test_model, job=db.Job("dummy_test"), + structures=[dummy_structure.id()]) + + config = self.get_configuration() + react_job = _DummyJob() + react_job.configure_run(self.manager, dummy_calculation, config) + react_job.prepare(config["daemon"]["job_dir"], dummy_calculation.id()) + react_job.systems["test_irc"] = test_calc + + react_job.settings[react_job.opt_key]['geoopt_coordinate_system'] = "cartesianWithoutRotTrans" + react_job.settings[react_job.job_key]["allow_exhaustive_product_decomposition"] = True + + with calculation_context(react_job): + ( + test_graph, + test_charges, + test_decision_lists, + test_names + ) = react_job.analyze_side("test_irc", 0, "test_forward", calculator_settings) + + assert len(test_names) == 33 # 32 water molecules + 1 iodine + assert len(set(test_names)) == 2 # 2 type of molecules + assert len(test_charges) == 33 + assert len(test_decision_lists) == 33 + ref_graph_1 = "o2FjD2FnomFFgYMAAQBhWoIZP7UZP7VhdoMCAAE=" # Iodine + ref_graph_2 = "pGFhgaVhYQBhYwJhb4GCAAFhcqNhbIKBAIEBYmxygYIAAWFzgYIAAWFzAWFjD2FnomFFgoMAAgCDAQIAYVqDAQEIYXaDAgAB" + assert masm.JsonSerialization.equal_molecules(test_graph.split(";")[0], ref_graph_1) + for tmp_graph in test_graph.split(";")[1:]: + assert masm.JsonSerialization.equal_molecules(tmp_graph, ref_graph_2) diff --git a/scine_puffin/tests/jobs/test_scine_react_ts_guess.py b/scine_puffin/tests/jobs/test_scine_react_ts_guess.py new file mode 100644 index 0000000..4d52795 --- /dev/null +++ b/scine_puffin/tests/jobs/test_scine_react_ts_guess.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +import os + +from ..testcases import ( + JobTestCase, + skip_without +) + +from ..db_setup import ( + add_calculation, + add_structure +) + +from ..resources import resource_path + + +class ScineReactTsGuessJobTest(JobTestCase): + + @skip_without('database', 'readuct') + def test_job_sn2(self): + # import Job + from scine_puffin.jobs.scine_react_ts_guess import ScineReactTsGuess + import scine_database as db + import scine_utilities as su + + # Setup DB for calculation + ts_opt = os.path.join(resource_path(), "CH3ClBr_ts.xyz") + structure = add_structure(self.manager, ts_opt, db.Label.TS_OPTIMIZED, charge=-1, multiplicity=1) + model = db.Model('dftb3', 'dftb3', '') + job = db.Job('scine_react_ts_guess') + # Settings + # Enhanced SD Optimizer to reduce number of steps + settings = { + "irc_irc_initial_step_size": 0.3, + "irc_convergence_max_iterations": 250, + "irc_sd_use_trust_radius": True, + "irc_sd_trust_radius": 0.05, + "irc_sd_dynamic_multiplier": 1.2, + 'spin_propensity_check': 0 + } + calculation = add_calculation(self.manager, model, job, [structure.id()], settings) + + # Run calculation/job + config = self.get_configuration() + job = ScineReactTsGuess() + job.prepare(config["daemon"]["job_dir"], calculation.id()) + self.run_job(job, calculation, config) + + # Check results + assert calculation.get_status() == db.Status.COMPLETE + results = calculation.get_results() + assert len(results.structure_ids) == 7 + assert len(results.property_ids) >= 10 + assert len(results.elementary_step_ids) == 3 + found_ts = False + for sid in results.elementary_step_ids: + step = db.ElementaryStep(sid, self.manager.get_collection('elementary_steps')) + if not step.has_transition_state(): + continue + found_ts = True + ts = db.Structure(step.get_transition_state(), self.manager.get_collection("structures")) + fit = su.QuaternionFit(ts.get_atoms().positions, structure.get_atoms().positions) + assert fit.get_rmsd() < 0.1 + assert step.has_spline() + assert found_ts diff --git a/scine_puffin/tests/jobs/test_scine_single_point_job.py b/scine_puffin/tests/jobs/test_scine_single_point_job.py index 4ed9003..4517232 100644 --- a/scine_puffin/tests/jobs/test_scine_single_point_job.py +++ b/scine_puffin/tests/jobs/test_scine_single_point_job.py @@ -6,6 +6,7 @@ """ import os +import numpy as np from ..testcases import ( JobTestCase, @@ -49,6 +50,8 @@ def test_energy(self): charge_props = structure.get_properties("atomic_charges") assert len(energy_props) == 1 results = calculation.get_results() + assert len(results.elementary_step_ids) == 0 + assert len(results.structure_ids) == 0 assert len(results.property_ids) == 2 assert energy_props[0] in results.property_ids @@ -66,3 +69,110 @@ def test_energy(self): self.assertAlmostEqual(charges[0], -0.67969209, delta=1e-3) self.assertAlmostEqual(charges[1], +0.33984604, delta=1e-3) self.assertAlmostEqual(charges[2], +0.33984604, delta=1e-3) + + @skip_without('database', 'readuct', 'swoose') + def test_qmmm_gaff(self): + # import Job + from scine_puffin.jobs.scine_single_point import ScineSinglePoint + from scine_puffin.jobs.scine_bond_orders import ScineBondOrders + from scine_database.energy_query_functions import get_energy_for_structure + import scine_database as db + + # Setup DB for calculation + water = os.path.join(resource_path(), "proline_acid_propanal_complex.xyz") + structure = add_structure(self.manager, water, db.Label.USER_GUESS) + + # Calculate bond orders + bond_order_model = db.Model('dftb3', 'dftb3', '') + bond_order_job = db.Job('scine_bond_orders') + bond_order_calculation = add_calculation(self.manager, bond_order_model, bond_order_job, [structure.id()]) + config = self.get_configuration() + job = ScineBondOrders() + job.prepare(config["daemon"]["job_dir"], bond_order_calculation.id()) + self.run_job(job, bond_order_calculation, config) + + # Set QM atoms + properties = self.manager.get_collection("properties") + structures = self.manager.get_collection("structures") + n_atoms = len(structure.get_atoms()) + qm_atoms = [i for i in range(n_atoms - 10)] # the 10 last atoms are propanal. We assign them as MM. + qm_atom_label = "qm_atoms" + prop = db.VectorProperty.make(qm_atom_label, bond_order_model, np.asarray(qm_atoms), properties) + structure.add_property(qm_atom_label, prop.id()) + prop.set_structure(structure.id()) + + # Set up + Run QM/MM + qmmm_model = db.Model('gfn2/gaff', 'gfn2', '') + qmmm_model.program = "xtb/swoose" + job = db.Job('scine_single_point') + qmmm_calculation = add_calculation(self.manager, qmmm_model, job, [structure.id()]) + settings = qmmm_calculation.get_settings() + settings["require_gradients"] = True + settings["require_charges"] = False + settings["electrostatic_embedding"] = True + qmmm_calculation.set_settings(settings) + + config = self.get_configuration() + job = ScineSinglePoint() + job.prepare(config["daemon"]["job_dir"], qmmm_calculation.id()) + self.run_job(job, qmmm_calculation, config) + + energy_reference = -26.352255863 + gradient_reference = [ + [8.237708e-04, 3.125493e-03, 2.131060e-03], + [1.293170e-03, -4.198595e-03, -2.841488e-03], + [-4.210309e-03, 8.769684e-03, 2.051029e-04], + [5.614330e-03, -2.083648e-03, 8.612634e-03], + [1.969390e-03, -4.311829e-03, -8.419946e-03], + [-1.046279e-03, -9.618556e-04, -2.861473e-03], + [-2.420135e-03, 8.110524e-05, -1.119392e-03], + [-1.704751e-03, 1.552419e-03, -1.657612e-03], + [1.221916e-03, 5.193619e-03, -3.087307e-03], + [-1.997496e-03, -4.045199e-03, 4.388533e-03], + [1.700178e-03, -3.329661e-03, 1.778612e-04], + [5.710796e-03, -3.283295e-05, -4.074882e-03], + [-1.365384e-03, -1.367140e-03, -1.667894e-03], + [1.574524e-02, -7.115792e-03, -9.660434e-03], + [-3.329011e-02, 9.832087e-03, 2.034265e-03], + [1.161146e-02, 1.462071e-04, 1.792546e-02], + [1.244628e-03, -3.579307e-04, 1.707141e-04], + [1.478093e-03, 2.555476e-02, 1.863031e-02], + [2.022248e-03, 1.952497e-02, 9.900692e-05], + [-4.767288e-04, -9.485054e-03, -8.243565e-04], + [5.520370e-04, -7.912631e-03, -1.061546e-02], + [-1.581161e-03, -1.971934e-02, -2.166321e-03], + [5.098476e-04, -2.937521e-03, -2.085972e-03], + [-3.207473e-03, -3.957743e-03, -2.584540e-03], + [3.027584e-03, -9.003679e-04, -1.412400e-03], + [-3.045696e-03, -3.790679e-04, -1.725070e-03], + [-1.791648e-04, -6.841384e-04, 2.429609e-03] + ] + assert qmmm_calculation.get_status() == db.Status.COMPLETE + gradient_property_ids = structure.query_properties("gradients", qmmm_model, properties) + assert len(gradient_property_ids) > 0 + gradient_property = db.DenseMatrixProperty(gradient_property_ids[0], properties) + assert np.max(np.abs(gradient_property.get_data() - gradient_reference)) < 1e-6 + + energy = get_energy_for_structure(structure, "electronic_energy", qmmm_model, structures, properties) + assert abs(energy - energy_reference) < 1e-6 + + mm_model = db.Model('gaff', '', '') + mm_model.program = "swoose" + mm_model.spin_mode = "" + mm_model.temperature = "" + mm_model.electronic_temperature = "" + mm_model.pressure = "" + job = db.Job('scine_single_point') + mm_calculation = add_calculation(self.manager, mm_model, job, [structure.id()]) + settings = mm_calculation.get_settings() + settings["require_gradients"] = True + mm_calculation.set_settings(settings) + + config = self.get_configuration() + job = ScineSinglePoint() + job.prepare(config["daemon"]["job_dir"], mm_calculation.id()) + self.run_job(job, mm_calculation, config) + gradient_property_ids = structure.query_properties("gradients", mm_model, properties) + assert len(gradient_property_ids) == 1 + energy = get_energy_for_structure(structure, "electronic_energy", mm_model, structures, properties) + assert abs(energy - 0.012852055) < 1e-6 diff --git a/scine_puffin/tests/jobs/test_scine_step_refinement_job.py b/scine_puffin/tests/jobs/test_scine_step_refinement_job.py deleted file mode 100644 index 712c672..0000000 --- a/scine_puffin/tests/jobs/test_scine_step_refinement_job.py +++ /dev/null @@ -1,362 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -__copyright__ = """ This code is licensed under the 3-clause BSD license. -Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. -See LICENSE.txt for details. -""" - -import os -import json - -from ..testcases import ( - JobTestCase, - skip_without -) - -from ..db_setup import ( - add_calculation, - add_compound_and_structure, - add_structure -) - -from ..resources import resource_path - - -class ScineStepRefinementJobTest(JobTestCase): - - @skip_without('database', 'readuct', 'molassembler') - def test_energy_starting_from_separated_reactants(self): - # import Job - from scine_puffin.jobs.scine_step_refinement import ScineStepRefinement - import scine_database as db - - # Amine-addition of the proline derivative to the aldehyde group of propanal. - # These structures where originally optimized with dftb3 - ts_guess_path = os.path.join(resource_path(), "ts_proline_acid_propanal.xyz") - reactant_one_path = os.path.join(resource_path(), "proline_acid.xyz") - reactant_two_path = os.path.join(resource_path(), "propanal.xyz") - ts_guess = add_structure(self.manager, ts_guess_path, db.Label.TS_OPTIMIZED) - compound_one = add_compound_and_structure(self.manager, reactant_one_path) - compound_two = add_compound_and_structure(self.manager, reactant_two_path) - reactant_one_guess = db.Structure(compound_one.get_centroid()) - reactant_two_guess = db.Structure(compound_two.get_centroid()) - reactant_one_guess.link(self.manager.get_collection('structures')) - reactant_two_guess.link(self.manager.get_collection('structures')) - graph_one = json.load(open(os.path.join(resource_path(), "proline_acid.json"), "r")) - graph_two = json.load(open(os.path.join(resource_path(), "propanal.json"), "r")) - reactant_one_guess.set_graph("masm_cbor_graph", graph_one["masm_cbor_graph"]) - reactant_one_guess.set_graph("masm_idx_map", graph_one["masm_idx_map"]) - reactant_one_guess.set_graph("masm_decision_list", graph_one["masm_decision_list"]) - reactant_two_guess.set_graph("masm_cbor_graph", graph_two["masm_cbor_graph"]) - reactant_two_guess.set_graph("masm_idx_map", graph_two["masm_idx_map"]) - reactant_two_guess.set_graph("masm_decision_list", graph_two["masm_decision_list"]) - - model = db.Model('dftb3', 'dftb3', '') - job = db.Job('scine_step_refinement') - settings = { - "nt_convergence_max_iterations": 600, - "nt_nt_total_force_norm": 0.1, - "nt_sd_factor": 1.0, - "nt_nt_use_micro_cycles": True, - "nt_nt_fixed_number_of_micro_cycles": True, - "nt_nt_number_of_micro_cycles": 10, - "nt_nt_filter_passes": 10, - "tsopt_convergence_max_iterations": 800, - "tsopt_convergence_step_max_coefficient": 0.002, - "tsopt_convergence_step_rms": 0.001, - "tsopt_convergence_gradient_max_coefficient": 0.0002, - "tsopt_convergence_gradient_rms": 0.0001, - "tsopt_convergence_requirement": 3, - "tsopt_convergence_delta_value": 1e-06, - "tsopt_optimizer": "bofill", - "tsopt_geoopt_coordinate_system": "cartesianWithoutRotTrans", - "tsopt_bofill_trust_radius": 0.2, - "tsopt_bofill_follow_mode": 0, - "irc_convergence_max_iterations": 75, - "irc_sd_factor": 2.0, - "irc_irc_initial_step_size": 0.3, - "irc_stop_on_error": False, - "irc_convergence_step_max_coefficient": 0.002, - "irc_convergence_step_rms": 0.001, - "irc_convergence_gradient_max_coefficient": 0.0002, - "irc_convergence_gradient_rms": 0.0001, - "irc_convergence_delta_value": 1e-06, - "irc_irc_coordinate_system": "cartesianWithoutRotTrans", - "ircopt_convergence_max_iterations": 800, - "ircopt_convergence_step_max_coefficient": 0.002, - "ircopt_convergence_step_rms": 0.001, - "ircopt_convergence_gradient_max_coefficient": 0.0002, - "ircopt_convergence_gradient_rms": 0.0001, - "ircopt_convergence_requirement": 3, - "ircopt_convergence_delta_value": 1e-06, - "ircopt_geoopt_coordinate_system": "cartesianWithoutRotTrans", - "ircopt_bfgs_use_trust_radius": True, - "ircopt_bfgs_trust_radius": 0.2, - "opt_convergence_max_iterations": 800, - "opt_convergence_step_max_coefficient": 0.002, - "opt_convergence_step_rms": 0.001, - "opt_convergence_gradient_max_coefficient": 0.0002, - "opt_convergence_gradient_rms": 0.0001, - "opt_convergence_requirement": 3, - "opt_convergence_delta_value": 1e-06, - "opt_geoopt_coordinate_system": "cartesianWithoutRotTrans", - "opt_bfgs_use_trust_radius": True, - "opt_bfgs_trust_radius": 0.4, - "imaginary_wavenumber_threshold": -30, - "nt_nt_associations": [ - 3, - 17, - 16, - 20 - ], - "nt_nt_dissociations": [], - "rc_x_alignment_0": [ - -0.405162890256947, - 0.382026105406949, - 0.830601641670804, - -0.382026105406949, - 0.754648889886101, - -0.533442693999341, - -0.830601641670804, - -0.533442693999341, - -0.159811780143048 - ], - "rc_x_alignment_1": [ - 0.881555658365378, - -0.439248859713409, - -0.172974161204658, - -0.311954059087669, - -0.817035288655183, - 0.484910303160151, - -0.354322291456116, - -0.373515429845424, - -0.857287546535394 - ], - "rc_x_rotation": 0.0, - "rc_x_spread": 3.48715302740618, - "rc_displacement": 0.0, - "rc_minimal_spin_multiplicity": False - } - - calculation = add_calculation(self.manager, model, job, [reactant_one_guess.id(), reactant_two_guess.id()], - settings) - auxiliaries = { - "transition-state-id": ts_guess.id() - } - calculation.set_auxiliaries(auxiliaries) - - # Run calculation/job - config = self.get_configuration() - job = ScineStepRefinement() - job.prepare(config["daemon"]["job_dir"], calculation.id()) - self.run_job(job, calculation, config) - - # Check results - structures = self.manager.get_collection("structures") - properties = self.manager.get_collection("properties") - elementary_steps = self.manager.get_collection("elementary_steps") - assert calculation.get_status() == db.Status.COMPLETE - assert structures.count(json.dumps({})) == 8 - selection = {"label": "complex_optimized"} - assert structures.count(json.dumps(selection)) == 1 - complex_structure = structures.get_one_structure(json.dumps(selection)) - complex_structure.link(structures) - selection = {"label": "ts_optimized"} - assert structures.count(json.dumps(selection)) == 2 - assert properties.count(json.dumps({})) == 15 - assert elementary_steps.count(json.dumps({})) == 2 - results = calculation.get_results() - assert len(results.property_ids) == 11 - assert len(results.structure_ids) == 5 - assert len(results.elementary_step_ids) == 2 - # The regular elementary step should be the last one in the list. - new_elementary_step = db.ElementaryStep(results.elementary_step_ids[-1], elementary_steps) - new_ts = db.Structure(new_elementary_step.get_transition_state(), structures) - assert new_ts.has_property('electronic_energy') - energy_props = new_ts.get_properties("electronic_energy") - assert energy_props[0] in results.property_ids - energy = db.NumberProperty(energy_props[0], properties) - self.assertAlmostEqual(energy.get_data(), -31.6595724342182, delta=1e-1) - - new_elementary_step = db.ElementaryStep(results.elementary_step_ids[0], elementary_steps) - assert new_elementary_step.get_type() == db.ElementaryStepType.BARRIERLESS - lhs_rhs_structures = new_elementary_step.get_reactants(db.Side.BOTH) - assert lhs_rhs_structures[1][0] == complex_structure.id() - assert len(lhs_rhs_structures[0]) == 2 - - reactants = new_elementary_step.get_reactants(db.Side.BOTH) - lhs_energies = [-20.9685007365807, -10.70501645756783] - rhs_energies = [-31.6786171872375] - structure_ids = reactants[0] + reactants[1] - for s_id in structure_ids: - structure = db.Structure(s_id, structures) - assert structure.get_model() == model - assert structure.has_property('electronic_energy') - assert structure.has_property('bond_orders') - energy_props = structure.get_properties("electronic_energy") - energy = db.NumberProperty(energy_props[0], properties).get_data() - # The ordering of the energies may change. - if energy > -11.0: - self.assertAlmostEqual(energy, lhs_energies[1], delta=1e-1) - elif energy > -22.0: - self.assertAlmostEqual(energy, lhs_energies[0], delta=1e-1) - else: - self.assertAlmostEqual(energy, rhs_energies[0], delta=1e-1) - - @skip_without('database', 'readuct', 'molassembler') - def test_energy_starting_from_complex(self): - # import Job - from scine_puffin.jobs.scine_step_refinement import ScineStepRefinement - import scine_database as db - - # Amine-addition of the proline derivative to the aldehyde group of propanal. - # These structures where originally optimized with dftb3 - ts_guess_path = os.path.join(resource_path(), "ts_proline_acid_propanal.xyz") - reactant_one_path = os.path.join(resource_path(), "proline_propanal_complex.xyz") - ts_guess = add_structure(self.manager, ts_guess_path, db.Label.TS_OPTIMIZED) - reactant_one_guess = add_structure(self.manager, reactant_one_path, db.Label.COMPLEX_OPTIMIZED) - graph_one = json.load(open(os.path.join(resource_path(), "proline_propanal_complex.json"), "r")) - reactant_one_guess.set_graph("masm_cbor_graph", graph_one["masm_cbor_graph"]) - reactant_one_guess.set_graph("masm_idx_map", graph_one["masm_idx_map"]) - - model = db.Model('dftb3', 'dftb3', '') - job = db.Job('scine_step_refinement') - settings = { - "nt_convergence_max_iterations": 600, - "nt_nt_total_force_norm": 0.1, - "nt_sd_factor": 1.0, - "nt_nt_use_micro_cycles": True, - "nt_nt_fixed_number_of_micro_cycles": True, - "nt_nt_number_of_micro_cycles": 10, - "nt_nt_filter_passes": 10, - "tsopt_convergence_max_iterations": 800, - "tsopt_convergence_step_max_coefficient": 0.002, - "tsopt_convergence_step_rms": 0.001, - "tsopt_convergence_gradient_max_coefficient": 0.0002, - "tsopt_convergence_gradient_rms": 0.0001, - "tsopt_convergence_requirement": 3, - "tsopt_convergence_delta_value": 1e-06, - "tsopt_optimizer": "bofill", - "tsopt_geoopt_coordinate_system": "cartesianWithoutRotTrans", - "tsopt_bofill_trust_radius": 0.2, - "tsopt_bofill_follow_mode": 0, - "irc_convergence_max_iterations": 75, - "irc_sd_factor": 2.0, - "irc_irc_initial_step_size": 0.3, - "irc_stop_on_error": False, - "irc_convergence_step_max_coefficient": 0.002, - "irc_convergence_step_rms": 0.001, - "irc_convergence_gradient_max_coefficient": 0.0002, - "irc_convergence_gradient_rms": 0.0001, - "irc_convergence_delta_value": 1e-06, - "irc_irc_coordinate_system": "cartesianWithoutRotTrans", - "ircopt_convergence_max_iterations": 800, - "ircopt_convergence_step_max_coefficient": 0.002, - "ircopt_convergence_step_rms": 0.001, - "ircopt_convergence_gradient_max_coefficient": 0.0002, - "ircopt_convergence_gradient_rms": 0.0001, - "ircopt_convergence_requirement": 3, - "ircopt_convergence_delta_value": 1e-06, - "ircopt_geoopt_coordinate_system": "cartesianWithoutRotTrans", - "ircopt_bfgs_use_trust_radius": True, - "ircopt_bfgs_trust_radius": 0.2, - "opt_convergence_max_iterations": 800, - "opt_convergence_step_max_coefficient": 0.002, - "opt_convergence_step_rms": 0.001, - "opt_convergence_gradient_max_coefficient": 0.0002, - "opt_convergence_gradient_rms": 0.0001, - "opt_convergence_requirement": 3, - "opt_convergence_delta_value": 1e-06, - "opt_geoopt_coordinate_system": "cartesianWithoutRotTrans", - "opt_bfgs_use_trust_radius": True, - "opt_bfgs_trust_radius": 0.4, - "imaginary_wavenumber_threshold": -30, - "nt_nt_associations": [ - 3, - 17, - 16, - 20 - ], - "nt_nt_dissociations": [], - "rc_x_alignment_0": [ - -0.405162890256947, - 0.382026105406949, - 0.830601641670804, - -0.382026105406949, - 0.754648889886101, - -0.533442693999341, - -0.830601641670804, - -0.533442693999341, - -0.159811780143048 - ], - "rc_x_alignment_1": [ - 0.881555658365378, - -0.439248859713409, - -0.172974161204658, - -0.311954059087669, - -0.817035288655183, - 0.484910303160151, - -0.354322291456116, - -0.373515429845424, - -0.857287546535394 - ], - "rc_x_rotation": 0.0, - "rc_x_spread": 3.48715302740618, - "rc_displacement": 0.0, - "rc_minimal_spin_multiplicity": False - } - - calculation = add_calculation(self.manager, model, job, - [reactant_one_guess.id()], - settings) - auxiliaries = { - "transition-state-id": ts_guess.id() - } - calculation.set_auxiliaries(auxiliaries) - - # Run calculation/job - config = self.get_configuration() - job = ScineStepRefinement() - job.prepare(config["daemon"]["job_dir"], calculation.id()) - self.run_job(job, calculation, config) - - # Check results - structures = self.manager.get_collection("structures") - properties = self.manager.get_collection("properties") - elementary_steps = self.manager.get_collection("elementary_steps") - assert calculation.get_status() == db.Status.COMPLETE - assert structures.count(json.dumps({})) == 7 - selection = {"label": "complex_optimized"} - assert structures.count(json.dumps(selection)) == 2 - complex_structure = structures.get_one_structure(json.dumps(selection)) - complex_structure.link(structures) - selection = {"label": "ts_optimized"} - assert structures.count(json.dumps(selection)) == 2 - assert properties.count(json.dumps({})) == 15 - assert elementary_steps.count(json.dumps({})) == 2 - results = calculation.get_results() - assert len(results.property_ids) == 11 - assert len(results.structure_ids) == 5 - assert len(results.elementary_step_ids) == 2 - # The regular elementary step should be the last one in the list. - new_elementary_step = db.ElementaryStep(results.elementary_step_ids[-1], elementary_steps) - new_ts = db.Structure(new_elementary_step.get_transition_state(), structures) - assert new_ts.has_property('electronic_energy') - energy_props = new_ts.get_properties("electronic_energy") - assert energy_props[0] in results.property_ids - energy = db.NumberProperty(energy_props[0], properties) - self.assertAlmostEqual(energy.get_data(), -31.6595724342182, delta=1e-1) - - reactants = new_elementary_step.get_reactants(db.Side.BOTH) - lhs_energies = [-31.6823167498272] - rhs_energies = [-31.6786171872375] - all_energies = lhs_energies + rhs_energies - structure_ids = reactants[0] + reactants[1] - for i, s_id in enumerate(structure_ids): - structure = db.Structure(s_id, structures) - assert structure.get_model() == model - assert structure.has_property('electronic_energy') - assert structure.has_property('bond_orders') - energy_props = structure.get_properties("electronic_energy") - energy = db.NumberProperty(energy_props[0], properties) - self.assertAlmostEqual(energy.get_data(), all_energies[i], delta=1e-1) diff --git a/scine_puffin/tests/masm_info_test.py b/scine_puffin/tests/masm_info_test.py index 58862f4..2395f3c 100644 --- a/scine_puffin/tests/masm_info_test.py +++ b/scine_puffin/tests/masm_info_test.py @@ -19,7 +19,7 @@ pass else: class MasmHelperTests(unittest.TestCase): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super(MasmHelperTests, self).__init__(*args, **kwargs) self.settings = { "sub_based_on_distance_connectivity": True, @@ -31,7 +31,7 @@ class MockStructure(object): graph_dict: dict = {} model: db.Model = db.Model("", "", "") - def __init__(self, atoms: utils.AtomCollection): + def __init__(self, atoms: utils.AtomCollection) -> None: self.atoms = atoms def get_atoms(self) -> utils.AtomCollection: diff --git a/scine_puffin/tests/resources/8-gly-chain-opt-solvated.charges.csv b/scine_puffin/tests/resources/8-gly-chain-opt-solvated.charges.csv new file mode 100644 index 0000000..76c6447 --- /dev/null +++ b/scine_puffin/tests/resources/8-gly-chain-opt-solvated.charges.csv @@ -0,0 +1,1753 @@ +-0.025284858 +0.2675181 +-0.33351357 +-0.43502759 +0.077716751 +0.077716751 +0.1490335 +-0.025284858 +0.2675181 +-0.16760902 +-0.43502759 +0.077716751 +0.077716751 +0.20578389 +-0.025284858 +0.2675181 +-0.16760902 +-0.43502759 +0.077716751 +0.077716751 +0.20578389 +-0.025284858 +0.2675181 +-0.16760902 +-0.43502759 +0.077716751 +0.077716751 +0.20578389 +0.20578389 +-0.025284858 +0.2675181 +-0.16760902 +-0.43502759 +0.077716751 +0.077716751 +0.20578389 +-0.025284858 +0.2675181 +-0.16760902 +-0.43502759 +0.077716751 +0.077716751 +0.20578389 +0.077716751 +-0.025284858 +0.2675181 +-0.16760902 +-0.43502759 +0.077716751 +0.077716751 +0.20578389 +-0.025284858 +0.2675181 +-0.16760902 +-0.43502759 +0.077716751 +-0.025284858 +0.2675181 +-0.16760902 +-0.43502759 +0.077716751 +0.077716751 +0.20578389 +-0.025284858 +0.35810891 +-0.16760902 +-0.43502759 +0.077716751 +0.077716751 +0.20578389 +-0.36232789 +0.1490335 +0.34345426 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 +-0.60092697 +0.29734551 +0.29734551 \ No newline at end of file diff --git a/scine_puffin/tests/resources/8-gly-chain-opt-solvated.pdb b/scine_puffin/tests/resources/8-gly-chain-opt-solvated.pdb new file mode 100644 index 0000000..5c8bb3b --- /dev/null +++ b/scine_puffin/tests/resources/8-gly-chain-opt-solvated.pdb @@ -0,0 +1,3509 @@ + +ATOM 1 C R0 A 1 -9.160 4.685 -0.865 1.00 0.00 C +ATOM 2 C R1 A 2 -8.327 3.401 -0.820 1.00 0.00 C +ATOM 3 N R2 A 3 -10.399 4.584 -0.106 1.00 0.00 N +ATOM 4 O R3 A 4 -8.675 2.412 -0.193 1.00 0.00 O +ATOM 5 H R4 A 5 -9.410 4.904 -1.911 1.00 0.00 H +ATOM 6 H R4 A 6 -8.527 5.498 -0.495 1.00 0.00 H +ATOM 7 H R5 A 7 -10.454 3.656 0.308 1.00 0.00 H +ATOM 8 C R0 A 8 -6.168 2.422 -1.551 1.00 0.00 C +ATOM 9 C R1 A 9 -4.910 3.059 -2.147 1.00 0.00 C +ATOM 10 N R6 A 10 -7.161 3.485 -1.486 1.00 0.00 N +ATOM 11 O R3 A 11 -4.963 3.577 -3.255 1.00 0.00 O +ATOM 12 H R4 A 12 -6.019 2.008 -0.552 1.00 0.00 H +ATOM 13 H R4 A 13 -6.516 1.621 -2.215 1.00 0.00 H +ATOM 14 H R7 A 14 -6.992 4.266 -2.102 1.00 0.00 H +ATOM 15 C R0 A 15 -2.657 3.882 -1.826 1.00 0.00 C +ATOM 16 C R1 A 16 -1.692 4.164 -0.667 1.00 0.00 C +ATOM 17 N R6 A 17 -3.802 3.098 -1.396 1.00 0.00 N +ATOM 18 O R3 A 18 -1.828 3.634 0.425 1.00 0.00 O +ATOM 19 H R4 A 19 -2.112 3.351 -2.615 1.00 0.00 H +ATOM 20 H R4 A 20 -3.033 4.817 -2.252 1.00 0.00 H +ATOM 21 H R7 A 21 -3.730 2.664 -0.485 1.00 0.00 H +ATOM 22 C R0 A 22 0.378 5.393 -0.070 1.00 0.00 C +ATOM 23 C R1 A 23 1.371 6.303 -0.810 1.00 0.00 C +ATOM 24 N R6 A 24 -0.687 4.998 -0.981 1.00 0.00 N +ATOM 25 O R3 A 25 0.977 7.008 -1.729 1.00 0.00 O +ATOM 26 H R4 A 26 -0.048 5.981 0.755 1.00 0.00 H +ATOM 27 H R4 A 27 0.851 4.506 0.355 1.00 0.00 H +ATOM 28 H R7 A 28 -0.683 5.495 -1.864 1.00 0.00 H +ATOM 29 H R7 A 29 10.895 13.076 -1.332 1.00 0.00 H +ATOM 30 C R0 A 30 3.599 7.260 -0.966 1.00 0.00 C +ATOM 31 C R1 A 31 5.070 6.958 -0.648 1.00 0.00 C +ATOM 32 N R6 A 32 2.648 6.320 -0.386 1.00 0.00 N +ATOM 33 O R3 A 33 5.417 6.033 0.066 1.00 0.00 O +ATOM 34 H R4 A 34 3.447 7.270 -2.050 1.00 0.00 H +ATOM 35 H R4 A 35 3.383 8.266 -0.586 1.00 0.00 H +ATOM 36 H R7 A 36 2.999 5.701 0.331 1.00 0.00 H +ATOM 37 C R0 A 37 7.373 7.805 -1.024 1.00 0.00 C +ATOM 38 C R1 A 38 8.073 9.056 -1.590 1.00 0.00 C +ATOM 39 N R6 A 39 5.923 7.856 -1.187 1.00 0.00 N +ATOM 40 O R3 A 40 8.083 9.265 -2.794 1.00 0.00 O +ATOM 41 H R4 A 41 7.604 7.642 0.030 1.00 0.00 H +ATOM 42 H R4 A 42 7.773 6.957 -1.598 1.00 0.00 H +ATOM 43 H R7 A 43 5.583 8.506 -1.881 1.00 0.00 H +ATOM 44 H R4 A 44 11.553 13.954 1.214 1.00 0.00 H +ATOM 45 C R0 A 45 9.553 10.963 -1.134 1.00 0.00 C +ATOM 46 C R1 A 46 9.708 12.036 -0.044 1.00 0.00 C +ATOM 47 N R6 A 47 8.718 9.847 -0.713 1.00 0.00 N +ATOM 48 O R3 A 48 9.166 11.938 1.044 1.00 0.00 O +ATOM 49 H R4 A 49 10.551 10.589 -1.392 1.00 0.00 H +ATOM 50 H R4 A 50 9.113 11.405 -2.034 1.00 0.00 H +ATOM 51 H R7 A 51 8.613 9.732 0.286 1.00 0.00 H +ATOM 52 C R0 A 52 10.817 14.209 0.447 1.00 0.00 C +ATOM 53 C R1 A 53 11.226 15.447 -0.371 1.00 0.00 C +ATOM 54 N R6 A 54 10.528 13.049 -0.394 1.00 0.00 N +ATOM 55 O R3 A 55 10.421 15.961 -1.134 1.00 0.00 O +ATOM 56 H R4 A 56 9.884 14.491 0.953 1.00 0.00 H +ATOM 57 C R0 A 57 12.738 17.318 -0.668 1.00 0.00 C +ATOM 58 C R1 A 58 14.219 17.639 -0.870 1.00 0.00 C +ATOM 59 N R6 A 59 12.447 15.983 -0.165 1.00 0.00 N +ATOM 60 O R3 A 60 15.126 16.890 -0.553 1.00 0.00 O +ATOM 61 H R4 A 61 12.200 17.446 -1.611 1.00 0.00 H +ATOM 62 H R4 A 62 12.349 18.055 0.047 1.00 0.00 H +ATOM 63 H R7 A 63 13.181 15.523 0.356 1.00 0.00 H +ATOM 64 C R0 A 64 15.733 19.406 -1.655 1.00 0.00 C +ATOM 65 C R8 A 65 15.685 20.753 -2.335 1.00 0.00 C +ATOM 66 N R6 A 66 14.421 18.862 -1.401 1.00 0.00 N +ATOM 67 O R3 A 67 14.683 21.361 -2.607 1.00 0.00 O +ATOM 68 H R4 A 68 16.323 19.505 -0.738 1.00 0.00 H +ATOM 69 H R4 A 69 16.304 18.751 -2.327 1.00 0.00 H +ATOM 70 H R7 A 70 13.652 19.462 -1.670 1.00 0.00 H +ATOM 71 O R9 A 71 16.936 21.154 -2.600 1.00 0.00 O +ATOM 72 H R5 A 72 -10.409 5.254 0.653 1.00 0.00 H +ATOM 73 H R10 A 73 17.117 21.379 -3.525 1.00 0.00 H +ATOM 74 O R11 A 74 -7.349 4.923 -4.734 1.00 0.00 O +ATOM 75 H R12 A 75 -6.393 5.202 -4.820 1.00 0.00 H +ATOM 76 H R12 A 76 -7.548 5.759 -4.223 1.00 0.00 H +ATOM 77 O R11 A 77 -4.482 -0.692 -3.195 1.00 0.00 O +ATOM 78 H R12 A 78 -5.371 -0.611 -2.743 1.00 0.00 H +ATOM 79 H R12 A 79 -4.537 0.266 -3.475 1.00 0.00 H +ATOM 80 O R11 A 80 11.603 15.700 -4.405 1.00 0.00 O +ATOM 81 H R12 A 81 12.283 16.389 -4.655 1.00 0.00 H +ATOM 82 H R12 A 82 11.526 16.106 -3.494 1.00 0.00 H +ATOM 83 O R11 A 83 7.537 14.049 -2.127 1.00 0.00 O +ATOM 84 H R12 A 84 7.295 15.018 -2.071 1.00 0.00 H +ATOM 85 H R12 A 85 7.798 14.169 -3.085 1.00 0.00 H +ATOM 86 O R11 A 86 -4.961 0.911 1.800 1.00 0.00 O +ATOM 87 H R12 A 87 -5.420 1.492 2.472 1.00 0.00 H +ATOM 88 H R12 A 88 -4.787 1.712 1.227 1.00 0.00 H +ATOM 89 O R11 A 89 8.951 8.795 2.730 1.00 0.00 O +ATOM 90 H R12 A 90 8.460 8.929 3.590 1.00 0.00 H +ATOM 91 H R12 A 91 8.092 8.884 2.225 1.00 0.00 H +ATOM 92 O R11 A 92 16.963 19.113 2.324 1.00 0.00 O +ATOM 93 H R12 A 93 17.123 19.956 1.811 1.00 0.00 H +ATOM 94 H R12 A 94 17.806 18.726 1.951 1.00 0.00 H +ATOM 95 O R11 A 95 3.295 10.917 -0.629 1.00 0.00 O +ATOM 96 H R12 A 96 4.099 10.325 -0.586 1.00 0.00 H +ATOM 97 H R12 A 97 3.150 10.791 0.353 1.00 0.00 H +ATOM 98 O R11 A 98 -12.792 2.707 -0.529 1.00 0.00 O +ATOM 99 H R12 A 99 -13.577 3.326 -0.572 1.00 0.00 H +ATOM 100 H R12 A 100 -13.142 2.324 0.326 1.00 0.00 H +ATOM 101 O R11 A 101 3.437 4.870 -4.361 1.00 0.00 O +ATOM 102 H R12 A 102 3.658 5.174 -3.434 1.00 0.00 H +ATOM 103 H R12 A 103 4.375 5.064 -4.648 1.00 0.00 H +ATOM 104 O R11 A 104 -1.008 3.496 -4.976 1.00 0.00 O +ATOM 105 H R12 A 105 -0.033 3.370 -4.792 1.00 0.00 H +ATOM 106 H R12 A 106 -0.831 4.437 -5.265 1.00 0.00 H +ATOM 107 O R11 A 107 20.211 18.805 0.682 1.00 0.00 O +ATOM 108 H R12 A 108 19.592 18.023 0.747 1.00 0.00 H +ATOM 109 H R12 A 109 20.626 18.550 1.555 1.00 0.00 H +ATOM 110 O R11 A 110 6.696 3.837 1.735 1.00 0.00 O +ATOM 111 H R12 A 111 7.451 3.846 1.080 1.00 0.00 H +ATOM 112 H R12 A 112 7.336 3.615 2.470 1.00 0.00 H +ATOM 113 O R11 A 113 18.063 17.093 -4.514 1.00 0.00 O +ATOM 114 H R12 A 114 18.284 17.397 -3.587 1.00 0.00 H +ATOM 115 H R12 A 115 19.001 17.287 -4.801 1.00 0.00 H +ATOM 116 O R11 A 116 -2.217 -0.630 1.035 1.00 0.00 O +ATOM 117 H R12 A 117 -1.274 -0.413 0.782 1.00 0.00 H +ATOM 118 H R12 A 118 -2.367 0.325 1.293 1.00 0.00 H +ATOM 119 O R11 A 119 11.389 21.162 -1.247 1.00 0.00 O +ATOM 120 H R12 A 120 10.835 20.999 -0.431 1.00 0.00 H +ATOM 121 H R12 A 121 11.831 21.936 -0.793 1.00 0.00 H +ATOM 122 O R11 A 122 4.643 8.421 -4.889 1.00 0.00 O +ATOM 123 H R12 A 123 4.384 7.754 -4.191 1.00 0.00 H +ATOM 124 H R12 A 124 5.524 7.962 -5.002 1.00 0.00 H +ATOM 125 O R11 A 125 13.951 15.958 2.811 1.00 0.00 O +ATOM 126 H R12 A 126 14.762 16.534 2.709 1.00 0.00 H +ATOM 127 H R12 A 127 14.520 15.221 3.177 1.00 0.00 H +ATOM 128 O R11 A 128 -14.752 5.235 1.404 1.00 0.00 O +ATOM 129 H R12 A 129 -14.301 4.349 1.505 1.00 0.00 H +ATOM 130 H R12 A 130 -13.932 5.603 0.965 1.00 0.00 H +ATOM 131 O R11 A 131 6.332 12.566 2.510 1.00 0.00 O +ATOM 132 H R12 A 132 6.757 11.793 2.038 1.00 0.00 H +ATOM 133 H R12 A 133 6.834 13.201 1.923 1.00 0.00 H +ATOM 134 O R11 A 134 15.464 13.918 -2.084 1.00 0.00 O +ATOM 135 H R12 A 135 15.646 13.141 -1.482 1.00 0.00 H +ATOM 136 H R12 A 136 16.309 13.730 -2.585 1.00 0.00 H +ATOM 137 O R11 A 137 -9.551 7.719 0.653 1.00 0.00 O +ATOM 138 H R12 A 138 -10.108 7.862 -0.165 1.00 0.00 H +ATOM 139 H R12 A 139 -9.726 8.662 0.936 1.00 0.00 H +ATOM 140 O R11 A 140 1.794 5.984 3.351 1.00 0.00 O +ATOM 141 H R12 A 141 2.598 5.840 2.775 1.00 0.00 H +ATOM 142 H R12 A 142 1.201 5.858 2.555 1.00 0.00 H +ATOM 143 O R11 A 143 -12.015 4.756 -3.472 1.00 0.00 O +ATOM 144 H R12 A 144 -11.292 4.668 -4.157 1.00 0.00 H +ATOM 145 H R12 A 145 -11.662 5.656 -3.216 1.00 0.00 H +ATOM 146 O R11 A 146 -9.048 1.340 -4.315 1.00 0.00 O +ATOM 147 H R12 A 147 -9.616 1.073 -3.536 1.00 0.00 H +ATOM 148 H R12 A 148 -8.235 1.012 -3.834 1.00 0.00 H +ATOM 149 O R11 A 149 10.711 13.965 4.174 1.00 0.00 O +ATOM 150 H R12 A 150 11.363 13.863 4.926 1.00 0.00 H +ATOM 151 H R12 A 151 11.436 14.339 3.596 1.00 0.00 H +ATOM 152 O R11 A 152 18.535 15.709 2.147 1.00 0.00 O +ATOM 153 H R12 A 153 19.395 15.334 1.801 1.00 0.00 H +ATOM 154 H R12 A 154 19.033 16.172 2.880 1.00 0.00 H +ATOM 155 O R11 A 155 -5.059 -1.800 -0.244 1.00 0.00 O +ATOM 156 H R12 A 156 -6.032 -1.754 -0.020 1.00 0.00 H +ATOM 157 H R12 A 157 -5.013 -2.721 0.143 1.00 0.00 H +ATOM 158 O R11 A 158 17.518 12.783 2.568 1.00 0.00 O +ATOM 159 H R12 A 159 18.378 12.408 2.222 1.00 0.00 H +ATOM 160 H R12 A 160 18.016 13.246 3.302 1.00 0.00 H +ATOM 161 O R11 A 161 9.163 17.337 2.863 1.00 0.00 O +ATOM 162 H R12 A 162 9.502 16.476 3.245 1.00 0.00 H +ATOM 163 H R12 A 163 9.573 17.106 1.981 1.00 0.00 H +ATOM 164 O R11 A 164 12.471 11.343 2.251 1.00 0.00 O +ATOM 165 H R12 A 165 13.157 11.104 1.564 1.00 0.00 H +ATOM 166 H R12 A 166 12.894 12.243 2.359 1.00 0.00 H +ATOM 167 O R11 A 167 7.964 5.238 -4.443 1.00 0.00 O +ATOM 168 H R12 A 168 8.259 5.850 -3.709 1.00 0.00 H +ATOM 169 H R12 A 169 7.039 5.611 -4.383 1.00 0.00 H +ATOM 170 O R11 A 170 -11.343 0.191 1.123 1.00 0.00 O +ATOM 171 H R12 A 171 -10.483 0.163 0.613 1.00 0.00 H +ATOM 172 H R12 A 172 -11.847 0.296 0.266 1.00 0.00 H +ATOM 173 O R11 A 173 12.237 19.025 -4.405 1.00 0.00 O +ATOM 174 H R12 A 174 12.687 18.856 -3.528 1.00 0.00 H +ATOM 175 H R12 A 175 11.912 19.909 -4.068 1.00 0.00 H +ATOM 176 O R11 A 176 10.948 6.635 -3.598 1.00 0.00 O +ATOM 177 H R12 A 177 10.498 7.397 -3.132 1.00 0.00 H +ATOM 178 H R12 A 178 11.027 7.188 -4.427 1.00 0.00 H +ATOM 179 O R11 A 179 -4.689 -1.161 -6.386 1.00 0.00 O +ATOM 180 H R12 A 180 -3.909 -1.658 -6.006 1.00 0.00 H +ATOM 181 H R12 A 181 -4.690 -1.771 -7.179 1.00 0.00 H +ATOM 182 O R11 A 182 1.438 7.201 -4.853 1.00 0.00 O +ATOM 183 H R12 A 183 2.082 7.153 -4.089 1.00 0.00 H +ATOM 184 H R12 A 184 2.048 7.836 -5.327 1.00 0.00 H +ATOM 185 O R11 A 185 6.761 16.483 0.078 1.00 0.00 O +ATOM 186 H R12 A 186 7.435 17.221 0.052 1.00 0.00 H +ATOM 187 H R12 A 187 6.052 17.139 0.336 1.00 0.00 H +ATOM 188 O R11 A 188 9.703 5.262 1.631 1.00 0.00 O +ATOM 189 H R12 A 189 9.243 5.842 2.304 1.00 0.00 H +ATOM 190 H R12 A 190 9.876 6.062 1.058 1.00 0.00 H +ATOM 191 O R11 A 191 -5.530 6.443 -0.394 1.00 0.00 O +ATOM 192 H R12 A 192 -5.839 5.841 -1.131 1.00 0.00 H +ATOM 193 H R12 A 193 -4.618 6.036 -0.445 1.00 0.00 H +ATOM 194 O R11 A 194 2.831 8.920 3.406 1.00 0.00 O +ATOM 195 H R12 A 195 3.326 8.132 3.040 1.00 0.00 H +ATOM 196 H R12 A 196 3.330 9.522 2.783 1.00 0.00 H +ATOM 197 O R11 A 197 -3.352 6.735 -4.716 1.00 0.00 O +ATOM 198 H R12 A 198 -3.598 6.020 -5.370 1.00 0.00 H +ATOM 199 H R12 A 199 -4.067 6.414 -4.095 1.00 0.00 H +ATOM 200 O R11 A 200 -0.925 4.554 4.019 1.00 0.00 O +ATOM 201 H R12 A 201 -0.716 3.776 4.611 1.00 0.00 H +ATOM 202 H R12 A 202 -1.390 5.008 4.779 1.00 0.00 H +ATOM 203 O R11 A 203 14.077 23.019 0.047 1.00 0.00 O +ATOM 204 H R12 A 204 15.001 22.646 -0.041 1.00 0.00 H +ATOM 205 H R12 A 205 14.343 23.808 -0.506 1.00 0.00 H +ATOM 206 O R11 A 206 9.571 3.190 -2.577 1.00 0.00 O +ATOM 207 H R12 A 207 8.923 2.511 -2.232 1.00 0.00 H +ATOM 208 H R12 A 208 9.976 3.267 -1.665 1.00 0.00 H +ATOM 209 O R11 A 209 -9.062 4.234 3.653 1.00 0.00 O +ATOM 210 H R12 A 210 -8.257 4.089 3.077 1.00 0.00 H +ATOM 211 H R12 A 211 -9.654 4.107 2.857 1.00 0.00 H +ATOM 212 O R11 A 212 16.417 21.588 -6.313 1.00 0.00 O +ATOM 213 H R12 A 213 17.374 21.868 -6.399 1.00 0.00 H +ATOM 214 H R12 A 214 16.219 22.425 -5.802 1.00 0.00 H +ATOM 215 O R11 A 215 3.151 3.085 -1.397 1.00 0.00 O +ATOM 216 H R12 A 216 2.821 2.252 -1.841 1.00 0.00 H +ATOM 217 H R12 A 217 3.511 2.540 -0.640 1.00 0.00 H +ATOM 218 O R11 A 218 2.229 10.525 -3.624 1.00 0.00 O +ATOM 219 H R12 A 219 2.452 9.553 -3.696 1.00 0.00 H +ATOM 220 H R12 A 220 1.907 10.522 -4.571 1.00 0.00 H +ATOM 221 O R11 A 221 7.773 11.123 -5.181 1.00 0.00 O +ATOM 222 H R12 A 222 8.716 10.972 -4.884 1.00 0.00 H +ATOM 223 H R12 A 223 7.990 12.077 -5.386 1.00 0.00 H +ATOM 224 O R11 A 224 -0.530 7.811 2.594 1.00 0.00 O +ATOM 225 H R12 A 225 -1.326 8.377 2.808 1.00 0.00 H +ATOM 226 H R12 A 226 -1.134 7.071 2.300 1.00 0.00 H +ATOM 227 O R11 A 227 -9.769 -3.045 0.744 1.00 0.00 O +ATOM 228 H R12 A 228 -9.820 -2.052 0.856 1.00 0.00 H +ATOM 229 H R12 A 229 -9.518 -2.923 -0.216 1.00 0.00 H +ATOM 230 O R11 A 230 11.277 22.499 -4.581 1.00 0.00 O +ATOM 231 H R12 A 231 11.035 23.468 -4.525 1.00 0.00 H +ATOM 232 H R12 A 232 11.538 22.620 -5.539 1.00 0.00 H +ATOM 233 O R11 A 233 5.666 1.736 -2.773 1.00 0.00 O +ATOM 234 H R12 A 234 6.551 1.303 -2.946 1.00 0.00 H +ATOM 235 H R12 A 235 6.015 2.598 -3.140 1.00 0.00 H +ATOM 236 O R11 A 236 5.070 7.383 -7.868 1.00 0.00 O +ATOM 237 H R12 A 237 5.755 7.805 -7.275 1.00 0.00 H +ATOM 238 H R12 A 238 4.830 8.282 -8.232 1.00 0.00 H +ATOM 239 O R11 A 239 19.995 21.415 -1.426 1.00 0.00 O +ATOM 240 H R12 A 240 19.436 22.101 -0.961 1.00 0.00 H +ATOM 241 H R12 A 241 19.237 20.764 -1.379 1.00 0.00 H +ATOM 242 O R11 A 242 14.786 18.553 -5.996 1.00 0.00 O +ATOM 243 H R12 A 243 14.242 17.715 -5.976 1.00 0.00 H +ATOM 244 H R12 A 244 15.519 18.089 -5.497 1.00 0.00 H +ATOM 245 O R11 A 245 -2.357 8.784 -2.404 1.00 0.00 O +ATOM 246 H R12 A 246 -1.968 7.875 -2.252 1.00 0.00 H +ATOM 247 H R12 A 247 -1.557 9.035 -2.949 1.00 0.00 H +ATOM 248 O R11 A 248 18.051 24.469 -0.738 1.00 0.00 O +ATOM 249 H R12 A 249 17.494 24.612 -1.556 1.00 0.00 H +ATOM 250 H R12 A 250 17.876 25.412 -0.455 1.00 0.00 H +ATOM 251 O R11 A 251 11.377 20.020 2.663 1.00 0.00 O +ATOM 252 H R12 A 252 11.153 20.810 2.091 1.00 0.00 H +ATOM 253 H R12 A 253 11.000 19.409 1.966 1.00 0.00 H +ATOM 254 O R11 A 254 -12.365 10.166 -0.972 1.00 0.00 O +ATOM 255 H R12 A 255 -12.162 9.215 -0.741 1.00 0.00 H +ATOM 256 H R12 A 256 -11.387 10.350 -1.073 1.00 0.00 H +ATOM 257 O R11 A 257 19.529 25.437 -3.525 1.00 0.00 O +ATOM 258 H R12 A 258 18.529 25.437 -3.525 1.00 0.00 H +ATOM 259 H R12 A 259 19.529 24.437 -3.525 1.00 0.00 H +ATOM 260 O R11 A 260 1.854 1.237 1.381 1.00 0.00 O +ATOM 261 H R12 A 261 2.738 0.781 1.481 1.00 0.00 H +ATOM 262 H R12 A 262 1.661 0.685 0.570 1.00 0.00 H +ATOM 263 O R11 A 263 12.018 8.164 1.833 1.00 0.00 O +ATOM 264 H R12 A 264 11.550 8.874 2.359 1.00 0.00 H +ATOM 265 H R12 A 265 12.728 8.111 2.535 1.00 0.00 H +ATOM 266 O R11 A 266 -8.104 8.315 -3.125 1.00 0.00 O +ATOM 267 H R12 A 267 -7.258 8.812 -2.929 1.00 0.00 H +ATOM 268 H R12 A 268 -8.447 9.102 -3.639 1.00 0.00 H +ATOM 269 O R11 A 269 -4.516 1.883 -6.490 1.00 0.00 O +ATOM 270 H R12 A 270 -3.638 1.419 -6.375 1.00 0.00 H +ATOM 271 H R12 A 271 -4.208 2.251 -7.367 1.00 0.00 H +ATOM 272 O R11 A 272 -8.129 -0.664 -1.744 1.00 0.00 O +ATOM 273 H R12 A 273 -8.644 -0.797 -2.591 1.00 0.00 H +ATOM 274 H R12 A 274 -8.549 0.237 -1.630 1.00 0.00 H +ATOM 275 O R11 A 275 -1.815 0.273 -4.355 1.00 0.00 O +ATOM 276 H R12 A 276 -0.907 0.655 -4.527 1.00 0.00 H +ATOM 277 H R12 A 277 -2.098 1.135 -3.936 1.00 0.00 H +ATOM 278 O R11 A 278 15.482 22.014 2.860 1.00 0.00 O +ATOM 279 H R12 A 279 15.541 21.179 3.406 1.00 0.00 H +ATOM 280 H R12 A 280 14.902 21.540 2.198 1.00 0.00 H +ATOM 281 O R11 A 281 18.977 21.908 1.621 1.00 0.00 O +ATOM 282 H R12 A 282 18.316 22.226 0.941 1.00 0.00 H +ATOM 283 H R12 A 283 18.264 21.921 2.321 1.00 0.00 H +ATOM 284 O R11 A 284 14.708 25.202 -3.335 1.00 0.00 O +ATOM 285 H R12 A 285 15.166 24.438 -3.790 1.00 0.00 H +ATOM 286 H R12 A 286 14.618 24.652 -2.504 1.00 0.00 H +ATOM 287 O R11 A 287 1.728 0.723 -5.058 1.00 0.00 O +ATOM 288 H R12 A 288 2.679 0.857 -4.780 1.00 0.00 H +ATOM 289 H R12 A 289 1.630 1.708 -5.197 1.00 0.00 H +ATOM 290 O R11 A 290 -6.282 -1.695 2.928 1.00 0.00 O +ATOM 291 H R12 A 291 -6.653 -2.127 2.106 1.00 0.00 H +ATOM 292 H R12 A 292 -7.195 -1.686 3.335 1.00 0.00 H +ATOM 293 O R11 A 293 -1.217 8.707 -5.989 1.00 0.00 O +ATOM 294 H R12 A 294 -0.278 8.877 -6.288 1.00 0.00 H +ATOM 295 H R12 A 295 -1.536 9.460 -6.565 1.00 0.00 H +ATOM 296 O R11 A 296 -5.488 6.728 -7.303 1.00 0.00 O +ATOM 297 H R12 A 297 -5.840 5.803 -7.442 1.00 0.00 H +ATOM 298 H R12 A 298 -5.166 6.748 -8.250 1.00 0.00 H +ATOM 299 O R11 A 299 13.409 11.584 -2.247 1.00 0.00 O +ATOM 300 H R12 A 300 13.079 10.751 -2.691 1.00 0.00 H +ATOM 301 H R12 A 301 13.769 11.038 -1.490 1.00 0.00 H +ATOM 302 O R11 A 302 -5.175 5.090 3.012 1.00 0.00 O +ATOM 303 H R12 A 303 -5.758 4.277 2.990 1.00 0.00 H +ATOM 304 H R12 A 304 -5.285 5.196 2.024 1.00 0.00 H +ATOM 305 O R11 A 305 14.212 24.189 -6.239 1.00 0.00 O +ATOM 306 H R12 A 306 13.829 24.933 -6.786 1.00 0.00 H +ATOM 307 H R12 A 307 13.662 23.529 -6.752 1.00 0.00 H +ATOM 308 O R11 A 308 13.055 18.766 5.276 1.00 0.00 O +ATOM 309 H R12 A 309 13.773 18.859 4.587 1.00 0.00 H +ATOM 310 H R12 A 310 13.714 18.360 5.909 1.00 0.00 H +ATOM 311 O R11 A 311 21.749 22.307 -4.000 1.00 0.00 O +ATOM 312 H R12 A 312 21.189 22.993 -3.535 1.00 0.00 H +ATOM 313 H R12 A 313 20.991 21.656 -3.953 1.00 0.00 H +ATOM 314 O R11 A 314 -2.808 2.041 4.071 1.00 0.00 O +ATOM 315 H R12 A 315 -2.598 1.263 4.663 1.00 0.00 H +ATOM 316 H R12 A 316 -3.272 2.495 4.832 1.00 0.00 H +ATOM 317 O R11 A 317 12.676 5.860 -0.199 1.00 0.00 O +ATOM 318 H R12 A 318 11.753 6.241 -0.144 1.00 0.00 H +ATOM 319 H R12 A 319 12.337 5.124 -0.785 1.00 0.00 H +ATOM 320 O R11 A 320 17.202 14.291 -5.589 1.00 0.00 O +ATOM 321 H R12 A 321 16.537 14.730 -4.985 1.00 0.00 H +ATOM 322 H R12 A 322 16.472 14.077 -6.238 1.00 0.00 H +ATOM 323 O R11 A 323 -1.976 10.455 0.909 1.00 0.00 O +ATOM 324 H R12 A 324 -2.515 10.673 1.723 1.00 0.00 H +ATOM 325 H R12 A 325 -2.421 9.562 0.853 1.00 0.00 H +ATOM 326 O R11 A 326 10.041 9.522 -6.979 1.00 0.00 O +ATOM 327 H R12 A 327 10.919 9.058 -6.865 1.00 0.00 H +ATOM 328 H R12 A 328 10.349 9.889 -7.857 1.00 0.00 H +ATOM 329 O R11 A 329 18.528 19.043 -7.080 1.00 0.00 O +ATOM 330 H R12 A 330 19.503 18.917 -6.897 1.00 0.00 H +ATOM 331 H R12 A 331 18.705 19.984 -7.370 1.00 0.00 H +ATOM 332 O R11 A 332 8.215 12.912 6.051 1.00 0.00 O +ATOM 333 H R12 A 333 8.607 13.828 5.962 1.00 0.00 H +ATOM 334 H R12 A 334 8.730 12.614 5.247 1.00 0.00 H +ATOM 335 O R11 A 335 -11.457 2.023 4.043 1.00 0.00 O +ATOM 336 H R12 A 336 -10.846 2.153 4.824 1.00 0.00 H +ATOM 337 H R12 A 337 -10.701 1.637 3.515 1.00 0.00 H +ATOM 338 O R11 A 338 15.419 9.713 0.385 1.00 0.00 O +ATOM 339 H R12 A 339 14.798 8.932 0.450 1.00 0.00 H +ATOM 340 H R12 A 340 15.653 9.449 -0.550 1.00 0.00 H +ATOM 341 O R11 A 341 8.578 14.617 -6.159 1.00 0.00 O +ATOM 342 H R12 A 342 9.233 14.493 -6.905 1.00 0.00 H +ATOM 343 H R12 A 343 7.856 14.222 -6.727 1.00 0.00 H +ATOM 344 O R11 A 344 5.991 7.247 4.132 1.00 0.00 O +ATOM 345 H R12 A 345 6.499 6.644 3.517 1.00 0.00 H +ATOM 346 H R12 A 346 5.138 6.992 3.677 1.00 0.00 H +ATOM 347 O R11 A 347 4.128 11.415 -5.930 1.00 0.00 O +ATOM 348 H R12 A 348 4.178 12.357 -6.262 1.00 0.00 H +ATOM 349 H R12 A 349 5.093 11.455 -5.670 1.00 0.00 H +ATOM 350 O R11 A 350 -7.481 1.998 -8.103 1.00 0.00 O +ATOM 351 H R12 A 351 -6.495 1.996 -8.268 1.00 0.00 H +ATOM 352 H R12 A 352 -7.499 2.990 -8.221 1.00 0.00 H +ATOM 353 O R11 A 353 14.005 8.226 -3.589 1.00 0.00 O +ATOM 354 H R12 A 354 14.471 9.035 -3.947 1.00 0.00 H +ATOM 355 H R12 A 355 14.023 8.621 -2.671 1.00 0.00 H +ATOM 356 O R11 A 356 0.742 11.440 3.436 1.00 0.00 O +ATOM 357 H R12 A 357 0.979 10.679 2.832 1.00 0.00 H +ATOM 358 H R12 A 358 -0.149 11.517 2.990 1.00 0.00 H +ATOM 359 O R11 A 359 11.543 26.099 -2.477 1.00 0.00 O +ATOM 360 H R12 A 360 12.042 26.958 -2.361 1.00 0.00 H +ATOM 361 H R12 A 361 11.357 26.335 -3.431 1.00 0.00 H +ATOM 362 O R11 A 362 0.979 -0.368 -1.790 1.00 0.00 O +ATOM 363 H R12 A 363 1.084 0.484 -2.303 1.00 0.00 H +ATOM 364 H R12 A 364 1.960 -0.373 -1.597 1.00 0.00 H +ATOM 365 O R11 A 365 12.622 11.350 -5.568 1.00 0.00 O +ATOM 366 H R12 A 366 11.629 11.235 -5.572 1.00 0.00 H +ATOM 367 H R12 A 367 12.508 12.341 -5.499 1.00 0.00 H +ATOM 368 O R11 A 368 18.894 14.311 -2.950 1.00 0.00 O +ATOM 369 H R12 A 369 19.812 14.701 -2.878 1.00 0.00 H +ATOM 370 H R12 A 370 19.160 13.571 -2.332 1.00 0.00 H +ATOM 371 O R11 A 371 4.310 4.233 4.183 1.00 0.00 O +ATOM 372 H R12 A 372 5.194 4.040 4.609 1.00 0.00 H +ATOM 373 H R12 A 373 3.853 3.681 4.881 1.00 0.00 H +ATOM 374 O R11 A 374 -2.044 3.951 -8.722 1.00 0.00 O +ATOM 375 H R12 A 375 -1.333 4.019 -8.022 1.00 0.00 H +ATOM 376 H R12 A 376 -2.739 3.869 -8.008 1.00 0.00 H +ATOM 377 O R11 A 377 -15.090 5.245 -2.543 1.00 0.00 O +ATOM 378 H R12 A 378 -15.059 5.428 -3.526 1.00 0.00 H +ATOM 379 H R12 A 379 -14.353 5.905 -2.397 1.00 0.00 H +ATOM 380 O R11 A 380 7.048 18.096 -3.374 1.00 0.00 O +ATOM 381 H R12 A 381 6.483 17.981 -4.190 1.00 0.00 H +ATOM 382 H R12 A 382 6.932 17.127 -3.156 1.00 0.00 H +ATOM 383 O R11 A 383 6.866 19.771 -0.726 1.00 0.00 O +ATOM 384 H R12 A 384 7.811 19.900 -1.026 1.00 0.00 H +ATOM 385 H R12 A 385 6.899 20.646 -0.244 1.00 0.00 H +ATOM 386 O R11 A 386 11.497 19.665 -7.542 1.00 0.00 O +ATOM 387 H R12 A 387 11.996 19.479 -8.388 1.00 0.00 H +ATOM 388 H R12 A 388 12.356 19.901 -7.088 1.00 0.00 H +ATOM 389 O R11 A 389 -6.027 8.911 1.612 1.00 0.00 O +ATOM 390 H R12 A 390 -5.333 8.986 0.896 1.00 0.00 H +ATOM 391 H R12 A 391 -6.746 9.016 0.925 1.00 0.00 H +ATOM 392 O R11 A 392 5.667 11.662 -2.771 1.00 0.00 O +ATOM 393 H R12 A 393 5.532 10.957 -3.467 1.00 0.00 H +ATOM 394 H R12 A 394 6.641 11.440 -2.735 1.00 0.00 H +ATOM 395 O R11 A 395 -10.463 7.883 -5.174 1.00 0.00 O +ATOM 396 H R12 A 396 -11.206 7.288 -4.867 1.00 0.00 H +ATOM 397 H R12 A 397 -9.794 7.206 -4.868 1.00 0.00 H +ATOM 398 O R11 A 398 6.792 15.532 4.674 1.00 0.00 O +ATOM 399 H R12 A 399 6.548 14.566 4.767 1.00 0.00 H +ATOM 400 H R12 A 400 6.143 15.622 3.918 1.00 0.00 H +ATOM 401 O R11 A 401 -1.260 12.039 -1.974 1.00 0.00 O +ATOM 402 H R12 A 402 -0.750 11.999 -1.116 1.00 0.00 H +ATOM 403 H R12 A 403 -1.926 11.388 -1.609 1.00 0.00 H +ATOM 404 O R11 A 404 -11.825 6.150 3.121 1.00 0.00 O +ATOM 405 H R12 A 405 -12.096 5.232 2.832 1.00 0.00 H +ATOM 406 H R12 A 406 -11.495 6.344 2.197 1.00 0.00 H +ATOM 407 O R11 A 407 7.806 9.727 6.102 1.00 0.00 O +ATOM 408 H R12 A 408 7.315 9.861 6.963 1.00 0.00 H +ATOM 409 H R12 A 409 6.947 9.816 5.598 1.00 0.00 H +ATOM 410 O R11 A 410 0.565 -0.472 3.546 1.00 0.00 O +ATOM 411 H R12 A 411 0.534 0.426 3.985 1.00 0.00 H +ATOM 412 H R12 A 412 1.463 -0.640 3.952 1.00 0.00 H +ATOM 413 O R11 A 413 -6.452 11.461 -0.495 1.00 0.00 O +ATOM 414 H R12 A 414 -5.583 11.628 -0.962 1.00 0.00 H +ATOM 415 H R12 A 415 -6.939 11.925 -1.235 1.00 0.00 H +ATOM 416 O R11 A 416 12.453 4.085 4.017 1.00 0.00 O +ATOM 417 H R12 A 417 11.834 4.500 3.350 1.00 0.00 H +ATOM 418 H R12 A 418 11.670 3.830 4.585 1.00 0.00 H +ATOM 419 O R11 A 419 5.986 -0.006 0.039 1.00 0.00 O +ATOM 420 H R12 A 420 6.766 -0.008 0.665 1.00 0.00 H +ATOM 421 H R12 A 421 5.489 -0.616 0.657 1.00 0.00 H +ATOM 422 O R11 A 422 9.302 23.147 3.647 1.00 0.00 O +ATOM 423 H R12 A 423 9.742 23.761 4.303 1.00 0.00 H +ATOM 424 H R12 A 424 10.194 22.933 3.249 1.00 0.00 H +ATOM 425 O R11 A 425 -14.773 11.839 0.833 1.00 0.00 O +ATOM 426 H R12 A 426 -14.934 12.741 0.432 1.00 0.00 H +ATOM 427 H R12 A 427 -14.950 11.413 -0.054 1.00 0.00 H +ATOM 428 O R11 A 428 11.294 6.920 5.223 1.00 0.00 O +ATOM 429 H R12 A 429 11.998 7.151 5.894 1.00 0.00 H +ATOM 430 H R12 A 430 11.823 7.378 4.510 1.00 0.00 H +ATOM 431 O R11 A 431 -7.783 -0.859 6.007 1.00 0.00 O +ATOM 432 H R12 A 432 -7.172 -0.730 6.788 1.00 0.00 H +ATOM 433 H R12 A 433 -7.026 -1.246 5.480 1.00 0.00 H +ATOM 434 O R11 A 434 1.168 11.650 -7.070 1.00 0.00 O +ATOM 435 H R12 A 435 0.876 12.159 -6.260 1.00 0.00 H +ATOM 436 H R12 A 436 0.281 11.190 -7.101 1.00 0.00 H +ATOM 437 O R11 A 437 3.932 11.329 5.253 1.00 0.00 O +ATOM 438 H R12 A 438 4.888 11.130 5.468 1.00 0.00 H +ATOM 439 H R12 A 439 4.212 12.165 4.781 1.00 0.00 H +ATOM 440 O R11 A 440 -1.576 -3.362 -3.123 1.00 0.00 O +ATOM 441 H R12 A 441 -1.471 -2.509 -3.635 1.00 0.00 H +ATOM 442 H R12 A 442 -0.595 -3.366 -2.929 1.00 0.00 H +ATOM 443 O R11 A 443 21.901 17.187 -6.367 1.00 0.00 O +ATOM 444 H R12 A 444 22.852 17.321 -6.088 1.00 0.00 H +ATOM 445 H R12 A 445 21.803 18.172 -6.506 1.00 0.00 H +ATOM 446 O R11 A 446 17.182 19.263 5.692 1.00 0.00 O +ATOM 447 H R12 A 447 16.292 18.810 5.738 1.00 0.00 H +ATOM 448 H R12 A 448 16.729 20.134 5.502 1.00 0.00 H +ATOM 449 O R11 A 449 1.989 5.846 -7.574 1.00 0.00 O +ATOM 450 H R12 A 450 2.809 6.083 -8.095 1.00 0.00 H +ATOM 451 H R12 A 451 2.511 5.161 -7.065 1.00 0.00 H +ATOM 452 O R11 A 452 9.652 11.622 9.240 1.00 0.00 O +ATOM 453 H R12 A 453 10.586 11.354 9.004 1.00 0.00 H +ATOM 454 H R12 A 454 9.580 12.127 8.380 1.00 0.00 H +ATOM 455 O R11 A 455 -16.668 15.357 0.504 1.00 0.00 O +ATOM 456 H R12 A 456 -15.988 15.279 1.233 1.00 0.00 H +ATOM 457 H R12 A 457 -15.978 15.763 -0.095 1.00 0.00 H +ATOM 458 O R11 A 458 11.562 16.329 6.365 1.00 0.00 O +ATOM 459 H R12 A 459 12.422 16.301 5.855 1.00 0.00 H +ATOM 460 H R12 A 460 11.058 16.434 5.508 1.00 0.00 H +ATOM 461 O R11 A 461 6.423 3.878 -6.890 1.00 0.00 O +ATOM 462 H R12 A 462 6.354 3.709 -5.907 1.00 0.00 H +ATOM 463 H R12 A 463 5.748 3.161 -7.061 1.00 0.00 H +ATOM 464 O R11 A 464 16.666 25.459 -7.520 1.00 0.00 O +ATOM 465 H R12 A 465 16.394 26.336 -7.126 1.00 0.00 H +ATOM 466 H R12 A 466 17.544 25.517 -7.044 1.00 0.00 H +ATOM 467 O R11 A 467 22.674 17.850 3.729 1.00 0.00 O +ATOM 468 H R12 A 468 23.372 17.136 3.769 1.00 0.00 H +ATOM 469 H R12 A 469 23.351 18.528 4.015 1.00 0.00 H +ATOM 470 O R11 A 470 -0.367 -2.364 -5.995 1.00 0.00 O +ATOM 471 H R12 A 471 -0.911 -3.203 -5.975 1.00 0.00 H +ATOM 472 H R12 A 472 0.366 -2.828 -5.497 1.00 0.00 H +ATOM 473 O R11 A 473 17.529 11.446 -7.790 1.00 0.00 O +ATOM 474 H R12 A 474 17.823 12.058 -7.055 1.00 0.00 H +ATOM 475 H R12 A 475 16.603 11.819 -7.729 1.00 0.00 H +ATOM 476 O R11 A 476 -3.757 -4.274 -7.734 1.00 0.00 O +ATOM 477 H R12 A 477 -2.804 -4.564 -7.646 1.00 0.00 H +ATOM 478 H R12 A 478 -3.611 -4.090 -8.706 1.00 0.00 H +ATOM 479 O R11 A 479 20.482 12.367 4.687 1.00 0.00 O +ATOM 480 H R12 A 480 19.666 12.089 4.181 1.00 0.00 H +ATOM 481 H R12 A 481 20.205 11.788 5.453 1.00 0.00 H +ATOM 482 O R11 A 482 -10.008 2.842 -9.567 1.00 0.00 O +ATOM 483 H R12 A 483 -9.707 3.593 -10.155 1.00 0.00 H +ATOM 484 H R12 A 484 -10.961 3.088 -9.740 1.00 0.00 H +ATOM 485 O R11 A 485 -0.581 14.973 -0.940 1.00 0.00 O +ATOM 486 H R12 A 486 -0.256 15.498 -0.154 1.00 0.00 H +ATOM 487 H R12 A 487 -0.586 14.143 -0.382 1.00 0.00 H +ATOM 488 O R11 A 488 -5.010 4.943 -10.441 1.00 0.00 O +ATOM 489 H R12 A 489 -4.698 4.668 -11.351 1.00 0.00 H +ATOM 490 H R12 A 490 -4.783 4.035 -10.089 1.00 0.00 H +ATOM 491 O R11 A 491 -2.854 11.746 -6.244 1.00 0.00 O +ATOM 492 H R12 A 492 -3.350 12.496 -6.682 1.00 0.00 H +ATOM 493 H R12 A 493 -2.104 11.862 -6.895 1.00 0.00 H +ATOM 494 O R11 A 494 -11.068 -4.547 -2.008 1.00 0.00 O +ATOM 495 H R12 A 495 -11.436 -5.155 -1.305 1.00 0.00 H +ATOM 496 H R12 A 496 -11.851 -3.937 -1.891 1.00 0.00 H +ATOM 497 O R11 A 497 -8.123 -0.945 -7.128 1.00 0.00 O +ATOM 498 H R12 A 498 -8.122 -1.791 -6.595 1.00 0.00 H +ATOM 499 H R12 A 499 -8.861 -0.586 -6.557 1.00 0.00 H +ATOM 500 O R11 A 500 20.258 11.686 -0.900 1.00 0.00 O +ATOM 501 H R12 A 501 20.977 12.345 -1.121 1.00 0.00 H +ATOM 502 H R12 A 502 20.351 11.280 -1.809 1.00 0.00 H +ATOM 503 O R11 A 503 22.048 14.653 -8.041 1.00 0.00 O +ATOM 504 H R12 A 504 22.828 14.156 -7.660 1.00 0.00 H +ATOM 505 H R12 A 505 22.047 14.044 -8.833 1.00 0.00 H +ATOM 506 O R11 A 506 20.989 26.565 -0.961 1.00 0.00 O +ATOM 507 H R12 A 507 21.983 26.476 -0.896 1.00 0.00 H +ATOM 508 H R12 A 508 21.063 27.539 -0.748 1.00 0.00 H +ATOM 509 O R11 A 509 2.125 -1.505 -7.518 1.00 0.00 O +ATOM 510 H R12 A 510 1.420 -0.799 -7.450 1.00 0.00 H +ATOM 511 H R12 A 511 2.233 -1.493 -6.524 1.00 0.00 H +ATOM 512 O R11 A 512 -1.591 13.692 3.050 1.00 0.00 O +ATOM 513 H R12 A 513 -2.274 13.751 3.778 1.00 0.00 H +ATOM 514 H R12 A 514 -1.035 14.381 3.515 1.00 0.00 H +ATOM 515 O R11 A 515 -14.804 -1.331 0.485 1.00 0.00 O +ATOM 516 H R12 A 516 -13.810 -1.257 0.403 1.00 0.00 H +ATOM 517 H R12 A 517 -14.893 -0.356 0.278 1.00 0.00 H +ATOM 518 O R11 A 518 13.728 27.836 -5.575 1.00 0.00 O +ATOM 519 H R12 A 519 14.389 27.120 -5.799 1.00 0.00 H +ATOM 520 H R12 A 520 13.605 27.438 -4.666 1.00 0.00 H +ATOM 521 O R11 A 521 -12.883 16.193 1.005 1.00 0.00 O +ATOM 522 H R12 A 522 -13.465 16.083 1.811 1.00 0.00 H +ATOM 523 H R12 A 523 -13.696 16.299 0.432 1.00 0.00 H +ATOM 524 O R11 A 524 4.311 20.966 1.667 1.00 0.00 O +ATOM 525 H R12 A 525 5.201 21.399 1.523 1.00 0.00 H +ATOM 526 H R12 A 526 4.399 20.496 0.788 1.00 0.00 H +ATOM 527 O R11 A 527 3.570 18.190 -4.990 1.00 0.00 O +ATOM 528 H R12 A 528 3.328 18.451 -5.925 1.00 0.00 H +ATOM 529 H R12 A 529 4.539 18.311 -5.207 1.00 0.00 H +ATOM 530 O R11 A 530 12.441 2.180 -0.492 1.00 0.00 O +ATOM 531 H R12 A 531 13.160 2.839 -0.713 1.00 0.00 H +ATOM 532 H R12 A 532 12.534 1.774 -1.401 1.00 0.00 H +ATOM 533 O R11 A 533 16.667 17.291 -9.153 1.00 0.00 O +ATOM 534 H R12 A 534 17.422 17.932 -9.291 1.00 0.00 H +ATOM 535 H R12 A 535 16.676 17.069 -10.128 1.00 0.00 H +ATOM 536 O R11 A 536 7.781 23.412 -0.931 1.00 0.00 O +ATOM 537 H R12 A 537 8.155 22.722 -1.551 1.00 0.00 H +ATOM 538 H R12 A 538 7.615 22.705 -0.244 1.00 0.00 H +ATOM 539 O R11 A 539 -3.914 -4.606 1.576 1.00 0.00 O +ATOM 540 H R12 A 540 -3.210 -4.375 2.247 1.00 0.00 H +ATOM 541 H R12 A 541 -3.385 -4.148 0.862 1.00 0.00 H +ATOM 542 O R11 A 542 15.363 3.964 -2.885 1.00 0.00 O +ATOM 543 H R12 A 543 14.703 3.504 -2.290 1.00 0.00 H +ATOM 544 H R12 A 544 15.296 4.788 -2.323 1.00 0.00 H +ATOM 545 O R11 A 545 -15.500 10.657 3.538 1.00 0.00 O +ATOM 546 H R12 A 546 -15.291 9.879 4.130 1.00 0.00 H +ATOM 547 H R12 A 547 -15.964 11.111 4.299 1.00 0.00 H +ATOM 548 O R11 A 548 8.487 21.336 -8.524 1.00 0.00 O +ATOM 549 H R12 A 549 8.136 21.057 -7.630 1.00 0.00 H +ATOM 550 H R12 A 550 9.418 21.333 -8.159 1.00 0.00 H +ATOM 551 O R11 A 551 12.881 26.079 0.501 1.00 0.00 O +ATOM 552 H R12 A 552 13.154 26.407 -0.404 1.00 0.00 H +ATOM 553 H R12 A 553 12.635 25.194 0.106 1.00 0.00 H +ATOM 554 O R11 A 554 13.284 5.055 -6.056 1.00 0.00 O +ATOM 555 H R12 A 555 13.288 4.055 -6.046 1.00 0.00 H +ATOM 556 H R12 A 556 13.242 5.065 -5.057 1.00 0.00 H +ATOM 557 O R11 A 557 -4.462 -0.771 6.093 1.00 0.00 O +ATOM 558 H R12 A 558 -3.797 -0.735 5.347 1.00 0.00 H +ATOM 559 H R12 A 559 -3.770 -1.177 6.690 1.00 0.00 H +ATOM 560 O R11 A 560 13.233 16.647 -8.826 1.00 0.00 O +ATOM 561 H R12 A 561 13.877 16.598 -8.062 1.00 0.00 H +ATOM 562 H R12 A 562 13.843 17.281 -9.300 1.00 0.00 H +ATOM 563 O R11 A 563 -11.809 7.039 -7.766 1.00 0.00 O +ATOM 564 H R12 A 564 -12.050 7.300 -8.700 1.00 0.00 H +ATOM 565 H R12 A 565 -10.840 7.159 -7.982 1.00 0.00 H +ATOM 566 O R11 A 566 -16.793 8.044 -3.454 1.00 0.00 O +ATOM 567 H R12 A 567 -15.848 8.174 -3.754 1.00 0.00 H +ATOM 568 H R12 A 568 -16.760 8.920 -2.972 1.00 0.00 H +ATOM 569 O R11 A 569 12.582 8.868 7.847 1.00 0.00 O +ATOM 570 H R12 A 570 12.727 9.856 7.793 1.00 0.00 H +ATOM 571 H R12 A 571 13.182 8.823 8.645 1.00 0.00 H +ATOM 572 O R11 A 572 0.503 -5.251 -1.496 1.00 0.00 O +ATOM 573 H R12 A 573 1.208 -5.020 -0.825 1.00 0.00 H +ATOM 574 H R12 A 574 1.032 -4.793 -2.210 1.00 0.00 H +ATOM 575 O R11 A 575 7.100 7.334 8.353 1.00 0.00 O +ATOM 576 H R12 A 576 7.819 7.993 8.132 1.00 0.00 H +ATOM 577 H R12 A 577 7.193 6.928 7.444 1.00 0.00 H +ATOM 578 O R11 A 578 23.631 17.791 0.178 1.00 0.00 O +ATOM 579 H R12 A 579 23.264 17.753 1.107 1.00 0.00 H +ATOM 580 H R12 A 580 22.705 17.894 -0.184 1.00 0.00 H +ATOM 581 O R11 A 581 7.594 19.792 4.224 1.00 0.00 O +ATOM 582 H R12 A 582 7.041 19.629 5.041 1.00 0.00 H +ATOM 583 H R12 A 583 8.036 20.566 4.678 1.00 0.00 H +ATOM 584 O R11 A 584 -12.513 -0.396 -4.602 1.00 0.00 O +ATOM 585 H R12 A 585 -11.775 0.250 -4.406 1.00 0.00 H +ATOM 586 H R12 A 586 -12.298 -0.897 -3.764 1.00 0.00 H +ATOM 587 O R11 A 587 10.806 30.107 -5.436 1.00 0.00 O +ATOM 588 H R12 A 588 10.223 29.312 -5.603 1.00 0.00 H +ATOM 589 H R12 A 589 11.605 29.584 -5.734 1.00 0.00 H +ATOM 590 O R11 A 590 11.196 0.806 4.738 1.00 0.00 O +ATOM 591 H R12 A 591 11.914 1.466 4.516 1.00 0.00 H +ATOM 592 H R12 A 592 11.288 0.400 3.829 1.00 0.00 H +ATOM 593 O R11 A 593 20.009 19.615 4.524 1.00 0.00 O +ATOM 594 H R12 A 594 19.404 19.965 3.809 1.00 0.00 H +ATOM 595 H R12 A 595 20.598 20.416 4.417 1.00 0.00 H +ATOM 596 O R11 A 596 1.680 7.704 -9.960 1.00 0.00 O +ATOM 597 H R12 A 597 1.845 8.161 -9.086 1.00 0.00 H +ATOM 598 H R12 A 598 2.666 7.604 -10.095 1.00 0.00 H +ATOM 599 O R11 A 599 15.820 27.370 -1.265 1.00 0.00 O +ATOM 600 H R12 A 600 14.934 27.786 -1.470 1.00 0.00 H +ATOM 601 H R12 A 601 15.408 26.867 -0.506 1.00 0.00 H +ATOM 602 O R11 A 602 -2.476 -0.520 -9.251 1.00 0.00 O +ATOM 603 H R12 A 603 -3.360 -0.987 -9.225 1.00 0.00 H +ATOM 604 H R12 A 604 -2.942 0.358 -9.353 1.00 0.00 H +ATOM 605 O R11 A 605 9.146 1.537 1.806 1.00 0.00 O +ATOM 606 H R12 A 606 8.150 1.605 1.740 1.00 0.00 H +ATOM 607 H R12 A 607 9.214 1.564 0.809 1.00 0.00 H +ATOM 608 O R11 A 608 14.837 20.597 -9.356 1.00 0.00 O +ATOM 609 H R12 A 609 15.394 20.349 -10.148 1.00 0.00 H +ATOM 610 H R12 A 610 15.308 21.477 -9.301 1.00 0.00 H +ATOM 611 O R11 A 611 -4.564 14.039 1.096 1.00 0.00 O +ATOM 612 H R12 A 612 -3.633 14.254 1.392 1.00 0.00 H +ATOM 613 H R12 A 613 -4.828 14.993 1.234 1.00 0.00 H +ATOM 614 O R11 A 614 -11.296 0.800 7.623 1.00 0.00 O +ATOM 615 H R12 A 615 -10.362 0.532 7.388 1.00 0.00 H +ATOM 616 H R12 A 616 -11.368 1.305 6.763 1.00 0.00 H +ATOM 617 O R11 A 617 25.399 15.687 -8.993 1.00 0.00 O +ATOM 618 H R12 A 618 24.969 15.088 -8.318 1.00 0.00 H +ATOM 619 H R12 A 619 24.775 16.425 -8.736 1.00 0.00 H +ATOM 620 O R11 A 620 3.364 1.499 -7.968 1.00 0.00 O +ATOM 621 H R12 A 621 3.417 0.684 -8.544 1.00 0.00 H +ATOM 622 H R12 A 622 3.826 1.008 -7.230 1.00 0.00 H +ATOM 623 O R11 A 623 18.567 14.504 -8.534 1.00 0.00 O +ATOM 624 H R12 A 624 18.846 14.003 -9.353 1.00 0.00 H +ATOM 625 H R12 A 625 17.618 14.228 -8.688 1.00 0.00 H +ATOM 626 O R11 A 626 23.130 15.326 -3.735 1.00 0.00 O +ATOM 627 H R12 A 627 23.849 15.985 -3.956 1.00 0.00 H +ATOM 628 H R12 A 628 23.223 14.920 -4.644 1.00 0.00 H +ATOM 629 O R11 A 629 16.813 10.118 4.125 1.00 0.00 O +ATOM 630 H R12 A 630 16.886 10.318 3.148 1.00 0.00 H +ATOM 631 H R12 A 631 17.795 9.933 4.160 1.00 0.00 H +ATOM 632 O R11 A 632 9.875 24.851 0.785 1.00 0.00 O +ATOM 633 H R12 A 633 10.143 25.538 0.109 1.00 0.00 H +ATOM 634 H R12 A 634 9.421 25.561 1.325 1.00 0.00 H +ATOM 635 O R11 A 635 -15.642 6.615 -6.531 1.00 0.00 O +ATOM 636 H R12 A 636 -16.443 6.313 -7.049 1.00 0.00 H +ATOM 637 H R12 A 637 -15.944 6.069 -5.748 1.00 0.00 H +ATOM 638 O R11 A 638 10.858 3.359 -5.326 1.00 0.00 O +ATOM 639 H R12 A 639 11.383 2.851 -4.643 1.00 0.00 H +ATOM 640 H R12 A 640 10.350 2.528 -5.553 1.00 0.00 H +ATOM 641 O R11 A 641 18.923 16.233 5.736 1.00 0.00 O +ATOM 642 H R12 A 642 19.598 16.879 5.378 1.00 0.00 H +ATOM 643 H R12 A 643 18.762 16.836 6.517 1.00 0.00 H +ATOM 644 O R11 A 644 -4.839 9.666 -4.820 1.00 0.00 O +ATOM 645 H R12 A 645 -5.396 9.809 -5.638 1.00 0.00 H +ATOM 646 H R12 A 646 -5.014 10.610 -4.537 1.00 0.00 H +ATOM 647 O R11 A 647 -2.039 6.985 -9.505 1.00 0.00 O +ATOM 648 H R12 A 648 -1.060 6.865 -9.341 1.00 0.00 H +ATOM 649 H R12 A 649 -1.921 7.978 -9.481 1.00 0.00 H +ATOM 650 O R11 A 650 2.727 13.688 -2.195 1.00 0.00 O +ATOM 651 H R12 A 651 2.144 12.875 -2.217 1.00 0.00 H +ATOM 652 H R12 A 652 2.617 13.794 -3.183 1.00 0.00 H +ATOM 653 O R11 A 653 7.043 15.397 8.186 1.00 0.00 O +ATOM 654 H R12 A 654 7.811 15.462 8.823 1.00 0.00 H +ATOM 655 H R12 A 655 7.615 15.776 7.458 1.00 0.00 H +ATOM 656 O R11 A 656 -11.706 12.269 -3.537 1.00 0.00 O +ATOM 657 H R12 A 657 -11.252 12.523 -4.391 1.00 0.00 H +ATOM 658 H R12 A 658 -12.529 12.756 -3.830 1.00 0.00 H +ATOM 659 O R11 A 659 -8.708 -4.313 -7.097 1.00 0.00 O +ATOM 660 H R12 A 660 -9.176 -3.932 -7.895 1.00 0.00 H +ATOM 661 H R12 A 661 -9.503 -4.887 -6.904 1.00 0.00 H +ATOM 662 O R11 A 662 8.875 -2.102 0.058 1.00 0.00 O +ATOM 663 H R12 A 663 7.878 -2.060 0.112 1.00 0.00 H +ATOM 664 H R12 A 664 8.917 -2.340 1.028 1.00 0.00 H +ATOM 665 O R11 A 665 12.248 9.233 -9.703 1.00 0.00 O +ATOM 666 H R12 A 666 12.740 8.672 -10.369 1.00 0.00 H +ATOM 667 H R12 A 667 12.118 9.942 -10.396 1.00 0.00 H +ATOM 668 O R11 A 668 20.376 28.826 1.652 1.00 0.00 O +ATOM 669 H R12 A 669 21.324 28.930 1.352 1.00 0.00 H +ATOM 670 H R12 A 670 20.135 29.676 1.184 1.00 0.00 H +ATOM 671 O R11 A 671 15.491 5.353 0.829 1.00 0.00 O +ATOM 672 H R12 A 672 15.564 4.356 0.856 1.00 0.00 H +ATOM 673 H R12 A 673 16.324 5.429 1.377 1.00 0.00 H +ATOM 674 O R11 A 674 22.373 11.493 -3.567 1.00 0.00 O +ATOM 675 H R12 A 675 22.565 12.231 -2.920 1.00 0.00 H +ATOM 676 H R12 A 676 21.526 11.952 -3.838 1.00 0.00 H +ATOM 677 O R11 A 677 -5.606 14.065 -3.660 1.00 0.00 O +ATOM 678 H R12 A 678 -5.859 13.283 -3.089 1.00 0.00 H +ATOM 679 H R12 A 679 -5.208 14.519 -2.862 1.00 0.00 H +ATOM 680 O R11 A 680 19.702 19.864 -10.175 1.00 0.00 O +ATOM 681 H R12 A 681 19.633 19.188 -9.441 1.00 0.00 H +ATOM 682 H R12 A 682 19.533 19.146 -10.851 1.00 0.00 H +ATOM 683 O R11 A 683 9.261 3.685 5.579 1.00 0.00 O +ATOM 684 H R12 A 684 9.199 3.425 4.616 1.00 0.00 H +ATOM 685 H R12 A 685 9.276 4.650 5.317 1.00 0.00 H +ATOM 686 O R11 A 686 -5.335 1.270 -10.970 1.00 0.00 O +ATOM 687 H R12 A 687 -5.169 1.727 -10.097 1.00 0.00 H +ATOM 688 H R12 A 688 -4.349 1.171 -11.105 1.00 0.00 H +ATOM 689 O R11 A 689 13.515 12.569 -8.399 1.00 0.00 O +ATOM 690 H R12 A 690 13.980 11.984 -7.735 1.00 0.00 H +ATOM 691 H R12 A 691 14.028 13.359 -8.063 1.00 0.00 H +ATOM 692 O R11 A 692 -12.467 9.223 3.870 1.00 0.00 O +ATOM 693 H R12 A 693 -13.007 8.807 3.137 1.00 0.00 H +ATOM 694 H R12 A 694 -12.725 10.132 3.543 1.00 0.00 H +ATOM 695 O R11 A 695 4.411 -0.858 3.330 1.00 0.00 O +ATOM 696 H R12 A 696 4.955 -0.242 2.760 1.00 0.00 H +ATOM 697 H R12 A 697 3.655 -0.794 2.678 1.00 0.00 H +ATOM 698 O R11 A 698 9.501 28.148 0.427 1.00 0.00 O +ATOM 699 H R12 A 699 9.647 28.620 1.296 1.00 0.00 H +ATOM 700 H R12 A 700 8.585 28.543 0.366 1.00 0.00 H +ATOM 701 O R11 A 701 13.653 24.223 4.198 1.00 0.00 O +ATOM 702 H R12 A 702 14.078 24.725 4.952 1.00 0.00 H +ATOM 703 H R12 A 703 12.880 24.858 4.211 1.00 0.00 H +ATOM 704 O R11 A 704 4.586 4.290 7.737 1.00 0.00 O +ATOM 705 H R12 A 705 5.236 4.756 7.137 1.00 0.00 H +ATOM 706 H R12 A 706 5.053 3.422 7.567 1.00 0.00 H +ATOM 707 O R11 A 707 6.738 1.391 6.622 1.00 0.00 O +ATOM 708 H R12 A 708 6.276 0.632 6.164 1.00 0.00 H +ATOM 709 H R12 A 709 7.042 1.741 5.737 1.00 0.00 H +ATOM 710 O R11 A 710 22.658 27.366 -3.509 1.00 0.00 O +ATOM 711 H R12 A 711 22.354 26.658 -4.146 1.00 0.00 H +ATOM 712 H R12 A 712 22.178 28.058 -4.049 1.00 0.00 H +ATOM 713 O R11 A 713 12.026 22.500 -8.568 1.00 0.00 O +ATOM 714 H R12 A 714 11.410 22.854 -9.271 1.00 0.00 H +ATOM 715 H R12 A 715 11.422 22.858 -7.857 1.00 0.00 H +ATOM 716 O R11 A 716 24.960 23.695 -4.706 1.00 0.00 O +ATOM 717 H R12 A 717 25.410 23.370 -5.539 1.00 0.00 H +ATOM 718 H R12 A 718 24.791 24.579 -5.143 1.00 0.00 H +ATOM 719 O R11 A 719 2.913 7.716 6.267 1.00 0.00 O +ATOM 720 H R12 A 720 3.104 7.169 7.082 1.00 0.00 H +ATOM 721 H R12 A 721 3.746 8.245 6.426 1.00 0.00 H +ATOM 722 O R11 A 722 9.146 4.976 -7.714 1.00 0.00 O +ATOM 723 H R12 A 723 9.826 5.666 -7.964 1.00 0.00 H +ATOM 724 H R12 A 724 9.069 5.383 -6.804 1.00 0.00 H +ATOM 725 O R11 A 725 -0.411 14.020 -4.475 1.00 0.00 O +ATOM 726 H R12 A 726 0.534 14.149 -4.775 1.00 0.00 H +ATOM 727 H R12 A 727 -0.378 14.895 -3.993 1.00 0.00 H +ATOM 728 O R11 A 728 10.084 5.343 8.136 1.00 0.00 O +ATOM 729 H R12 A 729 9.793 5.714 7.254 1.00 0.00 H +ATOM 730 H R12 A 730 10.916 5.897 8.095 1.00 0.00 H +ATOM 731 O R11 A 731 16.285 10.778 7.156 1.00 0.00 O +ATOM 732 H R12 A 732 17.096 11.347 7.291 1.00 0.00 H +ATOM 733 H R12 A 733 16.861 10.041 6.802 1.00 0.00 H +ATOM 734 O R11 A 734 17.237 10.986 -3.199 1.00 0.00 O +ATOM 735 H R12 A 735 17.626 11.786 -2.742 1.00 0.00 H +ATOM 736 H R12 A 736 16.328 11.237 -2.866 1.00 0.00 H +ATOM 737 O R11 A 737 -8.423 14.435 -0.413 1.00 0.00 O +ATOM 738 H R12 A 738 -7.771 15.161 -0.634 1.00 0.00 H +ATOM 739 H R12 A 739 -8.526 14.809 0.509 1.00 0.00 H +ATOM 740 O R11 A 740 14.611 6.975 4.588 1.00 0.00 O +ATOM 741 H R12 A 741 15.554 7.192 4.335 1.00 0.00 H +ATOM 742 H R12 A 742 14.461 7.930 4.845 1.00 0.00 H +ATOM 743 O R11 A 743 -14.030 3.795 -6.151 1.00 0.00 O +ATOM 744 H R12 A 744 -13.531 3.609 -6.998 1.00 0.00 H +ATOM 745 H R12 A 745 -13.171 4.031 -5.697 1.00 0.00 H +ATOM 746 O R11 A 746 7.316 10.872 -8.785 1.00 0.00 O +ATOM 747 H R12 A 747 7.872 10.247 -8.236 1.00 0.00 H +ATOM 748 H R12 A 748 7.838 11.647 -8.431 1.00 0.00 H +ATOM 749 O R11 A 749 3.561 15.295 5.110 1.00 0.00 O +ATOM 750 H R12 A 750 3.661 14.868 4.211 1.00 0.00 H +ATOM 751 H R12 A 751 4.242 14.665 5.484 1.00 0.00 H +ATOM 752 O R11 A 752 5.360 -0.977 -6.485 1.00 0.00 O +ATOM 753 H R12 A 753 4.711 -1.655 -6.140 1.00 0.00 H +ATOM 754 H R12 A 754 5.764 -0.900 -5.573 1.00 0.00 H +ATOM 755 O R11 A 755 -4.943 10.259 -8.816 1.00 0.00 O +ATOM 756 H R12 A 756 -4.228 10.360 -8.125 1.00 0.00 H +ATOM 757 H R12 A 757 -4.311 9.746 -9.397 1.00 0.00 H +ATOM 758 O R11 A 758 13.552 11.615 5.894 1.00 0.00 O +ATOM 759 H R12 A 759 12.959 11.376 5.126 1.00 0.00 H +ATOM 760 H R12 A 760 14.105 12.187 5.289 1.00 0.00 H +ATOM 761 O R11 A 761 22.400 22.855 0.201 1.00 0.00 O +ATOM 762 H R12 A 762 23.365 22.994 -0.018 1.00 0.00 H +ATOM 763 H R12 A 763 22.145 23.532 -0.488 1.00 0.00 H +ATOM 764 O R11 A 764 -11.774 6.493 6.266 1.00 0.00 O +ATOM 765 H R12 A 765 -11.055 6.585 5.577 1.00 0.00 H +ATOM 766 H R12 A 766 -11.114 6.087 6.899 1.00 0.00 H +ATOM 767 O R11 A 767 3.368 17.388 -0.842 1.00 0.00 O +ATOM 768 H R12 A 768 4.258 17.822 -0.986 1.00 0.00 H +ATOM 769 H R12 A 769 3.455 16.918 -1.721 1.00 0.00 H +ATOM 770 O R11 A 770 22.846 21.582 3.426 1.00 0.00 O +ATOM 771 H R12 A 771 23.270 22.083 4.180 1.00 0.00 H +ATOM 772 H R12 A 772 22.073 22.216 3.439 1.00 0.00 H +ATOM 773 O R11 A 773 3.587 -2.153 -3.449 1.00 0.00 O +ATOM 774 H R12 A 774 3.487 -2.265 -4.438 1.00 0.00 H +ATOM 775 H R12 A 775 4.323 -1.493 -3.598 1.00 0.00 H +ATOM 776 O R11 A 776 15.404 21.047 8.220 1.00 0.00 O +ATOM 777 H R12 A 777 16.172 21.113 8.858 1.00 0.00 H +ATOM 778 H R12 A 778 15.976 21.426 7.493 1.00 0.00 H +ATOM 779 O R11 A 779 9.149 24.683 -8.488 1.00 0.00 O +ATOM 780 H R12 A 780 9.200 25.648 -8.230 1.00 0.00 H +ATOM 781 H R12 A 781 10.091 24.723 -8.822 1.00 0.00 H +ATOM 782 O R11 A 782 15.955 26.388 2.192 1.00 0.00 O +ATOM 783 H R12 A 783 16.723 26.453 2.830 1.00 0.00 H +ATOM 784 H R12 A 784 16.527 26.766 1.465 1.00 0.00 H +ATOM 785 O R11 A 785 -15.223 1.632 2.234 1.00 0.00 O +ATOM 786 H R12 A 786 -15.445 2.607 2.239 1.00 0.00 H +ATOM 787 H R12 A 787 -15.020 1.684 1.256 1.00 0.00 H +ATOM 788 O R11 A 788 -8.255 0.713 3.160 1.00 0.00 O +ATOM 789 H R12 A 789 -8.808 0.550 3.976 1.00 0.00 H +ATOM 790 H R12 A 790 -7.813 1.487 3.614 1.00 0.00 H +ATOM 791 O R11 A 791 -7.056 10.875 4.440 1.00 0.00 O +ATOM 792 H R12 A 792 -6.167 11.309 4.296 1.00 0.00 H +ATOM 793 H R12 A 793 -6.969 10.405 3.561 1.00 0.00 H +ATOM 794 O R11 A 794 4.581 24.144 -0.148 1.00 0.00 O +ATOM 795 H R12 A 795 4.672 25.140 -0.146 1.00 0.00 H +ATOM 796 H R12 A 796 3.602 24.234 -0.335 1.00 0.00 H +ATOM 797 O R11 A 797 -12.457 3.972 8.124 1.00 0.00 O +ATOM 798 H R12 A 798 -11.787 3.539 7.520 1.00 0.00 H +ATOM 799 H R12 A 799 -12.956 4.312 7.326 1.00 0.00 H +ATOM 800 O R11 A 800 5.074 15.961 -6.734 1.00 0.00 O +ATOM 801 H R12 A 801 4.833 16.222 -7.669 1.00 0.00 H +ATOM 802 H R12 A 802 6.043 16.082 -6.951 1.00 0.00 H +ATOM 803 O R11 A 803 12.250 15.099 -11.283 1.00 0.00 O +ATOM 804 H R12 A 804 13.184 15.440 -11.182 1.00 0.00 H +ATOM 805 H R12 A 805 12.084 15.768 -12.007 1.00 0.00 H +ATOM 806 O R11 A 806 -17.986 11.702 -1.771 1.00 0.00 O +ATOM 807 H R12 A 807 -17.380 10.993 -2.129 1.00 0.00 H +ATOM 808 H R12 A 808 -17.201 12.165 -1.359 1.00 0.00 H +ATOM 809 O R11 A 809 17.395 28.300 -4.165 1.00 0.00 O +ATOM 810 H R12 A 810 17.850 28.555 -5.019 1.00 0.00 H +ATOM 811 H R12 A 811 16.573 28.788 -4.458 1.00 0.00 H +ATOM 812 O R11 A 812 -19.376 6.678 -5.027 1.00 0.00 O +ATOM 813 H R12 A 813 -19.420 6.752 -4.030 1.00 0.00 H +ATOM 814 H R12 A 814 -18.975 7.592 -5.078 1.00 0.00 H +ATOM 815 O R11 A 815 -4.287 4.834 5.975 1.00 0.00 O +ATOM 816 H R12 A 816 -5.045 5.045 6.593 1.00 0.00 H +ATOM 817 H R12 A 817 -4.076 4.018 6.513 1.00 0.00 H +ATOM 818 O R11 A 818 -11.192 2.441 -6.498 1.00 0.00 O +ATOM 819 H R12 A 819 -11.218 2.065 -5.572 1.00 0.00 H +ATOM 820 H R12 A 820 -10.391 1.878 -6.704 1.00 0.00 H +ATOM 821 O R11 A 821 8.725 8.870 -10.984 1.00 0.00 O +ATOM 822 H R12 A 822 9.380 8.148 -11.208 1.00 0.00 H +ATOM 823 H R12 A 823 8.601 8.475 -10.074 1.00 0.00 H +ATOM 824 O R11 A 824 -7.515 -3.700 7.516 1.00 0.00 O +ATOM 825 H R12 A 825 -6.631 -3.893 7.942 1.00 0.00 H +ATOM 826 H R12 A 826 -7.972 -4.253 8.213 1.00 0.00 H +ATOM 827 O R11 A 827 18.961 22.900 -9.019 1.00 0.00 O +ATOM 828 H R12 A 828 18.878 23.129 -8.049 1.00 0.00 H +ATOM 829 H R12 A 829 18.520 22.019 -8.849 1.00 0.00 H +ATOM 830 O R11 A 830 8.322 21.553 7.294 1.00 0.00 O +ATOM 831 H R12 A 831 8.662 21.202 8.166 1.00 0.00 H +ATOM 832 H R12 A 832 8.750 22.437 7.483 1.00 0.00 H +ATOM 833 O R11 A 833 3.565 16.909 2.446 1.00 0.00 O +ATOM 834 H R12 A 834 4.004 17.300 3.255 1.00 0.00 H +ATOM 835 H R12 A 835 4.461 16.781 2.021 1.00 0.00 H +ATOM 836 O R11 A 836 2.168 14.521 -7.070 1.00 0.00 O +ATOM 837 H R12 A 837 1.654 14.387 -7.917 1.00 0.00 H +ATOM 838 H R12 A 838 2.960 14.069 -7.478 1.00 0.00 H +ATOM 839 O R11 A 839 -3.946 -5.220 -3.542 1.00 0.00 O +ATOM 840 H R12 A 840 -4.414 -4.839 -4.339 1.00 0.00 H +ATOM 841 H R12 A 841 -4.742 -5.794 -3.349 1.00 0.00 H +ATOM 842 O R11 A 842 12.837 16.934 9.173 1.00 0.00 O +ATOM 843 H R12 A 843 13.708 16.903 9.662 1.00 0.00 H +ATOM 844 H R12 A 844 12.806 15.934 9.165 1.00 0.00 H +ATOM 845 O R11 A 845 -2.130 -3.300 5.352 1.00 0.00 O +ATOM 846 H R12 A 846 -2.749 -2.885 4.685 1.00 0.00 H +ATOM 847 H R12 A 847 -2.912 -3.555 5.920 1.00 0.00 H +ATOM 848 O R11 A 848 19.240 28.817 -7.383 1.00 0.00 O +ATOM 849 H R12 A 849 19.102 28.002 -6.820 1.00 0.00 H +ATOM 850 H R12 A 850 19.672 28.255 -8.089 1.00 0.00 H +ATOM 851 O R11 A 851 22.269 15.084 2.367 1.00 0.00 O +ATOM 852 H R12 A 852 22.017 15.482 1.485 1.00 0.00 H +ATOM 853 H R12 A 853 21.488 15.538 2.795 1.00 0.00 H +ATOM 854 O R11 A 854 -7.494 -2.538 10.372 1.00 0.00 O +ATOM 855 H R12 A 855 -7.946 -1.989 9.669 1.00 0.00 H +ATOM 856 H R12 A 856 -8.320 -3.094 10.469 1.00 0.00 H +ATOM 857 O R11 A 857 13.339 0.612 -4.974 1.00 0.00 O +ATOM 858 H R12 A 858 14.189 0.092 -5.065 1.00 0.00 H +ATOM 859 H R12 A 859 13.824 1.448 -5.231 1.00 0.00 H +ATOM 860 O R11 A 860 -0.408 8.479 6.123 1.00 0.00 O +ATOM 861 H R12 A 861 0.397 8.335 5.547 1.00 0.00 H +ATOM 862 H R12 A 862 -1.000 8.353 5.328 1.00 0.00 H +ATOM 863 O R11 A 863 2.418 12.849 7.407 1.00 0.00 O +ATOM 864 H R12 A 864 2.564 13.320 8.277 1.00 0.00 H +ATOM 865 H R12 A 865 1.501 13.243 7.346 1.00 0.00 H +ATOM 866 O R11 A 866 7.140 13.180 -10.935 1.00 0.00 O +ATOM 867 H R12 A 867 7.084 14.156 -10.727 1.00 0.00 H +ATOM 868 H R12 A 868 8.025 13.325 -11.377 1.00 0.00 H +ATOM 869 O R11 A 869 3.944 19.829 4.792 1.00 0.00 O +ATOM 870 H R12 A 870 3.783 20.731 4.391 1.00 0.00 H +ATOM 871 H R12 A 871 3.767 19.403 3.904 1.00 0.00 H +ATOM 872 O R11 A 872 3.697 15.246 -9.972 1.00 0.00 O +ATOM 873 H R12 A 873 3.965 15.932 -10.648 1.00 0.00 H +ATOM 874 H R12 A 874 3.244 15.955 -9.432 1.00 0.00 H +ATOM 875 O R11 A 875 -2.713 13.378 -9.819 1.00 0.00 O +ATOM 876 H R12 A 876 -3.629 13.065 -9.567 1.00 0.00 H +ATOM 877 H R12 A 877 -2.320 12.816 -9.091 1.00 0.00 H +ATOM 878 O R11 A 878 5.142 12.012 9.109 1.00 0.00 O +ATOM 879 H R12 A 879 5.326 11.097 9.467 1.00 0.00 H +ATOM 880 H R12 A 880 4.376 12.107 9.745 1.00 0.00 H +ATOM 881 O R11 A 881 0.772 -2.780 1.385 1.00 0.00 O +ATOM 882 H R12 A 882 1.072 -3.734 1.399 1.00 0.00 H +ATOM 883 H R12 A 883 1.522 -2.534 1.998 1.00 0.00 H +ATOM 884 O R11 A 884 9.272 18.539 6.837 1.00 0.00 O +ATOM 885 H R12 A 885 8.504 18.773 7.433 1.00 0.00 H +ATOM 886 H R12 A 886 9.507 17.777 7.441 1.00 0.00 H +ATOM 887 O R11 A 887 9.656 18.517 -10.360 1.00 0.00 O +ATOM 888 H R12 A 888 10.341 18.940 -9.767 1.00 0.00 H +ATOM 889 H R12 A 889 9.416 19.417 -10.724 1.00 0.00 H +ATOM 890 O R11 A 890 -14.720 1.597 -3.089 1.00 0.00 O +ATOM 891 H R12 A 891 -14.776 2.574 -2.882 1.00 0.00 H +ATOM 892 H R12 A 892 -13.834 1.742 -3.531 1.00 0.00 H +ATOM 893 O R11 A 893 15.546 15.776 6.592 1.00 0.00 O +ATOM 894 H R12 A 894 15.033 16.569 6.263 1.00 0.00 H +ATOM 895 H R12 A 895 15.412 15.324 5.710 1.00 0.00 H +ATOM 896 O R11 A 896 11.747 27.397 3.249 1.00 0.00 O +ATOM 897 H R12 A 897 10.861 27.813 3.044 1.00 0.00 H +ATOM 898 H R12 A 898 11.335 26.894 4.008 1.00 0.00 H +ATOM 899 O R11 A 899 -16.868 7.030 -0.014 1.00 0.00 O +ATOM 900 H R12 A 900 -17.767 7.418 -0.217 1.00 0.00 H +ATOM 901 H R12 A 901 -16.480 7.521 -0.794 1.00 0.00 H +ATOM 902 O R11 A 902 -9.172 -3.533 3.953 1.00 0.00 O +ATOM 903 H R12 A 903 -8.417 -3.524 3.298 1.00 0.00 H +ATOM 904 H R12 A 904 -8.532 -3.755 4.689 1.00 0.00 H +ATOM 905 O R11 A 905 6.219 24.773 -7.442 1.00 0.00 O +ATOM 906 H R12 A 906 6.435 23.825 -7.678 1.00 0.00 H +ATOM 907 H R12 A 907 7.024 24.809 -6.849 1.00 0.00 H +ATOM 908 O R11 A 908 -0.178 10.806 -9.661 1.00 0.00 O +ATOM 909 H R12 A 909 -0.234 11.782 -9.454 1.00 0.00 H +ATOM 910 H R12 A 910 0.708 10.951 -10.103 1.00 0.00 H +ATOM 911 O R11 A 911 14.016 1.145 2.039 1.00 0.00 O +ATOM 912 H R12 A 912 14.961 1.178 2.365 1.00 0.00 H +ATOM 913 H R12 A 913 14.145 2.020 1.574 1.00 0.00 H +ATOM 914 O R11 A 914 -6.635 2.851 6.253 1.00 0.00 O +ATOM 915 H R12 A 915 -6.827 2.137 6.927 1.00 0.00 H +ATOM 916 H R12 A 916 -7.616 2.976 6.107 1.00 0.00 H +ATOM 917 O R11 A 917 25.355 20.594 0.515 1.00 0.00 O +ATOM 918 H R12 A 918 24.600 21.250 0.510 1.00 0.00 H +ATOM 919 H R12 A 919 25.456 20.719 1.502 1.00 0.00 H +ATOM 920 O R11 A 920 -7.053 -2.951 -4.307 1.00 0.00 O +ATOM 921 H R12 A 921 -6.664 -2.151 -3.850 1.00 0.00 H +ATOM 922 H R12 A 922 -7.962 -2.700 -3.974 1.00 0.00 H +ATOM 923 O R11 A 923 -5.405 -4.518 4.627 1.00 0.00 O +ATOM 924 H R12 A 924 -5.184 -3.580 4.361 1.00 0.00 H +ATOM 925 H R12 A 925 -5.102 -4.324 5.560 1.00 0.00 H +ATOM 926 O R11 A 926 8.776 1.033 -8.149 1.00 0.00 O +ATOM 927 H R12 A 927 8.218 0.858 -7.337 1.00 0.00 H +ATOM 928 H R12 A 928 8.918 1.976 -7.849 1.00 0.00 H +ATOM 929 O R11 A 929 23.356 18.376 -9.823 1.00 0.00 O +ATOM 930 H R12 A 930 22.785 19.190 -9.714 1.00 0.00 H +ATOM 931 H R12 A 931 22.544 17.838 -10.052 1.00 0.00 H +ATOM 932 O R11 A 932 15.886 6.451 7.314 1.00 0.00 O +ATOM 933 H R12 A 933 15.742 6.069 8.227 1.00 0.00 H +ATOM 934 H R12 A 934 16.304 7.264 7.720 1.00 0.00 H +ATOM 935 O R11 A 935 -0.668 -2.477 8.134 1.00 0.00 O +ATOM 936 H R12 A 936 0.307 -2.301 7.997 1.00 0.00 H +ATOM 937 H R12 A 937 -0.794 -1.537 8.449 1.00 0.00 H +ATOM 938 O R11 A 938 5.943 4.917 -10.041 1.00 0.00 O +ATOM 939 H R12 A 939 5.465 4.042 -9.955 1.00 0.00 H +ATOM 940 H R12 A 940 5.661 5.162 -9.114 1.00 0.00 H +ATOM 941 O R11 A 941 -9.767 11.308 2.197 1.00 0.00 O +ATOM 942 H R12 A 942 -8.843 10.935 2.110 1.00 0.00 H +ATOM 943 H R12 A 943 -9.501 12.098 1.645 1.00 0.00 H +ATOM 944 O R11 A 944 1.805 -5.676 -5.274 1.00 0.00 O +ATOM 945 H R12 A 945 1.869 -4.679 -5.333 1.00 0.00 H +ATOM 946 H R12 A 946 2.297 -5.655 -4.403 1.00 0.00 H +ATOM 947 O R11 A 947 -0.114 4.597 7.551 1.00 0.00 O +ATOM 948 H R12 A 948 -0.073 4.140 6.662 1.00 0.00 H +ATOM 949 H R12 A 949 -0.501 5.409 7.115 1.00 0.00 H +ATOM 950 O R11 A 950 -12.601 -4.064 2.426 1.00 0.00 O +ATOM 951 H R12 A 951 -12.926 -3.461 3.155 1.00 0.00 H +ATOM 952 H R12 A 952 -12.232 -3.273 1.937 1.00 0.00 H +ATOM 953 O R11 A 953 6.050 23.534 -3.865 1.00 0.00 O +ATOM 954 H R12 A 954 6.516 22.949 -3.201 1.00 0.00 H +ATOM 955 H R12 A 955 6.563 24.323 -3.529 1.00 0.00 H +ATOM 956 O R11 A 956 14.177 1.125 6.267 1.00 0.00 O +ATOM 957 H R12 A 957 15.120 1.342 6.014 1.00 0.00 H +ATOM 958 H R12 A 958 14.027 2.080 6.524 1.00 0.00 H +ATOM 959 O R11 A 959 -9.264 -7.303 -3.110 1.00 0.00 O +ATOM 960 H R12 A 960 -9.864 -6.846 -3.766 1.00 0.00 H +ATOM 961 H R12 A 961 -8.807 -7.781 -3.861 1.00 0.00 H +ATOM 962 O R11 A 962 -3.856 -0.781 9.340 1.00 0.00 O +ATOM 963 H R12 A 963 -2.971 -0.975 9.765 1.00 0.00 H +ATOM 964 H R12 A 964 -4.312 -1.334 10.037 1.00 0.00 H +ATOM 965 O R11 A 965 15.839 25.063 -10.776 1.00 0.00 O +ATOM 966 H R12 A 966 16.594 24.411 -10.843 1.00 0.00 H +ATOM 967 H R12 A 967 15.216 24.380 -11.157 1.00 0.00 H +ATOM 968 O R11 A 968 -9.284 14.304 3.533 1.00 0.00 O +ATOM 969 H R12 A 969 -9.252 15.041 4.208 1.00 0.00 H +ATOM 970 H R12 A 970 -9.101 14.964 2.804 1.00 0.00 H +ATOM 971 O R11 A 971 11.956 29.896 -2.587 1.00 0.00 O +ATOM 972 H R12 A 972 11.973 29.251 -1.823 1.00 0.00 H +ATOM 973 H R12 A 973 12.097 29.140 -3.227 1.00 0.00 H +ATOM 974 O R11 A 974 -18.127 3.096 0.670 1.00 0.00 O +ATOM 975 H R12 A 975 -17.199 3.317 0.971 1.00 0.00 H +ATOM 976 H R12 A 976 -18.399 4.048 0.809 1.00 0.00 H +ATOM 977 O R11 A 977 16.777 8.507 -8.211 1.00 0.00 O +ATOM 978 H R12 A 978 17.194 7.791 -8.770 1.00 0.00 H +ATOM 979 H R12 A 979 17.044 8.016 -7.382 1.00 0.00 H +ATOM 980 O R11 A 980 6.758 17.564 -9.876 1.00 0.00 O +ATOM 981 H R12 A 981 7.419 16.848 -10.100 1.00 0.00 H +ATOM 982 H R12 A 982 6.636 17.166 -8.966 1.00 0.00 H +ATOM 983 O R11 A 983 21.145 24.868 3.448 1.00 0.00 O +ATOM 984 H R12 A 984 21.814 24.369 3.998 1.00 0.00 H +ATOM 985 H R12 A 985 20.712 25.208 4.282 1.00 0.00 H +ATOM 986 O R11 A 986 21.318 18.182 7.416 1.00 0.00 O +ATOM 987 H R12 A 987 20.580 18.652 6.933 1.00 0.00 H +ATOM 988 H R12 A 988 21.788 18.025 6.547 1.00 0.00 H +ATOM 989 O R11 A 989 -15.642 -1.544 3.929 1.00 0.00 O +ATOM 990 H R12 A 990 -15.142 -0.865 3.391 1.00 0.00 H +ATOM 991 H R12 A 991 -15.665 -2.154 3.137 1.00 0.00 H +ATOM 992 O R11 A 992 23.435 20.698 -6.140 1.00 0.00 O +ATOM 993 H R12 A 993 23.656 21.002 -5.214 1.00 0.00 H +ATOM 994 H R12 A 994 24.373 20.892 -6.428 1.00 0.00 H +ATOM 995 O R11 A 995 4.643 -3.573 1.092 1.00 0.00 O +ATOM 996 H R12 A 996 4.213 -4.173 1.767 1.00 0.00 H +ATOM 997 H R12 A 997 4.019 -2.835 1.349 1.00 0.00 H +ATOM 998 O R11 A 998 6.531 -3.887 -1.912 1.00 0.00 O +ATOM 999 H R12 A 999 5.671 -3.755 -1.418 1.00 0.00 H +ATOM 1000 H R12 A1000 6.178 -3.339 -2.671 1.00 0.00 H +ATOM 1001 O R11 A1001 0.919 3.291 -9.208 1.00 0.00 O +ATOM 1002 H R12 A1002 0.846 2.897 -10.124 1.00 0.00 H +ATOM 1003 H R12 A1003 0.988 4.205 -9.607 1.00 0.00 H +ATOM 1004 O R11 A1004 -8.189 6.972 4.699 1.00 0.00 O +ATOM 1005 H R12 A1005 -7.681 6.119 4.816 1.00 0.00 H +ATOM 1006 H R12 A1006 -8.793 6.716 5.454 1.00 0.00 H +ATOM 1007 O R11 A1007 10.659 12.128 -10.165 1.00 0.00 O +ATOM 1008 H R12 A1008 9.998 11.415 -9.934 1.00 0.00 H +ATOM 1009 H R12 A1009 10.977 12.141 -9.217 1.00 0.00 H +ATOM 1010 O R11 A1010 -17.612 9.850 1.194 1.00 0.00 O +ATOM 1011 H R12 A1011 -17.421 10.684 0.675 1.00 0.00 H +ATOM 1012 H R12 A1012 -18.159 10.380 1.843 1.00 0.00 H +ATOM 1013 O R11 A1013 20.947 19.038 -3.345 1.00 0.00 O +ATOM 1014 H R12 A1014 21.920 18.853 -3.488 1.00 0.00 H +ATOM 1015 H R12 A1015 20.722 18.450 -4.123 1.00 0.00 H +ATOM 1016 O R11 A1016 -15.361 15.306 -2.863 1.00 0.00 O +ATOM 1017 H R12 A1017 -15.585 14.929 -3.762 1.00 0.00 H +ATOM 1018 H R12 A1018 -14.571 14.695 -2.803 1.00 0.00 H +ATOM 1019 O R11 A1019 -8.712 10.697 -5.687 1.00 0.00 O +ATOM 1020 H R12 A1020 -8.444 11.383 -6.364 1.00 0.00 H +ATOM 1021 H R12 A1021 -9.165 11.406 -5.147 1.00 0.00 H +ATOM 1022 O R11 A1022 18.548 20.034 10.045 1.00 0.00 O +ATOM 1023 H R12 A1023 18.485 20.049 11.043 1.00 0.00 H +ATOM 1024 H R12 A1024 18.287 20.999 10.015 1.00 0.00 H +ATOM 1025 O R11 A1025 4.208 21.165 -3.124 1.00 0.00 O +ATOM 1026 H R12 A1026 3.577 20.395 -3.214 1.00 0.00 H +ATOM 1027 H R12 A1027 4.402 21.120 -4.104 1.00 0.00 H +ATOM 1028 O R11 A1028 -11.051 -1.004 -9.132 1.00 0.00 O +ATOM 1029 H R12 A1029 -10.601 -1.173 -8.255 1.00 0.00 H +ATOM 1030 H R12 A1030 -11.376 -0.120 -8.795 1.00 0.00 H +ATOM 1031 O R11 A1031 20.367 8.994 4.816 1.00 0.00 O +ATOM 1032 H R12 A1032 21.072 9.225 5.487 1.00 0.00 H +ATOM 1033 H R12 A1033 20.897 9.452 4.102 1.00 0.00 H +ATOM 1034 O R11 A1034 12.634 -1.131 0.296 1.00 0.00 O +ATOM 1035 H R12 A1035 11.783 -0.606 0.308 1.00 0.00 H +ATOM 1036 H R12 A1036 12.123 -1.965 0.502 1.00 0.00 H +ATOM 1037 O R11 A1037 18.012 7.532 -2.545 1.00 0.00 O +ATOM 1038 H R12 A1038 17.633 8.094 -1.809 1.00 0.00 H +ATOM 1039 H R12 A1039 17.106 7.146 -2.716 1.00 0.00 H +ATOM 1040 O R11 A1040 -1.545 1.806 8.823 1.00 0.00 O +ATOM 1041 H R12 A1041 -0.611 1.538 8.588 1.00 0.00 H +ATOM 1042 H R12 A1042 -1.617 2.311 7.963 1.00 0.00 H +ATOM 1043 O R11 A1043 -6.561 -7.044 1.085 1.00 0.00 O +ATOM 1044 H R12 A1044 -5.926 -6.279 0.980 1.00 0.00 H +ATOM 1045 H R12 A1045 -7.307 -6.401 1.256 1.00 0.00 H +ATOM 1046 O R11 A1046 -1.983 2.543 -12.120 1.00 0.00 O +ATOM 1047 H R12 A1047 -1.818 3.529 -12.095 1.00 0.00 H +ATOM 1048 H R12 A1048 -1.526 2.443 -11.237 1.00 0.00 H +ATOM 1049 O R11 A1049 -12.584 10.136 -5.844 1.00 0.00 O +ATOM 1050 H R12 A1050 -11.904 10.058 -5.114 1.00 0.00 H +ATOM 1051 H R12 A1051 -11.895 10.542 -6.443 1.00 0.00 H +ATOM 1052 O R11 A1052 13.481 3.822 9.561 1.00 0.00 O +ATOM 1053 H R12 A1053 13.522 3.365 8.673 1.00 0.00 H +ATOM 1054 H R12 A1054 13.094 4.634 9.125 1.00 0.00 H +ATOM 1055 O R11 A1055 -7.660 -7.065 4.190 1.00 0.00 O +ATOM 1056 H R12 A1056 -6.681 -6.863 4.222 1.00 0.00 H +ATOM 1057 H R12 A1057 -7.461 -7.969 3.813 1.00 0.00 H +ATOM 1058 O R11 A1058 3.128 6.718 -12.643 1.00 0.00 O +ATOM 1059 H R12 A1059 3.836 7.173 -12.104 1.00 0.00 H +ATOM 1060 H R12 A1060 2.977 6.070 -11.897 1.00 0.00 H +ATOM 1061 O R11 A1061 20.001 22.142 6.814 1.00 0.00 O +ATOM 1062 H R12 A1062 19.711 22.974 7.287 1.00 0.00 H +ATOM 1063 H R12 A1063 20.372 22.696 6.068 1.00 0.00 H +ATOM 1064 O R11 A1064 9.893 13.860 11.400 1.00 0.00 O +ATOM 1065 H R12 A1065 10.077 12.945 11.758 1.00 0.00 H +ATOM 1066 H R12 A1066 9.128 13.955 12.036 1.00 0.00 H +ATOM 1067 O R11 A1067 8.454 30.715 -3.311 1.00 0.00 O +ATOM 1068 H R12 A1068 8.965 30.676 -2.452 1.00 0.00 H +ATOM 1069 H R12 A1069 7.789 30.064 -2.945 1.00 0.00 H +ATOM 1070 O R11 A1070 26.597 16.712 -4.329 1.00 0.00 O +ATOM 1071 H R12 A1071 25.852 16.781 -4.993 1.00 0.00 H +ATOM 1072 H R12 A1072 26.667 15.731 -4.511 1.00 0.00 H +ATOM 1073 O R11 A1073 16.613 6.441 -5.248 1.00 0.00 O +ATOM 1074 H R12 A1074 17.531 6.831 -5.177 1.00 0.00 H +ATOM 1075 H R12 A1075 16.879 5.701 -4.631 1.00 0.00 H +ATOM 1076 O R11 A1076 16.152 17.896 9.310 1.00 0.00 O +ATOM 1077 H R12 A1077 15.517 17.550 8.619 1.00 0.00 H +ATOM 1078 H R12 A1078 16.626 18.428 8.608 1.00 0.00 H +ATOM 1079 O R11 A1079 17.090 24.210 4.215 1.00 0.00 O +ATOM 1080 H R12 A1080 16.647 23.940 5.070 1.00 0.00 H +ATOM 1081 H R12 A1081 17.921 24.443 4.720 1.00 0.00 H +ATOM 1082 O R11 A1082 -4.278 13.432 5.463 1.00 0.00 O +ATOM 1083 H R12 A1083 -4.502 14.222 4.892 1.00 0.00 H +ATOM 1084 H R12 A1084 -4.655 12.821 4.767 1.00 0.00 H +ATOM 1085 O R11 A1085 -17.575 13.521 3.271 1.00 0.00 O +ATOM 1086 H R12 A1086 -16.600 13.673 3.432 1.00 0.00 H +ATOM 1087 H R12 A1087 -17.509 12.627 3.713 1.00 0.00 H +ATOM 1088 O R11 A1088 3.127 11.907 -11.254 1.00 0.00 O +ATOM 1089 H R12 A1089 3.083 12.309 -12.169 1.00 0.00 H +ATOM 1090 H R12 A1090 3.201 12.822 -10.856 1.00 0.00 H +ATOM 1091 O R11 A1091 -6.967 -4.388 -1.064 1.00 0.00 O +ATOM 1092 H R12 A1092 -6.712 -4.053 -0.157 1.00 0.00 H +ATOM 1093 H R12 A1093 -6.007 -4.369 -1.342 1.00 0.00 H +ATOM 1094 O R11 A1094 -7.308 -0.977 -10.068 1.00 0.00 O +ATOM 1095 H R12 A1095 -7.153 -1.538 -10.881 1.00 0.00 H +ATOM 1096 H R12 A1096 -8.291 -0.976 -10.256 1.00 0.00 H +ATOM 1097 O R11 A1097 1.305 0.861 6.440 1.00 0.00 O +ATOM 1098 H R12 A1098 2.165 0.356 6.517 1.00 0.00 H +ATOM 1099 H R12 A1099 1.277 0.966 7.434 1.00 0.00 H +ATOM 1100 O R11 A1100 -6.552 0.219 11.905 1.00 0.00 O +ATOM 1101 H R12 A1101 -5.748 0.075 11.328 1.00 0.00 H +ATOM 1102 H R12 A1102 -7.144 0.093 11.109 1.00 0.00 H +ATOM 1103 O R11 A1103 -3.707 8.062 5.128 1.00 0.00 O +ATOM 1104 H R12 A1104 -3.322 8.096 4.205 1.00 0.00 H +ATOM 1105 H R12 A1105 -4.349 7.355 4.833 1.00 0.00 H +ATOM 1106 O R11 A1106 -4.581 2.653 9.933 1.00 0.00 O +ATOM 1107 H R12 A1107 -4.948 1.726 9.851 1.00 0.00 H +ATOM 1108 H R12 A1108 -4.619 2.756 8.939 1.00 0.00 H +ATOM 1109 O R11 A1109 3.098 14.240 0.883 1.00 0.00 O +ATOM 1110 H R12 A1110 2.574 14.372 1.724 1.00 0.00 H +ATOM 1111 H R12 A1111 2.251 14.261 0.351 1.00 0.00 H +ATOM 1112 O R11 A1112 10.141 -2.849 -2.604 1.00 0.00 O +ATOM 1113 H R12 A1113 10.753 -2.882 -3.394 1.00 0.00 H +ATOM 1114 H R12 A1114 10.533 -1.969 -2.337 1.00 0.00 H +ATOM 1115 O R11 A1115 12.150 26.885 7.208 1.00 0.00 O +ATOM 1116 H R12 A1116 12.014 26.864 6.218 1.00 0.00 H +ATOM 1117 H R12 A1117 12.129 25.886 7.233 1.00 0.00 H +ATOM 1118 O R11 A1118 -12.323 -1.403 4.769 1.00 0.00 O +ATOM 1119 H R12 A1119 -12.436 -0.411 4.699 1.00 0.00 H +ATOM 1120 H R12 A1120 -12.004 -1.300 5.711 1.00 0.00 H +ATOM 1121 O R11 A1121 7.256 -3.647 2.961 1.00 0.00 O +ATOM 1122 H R12 A1122 7.143 -3.329 3.902 1.00 0.00 H +ATOM 1123 H R12 A1123 8.247 -3.544 3.045 1.00 0.00 H +ATOM 1124 O R11 A1124 12.380 6.184 -8.828 1.00 0.00 O +ATOM 1125 H R12 A1125 13.225 5.841 -9.238 1.00 0.00 H +ATOM 1126 H R12 A1126 12.876 6.970 -8.462 1.00 0.00 H +ATOM 1127 O R11 A1127 -8.171 5.894 -12.457 1.00 0.00 O +ATOM 1128 H R12 A1128 -7.303 5.407 -12.364 1.00 0.00 H +ATOM 1129 H R12 A1129 -8.005 6.358 -11.587 1.00 0.00 H +ATOM 1130 O R11 A1130 -3.744 7.372 8.128 1.00 0.00 O +ATOM 1131 H R12 A1131 -4.440 6.670 8.278 1.00 0.00 H +ATOM 1132 H R12 A1132 -3.026 6.687 8.254 1.00 0.00 H +ATOM 1133 O R11 A1133 21.231 23.881 -6.897 1.00 0.00 O +ATOM 1134 H R12 A1134 21.570 24.291 -6.049 1.00 0.00 H +ATOM 1135 H R12 A1135 20.371 23.651 -6.441 1.00 0.00 H +ATOM 1136 O R11 A1136 -11.355 11.033 -9.133 1.00 0.00 O +ATOM 1137 H R12 A1137 -10.890 10.448 -8.469 1.00 0.00 H +ATOM 1138 H R12 A1138 -10.842 11.823 -8.797 1.00 0.00 H +ATOM 1139 O R11 A1139 2.797 2.149 9.693 1.00 0.00 O +ATOM 1140 H R12 A1140 2.446 1.220 9.814 1.00 0.00 H +ATOM 1141 H R12 A1141 2.826 2.010 8.703 1.00 0.00 H +ATOM 1142 O R11 A1142 -16.895 -0.264 -2.024 1.00 0.00 O +ATOM 1143 H R12 A1143 -17.573 -0.450 -1.313 1.00 0.00 H +ATOM 1144 H R12 A1144 -16.364 0.281 -1.376 1.00 0.00 H +ATOM 1145 O R11 A1145 -5.211 16.889 -1.235 1.00 0.00 O +ATOM 1146 H R12 A1146 -4.369 17.421 -1.318 1.00 0.00 H +ATOM 1147 H R12 A1147 -5.750 17.723 -1.352 1.00 0.00 H +ATOM 1148 O R11 A1148 -14.948 11.415 -3.410 1.00 0.00 O +ATOM 1149 H R12 A1149 -14.974 11.038 -2.484 1.00 0.00 H +ATOM 1150 H R12 A1150 -14.147 10.852 -3.616 1.00 0.00 H +ATOM 1151 O R11 A1151 -8.504 8.140 -9.359 1.00 0.00 O +ATOM 1152 H R12 A1152 -8.548 8.542 -10.274 1.00 0.00 H +ATOM 1153 H R12 A1153 -8.430 9.055 -8.961 1.00 0.00 H +ATOM 1154 O R11 A1154 -17.178 3.095 -7.521 1.00 0.00 O +ATOM 1155 H R12 A1155 -16.191 3.093 -7.687 1.00 0.00 H +ATOM 1156 H R12 A1156 -17.196 4.088 -7.640 1.00 0.00 H +ATOM 1157 O R11 A1157 8.458 -0.002 -4.159 1.00 0.00 O +ATOM 1158 H R12 A1158 8.697 -0.921 -4.472 1.00 0.00 H +ATOM 1159 H R12 A1159 9.211 0.377 -4.697 1.00 0.00 H +ATOM 1160 O R11 A1160 -0.850 -4.399 -9.622 1.00 0.00 O +ATOM 1161 H R12 A1161 -0.949 -4.511 -10.611 1.00 0.00 H +ATOM 1162 H R12 A1162 -0.113 -3.739 -9.771 1.00 0.00 H +ATOM 1163 O R11 A1163 -10.527 -5.429 6.218 1.00 0.00 O +ATOM 1164 H R12 A1164 -9.858 -5.862 5.614 1.00 0.00 H +ATOM 1165 H R12 A1165 -11.026 -5.089 5.420 1.00 0.00 H +ATOM 1166 O R11 A1166 -13.120 13.261 4.412 1.00 0.00 O +ATOM 1167 H R12 A1167 -13.691 12.450 4.285 1.00 0.00 H +ATOM 1168 H R12 A1168 -12.306 12.723 4.193 1.00 0.00 H +ATOM 1169 O R11 A1169 2.901 1.972 -11.738 1.00 0.00 O +ATOM 1170 H R12 A1170 3.890 2.003 -11.882 1.00 0.00 H +ATOM 1171 H R12 A1171 3.042 2.062 -10.752 1.00 0.00 H +ATOM 1172 O R11 A1172 26.705 19.363 -7.926 1.00 0.00 O +ATOM 1173 H R12 A1173 26.145 20.049 -7.460 1.00 0.00 H +ATOM 1174 H R12 A1174 25.947 18.712 -7.878 1.00 0.00 H +ATOM 1175 O R11 A1175 -15.119 5.952 4.481 1.00 0.00 O +ATOM 1176 H R12 A1176 -14.381 5.913 5.154 1.00 0.00 H +ATOM 1177 H R12 A1177 -15.103 6.951 4.521 1.00 0.00 H +ATOM 1178 O R11 A1178 -1.120 11.461 6.170 1.00 0.00 O +ATOM 1179 H R12 A1179 -1.183 11.475 7.168 1.00 0.00 H +ATOM 1180 H R12 A1180 -1.381 12.426 6.139 1.00 0.00 H +ATOM 1181 O R11 A1181 9.230 26.811 -5.215 1.00 0.00 O +ATOM 1182 H R12 A1182 9.247 26.167 -4.451 1.00 0.00 H +ATOM 1183 H R12 A1183 9.372 26.056 -5.855 1.00 0.00 H +ATOM 1184 O R11 A1184 2.952 9.735 8.899 1.00 0.00 O +ATOM 1185 H R12 A1185 2.965 8.748 8.740 1.00 0.00 H +ATOM 1186 H R12 A1186 1.965 9.697 9.055 1.00 0.00 H +ATOM 1187 O R11 A1187 24.190 23.451 -8.445 1.00 0.00 O +ATOM 1188 H R12 A1188 24.107 23.679 -7.475 1.00 0.00 H +ATOM 1189 H R12 A1189 23.749 22.569 -8.275 1.00 0.00 H +ATOM 1190 O R11 A1190 13.191 26.769 -9.254 1.00 0.00 O +ATOM 1191 H R12 A1191 12.305 26.357 -9.042 1.00 0.00 H +ATOM 1192 H R12 A1192 13.607 26.266 -8.497 1.00 0.00 H +ATOM 1193 O R11 A1193 -9.164 3.592 8.495 1.00 0.00 O +ATOM 1194 H R12 A1194 -9.735 2.781 8.368 1.00 0.00 H +ATOM 1195 H R12 A1195 -8.350 3.054 8.276 1.00 0.00 H +ATOM 1196 O R11 A1196 -3.514 6.939 -12.655 1.00 0.00 O +ATOM 1197 H R12 A1197 -3.572 6.957 -11.657 1.00 0.00 H +ATOM 1198 H R12 A1198 -3.252 7.904 -12.657 1.00 0.00 H +ATOM 1199 O R11 A1199 10.497 23.461 -12.332 1.00 0.00 O +ATOM 1200 H R12 A1200 10.962 22.876 -11.668 1.00 0.00 H +ATOM 1201 H R12 A1201 11.010 24.251 -11.996 1.00 0.00 H +ATOM 1202 O R11 A1202 7.788 24.667 5.801 1.00 0.00 O +ATOM 1203 H R12 A1203 7.841 25.128 4.915 1.00 0.00 H +ATOM 1204 H R12 A1204 6.972 24.175 5.496 1.00 0.00 H +ATOM 1205 O R11 A1205 21.398 30.807 -3.525 1.00 0.00 O +ATOM 1206 H R12 A1206 21.849 31.627 -3.173 1.00 0.00 H +ATOM 1207 H R12 A1207 20.511 31.174 -3.244 1.00 0.00 H +ATOM 1208 O R11 A1208 23.819 11.736 4.052 1.00 0.00 O +ATOM 1209 H R12 A1209 23.791 12.624 3.593 1.00 0.00 H +ATOM 1210 H R12 A1210 22.848 11.603 3.854 1.00 0.00 H +ATOM 1211 O R11 A1211 -1.387 15.506 -6.964 1.00 0.00 O +ATOM 1212 H R12 A1212 -0.442 15.636 -7.264 1.00 0.00 H +ATOM 1213 H R12 A1213 -1.354 16.382 -6.482 1.00 0.00 H +ATOM 1214 O R11 A1214 -6.369 -7.036 -5.286 1.00 0.00 O +ATOM 1215 H R12 A1215 -6.799 -6.782 -6.152 1.00 0.00 H +ATOM 1216 H R12 A1216 -5.962 -6.125 -5.219 1.00 0.00 H +ATOM 1217 O R11 A1217 -17.701 7.670 6.051 1.00 0.00 O +ATOM 1218 H R12 A1218 -17.972 6.752 5.762 1.00 0.00 H +ATOM 1219 H R12 A1219 -17.370 7.863 5.127 1.00 0.00 H +ATOM 1220 O R11 A1220 24.630 25.021 -1.450 1.00 0.00 O +ATOM 1221 H R12 A1221 23.969 25.338 -2.129 1.00 0.00 H +ATOM 1222 H R12 A1222 23.917 25.034 -0.749 1.00 0.00 H +ATOM 1223 O R11 A1223 -13.771 9.083 6.627 1.00 0.00 O +ATOM 1224 H R12 A1224 -12.803 8.855 6.723 1.00 0.00 H +ATOM 1225 H R12 A1225 -13.916 8.249 6.095 1.00 0.00 H +ATOM 1226 O R11 A1226 5.882 20.389 -6.215 1.00 0.00 O +ATOM 1227 H R12 A1227 6.029 21.236 -5.704 1.00 0.00 H +ATOM 1228 H R12 A1228 6.729 20.015 -5.838 1.00 0.00 H +ATOM 1229 O R11 A1229 12.167 21.748 5.962 1.00 0.00 O +ATOM 1230 H R12 A1230 11.943 21.132 5.206 1.00 0.00 H +ATOM 1231 H R12 A1231 11.216 21.716 6.270 1.00 0.00 H +ATOM 1232 O R11 A1232 0.363 4.267 -13.864 1.00 0.00 O +ATOM 1233 H R12 A1233 0.001 3.648 -14.561 1.00 0.00 H +ATOM 1234 H R12 A1234 -0.071 5.041 -14.327 1.00 0.00 H +ATOM 1235 O R11 A1235 -1.404 -6.336 -5.088 1.00 0.00 O +ATOM 1236 H R12 A1236 -1.919 -7.055 -5.555 1.00 0.00 H +ATOM 1237 H R12 A1237 -1.843 -5.647 -5.665 1.00 0.00 H +ATOM 1238 O R11 A1238 -9.530 0.712 -12.048 1.00 0.00 O +ATOM 1239 H R12 A1239 -9.892 0.279 -11.222 1.00 0.00 H +ATOM 1240 H R12 A1240 -10.149 1.486 -11.913 1.00 0.00 H +ATOM 1241 O R11 A1241 26.035 21.535 -2.435 1.00 0.00 O +ATOM 1242 H R12 A1242 27.014 21.653 -2.601 1.00 0.00 H +ATOM 1243 H R12 A1243 25.915 22.528 -2.439 1.00 0.00 H +ATOM 1244 O R11 A1244 4.000 23.741 5.548 1.00 0.00 O +ATOM 1245 H R12 A1245 4.451 22.855 5.650 1.00 0.00 H +ATOM 1246 H R12 A1246 4.820 24.109 5.110 1.00 0.00 H +ATOM 1247 O R11 A1247 5.711 8.927 11.140 1.00 0.00 O +ATOM 1248 H R12 A1248 6.522 9.496 11.275 1.00 0.00 H +ATOM 1249 H R12 A1249 6.287 8.190 10.786 1.00 0.00 H +ATOM 1250 O R11 A1250 -18.350 3.655 -3.456 1.00 0.00 O +ATOM 1251 H R12 A1251 -18.918 3.388 -2.678 1.00 0.00 H +ATOM 1252 H R12 A1252 -17.537 3.327 -2.976 1.00 0.00 H +ATOM 1253 O R11 A1253 18.723 9.474 0.577 1.00 0.00 O +ATOM 1254 H R12 A1254 17.914 9.276 1.131 1.00 0.00 H +ATOM 1255 H R12 A1255 18.238 10.233 0.142 1.00 0.00 H +ATOM 1256 O R11 A1256 10.492 1.406 8.005 1.00 0.00 O +ATOM 1257 H R12 A1257 9.645 1.684 8.458 1.00 0.00 H +ATOM 1258 H R12 A1258 10.769 2.365 7.937 1.00 0.00 H +ATOM 1259 O R11 A1259 18.556 5.898 3.649 1.00 0.00 O +ATOM 1260 H R12 A1260 18.204 6.219 4.528 1.00 0.00 H +ATOM 1261 H R12 A1261 17.631 5.917 3.270 1.00 0.00 H +ATOM 1262 O R11 A1262 2.784 -2.907 5.675 1.00 0.00 O +ATOM 1263 H R12 A1263 3.727 -2.690 5.423 1.00 0.00 H +ATOM 1264 H R12 A1264 2.634 -1.953 5.933 1.00 0.00 H +ATOM 1265 O R11 A1265 5.231 9.088 -10.712 1.00 0.00 O +ATOM 1266 H R12 A1266 6.051 9.326 -11.233 1.00 0.00 H +ATOM 1267 H R12 A1267 5.752 8.404 -10.203 1.00 0.00 H +ATOM 1268 O R11 A1268 -14.532 2.441 5.574 1.00 0.00 O +ATOM 1269 H R12 A1269 -13.765 2.506 6.211 1.00 0.00 H +ATOM 1270 H R12 A1270 -13.960 2.819 4.846 1.00 0.00 H +ATOM 1271 O R11 A1271 -13.968 -5.831 -0.135 1.00 0.00 O +ATOM 1272 H R12 A1272 -13.041 -5.610 0.166 1.00 0.00 H +ATOM 1273 H R12 A1273 -14.240 -4.879 0.003 1.00 0.00 H +ATOM 1274 O R11 A1274 -20.608 5.671 -0.740 1.00 0.00 O +ATOM 1275 H R12 A1275 -20.752 5.810 0.239 1.00 0.00 H +ATOM 1276 H R12 A1276 -19.941 4.953 -0.541 1.00 0.00 H +ATOM 1277 O R11 A1277 6.454 27.444 3.765 1.00 0.00 O +ATOM 1278 H R12 A1278 7.399 27.574 3.465 1.00 0.00 H +ATOM 1279 H R12 A1279 6.487 28.319 4.247 1.00 0.00 H +ATOM 1280 O R11 A1280 11.968 13.540 8.066 1.00 0.00 O +ATOM 1281 H R12 A1281 12.621 13.438 8.817 1.00 0.00 H +ATOM 1282 H R12 A1282 12.694 13.914 7.488 1.00 0.00 H +ATOM 1283 O R11 A1283 13.772 19.712 -12.184 1.00 0.00 O +ATOM 1284 H R12 A1284 13.347 18.964 -12.694 1.00 0.00 H +ATOM 1285 H R12 A1285 14.654 19.242 -12.231 1.00 0.00 H +ATOM 1286 O R11 A1286 -11.930 13.633 -0.346 1.00 0.00 O +ATOM 1287 H R12 A1287 -11.176 14.250 -0.122 1.00 0.00 H +ATOM 1288 H R12 A1288 -11.333 13.131 -0.972 1.00 0.00 H +ATOM 1289 O R11 A1289 -4.295 2.736 -14.169 1.00 0.00 O +ATOM 1290 H R12 A1290 -4.190 3.698 -13.917 1.00 0.00 H +ATOM 1291 H R12 A1291 -4.371 2.491 -13.203 1.00 0.00 H +ATOM 1292 O R11 A1292 20.962 11.677 -7.035 1.00 0.00 O +ATOM 1293 H R12 A1293 20.270 12.192 -7.540 1.00 0.00 H +ATOM 1294 H R12 A1294 20.293 11.478 -6.320 1.00 0.00 H +ATOM 1295 O R11 A1295 11.006 23.722 9.356 1.00 0.00 O +ATOM 1296 H R12 A1296 11.166 24.565 8.843 1.00 0.00 H +ATOM 1297 H R12 A1297 11.849 23.335 8.983 1.00 0.00 H +ATOM 1298 O R11 A1298 20.067 16.542 -11.094 1.00 0.00 O +ATOM 1299 H R12 A1299 19.809 15.874 -10.396 1.00 0.00 H +ATOM 1300 H R12 A1300 20.949 16.083 -11.207 1.00 0.00 H +ATOM 1301 O R11 A1301 14.542 30.372 -0.255 1.00 0.00 O +ATOM 1302 H R12 A1302 15.450 29.971 -0.139 1.00 0.00 H +ATOM 1303 H R12 A1303 14.790 31.113 0.369 1.00 0.00 H +ATOM 1304 O R11 A1304 -15.189 9.463 -7.913 1.00 0.00 O +ATOM 1305 H R12 A1305 -15.157 9.646 -8.895 1.00 0.00 H +ATOM 1306 H R12 A1306 -14.451 10.122 -7.767 1.00 0.00 H +ATOM 1307 O R11 A1307 -2.698 -7.732 -2.095 1.00 0.00 O +ATOM 1308 H R12 A1308 -1.719 -7.530 -2.064 1.00 0.00 H +ATOM 1309 H R12 A1309 -2.499 -8.637 -2.473 1.00 0.00 H +ATOM 1310 O R11 A1310 0.711 -5.122 3.973 1.00 0.00 O +ATOM 1311 H R12 A1311 0.368 -5.406 4.868 1.00 0.00 H +ATOM 1312 H R12 A1312 -0.005 -4.426 3.920 1.00 0.00 H +ATOM 1313 O R11 A1313 9.569 5.214 -10.906 1.00 0.00 O +ATOM 1314 H R12 A1314 8.910 4.755 -10.311 1.00 0.00 H +ATOM 1315 H R12 A1315 9.503 6.038 -10.344 1.00 0.00 H +ATOM 1316 O R11 A1316 4.968 17.824 7.538 1.00 0.00 O +ATOM 1317 H R12 A1317 5.737 18.380 7.222 1.00 0.00 H +ATOM 1318 H R12 A1318 5.228 17.101 6.898 1.00 0.00 H +ATOM 1319 O R11 A1319 -5.647 9.636 -11.960 1.00 0.00 O +ATOM 1320 H R12 A1320 -4.708 9.806 -12.259 1.00 0.00 H +ATOM 1321 H R12 A1321 -5.966 10.389 -12.536 1.00 0.00 H +ATOM 1322 O R11 A1322 19.567 14.122 8.247 1.00 0.00 O +ATOM 1323 H R12 A1323 20.308 13.989 7.588 1.00 0.00 H +ATOM 1324 H R12 A1324 19.543 15.096 8.023 1.00 0.00 H +ATOM 1325 O R11 A1325 -19.023 4.978 3.167 1.00 0.00 O +ATOM 1326 H R12 A1326 -19.581 5.496 2.518 1.00 0.00 H +ATOM 1327 H R12 A1327 -19.197 5.670 3.868 1.00 0.00 H +ATOM 1328 O R11 A1328 24.680 13.825 -1.100 1.00 0.00 O +ATOM 1329 H R12 A1329 24.220 14.405 -0.428 1.00 0.00 H +ATOM 1330 H R12 A1330 24.853 14.626 -1.673 1.00 0.00 H +ATOM 1331 O R11 A1331 -9.868 10.237 5.731 1.00 0.00 O +ATOM 1332 H R12 A1332 -9.325 9.564 5.228 1.00 0.00 H +ATOM 1333 H R12 A1333 -9.988 10.767 4.891 1.00 0.00 H +ATOM 1334 O R11 A1334 9.387 10.261 12.148 1.00 0.00 O +ATOM 1335 H R12 A1335 8.739 10.665 11.503 1.00 0.00 H +ATOM 1336 H R12 A1336 8.709 10.338 12.879 1.00 0.00 H +ATOM 1337 O R11 A1337 -10.205 -6.612 0.712 1.00 0.00 O +ATOM 1338 H R12 A1338 -10.530 -6.009 1.441 1.00 0.00 H +ATOM 1339 H R12 A1339 -9.835 -5.822 0.224 1.00 0.00 H +ATOM 1340 O R11 A1340 20.181 9.430 -4.544 1.00 0.00 O +ATOM 1341 H R12 A1341 19.367 9.990 -4.698 1.00 0.00 H +ATOM 1342 H R12 A1342 19.631 8.602 -4.650 1.00 0.00 H +ATOM 1343 O R11 A1343 16.169 3.761 4.525 1.00 0.00 O +ATOM 1344 H R12 A1344 15.360 3.562 5.079 1.00 0.00 H +ATOM 1345 H R12 A1345 15.685 4.520 4.090 1.00 0.00 H +ATOM 1346 O R11 A1346 0.747 15.045 -10.922 1.00 0.00 O +ATOM 1347 H R12 A1347 1.072 15.571 -10.136 1.00 0.00 H +ATOM 1348 H R12 A1348 0.741 14.215 -10.365 1.00 0.00 H +ATOM 1349 O R11 A1349 1.628 -4.568 9.720 1.00 0.00 O +ATOM 1350 H R12 A1350 1.515 -4.250 10.662 1.00 0.00 H +ATOM 1351 H R12 A1351 2.619 -4.465 9.805 1.00 0.00 H +ATOM 1352 O R11 A1352 1.752 18.053 9.703 1.00 0.00 O +ATOM 1353 H R12 A1353 2.687 17.785 9.468 1.00 0.00 H +ATOM 1354 H R12 A1354 1.681 18.558 8.843 1.00 0.00 H +ATOM 1355 O R11 A1355 -16.405 18.328 -3.390 1.00 0.00 O +ATOM 1356 H R12 A1356 -15.466 18.498 -3.689 1.00 0.00 H +ATOM 1357 H R12 A1357 -16.724 19.082 -3.966 1.00 0.00 H +ATOM 1358 O R11 A1358 -5.944 16.397 4.952 1.00 0.00 O +ATOM 1359 H R12 A1359 -6.223 17.311 5.245 1.00 0.00 H +ATOM 1360 H R12 A1360 -6.629 15.993 5.557 1.00 0.00 H +ATOM 1361 O R11 A1361 13.275 21.950 -14.664 1.00 0.00 O +ATOM 1362 H R12 A1362 12.791 21.892 -13.791 1.00 0.00 H +ATOM 1363 H R12 A1363 13.217 20.957 -14.762 1.00 0.00 H +ATOM 1364 O R11 A1364 -10.399 1.307 10.483 1.00 0.00 O +ATOM 1365 H R12 A1365 -10.594 1.776 11.345 1.00 0.00 H +ATOM 1366 H R12 A1366 -11.272 0.825 10.548 1.00 0.00 H +ATOM 1367 O R11 A1367 -0.646 13.636 9.163 1.00 0.00 O +ATOM 1368 H R12 A1368 -0.147 14.495 9.278 1.00 0.00 H +ATOM 1369 H R12 A1369 -0.832 13.872 8.209 1.00 0.00 H +ATOM 1370 O R11 A1370 6.878 24.270 -11.052 1.00 0.00 O +ATOM 1371 H R12 A1371 6.620 23.602 -10.354 1.00 0.00 H +ATOM 1372 H R12 A1372 7.759 23.811 -11.165 1.00 0.00 H +ATOM 1373 O R11 A1373 1.966 -4.905 -8.594 1.00 0.00 O +ATOM 1374 H R12 A1374 2.202 -5.797 -8.208 1.00 0.00 H +ATOM 1375 H R12 A1375 1.205 -4.829 -7.950 1.00 0.00 H +ATOM 1376 O R11 A1376 4.263 -7.865 -2.942 1.00 0.00 O +ATOM 1377 H R12 A1377 3.570 -7.350 -3.447 1.00 0.00 H +ATOM 1378 H R12 A1378 3.593 -8.064 -2.227 1.00 0.00 H +ATOM 1379 O R11 A1379 9.428 26.728 -12.626 1.00 0.00 O +ATOM 1380 H R12 A1380 9.939 26.689 -11.767 1.00 0.00 H +ATOM 1381 H R12 A1381 8.763 26.077 -12.260 1.00 0.00 H +ATOM 1382 O R11 A1382 -8.462 -7.537 8.470 1.00 0.00 O +ATOM 1383 H R12 A1383 -7.577 -7.969 8.297 1.00 0.00 H +ATOM 1384 H R12 A1384 -8.112 -6.674 8.103 1.00 0.00 H +ATOM 1385 O R11 A1385 0.885 21.055 2.056 1.00 0.00 O +ATOM 1386 H R12 A1386 1.192 22.001 2.164 1.00 0.00 H +ATOM 1387 H R12 A1387 1.414 20.791 2.863 1.00 0.00 H +ATOM 1388 O R11 A1388 -19.860 -1.595 -2.131 1.00 0.00 O +ATOM 1389 H R12 A1389 -18.929 -1.859 -2.384 1.00 0.00 H +ATOM 1390 H R12 A1390 -19.645 -0.640 -2.339 1.00 0.00 H +ATOM 1391 O R11 A1391 -14.331 -3.650 -2.937 1.00 0.00 O +ATOM 1392 H R12 A1392 -14.623 -3.141 -2.127 1.00 0.00 H +ATOM 1393 H R12 A1393 -15.218 -4.110 -2.968 1.00 0.00 H +ATOM 1394 O R11 A1394 -12.783 -3.925 7.852 1.00 0.00 O +ATOM 1395 H R12 A1395 -11.827 -4.124 8.067 1.00 0.00 H +ATOM 1396 H R12 A1396 -12.503 -3.089 7.380 1.00 0.00 H +ATOM 1397 O R11 A1397 -3.874 -9.129 -5.050 1.00 0.00 O +ATOM 1398 H R12 A1398 -3.769 -8.277 -5.562 1.00 0.00 H +ATOM 1399 H R12 A1399 -2.893 -9.133 -4.856 1.00 0.00 H +ATOM 1400 O R11 A1400 4.042 -0.613 -13.611 1.00 0.00 O +ATOM 1401 H R12 A1401 4.797 0.027 -13.750 1.00 0.00 H +ATOM 1402 H R12 A1402 4.051 -0.835 -14.586 1.00 0.00 H +ATOM 1403 O R11 A1403 13.479 -3.674 -1.696 1.00 0.00 O +ATOM 1404 H R12 A1404 12.558 -3.284 -1.721 1.00 0.00 H +ATOM 1405 H R12 A1405 13.868 -2.761 -1.820 1.00 0.00 H +ATOM 1406 O R11 A1406 14.440 13.977 10.709 1.00 0.00 O +ATOM 1407 H R12 A1407 15.051 14.106 11.489 1.00 0.00 H +ATOM 1408 H R12 A1408 15.196 13.590 10.181 1.00 0.00 H +ATOM 1409 O R11 A1409 -1.551 1.337 -16.117 1.00 0.00 O +ATOM 1410 H R12 A1410 -2.306 1.439 -15.469 1.00 0.00 H +ATOM 1411 H R12 A1411 -0.896 1.463 -15.372 1.00 0.00 H +ATOM 1412 O R11 A1412 -6.363 -9.854 5.246 1.00 0.00 O +ATOM 1413 H R12 A1413 -5.719 -9.244 4.783 1.00 0.00 H +ATOM 1414 H R12 A1414 -6.411 -9.219 6.017 1.00 0.00 H +ATOM 1415 O R11 A1415 -18.552 -0.711 4.863 1.00 0.00 O +ATOM 1416 H R12 A1416 -18.059 -0.841 5.724 1.00 0.00 H +ATOM 1417 H R12 A1417 -19.112 -0.001 5.291 1.00 0.00 H +ATOM 1418 O R11 A1418 -0.648 13.634 -13.294 1.00 0.00 O +ATOM 1419 H R12 A1419 -0.380 14.320 -13.970 1.00 0.00 H +ATOM 1420 H R12 A1420 -1.101 14.343 -12.754 1.00 0.00 H +ATOM 1421 O R11 A1421 -3.188 -12.103 -5.349 1.00 0.00 O +ATOM 1422 H R12 A1422 -3.089 -11.613 -4.483 1.00 0.00 H +ATOM 1423 H R12 A1423 -2.195 -12.196 -5.411 1.00 0.00 H +ATOM 1424 O R11 A1424 14.610 30.595 -4.570 1.00 0.00 O +ATOM 1425 H R12 A1425 14.935 31.121 -3.784 1.00 0.00 H +ATOM 1426 H R12 A1426 14.605 29.765 -4.013 1.00 0.00 H +ATOM 1427 O R11 A1427 -4.218 16.377 7.576 1.00 0.00 O +ATOM 1428 H R12 A1428 -3.602 16.369 8.364 1.00 0.00 H +ATOM 1429 H R12 A1429 -3.848 15.497 7.278 1.00 0.00 H +ATOM 1430 O R11 A1430 -2.691 7.967 11.486 1.00 0.00 O +ATOM 1431 H R12 A1431 -1.725 8.106 11.267 1.00 0.00 H +ATOM 1432 H R12 A1432 -2.945 8.644 10.796 1.00 0.00 H +ATOM 1433 O R11 A1433 23.129 13.703 -12.152 1.00 0.00 O +ATOM 1434 H R12 A1434 23.193 14.194 -11.283 1.00 0.00 H +ATOM 1435 H R12 A1435 24.125 13.723 -12.237 1.00 0.00 H +ATOM 1436 O R11 A1436 -15.627 -3.125 -5.943 1.00 0.00 O +ATOM 1437 H R12 A1437 -15.173 -3.947 -5.601 1.00 0.00 H +ATOM 1438 H R12 A1438 -15.373 -2.637 -5.108 1.00 0.00 H +ATOM 1439 O R11 A1439 -1.547 -7.693 4.775 1.00 0.00 O +ATOM 1440 H R12 A1440 -0.616 -7.957 4.522 1.00 0.00 H +ATOM 1441 H R12 A1441 -1.332 -6.738 4.568 1.00 0.00 H +ATOM 1442 O R11 A1442 11.631 6.896 11.269 1.00 0.00 O +ATOM 1443 H R12 A1443 11.831 7.647 10.640 1.00 0.00 H +ATOM 1444 H R12 A1444 11.312 6.339 10.502 1.00 0.00 H +ATOM 1445 O R11 A1445 21.321 27.539 5.619 1.00 0.00 O +ATOM 1446 H R12 A1446 21.865 26.783 5.254 1.00 0.00 H +ATOM 1447 H R12 A1447 21.937 27.603 6.404 1.00 0.00 H +ATOM 1448 O R11 A1448 1.292 6.708 10.292 1.00 0.00 O +ATOM 1449 H R12 A1449 0.369 7.088 10.347 1.00 0.00 H +ATOM 1450 H R12 A1450 0.954 5.971 9.707 1.00 0.00 H +ATOM 1451 O R11 A1451 -3.807 2.724 12.964 1.00 0.00 O +ATOM 1452 H R12 A1452 -3.834 3.524 13.563 1.00 0.00 H +ATOM 1453 H R12 A1453 -4.184 2.161 13.699 1.00 0.00 H +ATOM 1454 O R11 A1454 18.691 11.573 -11.148 1.00 0.00 O +ATOM 1455 H R12 A1455 18.633 11.591 -10.150 1.00 0.00 H +ATOM 1456 H R12 A1456 18.953 12.538 -11.150 1.00 0.00 H +ATOM 1457 O R11 A1457 17.920 16.212 11.458 1.00 0.00 O +ATOM 1458 H R12 A1458 17.250 16.011 10.742 1.00 0.00 H +ATOM 1459 H R12 A1459 17.465 17.084 11.638 1.00 0.00 H +ATOM 1460 O R11 A1460 -16.711 -0.778 -7.792 1.00 0.00 O +ATOM 1461 H R12 A1461 -17.047 0.137 -7.570 1.00 0.00 H +ATOM 1462 H R12 A1462 -17.353 -1.173 -7.135 1.00 0.00 H +ATOM 1463 O R11 A1463 21.668 6.652 7.544 1.00 0.00 O +ATOM 1464 H R12 A1464 22.212 7.268 6.974 1.00 0.00 H +ATOM 1465 H R12 A1465 20.912 6.716 6.893 1.00 0.00 H +ATOM 1466 O R11 A1466 23.343 28.999 -0.640 1.00 0.00 O +ATOM 1467 H R12 A1467 23.731 28.090 -0.488 1.00 0.00 H +ATOM 1468 H R12 A1468 24.143 29.250 -1.185 1.00 0.00 H +ATOM 1469 O R11 A1469 11.663 -3.304 3.301 1.00 0.00 O +ATOM 1470 H R12 A1470 12.605 -3.455 3.602 1.00 0.00 H +ATOM 1471 H R12 A1471 11.883 -2.352 3.089 1.00 0.00 H +ATOM 1472 O R11 A1472 -5.271 18.574 1.473 1.00 0.00 O +ATOM 1473 H R12 A1473 -4.323 18.679 1.172 1.00 0.00 H +ATOM 1474 H R12 A1474 -5.512 19.425 1.005 1.00 0.00 H +ATOM 1475 O R11 A1475 15.013 0.516 -7.969 1.00 0.00 O +ATOM 1476 H R12 A1476 15.862 -0.004 -8.060 1.00 0.00 H +ATOM 1477 H R12 A1477 15.497 1.352 -8.227 1.00 0.00 H +ATOM 1478 O R11 A1478 26.583 18.733 3.148 1.00 0.00 O +ATOM 1479 H R12 A1479 27.302 19.392 2.926 1.00 0.00 H +ATOM 1480 H R12 A1480 26.676 18.327 2.239 1.00 0.00 H +ATOM 1481 O R11 A1481 12.134 33.048 -4.224 1.00 0.00 O +ATOM 1482 H R12 A1482 11.755 32.142 -4.036 1.00 0.00 H +ATOM 1483 H R12 A1483 12.696 32.661 -4.955 1.00 0.00 H +ATOM 1484 O R11 A1484 24.928 18.515 -12.625 1.00 0.00 O +ATOM 1485 H R12 A1485 25.094 18.973 -11.752 1.00 0.00 H +ATOM 1486 H R12 A1486 25.914 18.416 -12.760 1.00 0.00 H +ATOM 1487 O R11 A1487 -12.087 7.194 -11.546 1.00 0.00 O +ATOM 1488 H R12 A1488 -11.152 7.535 -11.445 1.00 0.00 H +ATOM 1489 H R12 A1489 -12.253 7.863 -12.270 1.00 0.00 H +ATOM 1490 O R11 A1490 15.558 9.595 9.861 1.00 0.00 O +ATOM 1491 H R12 A1491 16.442 9.402 10.286 1.00 0.00 H +ATOM 1492 H R12 A1492 15.102 9.043 10.558 1.00 0.00 H +ATOM 1493 O R11 A1493 12.437 10.918 10.859 1.00 0.00 O +ATOM 1494 H R12 A1494 11.640 11.483 11.073 1.00 0.00 H +ATOM 1495 H R12 A1495 11.833 10.178 10.565 1.00 0.00 H +ATOM 1496 O R11 A1496 7.774 26.894 -2.227 1.00 0.00 O +ATOM 1497 H R12 A1497 8.513 27.539 -2.031 1.00 0.00 H +ATOM 1498 H R12 A1498 7.990 26.392 -1.390 1.00 0.00 H +ATOM 1499 O R11 A1499 -10.178 13.999 -6.292 1.00 0.00 O +ATOM 1500 H R12 A1500 -9.333 14.496 -6.096 1.00 0.00 H +ATOM 1501 H R12 A1501 -10.521 14.786 -6.805 1.00 0.00 H +ATOM 1502 O R11 A1502 3.888 10.373 13.506 1.00 0.00 O +ATOM 1503 H R12 A1503 3.330 10.891 12.857 1.00 0.00 H +ATOM 1504 H R12 A1504 3.714 11.065 14.207 1.00 0.00 H +ATOM 1505 O R11 A1505 9.285 28.469 6.086 1.00 0.00 O +ATOM 1506 H R12 A1506 9.623 28.004 6.904 1.00 0.00 H +ATOM 1507 H R12 A1507 8.819 27.632 5.801 1.00 0.00 H +ATOM 1508 O R11 A1508 -17.003 -4.381 4.102 1.00 0.00 O +ATOM 1509 H R12 A1509 -16.911 -5.359 3.916 1.00 0.00 H +ATOM 1510 H R12 A1510 -16.007 -4.291 4.117 1.00 0.00 H +ATOM 1511 O R11 A1511 19.252 5.615 -10.595 1.00 0.00 O +ATOM 1512 H R12 A1512 19.174 6.436 -10.029 1.00 0.00 H +ATOM 1513 H R12 A1513 20.073 5.347 -10.091 1.00 0.00 H +ATOM 1514 O R11 A1514 -2.314 16.548 1.253 1.00 0.00 O +ATOM 1515 H R12 A1515 -2.745 17.290 1.767 1.00 0.00 H +ATOM 1516 H R12 A1516 -1.482 17.096 1.160 1.00 0.00 H +ATOM 1517 O R11 A1517 1.708 6.704 13.416 1.00 0.00 O +ATOM 1518 H R12 A1518 2.682 6.880 13.280 1.00 0.00 H +ATOM 1519 H R12 A1519 1.581 7.644 13.731 1.00 0.00 H +ATOM 1520 O R11 A1520 15.219 21.410 11.650 1.00 0.00 O +ATOM 1521 H R12 A1521 16.017 21.603 12.221 1.00 0.00 H +ATOM 1522 H R12 A1522 15.787 20.849 11.047 1.00 0.00 H +ATOM 1523 O R11 A1523 -12.493 1.226 -14.423 1.00 0.00 O +ATOM 1524 H R12 A1524 -13.110 0.621 -13.920 1.00 0.00 H +ATOM 1525 H R12 A1525 -12.139 1.584 -13.560 1.00 0.00 H +ATOM 1526 O R11 A1526 4.079 26.508 -3.784 1.00 0.00 O +ATOM 1527 H R12 A1527 4.519 27.400 -3.888 1.00 0.00 H +ATOM 1528 H R12 A1528 4.693 26.294 -3.024 1.00 0.00 H +ATOM 1529 O R11 A1529 27.159 23.171 2.075 1.00 0.00 O +ATOM 1530 H R12 A1530 26.577 23.062 2.881 1.00 0.00 H +ATOM 1531 H R12 A1531 26.347 23.278 1.502 1.00 0.00 H +ATOM 1532 O R11 A1532 -6.891 15.847 -6.364 1.00 0.00 O +ATOM 1533 H R12 A1533 -5.897 15.758 -6.299 1.00 0.00 H +ATOM 1534 H R12 A1534 -6.817 16.822 -6.151 1.00 0.00 H +ATOM 1535 O R11 A1535 11.753 -3.168 -6.276 1.00 0.00 O +ATOM 1536 H R12 A1536 12.433 -2.478 -6.526 1.00 0.00 H +ATOM 1537 H R12 A1537 11.676 -2.761 -5.365 1.00 0.00 H +ATOM 1538 O R11 A1538 9.860 7.377 -13.946 1.00 0.00 O +ATOM 1539 H R12 A1539 9.167 7.892 -14.451 1.00 0.00 H +ATOM 1540 H R12 A1540 9.190 7.177 -13.230 1.00 0.00 H +ATOM 1541 O R11 A1541 22.732 11.492 6.886 1.00 0.00 O +ATOM 1542 H R12 A1542 22.706 12.292 7.485 1.00 0.00 H +ATOM 1543 H R12 A1543 22.356 10.929 7.622 1.00 0.00 H +ATOM 1544 O R11 A1544 -6.238 13.937 -11.079 1.00 0.00 O +ATOM 1545 H R12 A1545 -6.855 13.332 -10.575 1.00 0.00 H +ATOM 1546 H R12 A1546 -5.884 14.295 -10.215 1.00 0.00 H +ATOM 1547 O R11 A1547 4.640 -0.152 8.540 1.00 0.00 O +ATOM 1548 H R12 A1548 4.704 0.844 8.480 1.00 0.00 H +ATOM 1549 H R12 A1549 5.131 -0.132 9.410 1.00 0.00 H +ATOM 1550 O R11 A1550 2.984 8.711 -15.601 1.00 0.00 O +ATOM 1551 H R12 A1551 3.645 7.995 -15.826 1.00 0.00 H +ATOM 1552 H R12 A1552 2.862 8.313 -14.692 1.00 0.00 H +ATOM 1553 O R11 A1553 -21.594 7.865 1.691 1.00 0.00 O +ATOM 1554 H R12 A1554 -21.838 6.900 1.784 1.00 0.00 H +ATOM 1555 H R12 A1555 -22.243 7.956 0.935 1.00 0.00 H +ATOM 1556 O R11 A1556 0.436 10.432 12.662 1.00 0.00 O +ATOM 1557 H R12 A1557 1.088 10.329 13.413 1.00 0.00 H +ATOM 1558 H R12 A1558 1.161 10.805 12.083 1.00 0.00 H +ATOM 1559 O R11 A1559 -20.692 6.990 6.295 1.00 0.00 O +ATOM 1560 H R12 A1560 -20.353 6.130 6.677 1.00 0.00 H +ATOM 1561 H R12 A1561 -20.283 6.760 5.412 1.00 0.00 H +ATOM 1562 O R11 A1562 4.773 -4.888 -5.367 1.00 0.00 O +ATOM 1563 H R12 A1563 4.385 -5.481 -6.072 1.00 0.00 H +ATOM 1564 H R12 A1564 4.054 -4.214 -5.538 1.00 0.00 H +ATOM 1565 O R11 A1565 2.104 18.536 -10.136 1.00 0.00 O +ATOM 1566 H R12 A1566 1.511 18.297 -10.905 1.00 0.00 H +ATOM 1567 H R12 A1567 2.658 19.108 -10.742 1.00 0.00 H +ATOM 1568 O R11 A1568 -13.898 1.523 -9.146 1.00 0.00 O +ATOM 1569 H R12 A1569 -14.546 0.845 -8.801 1.00 0.00 H +ATOM 1570 H R12 A1570 -13.493 1.600 -8.234 1.00 0.00 H +ATOM 1571 O R11 A1571 -3.591 11.330 8.162 1.00 0.00 O +ATOM 1572 H R12 A1572 -3.654 11.070 7.198 1.00 0.00 H +ATOM 1573 H R12 A1573 -3.577 12.295 7.900 1.00 0.00 H +ATOM 1574 O R11 A1574 -7.699 19.505 4.208 1.00 0.00 O +ATOM 1575 H R12 A1575 -7.360 19.915 5.055 1.00 0.00 H +ATOM 1576 H R12 A1576 -8.559 19.275 4.664 1.00 0.00 H +ATOM 1577 O R11 A1577 8.200 23.553 10.514 1.00 0.00 O +ATOM 1578 H R12 A1578 8.127 23.622 11.509 1.00 0.00 H +ATOM 1579 H R12 A1579 7.806 24.467 10.422 1.00 0.00 H +ATOM 1580 O R11 A1580 -4.909 -10.908 -1.977 1.00 0.00 O +ATOM 1581 H R12 A1581 -4.204 -10.678 -1.306 1.00 0.00 H +ATOM 1582 H R12 A1582 -4.379 -10.450 -2.691 1.00 0.00 H +ATOM 1583 O R11 A1583 12.397 12.565 -12.957 1.00 0.00 O +ATOM 1584 H R12 A1584 12.502 13.418 -13.469 1.00 0.00 H +ATOM 1585 H R12 A1585 13.378 12.561 -12.763 1.00 0.00 H +ATOM 1586 O R11 A1586 1.543 -8.505 -6.501 1.00 0.00 O +ATOM 1587 H R12 A1587 2.262 -7.846 -6.722 1.00 0.00 H +ATOM 1588 H R12 A1588 1.636 -8.911 -7.410 1.00 0.00 H +ATOM 1589 O R11 A1589 18.928 11.415 9.530 1.00 0.00 O +ATOM 1590 H R12 A1590 18.932 11.373 8.531 1.00 0.00 H +ATOM 1591 H R12 A1591 17.928 11.425 9.526 1.00 0.00 H +ATOM 1592 O R11 A1592 -7.191 19.898 -1.292 1.00 0.00 O +ATOM 1593 H R12 A1593 -6.539 20.623 -1.513 1.00 0.00 H +ATOM 1594 H R12 A1594 -7.294 20.271 -0.370 1.00 0.00 H +ATOM 1595 O R11 A1595 16.141 20.281 -14.253 1.00 0.00 O +ATOM 1596 H R12 A1596 16.909 20.853 -14.542 1.00 0.00 H +ATOM 1597 H R12 A1597 16.206 20.660 -13.330 1.00 0.00 H +ATOM 1598 O R11 A1598 19.547 6.124 0.109 1.00 0.00 O +ATOM 1599 H R12 A1599 18.792 6.780 0.103 1.00 0.00 H +ATOM 1600 H R12 A1600 19.648 6.249 1.096 1.00 0.00 H +ATOM 1601 O R11 A1601 -2.779 -0.199 13.411 1.00 0.00 O +ATOM 1602 H R12 A1602 -3.301 -0.971 13.048 1.00 0.00 H +ATOM 1603 H R12 A1603 -2.007 -0.445 12.824 1.00 0.00 H +ATOM 1604 O R11 A1604 -11.294 16.906 3.576 1.00 0.00 O +ATOM 1605 H R12 A1605 -10.855 17.520 4.231 1.00 0.00 H +ATOM 1606 H R12 A1606 -10.402 16.692 3.178 1.00 0.00 H +ATOM 1607 O R11 A1607 -13.269 18.639 -1.889 1.00 0.00 O +ATOM 1608 H R12 A1608 -13.965 19.357 -1.875 1.00 0.00 H +ATOM 1609 H R12 A1609 -13.971 17.955 -1.694 1.00 0.00 H +ATOM 1610 O R11 A1610 26.515 13.010 -4.131 1.00 0.00 O +ATOM 1611 H R12 A1611 25.955 13.696 -3.666 1.00 0.00 H +ATOM 1612 H R12 A1612 25.757 12.360 -4.083 1.00 0.00 H +ATOM 1613 O R11 A1613 10.045 19.393 10.216 1.00 0.00 O +ATOM 1614 H R12 A1614 10.141 18.448 9.905 1.00 0.00 H +ATOM 1615 H R12 A1615 9.580 19.627 9.362 1.00 0.00 H +ATOM 1616 O R11 A1616 0.237 0.263 11.156 1.00 0.00 O +ATOM 1617 H R12 A1617 0.434 1.099 10.644 1.00 0.00 H +ATOM 1618 H R12 A1618 -0.026 -0.195 10.306 1.00 0.00 H +ATOM 1619 O R11 A1619 18.378 18.421 -13.076 1.00 0.00 O +ATOM 1620 H R12 A1620 18.668 19.300 -13.456 1.00 0.00 H +ATOM 1621 H R12 A1621 18.491 17.995 -13.974 1.00 0.00 H +ATOM 1622 O R11 A1622 -19.762 -5.366 2.413 1.00 0.00 O +ATOM 1623 H R12 A1623 -19.603 -4.713 1.672 1.00 0.00 H +ATOM 1624 H R12 A1624 -20.749 -5.262 2.293 1.00 0.00 H +ATOM 1625 O R11 A1625 16.296 3.315 10.589 1.00 0.00 O +ATOM 1626 H R12 A1626 16.982 3.075 9.902 1.00 0.00 H +ATOM 1627 H R12 A1627 16.719 4.215 10.697 1.00 0.00 H +ATOM 1628 O R11 A1628 7.435 -6.808 -2.115 1.00 0.00 O +ATOM 1629 H R12 A1629 6.766 -6.459 -2.771 1.00 0.00 H +ATOM 1630 H R12 A1630 6.764 -6.713 -1.380 1.00 0.00 H +ATOM 1631 O R11 A1631 -10.744 20.532 3.228 1.00 0.00 O +ATOM 1632 H R12 A1632 -10.383 19.606 3.112 1.00 0.00 H +ATOM 1633 H R12 A1633 -11.670 20.161 3.306 1.00 0.00 H +ATOM 1634 O R11 A1634 -13.167 14.592 -6.298 1.00 0.00 O +ATOM 1635 H R12 A1635 -14.053 14.180 -6.086 1.00 0.00 H +ATOM 1636 H R12 A1636 -12.751 14.088 -5.541 1.00 0.00 H +ATOM 1637 O R11 A1637 -8.607 -12.433 4.862 1.00 0.00 O +ATOM 1638 H R12 A1638 -7.903 -13.065 5.188 1.00 0.00 H +ATOM 1639 H R12 A1639 -8.055 -12.236 4.052 1.00 0.00 H +ATOM 1640 O R11 A1640 -4.126 -6.713 6.296 1.00 0.00 O +ATOM 1641 H R12 A1641 -3.967 -7.701 6.295 1.00 0.00 H +ATOM 1642 H R12 A1642 -3.473 -6.609 7.046 1.00 0.00 H +ATOM 1643 O R11 A1643 -10.595 9.055 8.435 1.00 0.00 O +ATOM 1644 H R12 A1644 -9.620 9.232 8.299 1.00 0.00 H +ATOM 1645 H R12 A1645 -10.721 9.996 8.750 1.00 0.00 H +ATOM 1646 O R11 A1646 6.436 13.759 11.876 1.00 0.00 O +ATOM 1647 H R12 A1647 6.293 14.177 10.979 1.00 0.00 H +ATOM 1648 H R12 A1648 6.054 14.572 12.315 1.00 0.00 H +ATOM 1649 O R11 A1649 1.978 23.215 -3.462 1.00 0.00 O +ATOM 1650 H R12 A1650 1.642 24.131 -3.240 1.00 0.00 H +ATOM 1651 H R12 A1651 1.336 22.820 -2.804 1.00 0.00 H +ATOM 1652 O R11 A1652 19.465 22.458 -12.228 1.00 0.00 O +ATOM 1653 H R12 A1653 20.425 22.431 -12.504 1.00 0.00 H +ATOM 1654 H R12 A1654 19.437 21.459 -12.224 1.00 0.00 H +ATOM 1655 O R11 A1655 -1.819 17.070 -3.933 1.00 0.00 O +ATOM 1656 H R12 A1656 -2.735 17.463 -4.019 1.00 0.00 H +ATOM 1657 H R12 A1657 -2.133 16.508 -3.168 1.00 0.00 H +ATOM 1658 O R11 A1658 21.521 24.828 -10.296 1.00 0.00 O +ATOM 1659 H R12 A1659 21.346 24.124 -10.984 1.00 0.00 H +ATOM 1660 H R12 A1660 21.786 24.121 -9.640 1.00 0.00 H +ATOM 1661 O R11 A1661 12.385 2.697 -8.676 1.00 0.00 O +ATOM 1662 H R12 A1662 13.341 2.977 -8.762 1.00 0.00 H +ATOM 1663 H R12 A1663 12.186 3.533 -8.165 1.00 0.00 H +ATOM 1664 O R11 A1664 8.399 4.390 11.164 1.00 0.00 O +ATOM 1665 H R12 A1665 9.333 4.122 10.929 1.00 0.00 H +ATOM 1666 H R12 A1666 8.327 4.895 10.304 1.00 0.00 H +ATOM 1667 O R11 A1667 -7.084 13.263 6.621 1.00 0.00 O +ATOM 1668 H R12 A1668 -7.592 13.443 7.464 1.00 0.00 H +ATOM 1669 H R12 A1669 -7.946 13.164 6.124 1.00 0.00 H +ATOM 1670 O R11 A1670 -2.789 4.905 -16.617 1.00 0.00 O +ATOM 1671 H R12 A1671 -2.464 5.431 -15.831 1.00 0.00 H +ATOM 1672 H R12 A1672 -2.795 4.075 -16.059 1.00 0.00 H +ATOM 1673 O R11 A1673 21.019 25.981 8.403 1.00 0.00 O +ATOM 1674 H R12 A1674 21.824 25.836 7.827 1.00 0.00 H +ATOM 1675 H R12 A1675 20.427 25.855 7.607 1.00 0.00 H +ATOM 1676 O R11 A1676 0.161 7.710 -14.362 1.00 0.00 O +ATOM 1677 H R12 A1677 -0.315 8.546 -14.092 1.00 0.00 H +ATOM 1678 H R12 A1678 0.105 7.374 -13.422 1.00 0.00 H +ATOM 1679 O R11 A1679 -10.297 6.545 -14.711 1.00 0.00 O +ATOM 1680 H R12 A1680 -10.341 6.619 -13.714 1.00 0.00 H +ATOM 1681 H R12 A1681 -9.896 7.459 -14.762 1.00 0.00 H +ATOM 1682 O R11 A1682 -6.647 -9.398 -7.880 1.00 0.00 O +ATOM 1683 H R12 A1683 -5.713 -9.056 -7.779 1.00 0.00 H +ATOM 1684 H R12 A1684 -6.813 -8.729 -8.605 1.00 0.00 H +ATOM 1685 O R11 A1685 13.634 30.843 2.762 1.00 0.00 O +ATOM 1686 H R12 A1686 12.667 30.591 2.796 1.00 0.00 H +ATOM 1687 H R12 A1687 13.382 31.775 2.499 1.00 0.00 H +ATOM 1688 O R11 A1688 -19.957 12.560 0.682 1.00 0.00 O +ATOM 1689 H R12 A1689 -19.920 13.024 1.567 1.00 0.00 H +ATOM 1690 H R12 A1690 -18.965 12.649 0.594 1.00 0.00 H +ATOM 1691 O R11 A1691 9.836 -1.405 9.796 1.00 0.00 O +ATOM 1692 H R12 A1692 9.343 -0.601 10.130 1.00 0.00 H +ATOM 1693 H R12 A1693 9.282 -1.398 8.964 1.00 0.00 H +ATOM 1694 O R11 A1694 25.841 10.046 -1.782 1.00 0.00 O +ATOM 1695 H R12 A1695 24.933 9.766 -2.094 1.00 0.00 H +ATOM 1696 H R12 A1696 25.770 10.882 -2.327 1.00 0.00 H +ATOM 1697 O R11 A1697 7.532 19.363 12.371 1.00 0.00 O +ATOM 1698 H R12 A1698 7.106 20.245 12.166 1.00 0.00 H +ATOM 1699 H R12 A1699 6.784 18.893 11.902 1.00 0.00 H +ATOM 1700 O R11 A1700 23.418 19.963 9.266 1.00 0.00 O +ATOM 1701 H R12 A1701 24.112 20.037 8.551 1.00 0.00 H +ATOM 1702 H R12 A1702 22.699 20.067 8.580 1.00 0.00 H +ATOM 1703 O R11 A1703 26.935 29.582 -1.091 1.00 0.00 O +ATOM 1704 H R12 A1704 26.266 29.931 -1.747 1.00 0.00 H +ATOM 1705 H R12 A1705 26.264 29.677 -0.356 1.00 0.00 H +ATOM 1706 O R11 A1706 -23.563 5.933 -2.410 1.00 0.00 O +ATOM 1707 H R12 A1707 -22.956 5.223 -2.768 1.00 0.00 H +ATOM 1708 H R12 A1708 -22.778 6.396 -1.998 1.00 0.00 H +ATOM 1709 O R11 A1709 4.508 6.098 10.754 1.00 0.00 O +ATOM 1710 H R12 A1710 4.039 6.807 11.280 1.00 0.00 H +ATOM 1711 H R12 A1711 5.218 6.046 11.457 1.00 0.00 H +ATOM 1712 O R11 A1712 -1.703 19.053 -7.481 1.00 0.00 O +ATOM 1713 H R12 A1713 -1.011 18.409 -7.807 1.00 0.00 H +ATOM 1714 H R12 A1714 -0.987 19.722 -7.281 1.00 0.00 H +ATOM 1715 O R11 A1715 12.317 30.305 -8.664 1.00 0.00 O +ATOM 1716 H R12 A1716 13.195 29.841 -8.549 1.00 0.00 H +ATOM 1717 H R12 A1717 12.625 30.672 -9.541 1.00 0.00 H +ATOM 1718 O R11 A1718 18.922 3.849 -6.110 1.00 0.00 O +ATOM 1719 H R12 A1719 18.472 4.611 -5.645 1.00 0.00 H +ATOM 1720 H R12 A1720 19.001 4.402 -6.939 1.00 0.00 H +ATOM 1721 O R11 A1721 9.337 31.776 7.085 1.00 0.00 O +ATOM 1722 H R12 A1722 9.540 30.824 7.316 1.00 0.00 H +ATOM 1723 H R12 A1723 10.314 31.960 6.984 1.00 0.00 H +ATOM 1724 O R11 A1724 15.554 11.716 -10.785 1.00 0.00 O +ATOM 1725 H R12 A1725 16.263 12.171 -10.246 1.00 0.00 H +ATOM 1726 H R12 A1726 15.403 11.067 -10.039 1.00 0.00 H +ATOM 1727 O R11 A1727 14.814 9.285 -12.808 1.00 0.00 O +ATOM 1728 H R12 A1728 14.714 9.173 -13.797 1.00 0.00 H +ATOM 1729 H R12 A1729 15.550 9.945 -12.957 1.00 0.00 H +ATOM 1730 O R11 A1730 20.438 33.121 -5.597 1.00 0.00 O +ATOM 1731 H R12 A1731 21.132 32.402 -5.573 1.00 0.00 H +ATOM 1732 H R12 A1732 20.512 33.226 -4.606 1.00 0.00 H +ATOM 1733 O R11 A1733 27.759 18.575 -0.186 1.00 0.00 O +ATOM 1734 H R12 A1734 28.515 18.584 -0.841 1.00 0.00 H +ATOM 1735 H R12 A1735 28.400 18.354 0.550 1.00 0.00 H +ATOM 1736 O R11 A1736 -14.469 4.846 -10.021 1.00 0.00 O +ATOM 1737 H R12 A1737 -14.574 5.321 -9.147 1.00 0.00 H +ATOM 1738 H R12 A1738 -13.607 5.328 -10.179 1.00 0.00 H +ATOM 1739 O R11 A1739 -11.197 17.105 -3.778 1.00 0.00 O +ATOM 1740 H R12 A1740 -10.704 16.235 -3.762 1.00 0.00 H +ATOM 1741 H R12 A1741 -11.853 16.722 -4.428 1.00 0.00 H +ATOM 1742 O R11 A1742 14.920 28.549 4.794 1.00 0.00 O +ATOM 1743 H R12 A1743 15.031 27.558 4.868 1.00 0.00 H +ATOM 1744 H R12 A1744 14.119 28.417 4.211 1.00 0.00 H +ATOM 1745 O R11 A1745 -7.122 9.664 -14.761 1.00 0.00 O +ATOM 1746 H R12 A1746 -6.831 10.542 -15.141 1.00 0.00 H +ATOM 1747 H R12 A1747 -7.008 9.238 -15.658 1.00 0.00 H +ATOM 1748 O R11 A1748 29.727 17.273 -5.274 1.00 0.00 O +ATOM 1749 H R12 A1749 29.474 17.670 -6.156 1.00 0.00 H +ATOM 1750 H R12 A1750 28.945 17.727 -4.846 1.00 0.00 H +ATOM 1751 O R11 A1751 -18.239 5.910 -9.642 1.00 0.00 O +ATOM 1752 H R12 A1752 -17.915 6.732 -9.173 1.00 0.00 H +ATOM 1753 H R12 A1753 -17.418 5.420 -9.351 1.00 0.00 H +CONECT 1 2 3 5 6 +CONECT 2 1 4 10 +CONECT 3 1 7 72 +CONECT 4 2 +CONECT 5 1 +CONECT 6 1 +CONECT 7 3 +CONECT 8 9 10 12 13 +CONECT 9 8 11 17 +CONECT 10 2 8 14 +CONECT 11 9 +CONECT 12 8 +CONECT 13 8 +CONECT 14 10 +CONECT 15 16 17 19 20 +CONECT 16 15 18 24 +CONECT 17 9 15 21 +CONECT 18 16 +CONECT 19 15 +CONECT 20 15 +CONECT 21 17 +CONECT 22 23 24 26 27 +CONECT 23 22 25 32 +CONECT 24 16 22 28 +CONECT 25 23 +CONECT 26 22 +CONECT 27 22 +CONECT 28 24 +CONECT 29 54 +CONECT 30 31 32 34 35 +CONECT 31 30 33 39 +CONECT 32 23 30 36 +CONECT 33 31 +CONECT 34 30 +CONECT 35 30 +CONECT 36 32 +CONECT 37 38 39 41 42 +CONECT 38 37 40 47 +CONECT 39 31 37 43 +CONECT 40 38 +CONECT 41 37 +CONECT 42 37 +CONECT 43 39 +CONECT 44 52 +CONECT 45 46 47 49 50 +CONECT 46 45 48 54 +CONECT 47 38 45 51 +CONECT 48 46 +CONECT 49 45 +CONECT 50 45 +CONECT 51 47 +CONECT 52 44 53 54 56 +CONECT 53 52 55 59 +CONECT 54 29 46 52 +CONECT 55 53 +CONECT 56 52 +CONECT 57 58 59 61 62 +CONECT 58 57 60 66 +CONECT 59 53 57 63 +CONECT 60 58 +CONECT 61 57 +CONECT 62 57 +CONECT 63 59 +CONECT 64 65 66 68 69 +CONECT 65 64 67 71 +CONECT 66 58 64 70 +CONECT 67 65 +CONECT 68 64 +CONECT 69 64 +CONECT 70 66 +CONECT 71 65 73 +CONECT 72 3 +CONECT 73 71 +CONECT 74 75 76 +CONECT 75 74 +CONECT 76 74 +CONECT 77 78 79 +CONECT 78 77 +CONECT 79 77 +CONECT 80 81 82 +CONECT 81 80 +CONECT 82 80 +CONECT 83 84 85 +CONECT 84 83 +CONECT 85 83 +CONECT 86 87 88 +CONECT 87 86 +CONECT 88 86 +CONECT 89 90 91 +CONECT 90 89 +CONECT 91 89 +CONECT 92 93 94 +CONECT 93 92 +CONECT 94 92 +CONECT 95 96 97 +CONECT 96 95 +CONECT 97 95 +CONECT 98 99 100 +CONECT 99 98 +CONECT 100 98 +CONECT 101 102 103 +CONECT 102 101 +CONECT 103 101 +CONECT 104 105 106 +CONECT 105 104 +CONECT 106 104 +CONECT 107 108 109 +CONECT 108 107 +CONECT 109 107 +CONECT 110 111 112 +CONECT 111 110 +CONECT 112 110 +CONECT 113 114 115 +CONECT 114 113 +CONECT 115 113 +CONECT 116 117 118 +CONECT 117 116 +CONECT 118 116 +CONECT 119 120 121 +CONECT 120 119 +CONECT 121 119 +CONECT 122 123 124 +CONECT 123 122 +CONECT 124 122 +CONECT 125 126 127 +CONECT 126 125 +CONECT 127 125 +CONECT 128 129 130 +CONECT 129 128 +CONECT 130 128 +CONECT 131 132 133 +CONECT 132 131 +CONECT 133 131 +CONECT 134 135 136 +CONECT 135 134 +CONECT 136 134 +CONECT 137 138 139 +CONECT 138 137 +CONECT 139 137 +CONECT 140 141 142 +CONECT 141 140 +CONECT 142 140 +CONECT 143 144 145 +CONECT 144 143 +CONECT 145 143 +CONECT 146 147 148 +CONECT 147 146 +CONECT 148 146 +CONECT 149 150 151 +CONECT 150 149 +CONECT 151 149 +CONECT 152 153 154 +CONECT 153 152 +CONECT 154 152 +CONECT 155 156 157 +CONECT 156 155 +CONECT 157 155 +CONECT 158 159 160 +CONECT 159 158 +CONECT 160 158 +CONECT 161 162 163 +CONECT 162 161 +CONECT 163 161 +CONECT 164 165 166 +CONECT 165 164 +CONECT 166 164 +CONECT 167 168 169 +CONECT 168 167 +CONECT 169 167 +CONECT 170 171 172 +CONECT 171 170 +CONECT 172 170 +CONECT 173 174 175 +CONECT 174 173 +CONECT 175 173 +CONECT 176 177 178 +CONECT 177 176 +CONECT 178 176 +CONECT 179 180 181 +CONECT 180 179 +CONECT 181 179 +CONECT 182 183 184 +CONECT 183 182 +CONECT 184 182 +CONECT 185 186 187 +CONECT 186 185 +CONECT 187 185 +CONECT 188 189 190 +CONECT 189 188 +CONECT 190 188 +CONECT 191 192 193 +CONECT 192 191 +CONECT 193 191 +CONECT 194 195 196 +CONECT 195 194 +CONECT 196 194 +CONECT 197 198 199 +CONECT 198 197 +CONECT 199 197 +CONECT 200 201 202 +CONECT 201 200 +CONECT 202 200 +CONECT 203 204 205 +CONECT 204 203 +CONECT 205 203 +CONECT 206 207 208 +CONECT 207 206 +CONECT 208 206 +CONECT 209 210 211 +CONECT 210 209 +CONECT 211 209 +CONECT 212 213 214 +CONECT 213 212 +CONECT 214 212 +CONECT 215 216 217 +CONECT 216 215 +CONECT 217 215 +CONECT 218 219 220 +CONECT 219 218 +CONECT 220 218 +CONECT 221 222 223 +CONECT 222 221 +CONECT 223 221 +CONECT 224 225 226 +CONECT 225 224 +CONECT 226 224 +CONECT 227 228 229 +CONECT 228 227 +CONECT 229 227 +CONECT 230 231 232 +CONECT 231 230 +CONECT 232 230 +CONECT 233 234 235 +CONECT 234 233 +CONECT 235 233 +CONECT 236 237 238 +CONECT 237 236 +CONECT 238 236 +CONECT 239 240 241 +CONECT 240 239 +CONECT 241 239 +CONECT 242 243 244 +CONECT 243 242 +CONECT 244 242 +CONECT 245 246 247 +CONECT 246 245 +CONECT 247 245 +CONECT 248 249 250 +CONECT 249 248 +CONECT 250 248 +CONECT 251 252 253 +CONECT 252 251 +CONECT 253 251 +CONECT 254 255 256 +CONECT 255 254 +CONECT 256 254 +CONECT 257 258 259 +CONECT 258 257 +CONECT 259 257 +CONECT 260 261 262 +CONECT 261 260 +CONECT 262 260 +CONECT 263 264 265 +CONECT 264 263 +CONECT 265 263 +CONECT 266 267 268 +CONECT 267 266 +CONECT 268 266 +CONECT 269 270 271 +CONECT 270 269 +CONECT 271 269 +CONECT 272 273 274 +CONECT 273 272 +CONECT 274 272 +CONECT 275 276 277 +CONECT 276 275 +CONECT 277 275 +CONECT 278 279 280 +CONECT 279 278 +CONECT 280 278 +CONECT 281 282 283 +CONECT 282 281 +CONECT 283 281 +CONECT 284 285 286 +CONECT 285 284 +CONECT 286 284 +CONECT 287 288 289 +CONECT 288 287 +CONECT 289 287 +CONECT 290 291 292 +CONECT 291 290 +CONECT 292 290 +CONECT 293 294 295 +CONECT 294 293 +CONECT 295 293 +CONECT 296 297 298 +CONECT 297 296 +CONECT 298 296 +CONECT 299 300 301 +CONECT 300 299 +CONECT 301 299 +CONECT 302 303 304 +CONECT 303 302 +CONECT 304 302 +CONECT 305 306 307 +CONECT 306 305 +CONECT 307 305 +CONECT 308 309 310 +CONECT 309 308 +CONECT 310 308 +CONECT 311 312 313 +CONECT 312 311 +CONECT 313 311 +CONECT 314 315 316 +CONECT 315 314 +CONECT 316 314 +CONECT 317 318 319 +CONECT 318 317 +CONECT 319 317 +CONECT 320 321 322 +CONECT 321 320 +CONECT 322 320 +CONECT 323 324 325 +CONECT 324 323 +CONECT 325 323 +CONECT 326 327 328 +CONECT 327 326 +CONECT 328 326 +CONECT 329 330 331 +CONECT 330 329 +CONECT 331 329 +CONECT 332 333 334 +CONECT 333 332 +CONECT 334 332 +CONECT 335 336 337 +CONECT 336 335 +CONECT 337 335 +CONECT 338 339 340 +CONECT 339 338 +CONECT 340 338 +CONECT 341 342 343 +CONECT 342 341 +CONECT 343 341 +CONECT 344 345 346 +CONECT 345 344 +CONECT 346 344 +CONECT 347 348 349 +CONECT 348 347 +CONECT 349 347 +CONECT 350 351 352 +CONECT 351 350 +CONECT 352 350 +CONECT 353 354 355 +CONECT 354 353 +CONECT 355 353 +CONECT 356 357 358 +CONECT 357 356 +CONECT 358 356 +CONECT 359 360 361 +CONECT 360 359 +CONECT 361 359 +CONECT 362 363 364 +CONECT 363 362 +CONECT 364 362 +CONECT 365 366 367 +CONECT 366 365 +CONECT 367 365 +CONECT 368 369 370 +CONECT 369 368 +CONECT 370 368 +CONECT 371 372 373 +CONECT 372 371 +CONECT 373 371 +CONECT 374 375 376 +CONECT 375 374 +CONECT 376 374 +CONECT 377 378 379 +CONECT 378 377 +CONECT 379 377 +CONECT 380 381 382 +CONECT 381 380 +CONECT 382 380 +CONECT 383 384 385 +CONECT 384 383 +CONECT 385 383 +CONECT 386 387 388 +CONECT 387 386 +CONECT 388 386 +CONECT 389 390 391 +CONECT 390 389 +CONECT 391 389 +CONECT 392 393 394 +CONECT 393 392 +CONECT 394 392 +CONECT 395 396 397 +CONECT 396 395 +CONECT 397 395 +CONECT 398 399 400 +CONECT 399 398 +CONECT 400 398 +CONECT 401 402 403 +CONECT 402 401 +CONECT 403 401 +CONECT 404 405 406 +CONECT 405 404 +CONECT 406 404 +CONECT 407 408 409 +CONECT 408 407 +CONECT 409 407 +CONECT 410 411 412 +CONECT 411 410 +CONECT 412 410 +CONECT 413 414 415 +CONECT 414 413 +CONECT 415 413 +CONECT 416 417 418 +CONECT 417 416 +CONECT 418 416 +CONECT 419 420 421 +CONECT 420 419 +CONECT 421 419 +CONECT 422 423 424 +CONECT 423 422 +CONECT 424 422 +CONECT 425 426 427 +CONECT 426 425 +CONECT 427 425 +CONECT 428 429 430 +CONECT 429 428 +CONECT 430 428 +CONECT 431 432 433 +CONECT 432 431 +CONECT 433 431 +CONECT 434 435 436 +CONECT 435 434 +CONECT 436 434 +CONECT 437 438 439 +CONECT 438 437 +CONECT 439 437 +CONECT 440 441 442 +CONECT 441 440 +CONECT 442 440 +CONECT 443 444 445 +CONECT 444 443 +CONECT 445 443 +CONECT 446 447 448 +CONECT 447 446 +CONECT 448 446 +CONECT 449 450 451 +CONECT 450 449 +CONECT 451 449 +CONECT 452 453 454 +CONECT 453 452 +CONECT 454 452 +CONECT 455 456 457 +CONECT 456 455 +CONECT 457 455 +CONECT 458 459 460 +CONECT 459 458 +CONECT 460 458 +CONECT 461 462 463 +CONECT 462 461 +CONECT 463 461 +CONECT 464 465 466 +CONECT 465 464 +CONECT 466 464 +CONECT 467 468 469 +CONECT 468 467 +CONECT 469 467 +CONECT 470 471 472 +CONECT 471 470 +CONECT 472 470 +CONECT 473 474 475 +CONECT 474 473 +CONECT 475 473 +CONECT 476 477 478 +CONECT 477 476 +CONECT 478 476 +CONECT 479 480 481 +CONECT 480 479 +CONECT 481 479 +CONECT 482 483 484 +CONECT 483 482 +CONECT 484 482 +CONECT 485 486 487 +CONECT 486 485 +CONECT 487 485 +CONECT 488 489 490 +CONECT 489 488 +CONECT 490 488 +CONECT 491 492 493 +CONECT 492 491 +CONECT 493 491 +CONECT 494 495 496 +CONECT 495 494 +CONECT 496 494 +CONECT 497 498 499 +CONECT 498 497 +CONECT 499 497 +CONECT 500 501 502 +CONECT 501 500 +CONECT 502 500 +CONECT 503 504 505 +CONECT 504 503 +CONECT 505 503 +CONECT 506 507 508 +CONECT 507 506 +CONECT 508 506 +CONECT 509 510 511 +CONECT 510 509 +CONECT 511 509 +CONECT 512 513 514 +CONECT 513 512 +CONECT 514 512 +CONECT 515 516 517 +CONECT 516 515 +CONECT 517 515 +CONECT 518 519 520 +CONECT 519 518 +CONECT 520 518 +CONECT 521 522 523 +CONECT 522 521 +CONECT 523 521 +CONECT 524 525 526 +CONECT 525 524 +CONECT 526 524 +CONECT 527 528 529 +CONECT 528 527 +CONECT 529 527 +CONECT 530 531 532 +CONECT 531 530 +CONECT 532 530 +CONECT 533 534 535 +CONECT 534 533 +CONECT 535 533 +CONECT 536 537 538 +CONECT 537 536 +CONECT 538 536 +CONECT 539 540 541 +CONECT 540 539 +CONECT 541 539 +CONECT 542 543 544 +CONECT 543 542 +CONECT 544 542 +CONECT 545 546 547 +CONECT 546 545 +CONECT 547 545 +CONECT 548 549 550 +CONECT 549 548 +CONECT 550 548 +CONECT 551 552 553 +CONECT 552 551 +CONECT 553 551 +CONECT 554 555 556 +CONECT 555 554 +CONECT 556 554 +CONECT 557 558 559 +CONECT 558 557 +CONECT 559 557 +CONECT 560 561 562 +CONECT 561 560 +CONECT 562 560 +CONECT 563 564 565 +CONECT 564 563 +CONECT 565 563 +CONECT 566 567 568 +CONECT 567 566 +CONECT 568 566 +CONECT 569 570 571 +CONECT 570 569 +CONECT 571 569 +CONECT 572 573 574 +CONECT 573 572 +CONECT 574 572 +CONECT 575 576 577 +CONECT 576 575 +CONECT 577 575 +CONECT 578 579 580 +CONECT 579 578 +CONECT 580 578 +CONECT 581 582 583 +CONECT 582 581 +CONECT 583 581 +CONECT 584 585 586 +CONECT 585 584 +CONECT 586 584 +CONECT 587 588 589 +CONECT 588 587 +CONECT 589 587 +CONECT 590 591 592 +CONECT 591 590 +CONECT 592 590 +CONECT 593 594 595 +CONECT 594 593 +CONECT 595 593 +CONECT 596 597 598 +CONECT 597 596 +CONECT 598 596 +CONECT 599 600 601 +CONECT 600 599 +CONECT 601 599 +CONECT 602 603 604 +CONECT 603 602 +CONECT 604 602 +CONECT 605 606 607 +CONECT 606 605 +CONECT 607 605 +CONECT 608 609 610 +CONECT 609 608 +CONECT 610 608 +CONECT 611 612 613 +CONECT 612 611 +CONECT 613 611 +CONECT 614 615 616 +CONECT 615 614 +CONECT 616 614 +CONECT 617 618 619 +CONECT 618 617 +CONECT 619 617 +CONECT 620 621 622 +CONECT 621 620 +CONECT 622 620 +CONECT 623 624 625 +CONECT 624 623 +CONECT 625 623 +CONECT 626 627 628 +CONECT 627 626 +CONECT 628 626 +CONECT 629 630 631 +CONECT 630 629 +CONECT 631 629 +CONECT 632 633 634 +CONECT 633 632 +CONECT 634 632 +CONECT 635 636 637 +CONECT 636 635 +CONECT 637 635 +CONECT 638 639 640 +CONECT 639 638 +CONECT 640 638 +CONECT 641 642 643 +CONECT 642 641 +CONECT 643 641 +CONECT 644 645 646 +CONECT 645 644 +CONECT 646 644 +CONECT 647 648 649 +CONECT 648 647 +CONECT 649 647 +CONECT 650 651 652 +CONECT 651 650 +CONECT 652 650 +CONECT 653 654 655 +CONECT 654 653 +CONECT 655 653 +CONECT 656 657 658 +CONECT 657 656 +CONECT 658 656 +CONECT 659 660 661 +CONECT 660 659 +CONECT 661 659 +CONECT 662 663 664 +CONECT 663 662 +CONECT 664 662 +CONECT 665 666 667 +CONECT 666 665 +CONECT 667 665 +CONECT 668 669 670 +CONECT 669 668 +CONECT 670 668 +CONECT 671 672 673 +CONECT 672 671 +CONECT 673 671 +CONECT 674 675 676 +CONECT 675 674 +CONECT 676 674 +CONECT 677 678 679 +CONECT 678 677 +CONECT 679 677 +CONECT 680 681 682 +CONECT 681 680 +CONECT 682 680 +CONECT 683 684 685 +CONECT 684 683 +CONECT 685 683 +CONECT 686 687 688 +CONECT 687 686 +CONECT 688 686 +CONECT 689 690 691 +CONECT 690 689 +CONECT 691 689 +CONECT 692 693 694 +CONECT 693 692 +CONECT 694 692 +CONECT 695 696 697 +CONECT 696 695 +CONECT 697 695 +CONECT 698 699 700 +CONECT 699 698 +CONECT 700 698 +CONECT 701 702 703 +CONECT 702 701 +CONECT 703 701 +CONECT 704 705 706 +CONECT 705 704 +CONECT 706 704 +CONECT 707 708 709 +CONECT 708 707 +CONECT 709 707 +CONECT 710 711 712 +CONECT 711 710 +CONECT 712 710 +CONECT 713 714 715 +CONECT 714 713 +CONECT 715 713 +CONECT 716 717 718 +CONECT 717 716 +CONECT 718 716 +CONECT 719 720 721 +CONECT 720 719 +CONECT 721 719 +CONECT 722 723 724 +CONECT 723 722 +CONECT 724 722 +CONECT 725 726 727 +CONECT 726 725 +CONECT 727 725 +CONECT 728 729 730 +CONECT 729 728 +CONECT 730 728 +CONECT 731 732 733 +CONECT 732 731 +CONECT 733 731 +CONECT 734 735 736 +CONECT 735 734 +CONECT 736 734 +CONECT 737 738 739 +CONECT 738 737 +CONECT 739 737 +CONECT 740 741 742 +CONECT 741 740 +CONECT 742 740 +CONECT 743 744 745 +CONECT 744 743 +CONECT 745 743 +CONECT 746 747 748 +CONECT 747 746 +CONECT 748 746 +CONECT 749 750 751 +CONECT 750 749 +CONECT 751 749 +CONECT 752 753 754 +CONECT 753 752 +CONECT 754 752 +CONECT 755 756 757 +CONECT 756 755 +CONECT 757 755 +CONECT 758 759 760 +CONECT 759 758 +CONECT 760 758 +CONECT 761 762 763 +CONECT 762 761 +CONECT 763 761 +CONECT 764 765 766 +CONECT 765 764 +CONECT 766 764 +CONECT 767 768 769 +CONECT 768 767 +CONECT 769 767 +CONECT 770 771 772 +CONECT 771 770 +CONECT 772 770 +CONECT 773 774 775 +CONECT 774 773 +CONECT 775 773 +CONECT 776 777 778 +CONECT 777 776 +CONECT 778 776 +CONECT 779 780 781 +CONECT 780 779 +CONECT 781 779 +CONECT 782 783 784 +CONECT 783 782 +CONECT 784 782 +CONECT 785 786 787 +CONECT 786 785 +CONECT 787 785 +CONECT 788 789 790 +CONECT 789 788 +CONECT 790 788 +CONECT 791 792 793 +CONECT 792 791 +CONECT 793 791 +CONECT 794 795 796 +CONECT 795 794 +CONECT 796 794 +CONECT 797 798 799 +CONECT 798 797 +CONECT 799 797 +CONECT 800 801 802 +CONECT 801 800 +CONECT 802 800 +CONECT 803 804 805 +CONECT 804 803 +CONECT 805 803 +CONECT 806 807 808 +CONECT 807 806 +CONECT 808 806 +CONECT 809 810 811 +CONECT 810 809 +CONECT 811 809 +CONECT 812 813 814 +CONECT 813 812 +CONECT 814 812 +CONECT 815 816 817 +CONECT 816 815 +CONECT 817 815 +CONECT 818 819 820 +CONECT 819 818 +CONECT 820 818 +CONECT 821 822 823 +CONECT 822 821 +CONECT 823 821 +CONECT 824 825 826 +CONECT 825 824 +CONECT 826 824 +CONECT 827 828 829 +CONECT 828 827 +CONECT 829 827 +CONECT 830 831 832 +CONECT 831 830 +CONECT 832 830 +CONECT 833 834 835 +CONECT 834 833 +CONECT 835 833 +CONECT 836 837 838 +CONECT 837 836 +CONECT 838 836 +CONECT 839 840 841 +CONECT 840 839 +CONECT 841 839 +CONECT 842 843 844 +CONECT 843 842 +CONECT 844 842 +CONECT 845 846 847 +CONECT 846 845 +CONECT 847 845 +CONECT 848 849 850 +CONECT 849 848 +CONECT 850 848 +CONECT 851 852 853 +CONECT 852 851 +CONECT 853 851 +CONECT 854 855 856 +CONECT 855 854 +CONECT 856 854 +CONECT 857 858 859 +CONECT 858 857 +CONECT 859 857 +CONECT 860 861 862 +CONECT 861 860 +CONECT 862 860 +CONECT 863 864 865 +CONECT 864 863 +CONECT 865 863 +CONECT 866 867 868 +CONECT 867 866 +CONECT 868 866 +CONECT 869 870 871 +CONECT 870 869 +CONECT 871 869 +CONECT 872 873 874 +CONECT 873 872 +CONECT 874 872 +CONECT 875 876 877 +CONECT 876 875 +CONECT 877 875 +CONECT 878 879 880 +CONECT 879 878 +CONECT 880 878 +CONECT 881 882 883 +CONECT 882 881 +CONECT 883 881 +CONECT 884 885 886 +CONECT 885 884 +CONECT 886 884 +CONECT 887 888 889 +CONECT 888 887 +CONECT 889 887 +CONECT 890 891 892 +CONECT 891 890 +CONECT 892 890 +CONECT 893 894 895 +CONECT 894 893 +CONECT 895 893 +CONECT 896 897 898 +CONECT 897 896 +CONECT 898 896 +CONECT 899 900 901 +CONECT 900 899 +CONECT 901 899 +CONECT 902 903 904 +CONECT 903 902 +CONECT 904 902 +CONECT 905 906 907 +CONECT 906 905 +CONECT 907 905 +CONECT 908 909 910 +CONECT 909 908 +CONECT 910 908 +CONECT 911 912 913 +CONECT 912 911 +CONECT 913 911 +CONECT 914 915 916 +CONECT 915 914 +CONECT 916 914 +CONECT 917 918 919 +CONECT 918 917 +CONECT 919 917 +CONECT 920 921 922 +CONECT 921 920 +CONECT 922 920 +CONECT 923 924 925 +CONECT 924 923 +CONECT 925 923 +CONECT 926 927 928 +CONECT 927 926 +CONECT 928 926 +CONECT 929 930 931 +CONECT 930 929 +CONECT 931 929 +CONECT 932 933 934 +CONECT 933 932 +CONECT 934 932 +CONECT 935 936 937 +CONECT 936 935 +CONECT 937 935 +CONECT 938 939 940 +CONECT 939 938 +CONECT 940 938 +CONECT 941 942 943 +CONECT 942 941 +CONECT 943 941 +CONECT 944 945 946 +CONECT 945 944 +CONECT 946 944 +CONECT 947 948 949 +CONECT 948 947 +CONECT 949 947 +CONECT 950 951 952 +CONECT 951 950 +CONECT 952 950 +CONECT 953 954 955 +CONECT 954 953 +CONECT 955 953 +CONECT 956 957 958 +CONECT 957 956 +CONECT 958 956 +CONECT 959 960 961 +CONECT 960 959 +CONECT 961 959 +CONECT 962 963 964 +CONECT 963 962 +CONECT 964 962 +CONECT 965 966 967 +CONECT 966 965 +CONECT 967 965 +CONECT 968 969 970 +CONECT 969 968 +CONECT 970 968 +CONECT 971 972 973 +CONECT 972 971 +CONECT 973 971 +CONECT 974 975 976 +CONECT 975 974 +CONECT 976 974 +CONECT 977 978 979 +CONECT 978 977 +CONECT 979 977 +CONECT 980 981 982 +CONECT 981 980 +CONECT 982 980 +CONECT 983 984 985 +CONECT 984 983 +CONECT 985 983 +CONECT 986 987 988 +CONECT 987 986 +CONECT 988 986 +CONECT 989 990 991 +CONECT 990 989 +CONECT 991 989 +CONECT 992 993 994 +CONECT 993 992 +CONECT 994 992 +CONECT 995 996 997 +CONECT 996 995 +CONECT 997 995 +CONECT 998 999 1000 +CONECT 999 998 +CONECT 1000 998 +CONECT 1001 1002 1003 +CONECT 1002 1001 +CONECT 1003 1001 +CONECT 1004 1005 1006 +CONECT 1005 1004 +CONECT 1006 1004 +CONECT 1007 1008 1009 +CONECT 1008 1007 +CONECT 1009 1007 +CONECT 1010 1011 1012 +CONECT 1011 1010 +CONECT 1012 1010 +CONECT 1013 1014 1015 +CONECT 1014 1013 +CONECT 1015 1013 +CONECT 1016 1017 1018 +CONECT 1017 1016 +CONECT 1018 1016 +CONECT 1019 1020 1021 +CONECT 1020 1019 +CONECT 1021 1019 +CONECT 1022 1023 1024 +CONECT 1023 1022 +CONECT 1024 1022 +CONECT 1025 1026 1027 +CONECT 1026 1025 +CONECT 1027 1025 +CONECT 1028 1029 1030 +CONECT 1029 1028 +CONECT 1030 1028 +CONECT 1031 1032 1033 +CONECT 1032 1031 +CONECT 1033 1031 +CONECT 1034 1035 1036 +CONECT 1035 1034 +CONECT 1036 1034 +CONECT 1037 1038 1039 +CONECT 1038 1037 +CONECT 1039 1037 +CONECT 1040 1041 1042 +CONECT 1041 1040 +CONECT 1042 1040 +CONECT 1043 1044 1045 +CONECT 1044 1043 +CONECT 1045 1043 +CONECT 1046 1047 1048 +CONECT 1047 1046 +CONECT 1048 1046 +CONECT 1049 1050 1051 +CONECT 1050 1049 +CONECT 1051 1049 +CONECT 1052 1053 1054 +CONECT 1053 1052 +CONECT 1054 1052 +CONECT 1055 1056 1057 +CONECT 1056 1055 +CONECT 1057 1055 +CONECT 1058 1059 1060 +CONECT 1059 1058 +CONECT 1060 1058 +CONECT 1061 1062 1063 +CONECT 1062 1061 +CONECT 1063 1061 +CONECT 1064 1065 1066 +CONECT 1065 1064 +CONECT 1066 1064 +CONECT 1067 1068 1069 +CONECT 1068 1067 +CONECT 1069 1067 +CONECT 1070 1071 1072 +CONECT 1071 1070 +CONECT 1072 1070 +CONECT 1073 1074 1075 +CONECT 1074 1073 +CONECT 1075 1073 +CONECT 1076 1077 1078 +CONECT 1077 1076 +CONECT 1078 1076 +CONECT 1079 1080 1081 +CONECT 1080 1079 +CONECT 1081 1079 +CONECT 1082 1083 1084 +CONECT 1083 1082 +CONECT 1084 1082 +CONECT 1085 1086 1087 +CONECT 1086 1085 +CONECT 1087 1085 +CONECT 1088 1089 1090 +CONECT 1089 1088 +CONECT 1090 1088 +CONECT 1091 1092 1093 +CONECT 1092 1091 +CONECT 1093 1091 +CONECT 1094 1095 1096 +CONECT 1095 1094 +CONECT 1096 1094 +CONECT 1097 1098 1099 +CONECT 1098 1097 +CONECT 1099 1097 +CONECT 1100 1101 1102 +CONECT 1101 1100 +CONECT 1102 1100 +CONECT 1103 1104 1105 +CONECT 1104 1103 +CONECT 1105 1103 +CONECT 1106 1107 1108 +CONECT 1107 1106 +CONECT 1108 1106 +CONECT 1109 1110 1111 +CONECT 1110 1109 +CONECT 1111 1109 +CONECT 1112 1113 1114 +CONECT 1113 1112 +CONECT 1114 1112 +CONECT 1115 1116 1117 +CONECT 1116 1115 +CONECT 1117 1115 +CONECT 1118 1119 1120 +CONECT 1119 1118 +CONECT 1120 1118 +CONECT 1121 1122 1123 +CONECT 1122 1121 +CONECT 1123 1121 +CONECT 1124 1125 1126 +CONECT 1125 1124 +CONECT 1126 1124 +CONECT 1127 1128 1129 +CONECT 1128 1127 +CONECT 1129 1127 +CONECT 1130 1131 1132 +CONECT 1131 1130 +CONECT 1132 1130 +CONECT 1133 1134 1135 +CONECT 1134 1133 +CONECT 1135 1133 +CONECT 1136 1137 1138 +CONECT 1137 1136 +CONECT 1138 1136 +CONECT 1139 1140 1141 +CONECT 1140 1139 +CONECT 1141 1139 +CONECT 1142 1143 1144 +CONECT 1143 1142 +CONECT 1144 1142 +CONECT 1145 1146 1147 +CONECT 1146 1145 +CONECT 1147 1145 +CONECT 1148 1149 1150 +CONECT 1149 1148 +CONECT 1150 1148 +CONECT 1151 1152 1153 +CONECT 1152 1151 +CONECT 1153 1151 +CONECT 1154 1155 1156 +CONECT 1155 1154 +CONECT 1156 1154 +CONECT 1157 1158 1159 +CONECT 1158 1157 +CONECT 1159 1157 +CONECT 1160 1161 1162 +CONECT 1161 1160 +CONECT 1162 1160 +CONECT 1163 1164 1165 +CONECT 1164 1163 +CONECT 1165 1163 +CONECT 1166 1167 1168 +CONECT 1167 1166 +CONECT 1168 1166 +CONECT 1169 1170 1171 +CONECT 1170 1169 +CONECT 1171 1169 +CONECT 1172 1173 1174 +CONECT 1173 1172 +CONECT 1174 1172 +CONECT 1175 1176 1177 +CONECT 1176 1175 +CONECT 1177 1175 +CONECT 1178 1179 1180 +CONECT 1179 1178 +CONECT 1180 1178 +CONECT 1181 1182 1183 +CONECT 1182 1181 +CONECT 1183 1181 +CONECT 1184 1185 1186 +CONECT 1185 1184 +CONECT 1186 1184 +CONECT 1187 1188 1189 +CONECT 1188 1187 +CONECT 1189 1187 +CONECT 1190 1191 1192 +CONECT 1191 1190 +CONECT 1192 1190 +CONECT 1193 1194 1195 +CONECT 1194 1193 +CONECT 1195 1193 +CONECT 1196 1197 1198 +CONECT 1197 1196 +CONECT 1198 1196 +CONECT 1199 1200 1201 +CONECT 1200 1199 +CONECT 1201 1199 +CONECT 1202 1203 1204 +CONECT 1203 1202 +CONECT 1204 1202 +CONECT 1205 1206 1207 +CONECT 1206 1205 +CONECT 1207 1205 +CONECT 1208 1209 1210 +CONECT 1209 1208 +CONECT 1210 1208 +CONECT 1211 1212 1213 +CONECT 1212 1211 +CONECT 1213 1211 +CONECT 1214 1215 1216 +CONECT 1215 1214 +CONECT 1216 1214 +CONECT 1217 1218 1219 +CONECT 1218 1217 +CONECT 1219 1217 +CONECT 1220 1221 1222 +CONECT 1221 1220 +CONECT 1222 1220 +CONECT 1223 1224 1225 +CONECT 1224 1223 +CONECT 1225 1223 +CONECT 1226 1227 1228 +CONECT 1227 1226 +CONECT 1228 1226 +CONECT 1229 1230 1231 +CONECT 1230 1229 +CONECT 1231 1229 +CONECT 1232 1233 1234 +CONECT 1233 1232 +CONECT 1234 1232 +CONECT 1235 1236 1237 +CONECT 1236 1235 +CONECT 1237 1235 +CONECT 1238 1239 1240 +CONECT 1239 1238 +CONECT 1240 1238 +CONECT 1241 1242 1243 +CONECT 1242 1241 +CONECT 1243 1241 +CONECT 1244 1245 1246 +CONECT 1245 1244 +CONECT 1246 1244 +CONECT 1247 1248 1249 +CONECT 1248 1247 +CONECT 1249 1247 +CONECT 1250 1251 1252 +CONECT 1251 1250 +CONECT 1252 1250 +CONECT 1253 1254 1255 +CONECT 1254 1253 +CONECT 1255 1253 +CONECT 1256 1257 1258 +CONECT 1257 1256 +CONECT 1258 1256 +CONECT 1259 1260 1261 +CONECT 1260 1259 +CONECT 1261 1259 +CONECT 1262 1263 1264 +CONECT 1263 1262 +CONECT 1264 1262 +CONECT 1265 1266 1267 +CONECT 1266 1265 +CONECT 1267 1265 +CONECT 1268 1269 1270 +CONECT 1269 1268 +CONECT 1270 1268 +CONECT 1271 1272 1273 +CONECT 1272 1271 +CONECT 1273 1271 +CONECT 1274 1275 1276 +CONECT 1275 1274 +CONECT 1276 1274 +CONECT 1277 1278 1279 +CONECT 1278 1277 +CONECT 1279 1277 +CONECT 1280 1281 1282 +CONECT 1281 1280 +CONECT 1282 1280 +CONECT 1283 1284 1285 +CONECT 1284 1283 +CONECT 1285 1283 +CONECT 1286 1287 1288 +CONECT 1287 1286 +CONECT 1288 1286 +CONECT 1289 1290 1291 +CONECT 1290 1289 +CONECT 1291 1289 +CONECT 1292 1293 1294 +CONECT 1293 1292 +CONECT 1294 1292 +CONECT 1295 1296 1297 +CONECT 1296 1295 +CONECT 1297 1295 +CONECT 1298 1299 1300 +CONECT 1299 1298 +CONECT 1300 1298 +CONECT 1301 1302 1303 +CONECT 1302 1301 +CONECT 1303 1301 +CONECT 1304 1305 1306 +CONECT 1305 1304 +CONECT 1306 1304 +CONECT 1307 1308 1309 +CONECT 1308 1307 +CONECT 1309 1307 +CONECT 1310 1311 1312 +CONECT 1311 1310 +CONECT 1312 1310 +CONECT 1313 1314 1315 +CONECT 1314 1313 +CONECT 1315 1313 +CONECT 1316 1317 1318 +CONECT 1317 1316 +CONECT 1318 1316 +CONECT 1319 1320 1321 +CONECT 1320 1319 +CONECT 1321 1319 +CONECT 1322 1323 1324 +CONECT 1323 1322 +CONECT 1324 1322 +CONECT 1325 1326 1327 +CONECT 1326 1325 +CONECT 1327 1325 +CONECT 1328 1329 1330 +CONECT 1329 1328 +CONECT 1330 1328 +CONECT 1331 1332 1333 +CONECT 1332 1331 +CONECT 1333 1331 +CONECT 1334 1335 1336 +CONECT 1335 1334 +CONECT 1336 1334 +CONECT 1337 1338 1339 +CONECT 1338 1337 +CONECT 1339 1337 +CONECT 1340 1341 1342 +CONECT 1341 1340 +CONECT 1342 1340 +CONECT 1343 1344 1345 +CONECT 1344 1343 +CONECT 1345 1343 +CONECT 1346 1347 1348 +CONECT 1347 1346 +CONECT 1348 1346 +CONECT 1349 1350 1351 +CONECT 1350 1349 +CONECT 1351 1349 +CONECT 1352 1353 1354 +CONECT 1353 1352 +CONECT 1354 1352 +CONECT 1355 1356 1357 +CONECT 1356 1355 +CONECT 1357 1355 +CONECT 1358 1359 1360 +CONECT 1359 1358 +CONECT 1360 1358 +CONECT 1361 1362 1363 +CONECT 1362 1361 +CONECT 1363 1361 +CONECT 1364 1365 1366 +CONECT 1365 1364 +CONECT 1366 1364 +CONECT 1367 1368 1369 +CONECT 1368 1367 +CONECT 1369 1367 +CONECT 1370 1371 1372 +CONECT 1371 1370 +CONECT 1372 1370 +CONECT 1373 1374 1375 +CONECT 1374 1373 +CONECT 1375 1373 +CONECT 1376 1377 1378 +CONECT 1377 1376 +CONECT 1378 1376 +CONECT 1379 1380 1381 +CONECT 1380 1379 +CONECT 1381 1379 +CONECT 1382 1383 1384 +CONECT 1383 1382 +CONECT 1384 1382 +CONECT 1385 1386 1387 +CONECT 1386 1385 +CONECT 1387 1385 +CONECT 1388 1389 1390 +CONECT 1389 1388 +CONECT 1390 1388 +CONECT 1391 1392 1393 +CONECT 1392 1391 +CONECT 1393 1391 +CONECT 1394 1395 1396 +CONECT 1395 1394 +CONECT 1396 1394 +CONECT 1397 1398 1399 +CONECT 1398 1397 +CONECT 1399 1397 +CONECT 1400 1401 1402 +CONECT 1401 1400 +CONECT 1402 1400 +CONECT 1403 1404 1405 +CONECT 1404 1403 +CONECT 1405 1403 +CONECT 1406 1407 1408 +CONECT 1407 1406 +CONECT 1408 1406 +CONECT 1409 1410 1411 +CONECT 1410 1409 +CONECT 1411 1409 +CONECT 1412 1413 1414 +CONECT 1413 1412 +CONECT 1414 1412 +CONECT 1415 1416 1417 +CONECT 1416 1415 +CONECT 1417 1415 +CONECT 1418 1419 1420 +CONECT 1419 1418 +CONECT 1420 1418 +CONECT 1421 1422 1423 +CONECT 1422 1421 +CONECT 1423 1421 +CONECT 1424 1425 1426 +CONECT 1425 1424 +CONECT 1426 1424 +CONECT 1427 1428 1429 +CONECT 1428 1427 +CONECT 1429 1427 +CONECT 1430 1431 1432 +CONECT 1431 1430 +CONECT 1432 1430 +CONECT 1433 1434 1435 +CONECT 1434 1433 +CONECT 1435 1433 +CONECT 1436 1437 1438 +CONECT 1437 1436 +CONECT 1438 1436 +CONECT 1439 1440 1441 +CONECT 1440 1439 +CONECT 1441 1439 +CONECT 1442 1443 1444 +CONECT 1443 1442 +CONECT 1444 1442 +CONECT 1445 1446 1447 +CONECT 1446 1445 +CONECT 1447 1445 +CONECT 1448 1449 1450 +CONECT 1449 1448 +CONECT 1450 1448 +CONECT 1451 1452 1453 +CONECT 1452 1451 +CONECT 1453 1451 +CONECT 1454 1455 1456 +CONECT 1455 1454 +CONECT 1456 1454 +CONECT 1457 1458 1459 +CONECT 1458 1457 +CONECT 1459 1457 +CONECT 1460 1461 1462 +CONECT 1461 1460 +CONECT 1462 1460 +CONECT 1463 1464 1465 +CONECT 1464 1463 +CONECT 1465 1463 +CONECT 1466 1467 1468 +CONECT 1467 1466 +CONECT 1468 1466 +CONECT 1469 1470 1471 +CONECT 1470 1469 +CONECT 1471 1469 +CONECT 1472 1473 1474 +CONECT 1473 1472 +CONECT 1474 1472 +CONECT 1475 1476 1477 +CONECT 1476 1475 +CONECT 1477 1475 +CONECT 1478 1479 1480 +CONECT 1479 1478 +CONECT 1480 1478 +CONECT 1481 1482 1483 +CONECT 1482 1481 +CONECT 1483 1481 +CONECT 1484 1485 1486 +CONECT 1485 1484 +CONECT 1486 1484 +CONECT 1487 1488 1489 +CONECT 1488 1487 +CONECT 1489 1487 +CONECT 1490 1491 1492 +CONECT 1491 1490 +CONECT 1492 1490 +CONECT 1493 1494 1495 +CONECT 1494 1493 +CONECT 1495 1493 +CONECT 1496 1497 1498 +CONECT 1497 1496 +CONECT 1498 1496 +CONECT 1499 1500 1501 +CONECT 1500 1499 +CONECT 1501 1499 +CONECT 1502 1503 1504 +CONECT 1503 1502 +CONECT 1504 1502 +CONECT 1505 1506 1507 +CONECT 1506 1505 +CONECT 1507 1505 +CONECT 1508 1509 1510 +CONECT 1509 1508 +CONECT 1510 1508 +CONECT 1511 1512 1513 +CONECT 1512 1511 +CONECT 1513 1511 +CONECT 1514 1515 1516 +CONECT 1515 1514 +CONECT 1516 1514 +CONECT 1517 1518 1519 +CONECT 1518 1517 +CONECT 1519 1517 +CONECT 1520 1521 1522 +CONECT 1521 1520 +CONECT 1522 1520 +CONECT 1523 1524 1525 +CONECT 1524 1523 +CONECT 1525 1523 +CONECT 1526 1527 1528 +CONECT 1527 1526 +CONECT 1528 1526 +CONECT 1529 1530 1531 +CONECT 1530 1529 +CONECT 1531 1529 +CONECT 1532 1533 1534 +CONECT 1533 1532 +CONECT 1534 1532 +CONECT 1535 1536 1537 +CONECT 1536 1535 +CONECT 1537 1535 +CONECT 1538 1539 1540 +CONECT 1539 1538 +CONECT 1540 1538 +CONECT 1541 1542 1543 +CONECT 1542 1541 +CONECT 1543 1541 +CONECT 1544 1545 1546 +CONECT 1545 1544 +CONECT 1546 1544 +CONECT 1547 1548 1549 +CONECT 1548 1547 +CONECT 1549 1547 +CONECT 1550 1551 1552 +CONECT 1551 1550 +CONECT 1552 1550 +CONECT 1553 1554 1555 +CONECT 1554 1553 +CONECT 1555 1553 +CONECT 1556 1557 1558 +CONECT 1557 1556 +CONECT 1558 1556 +CONECT 1559 1560 1561 +CONECT 1560 1559 +CONECT 1561 1559 +CONECT 1562 1563 1564 +CONECT 1563 1562 +CONECT 1564 1562 +CONECT 1565 1566 1567 +CONECT 1566 1565 +CONECT 1567 1565 +CONECT 1568 1569 1570 +CONECT 1569 1568 +CONECT 1570 1568 +CONECT 1571 1572 1573 +CONECT 1572 1571 +CONECT 1573 1571 +CONECT 1574 1575 1576 +CONECT 1575 1574 +CONECT 1576 1574 +CONECT 1577 1578 1579 +CONECT 1578 1577 +CONECT 1579 1577 +CONECT 1580 1581 1582 +CONECT 1581 1580 +CONECT 1582 1580 +CONECT 1583 1584 1585 +CONECT 1584 1583 +CONECT 1585 1583 +CONECT 1586 1587 1588 +CONECT 1587 1586 +CONECT 1588 1586 +CONECT 1589 1590 1591 +CONECT 1590 1589 +CONECT 1591 1589 +CONECT 1592 1593 1594 +CONECT 1593 1592 +CONECT 1594 1592 +CONECT 1595 1596 1597 +CONECT 1596 1595 +CONECT 1597 1595 +CONECT 1598 1599 1600 +CONECT 1599 1598 +CONECT 1600 1598 +CONECT 1601 1602 1603 +CONECT 1602 1601 +CONECT 1603 1601 +CONECT 1604 1605 1606 +CONECT 1605 1604 +CONECT 1606 1604 +CONECT 1607 1608 1609 +CONECT 1608 1607 +CONECT 1609 1607 +CONECT 1610 1611 1612 +CONECT 1611 1610 +CONECT 1612 1610 +CONECT 1613 1614 1615 +CONECT 1614 1613 +CONECT 1615 1613 +CONECT 1616 1617 1618 +CONECT 1617 1616 +CONECT 1618 1616 +CONECT 1619 1620 1621 +CONECT 1620 1619 +CONECT 1621 1619 +CONECT 1622 1623 1624 +CONECT 1623 1622 +CONECT 1624 1622 +CONECT 1625 1626 1627 +CONECT 1626 1625 +CONECT 1627 1625 +CONECT 1628 1629 1630 +CONECT 1629 1628 +CONECT 1630 1628 +CONECT 1631 1632 1633 +CONECT 1632 1631 +CONECT 1633 1631 +CONECT 1634 1635 1636 +CONECT 1635 1634 +CONECT 1636 1634 +CONECT 1637 1638 1639 +CONECT 1638 1637 +CONECT 1639 1637 +CONECT 1640 1641 1642 +CONECT 1641 1640 +CONECT 1642 1640 +CONECT 1643 1644 1645 +CONECT 1644 1643 +CONECT 1645 1643 +CONECT 1646 1647 1648 +CONECT 1647 1646 +CONECT 1648 1646 +CONECT 1649 1650 1651 +CONECT 1650 1649 +CONECT 1651 1649 +CONECT 1652 1653 1654 +CONECT 1653 1652 +CONECT 1654 1652 +CONECT 1655 1656 1657 +CONECT 1656 1655 +CONECT 1657 1655 +CONECT 1658 1659 1660 +CONECT 1659 1658 +CONECT 1660 1658 +CONECT 1661 1662 1663 +CONECT 1662 1661 +CONECT 1663 1661 +CONECT 1664 1665 1666 +CONECT 1665 1664 +CONECT 1666 1664 +CONECT 1667 1668 1669 +CONECT 1668 1667 +CONECT 1669 1667 +CONECT 1670 1671 1672 +CONECT 1671 1670 +CONECT 1672 1670 +CONECT 1673 1674 1675 +CONECT 1674 1673 +CONECT 1675 1673 +CONECT 1676 1677 1678 +CONECT 1677 1676 +CONECT 1678 1676 +CONECT 1679 1680 1681 +CONECT 1680 1679 +CONECT 1681 1679 +CONECT 1682 1683 1684 +CONECT 1683 1682 +CONECT 1684 1682 +CONECT 1685 1686 1687 +CONECT 1686 1685 +CONECT 1687 1685 +CONECT 1688 1689 1690 +CONECT 1689 1688 +CONECT 1690 1688 +CONECT 1691 1692 1693 +CONECT 1692 1691 +CONECT 1693 1691 +CONECT 1694 1695 1696 +CONECT 1695 1694 +CONECT 1696 1694 +CONECT 1697 1698 1699 +CONECT 1698 1697 +CONECT 1699 1697 +CONECT 1700 1701 1702 +CONECT 1701 1700 +CONECT 1702 1700 +CONECT 1703 1704 1705 +CONECT 1704 1703 +CONECT 1705 1703 +CONECT 1706 1707 1708 +CONECT 1707 1706 +CONECT 1708 1706 +CONECT 1709 1710 1711 +CONECT 1710 1709 +CONECT 1711 1709 +CONECT 1712 1713 1714 +CONECT 1713 1712 +CONECT 1714 1712 +CONECT 1715 1716 1717 +CONECT 1716 1715 +CONECT 1717 1715 +CONECT 1718 1719 1720 +CONECT 1719 1718 +CONECT 1720 1718 +CONECT 1721 1722 1723 +CONECT 1722 1721 +CONECT 1723 1721 +CONECT 1724 1725 1726 +CONECT 1725 1724 +CONECT 1726 1724 +CONECT 1727 1728 1729 +CONECT 1728 1727 +CONECT 1729 1727 +CONECT 1730 1731 1732 +CONECT 1731 1730 +CONECT 1732 1730 +CONECT 1733 1734 1735 +CONECT 1734 1733 +CONECT 1735 1733 +CONECT 1736 1737 1738 +CONECT 1737 1736 +CONECT 1738 1736 +CONECT 1739 1740 1741 +CONECT 1740 1739 +CONECT 1741 1739 +CONECT 1742 1743 1744 +CONECT 1743 1742 +CONECT 1744 1742 +CONECT 1745 1746 1747 +CONECT 1746 1745 +CONECT 1747 1745 +CONECT 1748 1749 1750 +CONECT 1749 1748 +CONECT 1750 1748 +CONECT 1751 1752 1753 +CONECT 1752 1751 +CONECT 1753 1751 +MASTER 0 0 0 0 0 0 0 0 1753 0 1753 0 +END diff --git a/scine_puffin/tests/resources/8-gly-chain-opt.xyz b/scine_puffin/tests/resources/8-gly-chain-opt.xyz new file mode 100644 index 0000000..5d635e0 --- /dev/null +++ b/scine_puffin/tests/resources/8-gly-chain-opt.xyz @@ -0,0 +1,75 @@ +73 +optimized with gfn2/gbsa(water) +C -9.1595144272 4.6848201752 -0.8650802970 +C -8.3268938065 3.4010460377 -0.8202669621 +N -10.3994550705 4.5835866928 -0.1055065915 +O -8.6749172211 2.4117383957 -0.1929113865 +H -9.4104986191 4.9036049843 -1.9105601311 +H -8.5269489288 5.4976859093 -0.4949254990 +H -10.4535350800 3.6558165550 0.3083664179 +C -6.1676840782 2.4219241142 -1.5506534576 +C -4.9104256630 3.0593540668 -2.1470673084 +N -7.1613955498 3.4846005440 -1.4855599403 +O -4.9633421898 3.5770325661 -3.2552559376 +H -6.0194573402 2.0077784061 -0.5520618558 +H -6.5163106918 1.6208312511 -2.2154092789 +H -6.9916310310 4.2664666176 -2.1023349762 +C -2.6565029621 3.8815040588 -1.8256149292 +C -1.6924971342 4.1638274193 -0.6671522856 +N -3.8015847206 3.0984392166 -1.3959696293 +O -1.8275794983 3.6344318390 0.4248850048 +H -2.1121926308 3.3509414196 -2.6153519154 +H -3.0325703621 4.8167910576 -2.2520260811 +H -3.7302973270 2.6635463238 -0.4849408865 +C 0.3780808151 5.3932375908 -0.0697760060 +C 1.3709466457 6.3027267456 -0.8096673489 +N -0.6869525313 4.9979267120 -0.9808021188 +O 0.9772907495 7.0078601837 -1.7286738157 +H -0.0479640402 5.9810371399 0.7552043200 +H 0.8505551815 4.5058879852 0.3547314703 +H -0.6828391552 5.4951367378 -1.8637034893 +H 10.8946123123 13.0760993958 -1.3322688341 +C 3.5987129211 7.2600550652 -0.9661633372 +C 5.0701255798 6.9577441216 -0.6475380659 +N 2.6476628780 6.3204603195 -0.3864783943 +O 5.4166164398 6.0328521729 0.0662100613 +H 3.4470429420 7.2703533173 -2.0497319698 +H 3.3825180531 8.2658376694 -0.5864674449 +H 2.9993774891 5.7014250755 0.3314624727 +C 7.3725528717 7.8049116135 -1.0237170458 +C 8.0727624893 9.0564985275 -1.5896153450 +N 5.9225201607 7.8563389778 -1.1874067783 +O 8.0825366974 9.2650108337 -2.7942357063 +H 7.6042203903 7.6423673630 0.0299255569 +H 7.7729682922 6.9569296837 -1.5977802277 +H 5.5834918022 8.5062551498 -1.8813923597 +H 11.5526790619 13.9536180496 1.2142589092 +C 9.5525121689 10.9628763199 -1.1344981194 +C 9.7082557678 12.0355529785 -0.0435910672 +N 8.7182674408 9.8465957642 -0.7131417990 +O 9.1657791138 11.9383478165 1.0436633825 +H 10.5511503220 10.5890989304 -1.3923562765 +H 9.1131210327 11.4052810669 -2.0335333347 +H 8.6134538651 9.7321224213 0.2858773470 +C 10.8166484833 14.2085371017 0.4467360377 +C 11.2261056900 15.4468488693 -0.3709571362 +N 10.5283384323 13.0489244461 -0.3935002387 +O 10.4212884903 15.9613714218 -1.1341047287 +H 9.8839187622 14.4905891418 0.9531626701 +C 12.7378778458 17.3180046082 -0.6683366895 +C 14.2190465927 17.6392669678 -0.8701280355 +N 12.4468984604 15.9826564789 -0.1653366834 +O 15.1255846024 16.8895244598 -0.5527139902 +H 12.1996307373 17.4464588165 -1.6113750935 +H 12.3493804932 18.0547847748 0.0465964042 +H 13.1805305481 15.5225667953 0.3559043705 +C 15.7333946228 19.4057788849 -1.6553828716 +C 15.6851034164 20.7534999847 -2.3352193832 +N 14.4208478928 18.8615360260 -1.4013431072 +O 14.6830844879 21.3607692719 -2.6068284512 +H 16.3232021332 19.5049571991 -0.7379171848 +H 16.3041763306 18.7514591217 -2.3274137974 +H 13.6517944336 19.4616889954 -1.6695697308 +O 16.9361629486 21.1544437408 -2.6001026630 +H -10.4089860916 5.2540292740 0.6528951526 +H 17.1167392731 21.3788852692 -3.5246484280 diff --git a/scine_puffin/tests/resources/c5of8.mol b/scine_puffin/tests/resources/c5of8.mol new file mode 100644 index 0000000..231228a --- /dev/null +++ b/scine_puffin/tests/resources/c5of8.mol @@ -0,0 +1,32 @@ +c5of8.mol +__Jmol-14_11292311103D 1 1.00000 0.00000 0 +Jmol version 14.32.3 2021-12-08 13:29 EXTRACT: ({0:13}) + 14 13 0 0 0 0 999 V2000 + -1.9696 1.0028 0.1585 C 0 0 0 0 0 0 + -1.0663 -0.0070 -0.8149 C 0 0 0 0 0 0 + 0.5273 -0.3073 -0.5078 C 0 0 2 0 0 0 + 0.5951 -1.7320 0.3330 C 0 0 0 0 0 0 + 1.3093 0.9343 0.2285 C 0 0 2 0 0 0 + -1.7817 -0.6060 -1.9573 O 0 0 0 0 0 0 + -3.1009 1.8218 -0.3710 F 0 0 0 0 0 0 + -1.6424 1.1262 1.6075 F 0 0 0 0 0 0 + 1.2138 -0.5287 -1.8149 F 0 0 0 0 0 0 + -0.4392 -2.8089 0.1312 F 0 0 0 0 0 0 + 1.6604 -2.0461 1.3283 F 0 0 0 0 0 0 + 0.8918 2.2929 -0.2800 F 0 0 0 0 0 0 + 2.7570 0.8092 -0.1857 F 0 0 0 0 0 0 + 1.3207 0.8601 1.7174 F 0 0 0 0 0 0 + 1 2 1 0 0 0 + 1 8 1 0 0 0 + 2 3 1 0 0 0 + 3 9 1 0 0 0 + 3 4 1 0 0 0 + 3 5 1 0 0 0 + 4 11 1 0 0 0 + 5 14 1 0 0 0 + 5 13 1 0 0 0 + 6 2 1 0 0 0 + 7 1 1 0 0 0 + 10 4 1 0 0 0 + 12 5 1 0 0 0 +M END diff --git a/scine_puffin/tests/resources/co2.json b/scine_puffin/tests/resources/co2.json new file mode 100644 index 0000000..399eed7 --- /dev/null +++ b/scine_puffin/tests/resources/co2.json @@ -0,0 +1,5 @@ +{ + "masm_cbor_graph" : "pGFhgaVhYQBhYwJhb4GCAAFhcqNhbIKBAIEBYmxygYIAAWFzgYIAAWFzAGFjD2FnomFFgoMAAgCDAQIAYVqDCAgGYXaDAgAB", + "masm_decision_list" : "", + "masm_idx_map" : "[(0, 0), (0, 2), (0, 1)]" +} diff --git a/scine_puffin/tests/resources/co2.xyz b/scine_puffin/tests/resources/co2.xyz new file mode 100644 index 0000000..ad325b4 --- /dev/null +++ b/scine_puffin/tests/resources/co2.xyz @@ -0,0 +1,5 @@ +3 + +O -1.1708043981 -0.0000000000 -0.0000000000 +C 0.0008999662 -0.0000000000 0.0000000000 +O 1.1699044319 -0.0000000000 0.0000000000 diff --git a/scine_puffin/tests/resources/iodine_in_water.xyz b/scine_puffin/tests/resources/iodine_in_water.xyz new file mode 100644 index 0000000..1a4c141 --- /dev/null +++ b/scine_puffin/tests/resources/iodine_in_water.xyz @@ -0,0 +1,100 @@ +98 + +I -1.1767483312 1.6563034829 -1.8119833258 +I -0.1967189536 0.8991698587 0.6730833099 +O 0.1139885045 -1.2516302985 -1.9798820760 +H -0.4812535979 -1.4855316698 -1.2422683272 +H 0.9643309465 -1.0070072377 -1.5554742279 +O -1.2665954137 -2.2342334364 -0.0405528077 +H -1.3679460549 -2.0060366345 0.8993757045 +H -0.5668119590 -2.9297148154 -0.0790476805 +O 3.1426902088 1.1354852194 0.8510230054 +H 3.2056257425 1.7519245742 0.0756733387 +H 2.6880977342 1.6584120409 1.5444039856 +O -3.9397387500 2.9850515076 -1.8572196192 +H -4.7594846017 3.4580006180 -1.9883815315 +H -3.4700476601 3.4096195709 -1.0951176998 +O -3.5204457052 -2.2115139725 -1.1914985768 +H -2.6223289096 -2.2087186856 -0.7889114418 +H -3.7491218059 -1.2630456977 -1.2726673657 +O -3.3990519977 1.3282399757 3.5663253267 +H -3.5689929527 0.7793811901 2.7710985507 +H -2.6111651788 0.9200193765 3.9573753766 +O 1.3709517852 2.3298829852 -4.5058281615 +H 1.9875720133 1.7787489322 -3.9973152444 +H 0.8458764951 1.7316477754 -5.0344764431 +O 0.0366205494 4.1927476372 2.3623114947 +H -0.8464216197 3.7974097836 2.4944276833 +H 0.7209193101 3.5116964546 2.4872221102 +O -2.6869691260 4.0599162954 0.1221546095 +H -1.8937823601 4.6078125690 0.0418376675 +H -2.6490286446 3.7467696851 1.0480516013 +O -0.2588495117 -3.6948188634 -2.7526333101 +H -0.1478465536 -2.7421206853 -2.5291749716 +H -1.2185846324 -3.7790140208 -2.9571752442 +O 1.9793293310 2.4472174816 2.7898162709 +H 2.6958080641 2.9309034406 3.1967522722 +H 1.8222875398 1.6376176890 3.3724994036 +O 2.7431586365 -1.0406957089 2.2068824411 +H 2.8872048598 -0.2497464337 1.6512767436 +H 3.6023889321 -1.4271745696 2.3737328474 +O -0.9578442194 0.3009365093 4.0378092586 +H -1.1063989534 -0.5034010199 3.4926417057 +H -0.8424301128 1.0274677762 3.4163811925 +O 2.4362084057 -0.5598173867 -1.0017664130 +H 2.6666490498 0.0235115457 -0.2537873749 +H 2.7249650289 -0.0837031624 -1.7993863088 +O -4.0666292722 0.3125340616 -1.0636325685 +H -4.0265061683 1.2372460167 -1.3426087549 +H -3.8442903837 0.2745991811 -0.1151350524 +O 1.6390367209 0.3794911523 4.1465029130 +H 2.0003579649 -0.2382353446 3.4860040004 +H 0.6726536898 0.2582343384 4.1767223518 +O 0.6311852341 -3.9486723208 -0.2978720948 +H 0.2481036233 -4.0722242312 -1.1910110960 +H 1.5485273607 -3.6424598883 -0.5507883820 +O 0.8365075708 -2.8270411968 2.1617526339 +H 0.8303918827 -3.2523619423 1.2924825743 +H 1.5450378821 -2.1590831249 2.1584975259 +O 2.8390569462 -3.0919960380 -1.2072284422 +H 2.6438851935 -3.2476727287 -2.1484142873 +H 2.7432193269 -2.1280531520 -1.0847095040 +O -4.4232371140 -2.5429790731 1.2216274982 +H -4.1478307990 -2.5080873457 0.2793882043 +H -5.3491580601 -2.7709602481 1.2524039513 +O -0.1365701617 4.2516508056 -3.6736410991 +H 0.2979282849 4.3620344570 -2.8056027195 +H 0.4543608951 3.6098261125 -4.1098213021 +O 1.4666574843 -0.9832991974 -4.1893294664 +H 1.7987584543 -1.8961646625 -4.1449539893 +H 0.7953562134 -0.9835245338 -3.4856571405 +O -1.3746714832 -1.7517455230 2.5287259701 +H -0.4530981765 -2.1785656332 2.4463325003 +H -1.9461373485 -2.3987787010 2.9480398356 +O -3.4509551578 -0.2065072822 1.4956806858 +H -3.9670511841 -1.0467137087 1.4473145878 +H -2.5894117717 -0.5510701685 1.7631004224 +O 2.1052498489 -3.4980885341 -3.7332562391 +H 2.4357065891 -4.2801290839 -4.1693130622 +H 1.1812471369 -3.6790337847 -3.4340757171 +O -2.1041597397 2.6472759160 -3.7597364909 +H -2.8297910441 2.9096882188 -3.1665687508 +H -1.4003456160 3.3590411712 -3.7748495867 +O 3.1882541343 2.6075671777 -1.2344751694 +H 3.0882711647 2.0229628131 -1.9934062584 +H 2.4656860428 3.2580549662 -1.2756088397 +O -2.7985055200 -3.8070201164 -3.1111933326 +H -3.1366346614 -3.1847943222 -2.4339713274 +H -3.3167824337 -4.6071386976 -3.0393806283 +O 1.0392172724 4.1230657649 -1.3069811763 +H 0.5454596953 4.7260537865 -0.7063896251 +H 0.5548787937 3.2834118638 -1.2258340380 +O -2.4172151546 3.4044016219 2.6300363176 +H -2.8459992444 2.5646700166 3.0024811455 +H -2.7024555280 4.1259917167 3.1927146187 +O 3.0019410396 0.7194686924 -3.2305238367 +H 2.4063554843 0.0021101374 -3.6608612585 +H 3.8257756676 0.7207726105 -3.7149629560 +O -0.3824561727 5.4903753812 0.2778730538 +H -0.1835435563 4.9641618667 1.1179743433 +H -0.0982506542 6.3861072618 0.4553233451 diff --git a/scine_puffin/tests/test_missing_dependency.py b/scine_puffin/tests/test_missing_dependency.py new file mode 100644 index 0000000..d76b0a0 --- /dev/null +++ b/scine_puffin/tests/test_missing_dependency.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +import pytest + +from ..utilities.imports import MissingDependency, MissingDependencyError, requires + + +@requires("arbitrary_dependency") +def _failing(_some_argument, _another_argument): + pass + + +@requires("pytest") +def _working(_some_argument, _another_argument): + pass + + +def test_requires(): + _working(1, 2) + with pytest.raises(MissingDependencyError): + _failing(1, 2) + + +def test_missing_dependency(): + missing_dependency = MissingDependency("arbitrary_dependency") + with pytest.raises(NameError): + _ = missing_dependency.arbitrary_attribute_name + with pytest.raises(MissingDependencyError): + _ = missing_dependency() + with pytest.raises(NameError): + _ = missing_dependency.some_method(42) diff --git a/scine_puffin/tests/testcases.py b/scine_puffin/tests/testcases.py index a64dcc1..29cba5d 100644 --- a/scine_puffin/tests/testcases.py +++ b/scine_puffin/tests/testcases.py @@ -5,97 +5,80 @@ See LICENSE.txt for details. """ +import sys import unittest from functools import wraps -from pkgutil import iter_modules -from typing import Callable, Dict, List, Union -from .db_setup import get_clean_db -from scine_puffin.config import Configuration +from typing import Callable, Dict, List, Union, TYPE_CHECKING import os import shutil +from scine_puffin.config import Configuration +from scine_puffin.utilities.imports import (module_exists, calculator_import_resolve, dependency_addition, + MissingDependency) +from .db_setup import get_clean_db -def module_exists(module_name: str) -> bool: - if module_name.lower() == "cp2k": - return os.getenv("CP2K_BINARY_PATH") is not None - elif module_name.lower() == "gaussian": - return os.getenv("GAUSSIAN_BINARY_PATH") is not None - elif module_name.lower() == "orca": - return os.getenv("ORCA_BINARY_PATH") is not None - elif module_name.lower() == "turbomole": - return os.getenv("TURBODIR") is not None - elif module_name.lower() == "ams": - possibles = ['AMSHOME', 'AMSBIN', 'AMS_BINARY_PATH'] - return any(os.getenv(p) is not None for p in possibles) - elif module_name.lower() == "mrcc": - return os.getenv("MRCC_BINARY_PATH") is not None - else: - return module_name in (name for loader, name, ispkg in iter_modules()) +if module_exists("pytest") or TYPE_CHECKING: + import pytest +else: + pytest = MissingDependency("pytest") def _skip(func: Callable, error: str): if module_exists("pytest"): - import pytest return pytest.mark.skip(reason=error)(func) else: return unittest.skip(error)(func) -def dependency_addition(dependencies: List[str]) -> List[str]: - # allow to give scine packages without 'scine_' prefix - short_terms = ['readuct', 'swoose', 'sparrow', 'molassembler', 'database', 'utilities', 'kinetx', 'xtb_wrapper', - 'ams_wrapper', 'serenity_wrapper', 'dftbplus_wrapper'] - dependencies = ['scine_' + d if d in short_terms else d for d in dependencies] - # dependencies of key as value list, only utilities must not be included - dependency_data = { - 'scine_readuct': ['scine_sparrow'], - } - for package, dependency in dependency_data.items(): - if package in dependencies: - dependencies += dependency - # add utilities - if any('scine' in d for d in dependencies): - dependencies.append('scine_utilities') - return list(set(dependencies)) # give back unique list - - def skip_without(*dependencies) -> Callable: + """ + This function is meant to be used as a decorator for individual unittest functions. It will skip the test if the + required dependencies are not installed or if the database is not running. + + Example + ------- + Add this function as a decorator with the required modules as arguments + + >>> @skip_without("readuct", "database") + >>> def test_function(): + >>> ... + + Returns + ------- + Callable + The wrapped function. + """ dependency_list: List[str] = list(dependencies) dependency_list = dependency_addition(dependency_list) def wrap(f: Callable): - if all(module_exists(d) for d in dependency_list): - @wraps(f) - def wrapped_f(*args, **kwargs): - calculator_import_resolve(dependency_list) - f(*args, **kwargs) - return wrapped_f - else: + + if not all(module_exists(d) for d in dependency_list): return _skip(f, "Test requires {:s}".format([d for d in dependency_list if not module_exists(d)][0])) - return wrap + @wraps(f) + def wrapped_f(*args, **kwargs): + calculator_import_resolve(dependency_list) + f(*args, **kwargs) + if "scine_database" not in dependency_list: + return wrapped_f + try: + get_clean_db() + return wrapped_f + except RuntimeError as e: + if module_exists("pytest"): + pytest.exit("{:s}\nFirst start database before running unittests.".format(str(e))) + else: + print("{:s}\nFirst start database before running unittests.".format(str(e))) + sys.exit(1) -def calculator_import_resolve(dependency_list: List[str]) -> None: - # ensure that calculators can be loaded - for d in dependency_list: - if d == "scine_sparrow": - import scine_sparrow # noqa # pylint: disable=(unused-import,import-error) - elif d == "scine_ams_wrapper": - import scine_ams_wrapper # noqa # pylint: disable=(unused-import,import-error) - elif d == "scine_dftbplus_wrapper": - import scine_dftbplus_wrapper # noqa # pylint: disable=(unused-import,import-error) - elif d == "scine_serenity_wrapper": - import scine_serenity_wrapper # noqa # pylint: disable=(unused-import,import-error) - elif d == "scine_swoose": - import scine_swoose # noqa # pylint: disable=(unused-import,import-error) - elif d == "scine_xtb_wrapper": - import scine_xtb_wrapper # noqa # pylint: disable=(unused-import,import-error) + return wrap class JobTestCase(unittest.TestCase): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.db_name = "default_puffin_unittest_db" self.start_dir = os.getcwd() diff --git a/scine_puffin/utilities/compound_and_flask_helpers.py b/scine_puffin/utilities/compound_and_flask_helpers.py index e17e942..e00a37b 100644 --- a/scine_puffin/utilities/compound_and_flask_helpers.py +++ b/scine_puffin/utilities/compound_and_flask_helpers.py @@ -1,14 +1,20 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -from typing import Union +from typing import Union, TYPE_CHECKING -import scine_database as db +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +@requires("database") def get_compound_or_flask(object_id: db.ID, object_type: db.CompoundOrFlask, compounds: db.Collection, flasks: db.Collection) -> Union[db.Compound, db.Flask]: """ @@ -17,22 +23,23 @@ def get_compound_or_flask(object_id: db.ID, object_type: db.CompoundOrFlask, com Parameters ---------- - object_id :: db.ID + object_id : db.ID The ID of the object to construct. - object_type :: db.CompoundOrFlask + object_type : db.CompoundOrFlask The label for Compound or Flaks. - compounds :: db.Collection + compounds : db.Collection The compounds collection. - flasks :: db.Collection + flasks : db.Collection The flasks collection. Returns ------- Either the flask or compound object. - Note - ---- - Raises a runtime error if the object_type is unknown. + Raises + ------ + RuntimeError + If the object_type is unknown. """ if object_type == db.CompoundOrFlask.COMPOUND: return db.Compound(object_id, compounds) diff --git a/scine_puffin/utilities/imports.py b/scine_puffin/utilities/imports.py new file mode 100644 index 0000000..9759b0b --- /dev/null +++ b/scine_puffin/utilities/imports.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +from functools import lru_cache, wraps +from pkgutil import iter_modules +from types import ModuleType +from typing import Callable, List +import os + + +class MissingDependencyError(Exception): + pass + + +@lru_cache +def module_exists(module_name: str) -> bool: + """ + Allows checking if a Python module is installed with the Python module given as a string. + Additionally, also checks for environment variables for specific programs that can be available to the Puffin + such as different quantum chemistry programs. + + Parameters + ---------- + module_name : str + The name of the module to check for. + + Examples + -------- + This function alleviates the challenge that no Scine module is a hard dependency for Puffin in order to allow + the Puffin to bootstrap itself by building the Scine modules, but also relies on the Scine modules in almost all + jobs. + The usage should be to add + >>> from __future__ import annotations + >>> from typing import TYPE_CHECKING + + to the top of the file and then use the function straight after the imports as follows: + >>> if module_exists("scine_database") or TYPE_CHECKING: + >>> import scine_database as db + + this allows typehinting all functions with database objects and still makes the file importable without the + database module. + + Returns + ------- + bool + True if the module is installed, False otherwise. + """ + if module_name.lower() == "cp2k": + return os.getenv("CP2K_BINARY_PATH") is not None + elif module_name.lower() == "gaussian": + return os.getenv("GAUSSIAN_BINARY_PATH") is not None + elif module_name.lower() == "orca": + return os.getenv("ORCA_BINARY_PATH") is not None + elif module_name.lower() == "turbomole": + return os.getenv("TURBODIR") is not None + elif module_name.lower() == "ams": + possibles = ['AMSHOME', 'AMSBIN', 'AMS_BINARY_PATH'] + return any(os.getenv(p) is not None for p in possibles) + elif module_name.lower() == "mrcc": + return os.getenv("MRCC_BINARY_PATH") is not None + else: + return module_name in (name for loader, name, ispkg in iter_modules()) + + +def calculator_import_resolve(dependency_list: List[str]) -> None: + """ + Automatically loads the calculators if they are in the dependency list. + + Parameters + ---------- + dependency_list : List[str] + The list of dependencies. + """ + for d in dependency_list: + if d == "scine_sparrow": + import scine_sparrow # noqa # pylint: disable=(unused-import,import-error) + elif d == "scine_ams_wrapper": + import scine_ams_wrapper # noqa # pylint: disable=(unused-import,import-error) + elif d == "scine_dftbplus_wrapper": + import scine_dftbplus_wrapper # noqa # pylint: disable=(unused-import,import-error) + elif d == "scine_serenity_wrapper": + import scine_serenity_wrapper # noqa # pylint: disable=(unused-import,import-error) + elif d == "scine_swoose": + import scine_swoose # noqa # pylint: disable=(unused-import,import-error) + elif d == "scine_xtb_wrapper": + import scine_xtb_wrapper # noqa # pylint: disable=(unused-import,import-error) + elif d == "scine_parrot": + import scine_parrot # noqa # pylint: disable=(unused-import,import-error) + + +def dependency_addition(dependencies: List[str]) -> List[str]: + """ + A utility function that adds the "scine" prefix to specific Scine dependencies and adds additional dependencies + based on the given dependencies, such as the utilities package or the Sparrow calculator to the ReaDuct dependency. + + Parameters + ---------- + dependencies : List[str] + The list of dependencies. + + Returns + ------- + List[str] + The updated list of dependencies with proper "scine" prefixes and additional dependencies. + """ + + # allow to give scine packages without 'scine_' prefix + short_terms = ['readuct', 'swoose', 'sparrow', 'molassembler', 'database', 'utilities', 'kinetx', 'xtb_wrapper', + 'ams_wrapper', 'serenity_wrapper', 'dftbplus_wrapper', 'parrot'] + dependencies = ['scine_' + d if d in short_terms else d for d in dependencies] + # dependencies of key as value list, only utilities must not be included + dependency_data = { + 'scine_readuct': ['scine_sparrow'], + } + for package, dependency in dependency_data.items(): + if package in dependencies: + dependencies += dependency + # add utilities + if any('scine' in d for d in dependencies): + dependencies.append('scine_utilities') + return list(set(dependencies)) # give back unique list + + +def requires(*dependencies) -> Callable: + """ + This function is meant to be used as a decorator for functions that require specific dependencies to be installed. + It checks if the given dependencies are installed and raises an ImportError if not. + Then it automatically loads the calculators if they are in the dependency list. + + Examples + -------- + Add this function as a decorator with the required modules as arguments + + >>> @requires("readuct", "database") + >>> def function(): + >>> ... + + Returns + ------- + Callable + The wrapped function. + + Raises + ------ + MissingDependencyError + If the given dependencies are not installed, but the function is called. + """ + dependency_list: List[str] = list(dependencies) + dependency_list = dependency_addition(dependency_list) + + def wrap(f: Callable): + + @wraps(f) + def wrapped_f(*args, **kwargs): + if not all(module_exists(d) for d in dependency_list): + raise MissingDependencyError(f"Execution of function requires {dependency_list} to be installed") + calculator_import_resolve(dependency_list) + return f(*args, **kwargs) + return wrapped_f + + return wrap + + +class MissingDependency(ModuleType): + """ + This class is meant to be used as a placeholder for missing dependencies. + It allows access to arbitrarily named attributes and methods, but raises a MissingDependencyError when accessed + or called. + """ + + def __getattribute__(self, key): + if key.startswith("__"): + return super().__getattribute__(key) + raise NameError(f"Attribute '{key}' access not allowed") + + def __getattr__(self, name): + def method(*args, **kwargs): + raise MissingDependencyError(f"Attribute '{name}' access not allowed") + return method + + def __call__(self, *args, **kwargs): + raise MissingDependencyError("Method execution not allowed") diff --git a/scine_puffin/utilities/kinetic_modeling_sensitivity_analysis.py b/scine_puffin/utilities/kinetic_modeling_sensitivity_analysis.py index 83d4517..f5910f2 100644 --- a/scine_puffin/utilities/kinetic_modeling_sensitivity_analysis.py +++ b/scine_puffin/utilities/kinetic_modeling_sensitivity_analysis.py @@ -4,6 +4,8 @@ See LICENSE.txt for details. """ +from copy import deepcopy +from multiprocessing import Pool, cpu_count from typing import Optional, Any, Tuple, List import numpy as np @@ -20,16 +22,16 @@ class RMSKineticModelingSensitivityAnalysis: To ensure that we can use multiprocessing, we cannot work with any Julia objects in the main thread. Julia objects may only be constructed after starting the parallel loop. - Parameters: - ----------- - rms_kinetic_model: RMSKineticModel + Parameters + ---------- + rms_kinetic_model : RMSKineticModel The microkinetic model. - n_cores: int + n_cores : int Number of cores to run the sensitivity analysis on. Note that if n > 1, RMS must not have been instantiated before in the python main process because Julia runs into trouble otherwise. - sample_size: int + sample_size : int Number of samples for Morris (5 - 25) or Sobol ( > 500) analysis. - distribution_shape: str (default 'unif') + distribution_shape : str (default 'unif') Shape of the parameter distribution to be assumed. Options are uniform distribution between error bounds ('unif') and truncated normal distributions ('truncnorm'). The normal distributions is only truncated to ensure non-negative reaction barriers. The standard deviation for the normal distribution is taken as the @@ -38,10 +40,11 @@ class RMSKineticModelingSensitivityAnalysis: parameter sampling if Morris sampling is used because it constructs discrete parameter levels within the distribution. - TODO: The number of level for Morris sampling should be an input argument. + TODO: The number of levels for the Morris sampling should be an input argument. """ + def __init__(self, rms_kinetic_model: RMSKineticModel, n_cores: int, sample_size: int, - distribution_shape: str = 'unif'): + distribution_shape: str = 'unif') -> None: self.rms_model = rms_kinetic_model self.n_cores = n_cores self.sample_size = sample_size @@ -99,11 +102,12 @@ def analyse_runs(self) -> List[Tuple[np.ndarray, np.ndarray]]: """ Calculate mean and variance of the outputs of the sensitivity analysis runs. - Return - ------ - Returns a list of tuples (tuple[0] -> mean ; tuple[1] -> variance) of the different sensitivity analysis - outputs (these can be aggregate-wise maximum concentrations, fluxes or just concentrations at specific time - points). + Returns + ------- + List[Tuple[np.ndarray, np.ndarray]] + Returns a list of tuples (tuple[0] -> mean ; tuple[1] -> variance) of the different sensitivity analysis + outputs (these can be aggregate-wise maximum concentrations, fluxes or just concentrations at specific time + points). """ if self.get_analysis().results is None: raise RuntimeError("Run the sensitivity analysis first, please.") @@ -121,16 +125,17 @@ def morris_sensitivities(self): samples for each parameter and p is the number of parameters (number of reactions + number of aggregates). The number of samples (N) is typically between 5 and 25. - Return - ------ + Returns + ------- Returns the sensitivity measures for the maximum and final concentrations for each parameter as a dictionary. The measures are: - max_mu: Maximum value of the Morris sensitivity measure mu for the parameter and maximum/final concentrations. - max_mu_star: Maximum value of the Morris sensitivity measure mu* for the parameter and maximum/final - concentrations. - max_sigma: Maximum value of the Morris sensitivity measure sigma for the parameter and maximum/final - concentrations. + max_mu: Maximum value of the Morris sensitivity measure mu for the parameter and maximum/final + concentrations. + max_mu_star: Maximum value of the Morris sensitivity measure mu* for the parameter and maximum/final + concentrations. + max_sigma: Maximum value of the Morris sensitivity measure sigma for the parameter and maximum/final + concentrations. """ problem = self._define_sampling_problem() # pylint: disable=no-member @@ -192,7 +197,6 @@ def evaluate_salib_parallel(problem, func, nprocs=None): nprocs : int, The number of processes. """ - from multiprocessing import Pool, cpu_count if problem._samples is None: raise RuntimeError("Sampling not yet conducted") @@ -295,7 +299,6 @@ def get_local_sensitivity_samples(self) -> Tuple[np.ndarray, List[int]]: Getter for the local sensitivity samples (parameter combinations). Parameters are distorted by their error bounds one-at-a-time from the baseline. """ - from copy import deepcopy parameter_bounds = self.get_parameter_bounds() f2r_mapping = self.get_reduced_parameter_mapping() full_parameters = self.rms_model.get_all_parameters() @@ -327,25 +330,23 @@ def one_at_a_time_differences(self, vertex_fluxes: np.ndarray, edge_fluxes: np.n Parameters ---------- - vertex_fluxes :: np.ndarray + vertex_fluxes : np.ndarray The vertex fluxes of the baseline model (the model with all parameters as their default). - edge_fluxes :: np.ndarray + edge_fluxes : np.ndarray The edge fluxes of the baseline model. - vertex_threshold :: float + vertex_threshold : float Vertex fluxes over this values are considered high and reduced to flux_replace. This should remove absolutely large but unimportant changes of the fluxes. - edge_threshold :: float + edge_threshold : float Edge fluxes over this values are considered high and reduced to flux_replace. This should remove absolutely large but unimportant changes of the fluxes. - flux_replace :: float + flux_replace : float The flux replacement value. - ref_max :: np.ndarray + ref_max : np.ndarray The maximum concentrations of the baseline model. - ref_final :: np.ndarray + ref_final : np.ndarray The final concentrations of the baseline model. """ - from multiprocessing import Pool - from copy import deepcopy self.set_prescreening_condition(vertex_fluxes, edge_fluxes, vertex_threshold, edge_threshold) samples, parameter_indices = self.get_local_sensitivity_samples() n_samples = samples.shape[0] @@ -447,15 +448,17 @@ def _reduced_to_full_parameters(metric: np.ndarray, mapping: List[Tuple[int, int """ Parameters ---------- - metric :: np.ndarray + metric : np.ndarray The metric in the reduced parameter set. - mapping :: List[Tuple[int, int]] + mapping : List[Tuple[int, int]] Full parameter index - reduced parameter index tuples. - n_total_params :: int + n_total_params : int Total number of parameters. + Returns ------- - The metric in the full parameter dimensions. + np.ndarray + The metric in the full parameter dimensions. """ assert metric.shape == (len(mapping),) full_metric = np.zeros(n_total_params) @@ -471,14 +474,18 @@ def _update_by_reduced_parameters(update: np.ndarray, full_set: np.ndarray, Parameters ---------- - update :: np.ndarray + update : np.ndarray The new parameter values. - full_set :: np.ndarray + full_set : np.ndarray The full set of parameters. - mapping :: List[Tuple[int, int]] + mapping : List[Tuple[int, int]] The indices of the update values in the full parameter set. + + Returns + ------- + np.ndarray + The updated full parameter set. """ - from copy import deepcopy assert len(update) == len(mapping) params = deepcopy(full_set) for full_index, reduced_index in mapping: @@ -525,7 +532,7 @@ def salib_wrapped_kinetic_modeling(self, params_set: np.ndarray): Parameters ---------- - params_set :: np.ndarray + params_set : np.ndarray An array containing a set of parameters for the run. """ import os diff --git a/scine_puffin/utilities/masm_helper.py b/scine_puffin/utilities/masm_helper.py index b0c972b..3b14614 100644 --- a/scine_puffin/utilities/masm_helper.py +++ b/scine_puffin/utilities/masm_helper.py @@ -1,18 +1,30 @@ # -*- coding: utf-8 -*- """masm_helper.py: Collection of common procedures to be carried out with molassembler""" +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ from copy import deepcopy -from typing import Any, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Dict, List, Optional, Set, Tuple, Union, TYPE_CHECKING import math import sys -import scine_database as db -import scine_molassembler as masm -import scine_utilities as utils +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_molassembler") or TYPE_CHECKING: + import scine_molassembler as masm +else: + masm = MissingDependency("scine_molassembler") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") def get_molecules_result( @@ -30,24 +42,24 @@ def get_molecules_result( Parameters ---------- - atoms :: utils.AtomCollection + atoms : utils.AtomCollection The atom collection to be interpreted. - bond_orders :: utils.BondOrderCollection + bond_orders : utils.BondOrderCollection The bond order collection to be interpreted. - connectivity_settings :: Dict[str, Union[bool, int]] + connectivity_settings : Dict[str, Union[bool, int]] Settings describing whether to use the connectivity as predicted based on inter-atomic distances by the utils.BondDetector. - pbc_string :: str + pbc_string : str The string specifying periodic boundaries, empty string represents no periodic boundaries. - unimportant_atoms :: Optional[Union[List[int], Set[int]]] + unimportant_atoms : Optional[Union[List[int], Set[int]]] The indices of atoms for which no stereopermutators shall be determined. - modifications :: Optional[List[Tuple[int, int, float]]] + modifications : Optional[List[Tuple[int, int, float]]] Manual bond modifications. They are specified as a list with each element containing the indices and the new bond order between those two indices Returns ------- - masm_results :: masm.interpret.MoleculesResult + masm_results : masm.interpret.MoleculesResult The result of the molassembler interpretation. """ @@ -116,11 +128,11 @@ def _modify_based_on_distances( def get_cbor_graph_from_molecule(molecule: masm.Molecule): """ - Generates the canonicalized CBOR graph from a single masm.Molecule + Generates the canonical CBOR graph from a single masm.Molecule Parameters ---------- - molecule :: masm.Molecule + molecule : masm.Molecule The molecule of which a graph is to be generated Returns @@ -157,21 +169,21 @@ def get_cbor_graph( Parameters ---------- - atoms :: utils.AtomCollection + atoms : utils.AtomCollection The atom collection to be interpreted. - bond_orders :: utils.BondOrderCollection + bond_orders : utils.BondOrderCollection The bond order collection to be interpreted. - connectivity_settings :: Dict[str, Union[bool, int]] + connectivity_settings : Dict[str, Union[bool, int]] Settings describing whether to use the connectivity as predicted based on inter-atomic distances by the utils.BondDetector. - pbc_string :: str + pbc_string : str The string specifying periodic boundaries, empty string represents no periodic boundaries. - unimportant_atoms :: Optional[Union[List[int], Set[int]]] + unimportant_atoms : Optional[Union[List[int], Set[int]]] The indices of atoms for which no stereopermutators shall be determined. Returns ------- - masm_cbor_graphs :: str + masm_cbor_graphs : str The cbor graphs as sorted strings separated by semicolons. """ @@ -196,14 +208,14 @@ def get_decision_list_from_molecule(molecule: masm.Molecule, atoms: utils.AtomCo Parameters ---------- - molecule :: masm.Molecule + molecule : masm.Molecule The molecule. - atoms :: utils.AtomCollection + atoms : utils.AtomCollection The atoms and their positions in the molecule. Returns ------- - masm_decision_list :: str + masm_decision_list : str The dihedral decision list for rotatable bonds. """ @@ -240,21 +252,21 @@ def get_decision_lists( Parameters ---------- - atoms :: utils.AtomCollection + atoms : utils.AtomCollection The atom collection to be interpreted. - bond_orders :: utils.BondOrderCollection + bond_orders : utils.BondOrderCollection The bond order collection to be interpreted. - connectivity_settings :: Dict[str, Union[bool, int]] + connectivity_settings : Dict[str, Union[bool, int]] Settings describing whether to use the connectivity as predicted based on inter-atomic distances by the utils.BondDetector. - pbc_string :: str + pbc_string : str The string specifying periodic boundaries, empty string represents no periodic boundaries. - unimportant_atoms :: Union[List[int], None] + unimportant_atoms : Union[List[int], None] The indices of atoms for which no stereopermutators shall be determined. Returns ------- - masm_decision_lists :: List[str] + masm_decision_lists : List[str] The dihedral decision lists for rotatable bonds in all molecules in the given input. """ masm_results = get_molecules_result(atoms, bond_orders, connectivity_settings, pbc_string, unimportant_atoms) @@ -272,21 +284,21 @@ def add_masm_info( bo_collection: utils.BondOrderCollection, connectivity_settings: Dict[str, Union[bool, int]], unimportant_atoms: Union[List[int], Set[int], None] = None, -): +) -> None: """ Generates a structure's CBOR graph and decision lists and adds them to the database. Parameters ---------- - structure :: db.Structure + structure : db.Structure The structure to be analyzed. It has to be linked to a database. - bo_collection :: utils.BondOrderCollection + bo_collection : utils.BondOrderCollection The bond order collection to be interpreted. - connectivity_settings :: Dict[str, Union[bool, int]] + connectivity_settings : Dict[str, Union[bool, int]] Settings describing whether to use the connectivity as predicted based on inter-atomic distances by the utils.BondDetector. - unimportant_atoms :: Union[List[int], None] + unimportant_atoms : Union[List[int], None] The indices of atoms for which no stereopermutators shall be determined. """ pbc_string = structure.get_model().periodic_boundaries @@ -294,7 +306,7 @@ def add_masm_info( try: masm_results = get_molecules_result(atoms, bo_collection, connectivity_settings, pbc_string, unimportant_atoms) except BaseException as e: - if structure.get_label() == db.Label.TS_OPTIMIZED: + if structure.get_label() in [db.Label.TS_OPTIMIZED, db.Label.TS_GUESS]: print("Molassembler could not generate a graph for TS as it is designed for Minima") return raise e @@ -311,7 +323,7 @@ def add_masm_info( # canonicalization reordering positions[i] = m.apply_canonicalization_map(ordering, positions[i]) # Apply the canonicalization reordering to the atom mapping - atom_map = [(c, ordering[j]) if c == i else (c, j) for c, j in atom_map] + atom_map = [(c, ordering[j]) if c == i else (c, j) for c, j in atom_map] # type: ignore # Generate a canonical b64-encoded cbor serialization of the molecule serialization = masm.JsonSerialization(m) binary = serialization.to_binary(masm.JsonSerialization.BinaryFormat.CBOR) @@ -346,5 +358,5 @@ def add_masm_info( # Apply new ordering to the atom map and store it, too new_component_order = [x["component"] for x in properties] - atom_map = [(new_component_order.index(c), i) for c, i in atom_map] + atom_map = [(new_component_order.index(c), i) for c, i in atom_map] # type: ignore structure.set_graph("masm_idx_map", str(atom_map)) diff --git a/scine_puffin/utilities/program_helper.py b/scine_puffin/utilities/program_helper.py index 9483122..a6328b7 100644 --- a/scine_puffin/utilities/program_helper.py +++ b/scine_puffin/utilities/program_helper.py @@ -1,25 +1,37 @@ # -*- coding: utf-8 -*- +from __future__ import annotations """program_helper.py: Collection of common procedures to be carried out depending on underlying calculators""" __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -from typing import Any, Dict, Tuple, Union -import scine_database as db -import scine_utilities as utils +from abc import ABC, abstractmethod +from typing import Any, Dict, Tuple, Union, List, Optional, TYPE_CHECKING +from scine_puffin.utilities.imports import module_exists, MissingDependency -class ProgramHelper: +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") + + +class ProgramHelper(ABC): """ A common interface for all helper classes for specific Scine calculators """ - def __init__(self, *arg, **kwargs): - self.helper_settings = [] + def __init__(self, *arg, **kwargs) -> None: + self.helper_settings: List[str] = [] @staticmethod - def program(): + @abstractmethod + def program() -> str: """ Must be implemented by every subclass. Returns the name of the program the helper is written for. @@ -33,19 +45,19 @@ def get_correct_helper( manager: db.Manager, structure: db.Structure, calculation: db.Calculation, - ): + ) -> Optional[ProgramHelper]: """ Returns the correct ProgramHelper Child class for the given program name or None if no suitable helper exists. Parameters ---------- - program :: str - The name of the program, not case sensitive. - manager :: db.Manager (Scine::Database::Manager) + program : str + The name of the program, not case-sensitive. + manager : db.Manager (Scine::Database::Manager) The manager of the database. - structure :: db.Structure (Scine::Database::Structure) + structure : db.Structure (Scine::Database::Structure) The structure to be calculated, necessary for init of child. - calculation :: db.Calculation (Scine::Database::Calculation) + calculation : db.Calculation (Scine::Database::Calculation) The calculation that is carried out in the current job. """ for Child in cls.__subclasses__()[1:]: # parent is first element @@ -53,30 +65,32 @@ def get_correct_helper( return Child(manager, structure, calculation) return None + @abstractmethod def calculation_preprocessing( self, calculator: utils.core.Calculator, calculation_settings: utils.ValueCollection, - ): + ) -> None: """ - Makes necessary preparations before the calculation in a job. + Makes the necessary preparations before the calculation in a job. This function has to be implemented by every subclass. Parameters ---------- - calculator :: utils.core.calculator Scine::Core::Calculator) + calculator : utils.core.calculator Scine::Core::Calculator) The calculator to work on/with. - calculation_settings :: utils.ValueCollection (Scine::Utils::ValueCollection) + calculation_settings : utils.ValueCollection (Scine::Utils::ValueCollection) The settings of the calculation in the database. """ raise NotImplementedError + @abstractmethod def calculation_postprocessing( self, calculation: db.Calculation, old_structure: db.Structure, new_structure: Union[db.Structure, None] = None, - ): + ) -> None: """ Write additional information into the database after the calculation in a job. This function has to be implemented by every subclass. @@ -84,18 +98,18 @@ def calculation_postprocessing( Parameters ---------- - calculation :: db.Calculation (Scine::Database::Calculation) + calculation : db.Calculation (Scine::Database::Calculation) The calculation that triggered the execution of the job. - old_structure :: db.Structure (Scine::Database::Structure) + old_structure : db.Structure (Scine::Database::Structure) The structure which was calculated - new_structure :: db.Structure (Scine::Database::Structure) + new_structure : db.Structure (Scine::Database::Structure) An optional resulting structure from the Calculation (e.g. optimized structure) """ raise NotImplementedError class Cp2kHelper(ProgramHelper): - def __init__(self, manager: db.Manager, structure: db.Structure, calculation: db.Calculation): + def __init__(self, manager: db.Manager, structure: db.Structure, calculation: db.Calculation) -> None: super().__init__() self.helper_settings = [ "energy_accuracy", @@ -116,14 +130,14 @@ def __init__(self, manager: db.Manager, structure: db.Structure, calculation: db calculation.set_settings(settings) @staticmethod - def program(): + def program() -> str: return "cp2k" def calculation_preprocessing( self, calculator: utils.core.Calculator, calculation_settings: utils.ValueCollection, - ): + ) -> None: if self.cutoff_optimization_necessary: self.optimize_cutoffs(calculator, calculation_settings) @@ -132,12 +146,12 @@ def calculation_postprocessing( calculation: db.Calculation, old_structure: db.Structure, new_structure: Union[db.Structure, None] = None, - ): + ) -> None: if new_structure is None: return settings = calculation.get_settings() - cutoff = settings[self.cutoff_name] - rel_cutoff = settings[self.rel_cutoff_name] + cutoff: float = float(settings[self.cutoff_name]) # type: ignore + rel_cutoff: float = float(settings[self.rel_cutoff_name]) # type: ignore if self.structure_has_cutoff_properties(old_structure): if not self._compare_and_save_cutoff_properties(old_structure, new_structure, cutoff, rel_cutoff): self.make_new_cutoff_properties(calculation, new_structure, cutoff, rel_cutoff) @@ -159,14 +173,14 @@ def cutoff_handling(self, structure: db.Structure, settings: utils.ValueCollecti # if none specified, try to find properties if self.cutoff_name not in settings and self.rel_cutoff_name not in settings: cutoff, rel_cutoff = self.cutoffs_from_properties(structure) - if cutoff is not None: + if cutoff is not None and rel_cutoff is not None: settings[self.cutoff_name] = cutoff settings[self.rel_cutoff_name] = rel_cutoff else: self.cutoff_optimization_necessary = True # do nothing if cutoffs present in settings - def cutoffs_from_properties(self, structure: db.Structure) -> Tuple[Union[float, None], Union[float, None]]: + def cutoffs_from_properties(self, structure: db.Structure) -> Union[Tuple[float, float], Tuple[None, None]]: property_id1 = None property_id2 = None possible_ref = None @@ -191,16 +205,14 @@ def cutoffs_from_properties(self, structure: db.Structure) -> Tuple[Union[float, property_id2 = struc.get_property(self.rel_cutoff_name) possible_ref = struc # set properties for the currently calculated structure to avoid lookup next time - if property_id1 is not None: + if property_id1 is not None and property_id2 is not None: structure.set_property(self.cutoff_name, property_id1) structure.set_property(self.rel_cutoff_name, property_id2) if property_id1 is not None and property_id2 is not None: - prop = db.NumberProperty(property_id1) - prop.link(self.properties) + prop = db.NumberProperty(property_id1, self.properties) cutoff = prop.get_data() - prop = db.NumberProperty(property_id2) - prop.link(self.properties) + prop = db.NumberProperty(property_id2, self.properties) rel_cutoff = prop.get_data() return cutoff, rel_cutoff return None, None @@ -229,7 +241,7 @@ def make_new_cutoff_properties( structure: db.Structure, cutoff: float, rel_cutoff: float, - ): + ) -> None: p1 = db.NumberProperty.make( self.cutoff_name, calculation.get_model(), @@ -249,19 +261,19 @@ def make_new_cutoff_properties( structure.set_property(self.cutoff_name, p1.id()) structure.set_property(self.rel_cutoff_name, p2.id()) - def structure_has_cutoff_properties(self, structure): + def structure_has_cutoff_properties(self, structure: db.Structure) -> bool: return structure.has_property(self.cutoff_name) and structure.has_property(self.rel_cutoff_name) - def extract_cutoff_optimization_settings(self, settings: utils.ValueCollection): + def extract_cutoff_optimization_settings(self, settings: utils.ValueCollection) -> None: if "optimize_cutoffs" in settings.keys(): - self.cutoff_optimization_necessary = settings["optimize_cutoffs"] + self.cutoff_optimization_necessary = settings["optimize_cutoffs"] # type: ignore del settings["optimize_cutoffs"] for key in self.helper_settings: if key in settings.keys(): self.cutoff_optimization_settings[key] = settings[key] del settings[key] - def optimize_cutoffs(self, system, settings: utils.ValueCollection): + def optimize_cutoffs(self, system: utils.core.Calculator, settings: utils.ValueCollection) -> None: optimizer = utils.Cp2kCutoffOptimizer(system) optimizer.determine_optimal_grid_cutoffs(**self.cutoff_optimization_settings) # update calculation settings for later property saving diff --git a/scine_puffin/utilities/properties.py b/scine_puffin/utilities/properties.py index 844194e..4b01e98 100644 --- a/scine_puffin/utilities/properties.py +++ b/scine_puffin/utilities/properties.py @@ -16,7 +16,7 @@ def single_particle_energy_to_matrix(x: utils.SingleParticleEnergies) -> np.ndar Parameters ---------- - x :: utils.SingleParticleEnergies + x : utils.SingleParticleEnergies The single particle energies to be converted to numpy matrices. It contains the energies and flags denoting the restricted or unrestricted case. @@ -35,16 +35,15 @@ def single_particle_energy_from_matrix(x: np.ndarray) -> utils.SingleParticleEne Parameters ---------- - x :: np.ndarray + x : np.ndarray The array of dimension $1 \times N$ for the restricted case or $2 \times N$ for the unrestricted case, where $N$ is the number energies. """ if np.shape(x)[0] == 1: # restricted case a = utils.SingleParticleEnergies.make_restricted() - a.restricted_energies = x[0] + a.set_restricted(x[0]) return a else: a = utils.SingleParticleEnergies.make_unrestricted() - a.alpha = x[0] - a.beta = x[1] + a.set_unrestricted(x[0], x[1]) return a diff --git a/scine_puffin/utilities/qm_mm_settings.py b/scine_puffin/utilities/qm_mm_settings.py new file mode 100644 index 0000000..c85dda8 --- /dev/null +++ b/scine_puffin/utilities/qm_mm_settings.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +from typing import TYPE_CHECKING + +from scine_puffin.utilities.scine_helper import SettingsManager +from scine_puffin.utilities.imports import module_exists, MissingDependency +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") + + +# TODO: Checking for QM/MM or MM calculations like this seems like a bad idea. However, there is no method family +# QM/MM or MM at the moment. +def contains_mm(model: db.Model) -> bool: + method_family = model.method_family.lower() + return "gaff" in method_family or "sfam" in method_family + + +def is_qm_mm(model: db.Model) -> bool: + method_family = model.method_family.lower() + return contains_mm(model) and "/" in method_family + + +def prepare_optional_settings(structure: db.Structure, calculation: db.Calculation, + settings_manager: SettingsManager, properties: db.Collection, + skip_qm_atoms: bool = False) -> None: + from scine_puffin.jobs.swoose_qmmm_forces import SwooseQmmmForces + + settings = calculation.get_settings() + calculator_settings = settings_manager.calculator_settings + method_family = calculation.get_model().method_family.lower() + if contains_mm(calculation.get_model()): + connectivity_file_name: str = "connectivity.dat" + SwooseQmmmForces.write_connectivity_file(connectivity_file_name, properties, structure) + calculator_settings['mm_connectivity_file'] = connectivity_file_name + print("Writing connectivity file ", connectivity_file_name) + if "gaff" in method_family: + charge_file_name = "atomic_charges.csv" + SwooseQmmmForces.write_partial_charge_file(charge_file_name, properties, structure) + calculator_settings['gaff_atomic_charges_file'] = charge_file_name + print("Gaff point charge file: ", charge_file_name) + if "sfam" in method_family: + parameter_file_name: str = "sfam-parameters.dat" + SwooseQmmmForces.write_connectivity_file(parameter_file_name, properties, structure) + calculator_settings['mm_parameter_file'] = parameter_file_name + + if is_qm_mm(calculation.get_model()): + if not skip_qm_atoms: + calculator_settings['qm_atoms'] = SwooseQmmmForces.get_qm_atoms(properties, structure) + if "electrostatic_embedding" in settings: + calculator_settings['electrostatic_embedding'] = settings['electrostatic_embedding'] + print("Using electrostatic embedding: ", calculator_settings['electrostatic_embedding']) diff --git a/scine_puffin/utilities/reaction_transfer_helper.py b/scine_puffin/utilities/reaction_transfer_helper.py index 7660c44..829e29a 100644 --- a/scine_puffin/utilities/reaction_transfer_helper.py +++ b/scine_puffin/utilities/reaction_transfer_helper.py @@ -1,18 +1,23 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -from typing import List, Set, Union, Optional +from typing import List, Set, Union, Optional, TYPE_CHECKING import sys import numpy as np -import scine_database as db from .transfer_helper import TransferHelper from .surface_helper import update_slab_dict from scine_puffin.jobs.templates.scine_react_job import ReactJob +from scine_puffin.utilities.imports import module_exists, MissingDependency +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class ReactionTransferHelper(TransferHelper): @@ -123,7 +128,7 @@ def _surface_indices_impl(self, old_structures: List[db.Structure], new_structur """ new_surface_indices = self._determine_new_indices(old_structures, new_structures) calculation = self.react_job.get_calculation() - thresh = self.react_job.settings[self.react_job.job_key]["n_surface_atom_threshold"] + thresh = self.react_job.connectivity_settings["n_surface_atom_threshold"] for new_indices, new_structure in zip(new_surface_indices, new_structures): # do not transfer single surface atom, since we assume that this means we don't have a surface anymore if len(new_indices) > thresh: @@ -152,7 +157,7 @@ def _slab_dict_impl(self, old_structures: List[db.Structure], new_structures: Li # no slab dict in all old_structures return new_surface_indices = self._determine_new_indices(old_structures, new_structures) - thresh = self.react_job.settings[self.react_job.job_key]["n_surface_atom_threshold"] + thresh = self.react_job.connectivity_settings["n_surface_atom_threshold"] for new_indices, new_structure in zip(new_surface_indices, new_structures): if len(new_indices) > thresh: self._sanity_checks(new_structure, self.slab_dict_name) diff --git a/scine_puffin/utilities/rms_input_file_creator.py b/scine_puffin/utilities/rms_input_file_creator.py index 3ae666b..29a5bb0 100644 --- a/scine_puffin/utilities/rms_input_file_creator.py +++ b/scine_puffin/utilities/rms_input_file_creator.py @@ -4,11 +4,16 @@ See LICENSE.txt for details. """ -from typing import List, Dict, Optional, Tuple, Union, Any +from typing import List, Dict, Optional, Tuple, Union, Any, TYPE_CHECKING import yaml import numpy as np -import scine_utilities as utils +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") def create_single_species_entry(name: str, h: float, s: float): @@ -20,15 +25,15 @@ def create_single_species_entry(name: str, h: float, s: float): Parameters ---------- - name :: str + name : str The species name (unique identifier). - h :: float + h : float The species enthalpy in J/mol. - s :: float + s : float The species entropy in J/mol/K. - Return - ------ + Returns + ------- The species entry. """ species_type_str = "Species" @@ -63,17 +68,17 @@ def create_rms_phase_entry(aggregate_str_ids: List[str], enthalpies: List[float] Parameters ---------- - aggregate_str_ids :: List[str] + aggregate_str_ids : List[str] The aggregate IDs as strings. - enthalpies :: List[float] + enthalpies : List[float] The aggregate enthalpies (same ordering as for the aggregate_str_ids) in J/mol. - entropies :: List[float] + entropies : List[float] The aggregate entropies (note the ordering) in J/mol/K. - solvent_name :: Optional[str] (default None) + solvent_name : Optional[str] (default None) The name of an additional solvent species that is added to the species if provided. - Return - ------ + Returns + ------- The species a list of one dictionary. """ species_list = [] @@ -85,28 +90,30 @@ def create_rms_phase_entry(aggregate_str_ids: List[str], enthalpies: List[float] def create_arrhenius_reaction_entry(reactant_names: List[str], product_names: List[str], e_a: float, n: float, a: float, - type_str: str = "ElementaryReaction") -> Dict: + type_str: str = "ElementaryReaction") -> Dict[str, Any]: """ Create a reaction entry in the RMS format assuming that the rate constant is given by the Arrhenius equation: k = a / T^n exp(-e_a/(k_B T)). Parameters ---------- - reactant_names :: List[str] + reactant_names : List[str] Species names of the reactions LHS (the names must correspond to an entry in the RMS phase dictionary). - product_names :: List[str] + product_names : List[str] Species names of the reactions RHS (the names must correspond to an entry in the RMS phase dictionary). - e_a :: float + e_a : float Activation energy in J/mol. - n :: float + n : float Temperature exponent. - a :: float + a : float Arrhenius prefactor. - type_str :: str (default 'ElementaryReaction') + type_str : str (default 'ElementaryReaction') Type of the reaction entry (see the RMS documentation for other options). - Return - ------ - The reaction entry. + + Returns + ------- + Dict[str, Any] + The reaction entry. """ kinetics_type_str: str = "Arrhenius" return { @@ -124,7 +131,7 @@ def create_arrhenius_reaction_entry(reactant_names: List[str], product_names: Li def create_rms_reaction_entry(prefactors: List[float], temperature_exponents: List[float], activation_energies: Union[List[float], np.ndarray], - reactant_list: List[Tuple[List[str], List[str]]]) -> List[Dict]: + reactant_list: List[Tuple[List[str], List[str]]]) -> List[Dict[str, Any]]: """ Create the reaction entries for the RMS input dictionary assuming Arrhenius kinetics and that all reactions are Elementary Reactions (according to the RMS definition): @@ -134,17 +141,19 @@ def create_rms_reaction_entry(prefactors: List[float], temperature_exponents: Li Parameters ---------- - prefactors :: List[float] + prefactors : List[float] Arrhenius prefactors (a in the equation above). - temperature_exponents :: List[float] + temperature_exponents : List[float] Temperature exonents (n in the equation above). - activation_energies :: Union[List[float], np.ndarray] + activation_energies : Union[List[float], np.ndarray] Activation energies (e_a in the equation above). - reactant_list :: List[Tuple[List[str], List[str]]] + reactant_list : List[Tuple[List[str], List[str]]] LHS (tuple[0]) and RHS (tuple[1]) of all reactions. - Return - ------ - All reaction entries as a list of dictionaries. + + Returns + ------- + List[Dict[str, Any]] + All reaction entries as a list of dictionaries. """ reaction_type_str = "ElementaryReaction" reaction_list = [] @@ -161,12 +170,13 @@ def create_rms_units_entry(units: Optional[Dict] = None) -> Dict: Parameters ---------- - units :: Optional[Dict] (default None) + units : Optional[Dict] (default None) The units as a dictionary. - Return - ------ - If no units were provided, an empty dictionary is returned. + Returns + ------- + Dict + If no units were provided, an empty dictionary is returned. """ if units is None: units = {} @@ -179,16 +189,17 @@ def create_solvent_entry(solvent: str, solvent_viscosity: Optional[float], solve Parameters ---------- - solvent :: str + solvent : str The solvent name (e.g., water) to extract tabulated viscosity values. - solvent_viscosity :: Optional[float] + solvent_viscosity : Optional[float] The solvent's viscosity in Pa s. - solvent_id_str :: Optional[str] + solvent_id_str : Optional[str] The string id of the solvent compound. Only required if the solvent is a reacting species. - Return - ------ - The solvent entry as a dictionary. + Returns + ------- + Dict + The solvent entry as a dictionary. """ if solvent_viscosity is None: solvent_viscosity = get_default_viscosity(solvent) @@ -208,33 +219,33 @@ def create_rms_yml_file(aggregate_str_ids: List[str], activation_energies: Union[List[float], np.ndarray], reactants: List[Tuple[List[str], List[str]]], file_name: str, solvent_name: Optional[str] = None, solvent_viscosity: Optional[float] = None, - solvent_aggregate_index: Optional[int] = None): + solvent_aggregate_index: Optional[int] = None) -> None: """ Write the yml file input for RMS. Parameters ---------- - aggregate_str_ids :: List[str] + aggregate_str_ids : List[str] The list of aggregate string ids to be added as RMS species. - enthalpies :: List[float] + enthalpies : List[float] The list of enthalpies for the aggregates (in J/mol). - entropies :: List[float] + entropies : List[float] The list of the aggregates entropies (in J/mol/K). - prefactors :: List[float] + prefactors : List[float] The list of the Arrhenius prefactors. - temperature_exponents :: List[float] + temperature_exponents : List[float] The list of the temperature exponents. - activation_energies :: Union[List[float], np.ndarray] + activation_energies : Union[List[float], np.ndarray] The activation energies in J/mol. - reactants :: List[Tuple[List[str], List[str]]] + reactants : List[Tuple[List[str], List[str]]] LHS (tuple[0]) and RHS (tuple[1]) of all reactions. - file_name :: str + file_name : str The filename for the yml file. - solvent_name :: Optional[str] (default None) + solvent_name : Optional[str] (default None) The solvent name. - solvent_viscosity :: Optional[float] (default None) + solvent_viscosity : Optional[float] (default None) The solvent's viscosity in Pa s. - solvent_aggregate_index :: Optional[int] (default None) + solvent_aggregate_index : Optional[int] (default None) The index of the solvent in the aggregte id list. This is only required if the solvent is a reacting species. """ solvent_aggregate_id_str = None @@ -263,13 +274,13 @@ def resolve_rms_solver(solver_name: str, reactor: Any): Parameters ---------- - solver_name :: str + solver_name : str The solver name. - reactor :: rms.Reactor + reactor : rms.Reactor The julia RMS reactor object. - Return - ------ + Returns + ------- Returns the selected ODE solver as a julia Differential Equation object. """ # pylint: disable=import-error @@ -294,22 +305,23 @@ def resolve_rms_phase(phase_name: str, rms_species: Any, rms_reactions: Any, rms Parameters ---------- - phase_name :: str + phase_name : str The name of the phase. Options are 'ideal_gas' and 'ideal_dilute_solution'. - rms_species :: RMS species object + rms_species : RMS species object The RMS species. - rms_reactions :: RMS Reaction list + rms_reactions : RMS Reaction list The RMS reactions. - rms_solvent :: RMS solvent object + rms_solvent : RMS solvent object The RMS solvetn object. - diffusion_limited :: bool + diffusion_limited : bool If true, diffusion limitations are enforced. - site_density :: Optional[float] + site_density : Optional[float] The site density for surface reactions. - Return - ------ - Returns the RMS phase object. + Returns + ------- + Any + Returns the RMS phase object. """ # pylint: disable=import-error from julia import ReactionMechanismSimulator as rms @@ -333,18 +345,19 @@ def get_ideal_dilute_solution(rms_species: Any, rms_reactions: Any, rms_solvent: Parameters ---------- - rms_species :: RMS species object + rms_species : RMS species object The RMS species. - rms_reactions :: RMS Reaction list + rms_reactions : RMS Reaction list The RMS reactions. - rms_solvent :: RMS solvent object + rms_solvent : RMS solvent object The RMS solvetn object. - diffusion_limited :: bool + diffusion_limited : bool If true, diffusion limitations are enforced. - Return - ------ - The rms.IdealDiluteSolution object. + Returns + ------- + Any + The rms.IdealDiluteSolution object. """ # pylint: disable=import-error from julia import ReactionMechanismSimulator as rms @@ -360,17 +373,17 @@ def get_ideal_surface(rms_species, rms_reactions, diffusion_limited: bool, site_ Parameters ---------- - rms_species :: RMS species object + rms_species : RMS species object The RMS species. - rms_reactions :: RMS Reaction list object + rms_reactions : RMS Reaction list object The RMS reactions. - diffusion_limited :: bool + diffusion_limited : bool If true, diffusion limitations are enforced. - site_density :: float + site_density : float The site density for surface reactions. - Return - ------ + Returns + ------- The rms.IdealSurface object. """ # pylint: disable=import-error @@ -384,21 +397,22 @@ def get_ideal_surface(rms_species, rms_reactions, diffusion_limited: bool, site_ diffusionlimited=diffusion_limited) -def get_default_viscosity(solvent_name: str): +def get_default_viscosity(solvent_name: str) -> float: """ Getter for tabulated solvent viscosity (in Pa s). Tabulated values are at 25 celsius. Source: https://hbcp.chemnetbase.com/faces/contents/InteractiveTable.xhtml Accessed on 23.01.2023, 14:00 - Parameter - --------- - solvent_name :: str + Parameters + ---------- + solvent_name : str The solvent's name. - Return - ------ - The solvent's viscosity (in Pa s). + Returns + ------- + float + The solvent's viscosity (in Pa s). """ viscosities = { "water": 0.890, @@ -411,14 +425,14 @@ def get_default_viscosity(solvent_name: str): "hexane": 0.240, "toluene": 0.560, "chloroform": 0.537, - "nitrobenzene": 1.863, - "aceticacid": 1.056, + "nitrobenzene": 1.863, + "aceticacid": 1.056, "acetonitrile": 0.369, "aniline": 3.85, "benzylalcohol": 5.47, "bromoform": 1.857, - "butanol": 2.54, - "tertbutanol": 4.31, + "butanol": 2.54, + "tertbutanol": 4.31, "carbontetrachloride": 0.908, "cyclohexane": 0.894, "cyclohexanone": 2.02, diff --git a/scine_puffin/utilities/rms_kinetic_model.py b/scine_puffin/utilities/rms_kinetic_model.py index 3fbd21e..9756427 100644 --- a/scine_puffin/utilities/rms_kinetic_model.py +++ b/scine_puffin/utilities/rms_kinetic_model.py @@ -1,28 +1,42 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ from datetime import datetime -from typing import List, Dict, Optional, Tuple, Union, Any -import numpy as np +from typing import List, Dict, Optional, Tuple, Union, Any, TYPE_CHECKING +import math -import scine_database as db +import numpy as np +from scipy import integrate from .rms_input_file_creator import create_rms_yml_file, resolve_rms_phase, resolve_rms_solver from ..programs.rms import JuliaPrecompiler +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + db = MissingDependency("scine_utilities") + class RMSKineticModel: """ This class provides an interface to the ReactionMechanismSimulator (RMS) for kinetic modeling. """ - def __init__(self, settings: Dict, manager: db.Manager, model: db.Model, rms_path: str, rms_file_name: str): + + def __init__(self, settings: Dict, manager: db.Manager, model: db.Model, rms_path: str, rms_file_name: str) -> None: """ Parameters: ----------- - settings : Dict[str, Any] + settings : Dict[str, Any] The settings of the kinetic modeling calculation. This must contain: * The activation energies 'ea'. * The enthalpies 'enthalpies'. @@ -31,14 +45,14 @@ def __init__(self, settings: Dict, manager: db.Manager, model: db.Model, rms_pat * The arrhenius temperature exponents 'arrhenius_temperature_exponents'. * The lower and upper uncertainty bounds for the activation energies and enthalpies. * General settings for the kinetic modeling: Diffusion limited, phase type, aggregate ids, reaction ids, - kinetic modeling solver, start concentrations, maximum integration time. - manager : db.Manager + kinetic modeling solver, start concentrations, maximum integration time. + manager : db.Manager The database manager. - model : db.Model + model : db.Model The main electronic structure model (used for default temperature and pressure). - rms_path : str + rms_path : str The path to the RMS shared library. - rms_file_name : str + rms_file_name : str The base file name for the RMS input file. """ self.settings = settings @@ -81,7 +95,7 @@ def __init__(self, settings: Dict, manager: db.Manager, model: db.Model, rms_pat self.solvent_species_added: bool = False - self.solvent: str = self.settings["reactor_solvent"] + self.solvent: Optional[str] = self.settings["reactor_solvent"] if self.solvent == "none": self.solvent = model.solvent if model.solvent != "none" else None if self.settings["solvent_aggregate_str_id"] != "none": @@ -110,7 +124,7 @@ def __init__(self, settings: Dict, manager: db.Manager, model: db.Model, rms_pat enforce_mass_balance = bool(settings["enforce_mass_balance"]) self.__sanity_checks(manager, enforce_mass_balance) - def create_yml_file(self, file_name: Optional[str], h: Optional[List[float]] = None, + def create_yml_file(self, file_name: Optional[str], h: Optional[List[float]] = None, s: Optional[List[float]] = None, a: Optional[List[float]] = None, n: Optional[List[float]] = None, ea: Optional[Union[List[float], np.ndarray]] = None): """ @@ -135,7 +149,7 @@ def create_yml_file(self, file_name: Optional[str], h: Optional[List[float]] = create_rms_yml_file(self.a_str_ids, h, s, a, n, ea, self.reactants, file_name, self.solvent, self.viscosity, self.solvent_index) - def _get_phase(self, file_name: Optional[str] = None, h: Optional[List[float]] = None, + def _get_phase(self, file_name: Optional[str] = None, h: Optional[List[float]] = None, s: Optional[List[float]] = None, a: Optional[List[float]] = None, n: Optional[List[float]] = None, ea: Optional[Union[List[float], np.ndarray]] = None): """ @@ -242,8 +256,8 @@ def _calculate_weight(a_str_id: str, compounds: db.Collection, flasks: db.Collec """ Calculate the molecular weight of the given aggregate """ - import scine_utilities as utils a_id = db.ID(a_str_id) + aggregate: Union[db.Compound, db.Flask] aggregate = db.Compound(a_id, compounds) if not aggregate.exists(): aggregate = db.Flask(a_id, flasks) @@ -295,7 +309,6 @@ def calculate_adjoined_sensitivities(self, simulation, reactor, absolute_vertex_ """ Run an adjoined sensitivity analysis for the microkinetic model. """ - import math # pylint: disable=import-error from julia import ReactionMechanismSimulator as rms # pylint: enable=import-error @@ -414,7 +427,6 @@ def _calculate_absolute_edge_flux(self, simulation, t_max: float, solution=None) """ Calculate the absolute edge fluxes. """ - from scipy import integrate # pylint: disable=import-error from julia import ReactionMechanismSimulator as rms # pylint: enable=import-error @@ -463,18 +475,19 @@ def translate_minimum_change_to_barriers(self, ea: np.ndarray, h: Union[List[flo Parameters ---------- - ea :: np.ndarray + ea : np.ndarray The activation energies (in J/mol). - h :: Union[List[float], np.ndarray] + h : Union[List[float], np.ndarray] The enthalpies. - s :: Union[List[float], np.ndarray] + s : Union[List[float], np.ndarray] The entropies. - a_str_id :: str + a_str_id : str The aggregate for which the enthalpy is changed. - Return - ------ - Returns the updated activation energies. + Returns + ------- + np.ndarray + Returns the updated activation energies. """ for r_str_id in self.get_aggregate_to_reaction_map()[a_str_id]: r_index = self.r_str_ids.index(r_str_id) @@ -496,14 +509,17 @@ def ensure_non_negative_barriers(self, ea: np.ndarray, h: Union[List[float], np. Parameters ---------- - ea :: np.ndarray + ea : np.ndarray The activation energies (in J/mol). - h :: Union[List[float], np.ndarray] + h : Union[List[float], np.ndarray] The enthalpies. - s :: Union[List[float], np.ndarray] + s : Union[List[float], np.ndarray] The entropies. - Returns the updated activation energies. + Returns + ------- + np.ndarray + Returns the updated activation energies. """ for a_str_id in self.a_str_ids: ea = self.translate_minimum_change_to_barriers(ea, h, s, a_str_id) diff --git a/scine_puffin/utilities/scine_helper.py b/scine_puffin/utilities/scine_helper.py index dcb8fee..1630a36 100644 --- a/scine_puffin/utilities/scine_helper.py +++ b/scine_puffin/utilities/scine_helper.py @@ -1,18 +1,26 @@ # -*- coding: utf-8 -*- +from __future__ import annotations """scine_helper.py: Collection of common procedures to be carried out in scine jobs""" __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ -from typing import List, Tuple, Union +from typing import List, Tuple, Union, Dict, TYPE_CHECKING, Any, Optional import copy import sys -import scine_database as db -import scine_utilities as utils - from scine_puffin.config import Configuration +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") class SettingsManager: @@ -24,22 +32,22 @@ def __init__( self, method_family: str, program: str, - calculator_settings: Union[dict, None] = None, - task_settings: Union[dict, None] = None, - ): + calculator_settings: Union[utils.Settings, None] = None, + task_settings: Union[Dict[str, Any], None] = None, + ) -> None: """ Constructor of the SettingsManager. Initializes class with method_family and program to get the available settings for the calculator. Parameters ---------- - method_family :: db.Model.method_family + method_family : db.Model.method_family The method_family to be used in the calculator. - program :: db.Model.program + program : db.Model.program The program to be used in the calculator. - calculator_settings :: dict + calculator_settings : utils.Settings Settings specific to the calculator. 'None' by default. This should be the full settings of the calculator. - task_settings :: dict + task_settings : dict Dictionary of settings specific to the task. 'None' by default. """ @@ -49,22 +57,23 @@ def __init__( if calculator_settings is None: self.calculator_settings = utils.Settings( "calc_settings", - {"program": program, **self.default_calculator_settings}, + {"program": program, **self.default_calculator_settings}, # type: ignore ) else: - self.calculator_settings = utils.Settings("calc_settings", {"program": program, **calculator_settings}) + self.calculator_settings = utils.Settings("calc_settings", + {"program": program, **calculator_settings}) # type: ignore # Set the task settings to an empty dict if the parameter is 'None'; otherwise set the given dict self.task_settings = {} if task_settings is None else copy.deepcopy(task_settings) self.cores_were_set = False self.memory_was_set = False - def setting_is_available(self, setting_key: str): + def setting_is_available(self, setting_key: str) -> bool: """ Check, if given setting_key is a setting in the calculator Parameters ---------- - setting_key :: list + setting_key : list The settings_key to be checked. """ @@ -79,35 +88,36 @@ def setting_is_available(self, setting_key: str): ) return False - def separate_settings(self, calculation_settings: utils.ValueCollection): + def separate_settings(self, calculation_settings: utils.ValueCollection) -> None: """ Extract calculator and task settings from the given calculation settings. Uses the information of the settings which are available for a calculator. Parameters ---------- - calculation_settings :: dict + calculation_settings : dict The dictionary of the calculation settings (read from database). """ self.calculator_settings.update(calculation_settings) - for key, value in calculation_settings.items(): + for key, value in dict(calculation_settings).items(): if key not in self.calculator_settings.keys(): self.task_settings[key] = value self.cores_were_set = "external_program_nprocs" in calculation_settings self.memory_was_set = "external_program_memory" in calculation_settings - def update_calculator_settings(self, structure: Union[None, db.Structure], model: db.Model, resources: dict): + def update_calculator_settings(self, structure: Union[None, db.Structure], model: db.Model, resources: dict) \ + -> None: """ Update calculator settings with information about the structure, the model, the resources and the available_calculator settings. Parameters ---------- - structure :: db.Structure + structure : db.Structure The database structure object to be analysed. - model :: db.Model + model : db.Model The database model object to be used. - resources :: dict + resources : dict Resources of the calculator (config['resources']). """ # Update structure related settings @@ -125,7 +135,7 @@ def update_calculator_settings(self, structure: Union[None, db.Structure], model if "external_program_nprocs" in self.calculator_settings.keys(): set_cores = self.calculator_settings["external_program_nprocs"] # Check, if settings are requesting more than available - if set_cores > int(resources["cores"]): + if set_cores > int(resources["cores"]): # type: ignore sys.stderr.write( "WARNING: Do not request more than you can chew.\n 'external_program_nprocs' is " "reset to " + str(resources["cores"]) + ".\n" @@ -136,7 +146,7 @@ def update_calculator_settings(self, structure: Union[None, db.Structure], model self.calculator_settings["external_program_nprocs"] = int(resources["cores"]) if "external_program_memory" in self.calculator_settings.keys(): - set_memory = int(self.calculator_settings["external_program_memory"]) + set_memory = int(self.calculator_settings["external_program_memory"]) # type: ignore # Check, if settings are requesting more than available if set_memory > int(resources["memory"] * 1024): sys.stderr.write( @@ -154,21 +164,22 @@ def prepare_readuct_task( calculation: db.Calculation, settings: utils.ValueCollection, resources: dict, - ) -> Tuple[dict, List[str]]: + ) -> Tuple[Dict[str, Optional[utils.core.Calculator]], List[str]]: """ Constructs a dictionary with a Scine Calculator based on the calculation settings and resources to have an input for a ReaDuct task Parameters ---------- - structure :: db.Structure + structure : db.Structure The database structure object to be analysed. - calculation :: db.Calculation + calculation : db.Calculation The database calculation that shall be calculated with ReaDuct. - settings :: utils.ValueCollection + settings : utils.ValueCollection The settings of the database calculation. - resources :: dict + resources : dict Resources of the calculator (config['resources']). + Returns ------- Tuple[dict, List[str]]] @@ -208,11 +219,11 @@ def update_model( Parameters ---------- - calculator :: utils.core.Calculator + calculator : utils.core.Calculator The calculator that performed the calculation. - calculation :: db.Calculation + calculation : db.Calculation The database calculation that is currently run. - config :: scine_puffin.config.Configuration + config : scine_puffin.config.Configuration The current configuration of the Puffin necessary for the program's version. """ model = calculation.get_model() diff --git a/scine_puffin/utilities/surface_helper.py b/scine_puffin/utilities/surface_helper.py index 4f295c0..0b9b335 100644 --- a/scine_puffin/utilities/surface_helper.py +++ b/scine_puffin/utilities/surface_helper.py @@ -1,17 +1,28 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ from ast import literal_eval +from typing import TYPE_CHECKING from platform import python_version from pymatgen.core import Lattice from pymatgen.core.surface import Slab import pymatgen -import scine_database as db -import scine_utilities as utils + +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") def get_slab_dict(structure: db.Structure, properties: db.Collection) -> dict: diff --git a/scine_puffin/utilities/task_to_readuct_call.py b/scine_puffin/utilities/task_to_readuct_call.py new file mode 100644 index 0000000..3dd26fd --- /dev/null +++ b/scine_puffin/utilities/task_to_readuct_call.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +__copyright__ = """ This code is licensed under the 3-clause BSD license. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. +See LICENSE.txt for details. +""" + +from enum import Enum + + +class SubTaskToReaductCall(Enum): + OPT = "run_opt_task" + RCOPT = "run_opt_task" + IRCOPT = "run_opt_task" + IRC = "run_irc_task" + TSOPT = "run_tsopt_task" + NT = "run_nt_task" + NT2 = "run_nt2_task" + AFIR = "run_afir_task" + SP = "run_sp_task" diff --git a/scine_puffin/utilities/transfer_helper.py b/scine_puffin/utilities/transfer_helper.py index 70b5247..fcd14e8 100644 --- a/scine_puffin/utilities/transfer_helper.py +++ b/scine_puffin/utilities/transfer_helper.py @@ -1,13 +1,19 @@ # -*- coding: utf-8 -*- +from __future__ import annotations __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ from abc import ABC, abstractmethod -from typing import List +from typing import List, TYPE_CHECKING -import scine_database as db +from scine_puffin.utilities.imports import module_exists, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") class TransferHelper(ABC): @@ -44,10 +50,10 @@ def transfer_properties_between_multiple(self, Parameters ---------- - old_structure : db.Structure - The structure holding the properties - new_structure : db.Structure - The structure receiving the properties + old_structures : List[db.Structure] + The structures holding the properties + new_structures : List[db.Structure] + The structures receiving the properties properties_to_transfer: List[str] The names of the properties to transfer """ @@ -65,7 +71,7 @@ def simple_transfer_all(old_structure: db.Structure, new_structure: db.Structure The structure holding the properties new_structure : db.Structure The structure receiving the properties - properties_to_transfer : List[str] + properties : List[str] The names of the properties to transfer """ for prop in properties: diff --git a/scine_puffin/utilities/turbomole_helper.py b/scine_puffin/utilities/turbomole_helper.py index 9d02c31..628cd01 100644 --- a/scine_puffin/utilities/turbomole_helper.py +++ b/scine_puffin/utilities/turbomole_helper.py @@ -1,21 +1,33 @@ # -*- coding: utf-8 -*- +from __future__ import annotations """turbomole_helper.py: Collection of common procedures to be carried out with turbomole""" __copyright__ = """ This code is licensed under the 3-clause BSD license. Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """ +import math import os import re import sys from subprocess import Popen, PIPE, run -from typing import List, Tuple +from typing import List, Tuple, TYPE_CHECKING -from scine_puffin.jobs.templates.job import TurbomoleJob +from scine_puffin.jobs.templates.turbomole_job import TurbomoleJob +from scine_puffin.utilities.imports import module_exists, requires, MissingDependency + +if module_exists("scine_database") or TYPE_CHECKING: + import scine_database as db +else: + db = MissingDependency("scine_database") +if module_exists("scine_utilities") or TYPE_CHECKING: + import scine_utilities as utils +else: + utils = MissingDependency("scine_utilities") class TurbomoleHelper: - def __init__(self): + def __init__(self) -> None: self.define_input = "tm.input" self.input_structure = "system.xyz" self.coord_file = "coord" @@ -30,19 +42,19 @@ def __init__(self): self.turbomole_job = TurbomoleJob() - def check_settings_availability(self, job, settings: dict): - import scine_utilities.settings_names as sn + @requires("utilities") + def check_settings_availability(self, job, settings: utils.ValueCollection) -> None: # All available settings that are implemented for Turbomole available_settings = [ "cartesian_constraints", - sn.max_scf_iterations, + utils.settings_names.max_scf_iterations, "transform_coordinates", - sn.scf_damping, - sn.self_consistence_criterion, + utils.settings_names.scf_damping, + utils.settings_names.self_consistence_criterion, "scf_orbitalshift", "calculate_loewdin_charges", - sn.spin_mode, + utils.settings_names.spin_mode, ] available_settings_structure_optimization = [ @@ -59,8 +71,11 @@ def check_settings_availability(self, job, settings: dict): if key not in settings_to_check: raise NotImplementedError("Error: The key '{}' was not recognized.".format(key)) - # Executes any Turbomole pre- or postprocessing tool - def execute(self, args, input_file=None, error_test=True, stdout_tofile=True): + @staticmethod + def execute(args, input_file=None, error_test=True, stdout_tofile=True): + """ + Executes any Turbomole pre- or postprocessing tool + """ if isinstance(args, str): args = args.split() @@ -71,6 +86,7 @@ def execute(self, args, input_file=None, error_test=True, stdout_tofile=True): out = open(out_file, "w") else: out = PIPE + out_file = "test.out" if input_file: with open(input_file, "r") as f: @@ -93,11 +109,11 @@ def execute(self, args, input_file=None, error_test=True, stdout_tofile=True): if not stdout_tofile: return res[0].decode("utf-8", errors='replace') - # Converts xyz file to Turbomole coord file - - def write_coord_file(self, settings: dict): - - import scine_utilities as utils + @requires("utilities") + def write_coord_file(self, settings: utils.ValueCollection) -> None: + """ + Converts xyz file to Turbomole coord file + """ # Read in xyz file and transform coordinates from Angstrom to Bohr xyz, _ = utils.io.read(self.input_structure) @@ -121,7 +137,7 @@ def write_coord_file(self, settings: dict): for j, line in enumerate(coord_in_bohr): if constraints_set: constraint_atoms = settings["cartesian_constraints"] - if (j + 1) in constraint_atoms: + if (j + 1) in constraint_atoms: # type: ignore coord.write(line + " f" + "\n") else: coord.write(line + "\n") @@ -129,10 +145,12 @@ def write_coord_file(self, settings: dict): coord.write(line + "\n") coord.write("$end\n") - # Generates input file for the preprocessing tool define - def prepare_define_session(self, structure, model, settings: dict, job): - import scine_utilities.settings_names as sn - + @requires("utilities") + def prepare_define_session(self, structure: db.Structure, model: db.Model, + settings: utils.ValueCollection, job: db.Job): + """ + Generates input file for the preprocessing tool define + """ with open(self.define_input, "w") as define_input: define_input.write("\n\na {}\n".format(self.coord_file)) @@ -143,12 +161,13 @@ def prepare_define_session(self, structure, model, settings: dict, job): transform_coordinates_set = True if "transform_coordinates" in settings: - transform_coordinates_set = settings["transform_coordinates"] + transform_coordinates_set = settings["transform_coordinates"] # type: ignore spin_mode = "any" spin_mode_is_set = False - if sn.spin_mode in settings: - spin_mode = settings[sn.spin_mode] + if utils.settings_names.spin_mode in settings: + spin_mode = settings[utils.settings_names.spin_mode] # type: ignore + assert isinstance(spin_mode, str) spin_mode_is_set = True if spin_mode not in self.available_spin_modes: @@ -196,15 +215,15 @@ def prepare_define_session(self, structure, model, settings: dict, job): raise NotImplementedError("Invalid dispersion correction!") # Max number of SCF iterations - if sn.max_scf_iterations in settings: - define_input.write("scf\niter\n{}\n".format(int(settings[sn.max_scf_iterations]))) + if utils.settings_names.max_scf_iterations in settings: + define_input.write("scf\niter\n{}\n".format( + int(settings[utils.settings_names.max_scf_iterations]))) # type: ignore define_input.write("\n*") - # Runs the Turbomole preprocessing tool define - def initialize(self, model, settings: dict): - - import math - import scine_utilities.settings_names as sn + def initialize(self, model: db.Model, settings: utils.ValueCollection) -> None: + """ + Runs the Turbomole preprocessing tool define + """ self.execute( os.path.join(self.turbomole_job.turboexe, "define"), @@ -219,8 +238,8 @@ def initialize(self, model, settings: dict): # Add damping for SCF if requested # TODO: SCF damping setting should allow for the choice of custom damping parameters - if sn.scf_damping in settings: - if settings[sn.scf_damping]: + if utils.settings_names.scf_damping in settings: + if settings[utils.settings_names.scf_damping]: run( r"sed -i '/$scfdamp*/c\$scfdamp start=8.500 step=0.10 min=0.50' {}".format(self.control_file), shell=True, @@ -229,13 +248,14 @@ def initialize(self, model, settings: dict): if "scf_orbitalshift" in settings: run( r"sed -i '/$scforbitalshift */c\$scforbitalshift closedshell={}' {}".format( - float(settings["scf_orbitalshift"]), self.control_file + float(settings["scf_orbitalshift"]), self.control_file # type: ignore ), shell=True, ) # SCF convergence criterion - if sn.self_consistence_criterion in settings: - convergence_threshold = int(round(-math.log10(settings[sn.self_consistence_criterion]))) + if utils.settings_names.self_consistence_criterion in settings: + convergence_threshold = int(round( + -math.log10(settings[utils.settings_names.self_consistence_criterion]))) # type: ignore run( r"sed -i '/$scfconv*/c\$scfconv {}' {}".format(convergence_threshold, self.control_file), shell=True, @@ -261,8 +281,10 @@ def initialize(self, model, settings: dict): + "Turbomole accepts all functionals in lower case letters only." ) - # Parse Loewdin charges - def get_loewdin_charges(self, natoms, calculation_settings) -> Tuple[bool, List[float]]: + def get_loewdin_charges(self, natoms: int, calculation_settings: utils.ValueCollection) -> Tuple[bool, List[float]]: + """ + Parse Loewdin charges + """ # Execute Turbomole postprocessing tool proper with open(self.proper_input_file, "a") as proper_file: @@ -272,6 +294,7 @@ def get_loewdin_charges(self, natoms, calculation_settings) -> Tuple[bool, List[ input_file=self.proper_input_file, ) loewdin_charges = [] + charge_lines = [] # Read atomic charges from proper output with open(self.proper_file, "r") as file: lines = file.readlines() @@ -296,8 +319,10 @@ def get_loewdin_charges(self, natoms, calculation_settings) -> Tuple[bool, List[ return atomic_charges_set, loewdin_charges - # Parse energy file def parse_energy_file(self) -> float: + """ + Parse energy file + """ try: with open(self.energy_file, "r") as file: lines = file.readlines() @@ -312,12 +337,12 @@ def parse_energy_file(self) -> float: except FileNotFoundError as e: raise RuntimeError("Energy file is not accessible because the job failed.") from e - def evaluate_spin_mode(self, calculation_settings) -> str: - import scine_utilities.settings_names as sn + @requires("utilities") + def evaluate_spin_mode(self, calculation_settings: utils.ValueCollection) -> str: with open(self.control_file) as f: if "uhf" and "uhfmo" in f.read(): - if calculation_settings.get(sn.spin_mode) == "restricted": + if calculation_settings.get(utils.settings_names.spin_mode) == "restricted": sys.stderr.write( "Requested restricted calculation was converted to an unrestricted calculation " "with multiplicity != 1. Please enforce unrestricted singlet for this case." diff --git a/scripts/rms/scine2rms.py b/scripts/rms/scine2rms.py index e9844a4..d593f77 100644 --- a/scripts/rms/scine2rms.py +++ b/scripts/rms/scine2rms.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- __copyright__ = """ This code is licensed under the 3-clause BSD license. -Copyright ETH Zurich, Laboratory of Physical Chemistry, Reiher Group. +Copyright ETH Zurich, Department of Chemistry and Applied Biosciences, Reiher Group. See LICENSE.txt for details. """