diff --git a/atomisticparsers/__init__.py b/atomisticparsers/__init__.py index 5df4d622..27328e56 100644 --- a/atomisticparsers/__init__.py +++ b/atomisticparsers/__init__.py @@ -74,7 +74,7 @@ def load(self): python_package='atomisticparsers.asap', mainfile_binary_header_re=b'AFFormatASE\\-Trajectory', mainfile_mime_re='application/octet-stream', - mainfile_name_re=r'.*.traj$', + mainfile_name_re=r'.*.traj$', # can this be specified here? to directly check for the emt calculator maybe? somehow we need to seperate the general ase/traj parser from the asap parser parser_class_name='atomisticparsers.asap.AsapParser', code_name='ASAP', code_homepage='https://wiki.fysik.dtu.dk/asap', @@ -94,6 +94,33 @@ def load(self): }, ) +ase_parser_entry_point = EntryPoint( + name='parsers/ase', + aliases=['parsers/ase'], + description='NOMAD parser for ASE.', + python_package='atomisticparsers.ase', + mainfile_binary_header_re=b'.+?ASE\\-Trajectory', + mainfile_mime_re='application/octet-stream', + mainfile_name_re=r'.*.traj$', + parser_class_name='atomisticparsers.ase.AseParser', + code_name='ASE', + code_homepage='https://wiki.fysik.dtu.dk/ase', + code_category='Atomistic code', + metadata={ + 'codeCategory': 'Atomistic code', + 'codeLabel': 'ASE', + 'codeLabelStyle': 'all in capitals', + 'codeName': 'ase', + 'codeUrl': 'https://wiki.fysik.dtu.dk/ase', + 'parserDirName': 'dependencies/parsers/atomistic/atomisticparsers/ase/', + 'parserGitUrl': 'https://github.com/nomad-coe/atomistic-parsers.git', + 'parserSpecific': '', + 'preamble': '', + 'status': 'production', + 'tableOfFiles': '', + }, +) + bopfox_parser_entry_point = EntryPoint( name='parsers/bopfox', aliases=['parsers/bopfox'], diff --git a/atomisticparsers/asap/parser.py b/atomisticparsers/asap/parser.py index 9143bea4..206f4f9b 100644 --- a/atomisticparsers/asap/parser.py +++ b/atomisticparsers/asap/parser.py @@ -16,182 +16,41 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from simulationworkflowschema.geometry_optimization import GeometryOptimization +from simulationworkflowschema.molecular_dynamics import MolecularDynamics +from atomisticparsers.utils import ASETrajParser +from .metainfo import asap # pylint: disable=unused-import -import os -import logging -import numpy as np -from ase.io.trajectory import Trajectory - -from nomad.units import ureg -from nomad.parsing.file_parser import FileParser -from runschema.run import Run, Program -from runschema.method import Method, ForceField, Model -from simulationworkflowschema import GeometryOptimization, GeometryOptimizationMethod -from atomisticparsers.utils import MDParser -from .metainfo.asap import MolecularDynamics # pylint: disable=unused-import - - -class TrajParser(FileParser): - def __init__(self): - super().__init__() - - @property - def traj(self): - if self._file_handler is None: - try: - self._file_handler = Trajectory(self.mainfile, 'r') - # check if traj file is really asap - if 'calculator' in self._file_handler.backend.keys(): - if self._file_handler.backend.calculator.name != 'emt': # pylint: disable=E1101 - self.logger.error('Trajectory is not ASAP.') - self._file_handler = None - except Exception: - self.logger.error('Error reading trajectory file.') - return self._file_handler - - def get_version(self): - if hasattr(self.traj, 'ase_version') and self.traj.ase_version: - return self.traj.ase_version - else: - return '3.x.x' - - def parse(self): - pass - - -class AsapParser(MDParser): - def __init__(self): - self.traj_parser = TrajParser() - super().__init__() +class AsapParser(ASETrajParser): def parse_method(self): + super().parse_method() traj = self.traj_parser.traj - sec_method = Method() - self.archive.run[0].method.append(sec_method) - - if traj[0].calc is not None: - sec_method.force_field = ForceField(model=[Model(name=traj[0].calc.name)]) description = traj.description if hasattr(traj, 'description') else dict() if not description: return - calc_type = description.get('type') - if calc_type == 'optimization': - workflow = GeometryOptimization(method=GeometryOptimizationMethod()) + workflow = self.archive.workflow2 + if isinstance(workflow, GeometryOptimization): workflow.x_asap_maxstep = description.get('maxstep', 0) - workflow.method.method = description.get('optimizer', '').lower() - self.archive.workflow2 = workflow - elif calc_type == 'molecular-dynamics': - data = {} - data['x_asap_timestep'] = description.get('timestep', 0) - data['x_asap_temperature'] = description.get('temperature', 0) + elif isinstance(workflow, MolecularDynamics): + workflow.x_asap_timestep = description.get('timestep', 0) + workflow.x_asap_temperature = description.get('temperature', 0) md_type = description.get('md-type', '') - thermodynamic_ensemble = None if 'Langevin' in md_type: - data['x_asap_langevin_friction'] = description.get('friction', 0) - thermodynamic_ensemble = 'NVT' - elif 'NVT' in md_type: - thermodynamic_ensemble = 'NVT' - elif 'Verlet' in md_type: - thermodynamic_ensemble = 'NVE' - elif 'NPT' in md_type: - thermodynamic_ensemble = 'NPT' - data['method'] = {'thermodynamic_ensemble': thermodynamic_ensemble} - self.parse_md_workflow(data) + workflow.x_asap_langevin_friction = description.get('friction', 0) def write_to_archive(self) -> None: self.traj_parser.mainfile = self.mainfile - if self.traj_parser.traj is None: - return - - sec_run = Run() - self.archive.run.append(sec_run) - sec_run.program = Program(name='ASAP', version=self.traj_parser.get_version()) - - # TODO do we build the topology and method for each frame - self.parse_method() - - # set up md parser - self.n_atoms = max( - [traj.get_global_number_of_atoms() for traj in self.traj_parser.traj] - ) - steps = [ - (traj.description if hasattr(traj, 'description') else dict()).get( - 'interval', 1 - ) - * n - for n, traj in enumerate(self.traj_parser.traj) - ] - self.trajectory_steps = steps - self.thermodynamics_steps = steps - - def get_constraint_name(constraint): - def index(): - d = constraint['kwargs'].get('direction') - return ((d / np.linalg.norm(d)) ** 2).argsort()[2] - - name = constraint.get('name') - if name == 'FixedPlane': - return ['fix_yz', 'fix_xz', 'fix_xy'][index()] - elif name == 'FixedLine': - return ['fix_x', 'fix_y', 'fix_z'][index()] - elif name == 'FixAtoms': - return 'fix_xyz' - else: - return name - for step in self.trajectory_steps: - traj = self.traj_parser.traj[steps.index(step)] - lattice_vectors = traj.get_cell() * ureg.angstrom - labels = traj.get_chemical_symbols() - positions = traj.get_positions() * ureg.angstrom - periodic = traj.get_pbc() - if (velocities := traj.get_velocities()) is not None: - velocities = velocities * (ureg.angstrom / ureg.fs) + # check if traj file is really asap + if 'calculator' in self.traj_parser.traj.backend.keys(): + if self.traj_parser.traj.backend.calculator.name != 'emt': # pylint: disable=E1101 + self.logger.error('Trajectory is not ASAP.') + return - constraints = [] - for constraint in traj.constraints: - as_dict = constraint.todict() - indices = as_dict['kwargs'].get('a', as_dict['kwargs'].get('indices')) - indices = ( - indices - if isinstance(indices, (np.ndarray, list)) - else [int(indices)] - ) - constraints.append( - dict( - atom_indices=[np.asarray(indices)], - kind=get_constraint_name(as_dict), - ) - ) - self.parse_trajectory_step( - dict( - atoms=dict( - lattice_vectors=lattice_vectors, - labels=labels, - positions=positions, - periodic=periodic, - velocities=velocities, - ), - constraint=constraints, - ) - ) + super().write_to_archive() - for step in self.thermodynamics_steps: - try: - traj = self.traj_parser.traj[steps.index(step)] - if (total_energy := traj.get_total_energy()) is not None: - total_energy = total_energy * ureg.eV - if (forces := traj.get_forces()) is not None: - forces = forces * ureg.eV / ureg.angstrom - if (forces_raw := traj.get_forces(apply_constraint=False)) is not None: - forces_raw * ureg.eV / ureg.angstrom - self.parse_thermodynamics_step( - dict( - energy=dict(total=dict(value=total_energy)), - forces=dict(total=dict(value=forces, value_raw=forces_raw)), - ) - ) - except Exception: - pass + if self.archive.run: + self.archive.run[0].program.name = 'ASAP' diff --git a/atomisticparsers/ase/__init__.py b/atomisticparsers/ase/__init__.py new file mode 100644 index 00000000..0d5d438d --- /dev/null +++ b/atomisticparsers/ase/__init__.py @@ -0,0 +1,19 @@ +# +# Copyright The NOMAD Authors. +# +# This file is part of NOMAD. +# See https://nomad-lab.eu for further info. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from .parser import AseParser diff --git a/atomisticparsers/ase/__main__.py b/atomisticparsers/ase/__main__.py new file mode 100644 index 00000000..d527dd11 --- /dev/null +++ b/atomisticparsers/ase/__main__.py @@ -0,0 +1,31 @@ +# +# Copyright The NOMAD Authors. +# +# This file is part of NOMAD. +# See https://nomad-lab.eu for further info. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import sys +import json +import logging + +from nomad.utils import configure_logging +from nomad.datamodel import EntryArchive +from atomisticparsers.ase import AseParser + +if __name__ == '__main__': + configure_logging(console_log_level=logging.DEBUG) + archive = EntryArchive() + AseParser().parse(sys.argv[1], archive, logging) + json.dump(archive.m_to_dict(), sys.stdout, indent=2) diff --git a/atomisticparsers/ase/parser.py b/atomisticparsers/ase/parser.py new file mode 100644 index 00000000..71e39729 --- /dev/null +++ b/atomisticparsers/ase/parser.py @@ -0,0 +1,30 @@ +# +# Copyright The NOMAD Authors. +# +# This file is part of NOMAD. +# See https://nomad-lab.eu for further info. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from atomisticparsers.utils import ASETrajParser + + +class AseParser(ASETrajParser): + def write_to_archive(self) -> None: + super().write_to_archive() + + if self.archive.run: + self.archive.run[0].program.name = 'ASE' + + # TODO add tests diff --git a/atomisticparsers/gromacs/parser.py b/atomisticparsers/gromacs/parser.py index 6fa84a73..832c5c0c 100644 --- a/atomisticparsers/gromacs/parser.py +++ b/atomisticparsers/gromacs/parser.py @@ -53,7 +53,9 @@ x_gromacs_section_input_output_files, ) from atomisticparsers.utils import MDAnalysisParser, MDParser -from simulationworkflowschema.molecular_dynamics import get_bond_list_from_model_contributions +from simulationworkflowschema.molecular_dynamics import ( + get_bond_list_from_model_contributions, +) re_float = r'[-+]?\d+\.*\d*(?:[Ee][-+]\d+)?' re_n = r'[\n\r]' diff --git a/atomisticparsers/lammps/parser.py b/atomisticparsers/lammps/parser.py index a2008007..3d9b7fc8 100644 --- a/atomisticparsers/lammps/parser.py +++ b/atomisticparsers/lammps/parser.py @@ -44,7 +44,9 @@ x_lammps_section_control_parameters, ) from atomisticparsers.utils import MDAnalysisParser, MDParser -from simulationworkflowschema.molecular_dynamics import get_bond_list_from_model_contributions +from simulationworkflowschema.molecular_dynamics import ( + get_bond_list_from_model_contributions, +) re_float = r'[-+]?\d+\.*\d*(?:[Ee][-+]\d+)?' diff --git a/atomisticparsers/utils/__init__.py b/atomisticparsers/utils/__init__.py index 696b48a0..d27b4bd2 100644 --- a/atomisticparsers/utils/__init__.py +++ b/atomisticparsers/utils/__init__.py @@ -17,6 +17,6 @@ # limitations under the License. from .mdanalysis import MDAnalysisParser -from .parsers import MDParser +from .parsers import MDParser, ASETrajParser MOL = 6.022140857e23 diff --git a/atomisticparsers/utils/mdanalysis.py b/atomisticparsers/utils/mdanalysis.py index a85187a7..278b9e61 100644 --- a/atomisticparsers/utils/mdanalysis.py +++ b/atomisticparsers/utils/mdanalysis.py @@ -35,7 +35,10 @@ from nomad.units import ureg from nomad.parsing.file_parser import FileParser -from simulationworkflowschema.molecular_dynamics import BeadGroup, shifted_correlation_average +from simulationworkflowschema.molecular_dynamics import ( + BeadGroup, + shifted_correlation_average, +) MOL = 6.022140857e23 diff --git a/atomisticparsers/utils/parsers.py b/atomisticparsers/utils/parsers.py index a301b6bb..ac67c274 100644 --- a/atomisticparsers/utils/parsers.py +++ b/atomisticparsers/utils/parsers.py @@ -20,15 +20,23 @@ from typing import Any, Dict, List, Union import numpy as np from collections.abc import Iterable +from ase.io.trajectory import Trajectory from nomad.utils import get_logger -from nomad.metainfo import MSection, SubSection, Quantity -from nomad.parsing.file_parser import Parser -from runschema.run import Run +from nomad.units import ureg +from nomad.metainfo import MSection +from nomad.parsing.file_parser import Parser, FileParser +from runschema.run import Run, Program from runschema.system import System from runschema.calculation import Calculation -from runschema.method import Interaction, Model -from simulationworkflowschema import MolecularDynamics +from runschema.method import Interaction, Model, Method, ForceField +from simulationworkflowschema.geometry_optimization import ( + GeometryOptimization, + GeometryOptimizationMethod, +) +from simulationworkflowschema.molecular_dynamics import ( + MolecularDynamics, +) class MDParser(Parser): @@ -224,3 +232,161 @@ def parse_interactions_by_type( sec_model.contributions.append(sec_interaction) self.parse_section(interaction_type_dict, sec_interaction) # TODO Shift Gromacs and Lammps parsers to use this function as well if possible + + +class TrajParser(FileParser): + def __init__(self): + super().__init__() + + @property + def traj(self): + if self._file_handler is None: + try: + self._file_handler = Trajectory(self.mainfile, 'r') + except Exception: + self.logger.error('Error reading trajectory file.') + return self._file_handler + + def get_version(self): + if hasattr(self.traj, 'ase_version') and self.traj.ase_version: + return self.traj.ase_version + else: + return '3.x.x' + + def parse(self): + pass + + +class ASETrajParser(MDParser): + def __init__(self): + self.traj_parser = TrajParser() + super().__init__() + + def parse_method(self): + traj = self.traj_parser.traj + + sec_method = Method() + self.archive.run[0].method.append(sec_method) + + if traj[0].calc is not None: + sec_method.force_field = ForceField(model=[Model(name=traj[0].calc.name)]) + + description = traj.description if hasattr(traj, 'description') else dict() + if not description: + return + + calc_type = description.get('type') + if calc_type == 'optimization': + workflow = GeometryOptimization(method=GeometryOptimizationMethod()) + workflow.method.method = description.get('optimizer', '').lower() + self.archive.workflow2 = workflow + elif calc_type == 'molecular-dynamics': + data = {} + md_type = description.get('md-type', '') + thermodynamic_ensemble = None + if 'Langevin' in md_type: + thermodynamic_ensemble = 'NVT' + elif 'NVT' in md_type: + thermodynamic_ensemble = 'NVT' + elif 'Verlet' in md_type: + thermodynamic_ensemble = 'NVE' + elif 'NPT' in md_type: + thermodynamic_ensemble = 'NPT' + data['method'] = {'thermodynamic_ensemble': thermodynamic_ensemble} + self.parse_md_workflow(data) + + def write_to_archive(self): + self.traj_parser.mainfile = self.mainfile + if self.traj_parser.traj is None: + return + + sec_run = Run() + self.archive.run.append(sec_run) + sec_run.program = Program(version=self.traj_parser.get_version()) + + # TODO do we build the topology and method for each frame + self.parse_method() + + # set up md parser + self.n_atoms = max( + [traj.get_global_number_of_atoms() for traj in self.traj_parser.traj] + ) + steps = [ + (traj.description if hasattr(traj, 'description') else dict()).get( + 'interval', 1 + ) + * n + for n, traj in enumerate(self.traj_parser.traj) + ] + self.trajectory_steps = steps + self.thermodynamics_steps = steps + + def get_constraint_name(constraint): + def index(): + d = constraint['kwargs'].get('direction') + return ((d / np.linalg.norm(d)) ** 2).argsort()[2] + + name = constraint.get('name') + if name == 'FixedPlane': + return ['fix_yz', 'fix_xz', 'fix_xy'][index()] + elif name == 'FixedLine': + return ['fix_x', 'fix_y', 'fix_z'][index()] + elif name == 'FixAtoms': + return 'fix_xyz' + else: + return name + + for step in self.trajectory_steps: + traj = self.traj_parser.traj[steps.index(step)] + lattice_vectors = traj.get_cell() * ureg.angstrom + labels = traj.get_chemical_symbols() + positions = traj.get_positions() * ureg.angstrom + periodic = traj.get_pbc() + if (velocities := traj.get_velocities()) is not None: + velocities = velocities * (ureg.angstrom / ureg.fs) + + constraints = [] + for constraint in traj.constraints: + as_dict = constraint.todict() + indices = as_dict['kwargs'].get('a', as_dict['kwargs'].get('indices')) + indices = ( + indices + if isinstance(indices, (np.ndarray, list)) + else [int(indices)] + ) + constraints.append( + dict( + atom_indices=[np.asarray(indices)], + kind=get_constraint_name(as_dict), + ) + ) + self.parse_trajectory_step( + dict( + atoms=dict( + lattice_vectors=lattice_vectors, + labels=labels, + positions=positions, + periodic=periodic, + velocities=velocities, + ), + constraint=constraints, + ) + ) + + for step in self.thermodynamics_steps: + try: + traj = self.traj_parser.traj[steps.index(step)] + if (total_energy := traj.get_total_energy()) is not None: + total_energy = total_energy * ureg.eV + if (forces := traj.get_forces()) is not None: + forces = forces * ureg.eV / ureg.angstrom + if (forces_raw := traj.get_forces(apply_constraint=False)) is not None: + forces_raw * ureg.eV / ureg.angstrom + self.parse_thermodynamics_step( + dict( + energy=dict(total=dict(value=total_energy)), + forces=dict(total=dict(value=forces, value_raw=forces_raw)), + ) + ) + except Exception: + pass diff --git a/pyproject.toml b/pyproject.toml index 88c685a9..e4e16394 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,7 @@ include = [ [project.entry-points.'nomad.plugin'] amberparser = "atomisticparsers:amber_parser_entry_point" asapparser = "atomisticparsers:asap_parser_entry_point" +aseparser = "atomisticparsers:ase_parser_entry_point" bopfoxparser = "atomisticparsers:bopfox_parser_entry_point" dftbplusparser = "atomisticparsers:dftbplus_parser_entry_point" dlpolyparser = "atomisticparsers:dlpoly_parser_entry_point" diff --git a/tests/data/ase/CH3OPd45_0.0.traj b/tests/data/ase/CH3OPd45_0.0.traj new file mode 100755 index 00000000..a8daa73f Binary files /dev/null and b/tests/data/ase/CH3OPd45_0.0.traj differ diff --git a/tests/data/ase/ML-NEB.traj b/tests/data/ase/ML-NEB.traj new file mode 100755 index 00000000..fbee37e9 Binary files /dev/null and b/tests/data/ase/ML-NEB.traj differ diff --git a/tests/data/ase/alloy_slab.traj b/tests/data/ase/alloy_slab.traj new file mode 100755 index 00000000..c8be6a63 Binary files /dev/null and b/tests/data/ase/alloy_slab.traj differ diff --git a/tests/data/ase/initial.traj b/tests/data/ase/initial.traj new file mode 100755 index 00000000..ee6d1a97 Binary files /dev/null and b/tests/data/ase/initial.traj differ diff --git a/tests/data/ase/neb1.traj b/tests/data/ase/neb1.traj new file mode 100755 index 00000000..a517e674 Binary files /dev/null and b/tests/data/ase/neb1.traj differ