diff --git a/MDANSE/Src/MDANSE/Framework/AtomMapping/atom_mapping.py b/MDANSE/Src/MDANSE/Framework/AtomMapping/atom_mapping.py index f4473127f..f518f8a0e 100644 --- a/MDANSE/Src/MDANSE/Framework/AtomMapping/atom_mapping.py +++ b/MDANSE/Src/MDANSE/Framework/AtomMapping/atom_mapping.py @@ -22,22 +22,67 @@ class AtomLabel: - def __init__(self, atm_label, **kwargs): - self.atm_label = atm_label + def __init__(self, atm_label: str, **kwargs): + """Creates an atom label object which is used for atom mapping + and atom type guessing. + + Parameters + ---------- + atm_label : str + The main atom label. + kwargs + The other atom label. + """ + # use translations since it's faster than the alternative + # methods as of writing e.g. re.sub + translation = str.maketrans("", "", ";=") + self.atm_label = atm_label.translate(translation) self.grp_label = f"" if kwargs: for k, v in kwargs.items(): - self.grp_label += f"{k}={v};" + self.grp_label += f"{k}={str(v).translate(translation)};" self.grp_label = self.grp_label[:-1] self.mass = kwargs.get("mass", None) if self.mass is not None: self.mass = float(self.mass) def __eq__(self, other: object) -> bool: + """Used to check if atom labels are equal. + + Parameters + ---------- + other : AtomLabel + The other atom label to compare against. + + Returns + ------- + bool + True if all attributes are equal. + + Raises + ------ + AssertionError + If the other object is not an AtomLabel. + """ if not isinstance(other, AtomLabel): AssertionError(f"{other} should be an instance of AtomLabel.") - if self.grp_label == other.grp_label and self.atm_label == other.atm_label: + if ( + self.grp_label == other.grp_label + and self.atm_label == other.atm_label + and self.mass == other.mass + ): return True + else: + return False + + def __hash__(self) -> int: + """ + Returns + ------- + int + A hash of the object in its current state. + """ + return hash((self.atm_label, self.grp_label, self.mass)) def guess_element(atm_label: str, mass: Union[float, int, None] = None) -> str: @@ -179,6 +224,30 @@ def fill_remaining_labels( mapping[grp_label][atm_label] = guess_element(atm_label, label.mass) +def mapping_to_labels(mapping: dict[str, dict[str, str]]) -> list[AtomLabel]: + """Converts the mapping back into a list of labels. + + Parameters + ---------- + mapping : dict[str, dict[str, str]] + The atom mapping dictionary. + + Returns + ------- + list[AtomLabel] + List of atom labels from the mapping. + """ + labels = [] + for grp_label, atm_map in mapping.items(): + kwargs = {} + if grp_label: + for k, v in [i.split("=") for i in grp_label.split(";")]: + kwargs[k] = v + for atm_label in atm_map.keys(): + labels.append(AtomLabel(atm_label, **kwargs)) + return labels + + def check_mapping_valid(mapping: dict[str, dict[str, str]], labels: list[AtomLabel]): """Given a list of labels check that the mapping is valid. @@ -194,11 +263,17 @@ def check_mapping_valid(mapping: dict[str, dict[str, str]], labels: list[AtomLab bool True if the mapping is valid. """ + pattern = re.compile("^([A-Za-z]\w*=[^=;]+(;[A-Za-z]\w*=[^=;]+)*)*$") + if not all([pattern.match(grp_label) for grp_label in mapping.keys()]): + return False + + if set(mapping_to_labels(mapping)) != set(labels): + return False + for label in labels: grp_label = label.grp_label atm_label = label.atm_label - if grp_label not in mapping or atm_label not in mapping[grp_label]: - return False if mapping[grp_label][atm_label] not in ATOMS_DATABASE: return False + return True diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/ASEFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/ASEFileConfigurator.py index 3f34a2050..3323569b5 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/ASEFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/ASEFileConfigurator.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from typing import Iterable + from ase.io import iread, read from ase.io.trajectory import Trajectory as ASETrajectory @@ -37,16 +39,12 @@ def parse(self): self["element_list"] = first_frame.get_chemical_symbols() - def get_atom_labels(self) -> list[AtomLabel]: + def atom_labels(self) -> Iterable[AtomLabel]: """ - Returns - ------- - list[AtomLabel] - An ordered list of atom labels. + Yields + ------ + AtomLabel + An atom label. """ - labels = [] for atm_label in self["element_list"]: - label = AtomLabel(atm_label) - if label not in labels: - labels.append(label) - return labels + yield AtomLabel(atm_label) diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/AtomMappingConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/AtomMappingConfigurator.py index 9ca6e5cda..c82ea71ab 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/AtomMappingConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/AtomMappingConfigurator.py @@ -53,10 +53,10 @@ def configure(self, value) -> None: file_configurator = self._configurable[self._dependencies["input_file"]] if not file_configurator._valid: - self.error_status = "Input file not selected." + self.error_status = "Input file not selected or valid." return - labels = file_configurator.get_atom_labels() + labels = file_configurator.labels try: fill_remaining_labels(value, labels) except AttributeError: diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/ConfigFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/ConfigFileConfigurator.py index 9c507f019..33bd20fe5 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/ConfigFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/ConfigFileConfigurator.py @@ -13,12 +13,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from typing import Iterable import re + import numpy as np from MDANSE.Core.Error import Error from MDANSE.Framework.AtomMapping import AtomLabel - from .FileWithAtomDataConfigurator import FileWithAtomDataConfigurator from MDANSE.MLogging import LOG @@ -186,16 +187,12 @@ def parse(self): except: LOG.error(f"LAMMPS ConfigFileConfigurator failed to find a unit cell") - def get_atom_labels(self) -> list[AtomLabel]: + def atom_labels(self) -> Iterable[AtomLabel]: """ - Returns - ------- - list[AtomLabel] - An ordered list of atom labels. + Yields + ------ + AtomLabel + An atom label. """ - labels = [] for idx, mass in self["elements"]: - label = AtomLabel(str(idx), mass=mass) - if label not in labels: - labels.append(label) - return labels + yield AtomLabel(str(idx), mass=mass) diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/FieldFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/FieldFileConfigurator.py index 9c44341b7..de7308373 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/FieldFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/FieldFileConfigurator.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from typing import Iterable import re import numpy as np @@ -23,7 +24,6 @@ ) from MDANSE.Core.Error import Error from MDANSE.Framework.AtomMapping import get_element_from_mapping, AtomLabel - from .FileWithAtomDataConfigurator import FileWithAtomDataConfigurator @@ -114,20 +114,16 @@ def parse(self): first = last + 1 - def get_atom_labels(self) -> list[AtomLabel]: + def atom_labels(self) -> Iterable[AtomLabel]: """ - Returns - ------- - list[AtomLabel] - An ordered list of atom labels. + Yields + ------ + AtomLabel + An atom label. """ - labels = [] for mol_name, _, atomic_contents, masses, _ in self["molecules"]: for atm_label, mass in zip(atomic_contents, masses): - label = AtomLabel(atm_label, molecule=mol_name, mass=mass) - if label not in labels: - labels.append(label) - return labels + yield AtomLabel(atm_label, molecule=mol_name, mass=mass) def get_atom_charges(self) -> np.ndarray: """Returns an array of partial electric charges diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/FileWithAtomDataConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/FileWithAtomDataConfigurator.py index 5e29ab12c..ba5228358 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/FileWithAtomDataConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/FileWithAtomDataConfigurator.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from typing import Iterable from abc import abstractmethod import traceback @@ -37,6 +38,12 @@ def configure(self, filepath: str) -> None: self.parse() except Exception as e: self.error_status = f"File parsing error {e}: {traceback.format_exc()}" + return + + self.labels = self.unique_labels() + if len(self.labels) == 0: + self.error_status = f"Unable to generate atom labels" + return @abstractmethod def parse(self) -> None: @@ -44,6 +51,14 @@ def parse(self) -> None: pass @abstractmethod - def get_atom_labels(self) -> list[AtomLabel]: - """Return the atoms labels in the file.""" - pass + def atom_labels(self) -> Iterable[AtomLabel]: + """Yields atom labels""" + + def unique_labels(self) -> list[AtomLabel]: + """ + Returns + ------- + list[AtomLabel] + An ordered list of atom labels. + """ + return list(set(self.atom_labels())) diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/CoordinateFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/MDAnalysisCoordinateFileConfigurator.py similarity index 66% rename from MDANSE/Src/MDANSE/Framework/Configurators/CoordinateFileConfigurator.py rename to MDANSE/Src/MDANSE/Framework/Configurators/MDAnalysisCoordinateFileConfigurator.py index da654cc2f..51c8da812 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/CoordinateFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/MDAnalysisCoordinateFileConfigurator.py @@ -13,17 +13,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import ast -import os from typing import Union import MDAnalysis as mda -from MDANSE import PLATFORM from MDANSE.Framework.Configurators.IConfigurator import IConfigurator +from .MultiInputFileConfigurator import MultiInputFileConfigurator -class CoordinateFileConfigurator(IConfigurator): +class MDAnalysisCoordinateFileConfigurator(MultiInputFileConfigurator): _default = ("", "AUTO") @@ -41,46 +39,7 @@ def configure(self, setting: tuple[Union[str, list], str]): string of the coordinate file format. """ values, format = setting - - self["values"] = self._default - self._original_input = values - - if type(values) is str: - if values: - try: - values = ast.literal_eval(values) - except (SyntaxError, ValueError) as e: - self.error_status = f"Unable to evaluate string: {e}" - return - if type(values) is not list: - self.error_status = ( - f"Input values should be able to be evaluated as a list" - ) - return - else: - values = [] - - if type(values) is list: - if not all([type(value) is str for value in values]): - self.error_status = f"Input values should be a list of str" - return - else: - self.error_status = f"Input values should be able to be evaluated as a list" - return - - values = [PLATFORM.get_path(value) for value in values] - - none_exist = [] - for value in values: - if not os.path.isfile(value): - none_exist.append(value) - - if none_exist: - self.error_status = f"The files {', '.join(none_exist)} do not exist." - return - - self["values"] = values - self["filenames"] = values + super().configure(values) if format == "AUTO" or not self["filenames"]: self["format"] = None @@ -95,7 +54,7 @@ def configure(self, setting: tuple[Union[str, list], str]): if topology_configurator._valid: try: if len(self["filenames"]) <= 1 or self["format"] is None: - traj = mda.Universe( + _ = mda.Universe( topology_configurator["filename"], *self["filenames"], format=self["format"], @@ -103,7 +62,7 @@ def configure(self, setting: tuple[Union[str, list], str]): ).trajectory else: coord_files = [(i, self["format"]) for i in self["filenames"]] - traj = mda.Universe( + _ = mda.Universe( topology_configurator["filename"], coord_files, topology_format=topology_configurator["format"], @@ -116,8 +75,6 @@ def configure(self, setting: tuple[Union[str, list], str]): self.error_status = "Requires valid topology file." return - self.error_status = "OK" - @property def wildcard(self): return self._wildcard diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/TopologyFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/MDAnalysisTopologyFileConfigurator.py similarity index 78% rename from MDANSE/Src/MDANSE/Framework/Configurators/TopologyFileConfigurator.py rename to MDANSE/Src/MDANSE/Framework/Configurators/MDAnalysisTopologyFileConfigurator.py index 64c820e47..c454dc99c 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/TopologyFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/MDAnalysisTopologyFileConfigurator.py @@ -13,13 +13,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from typing import Iterable + import MDAnalysis as mda from MDANSE.Framework.AtomMapping import AtomLabel from .FileWithAtomDataConfigurator import FileWithAtomDataConfigurator -class TopologyFileConfigurator(FileWithAtomDataConfigurator): +class MDAnalysisTopologyFileConfigurator(FileWithAtomDataConfigurator): _default = ("", "AUTO") @@ -51,23 +53,26 @@ def parse(self) -> None: self["filename"], topology_format=self["format"] ).atoms - def get_atom_labels(self) -> list[AtomLabel]: + def atom_labels(self) -> Iterable[AtomLabel]: """ - Returns - ------- - list[AtomLabel] - An ordered list of atom labels. + Yields + ------ + AtomLabel + An atom label. """ - labels = [] + args = [] + for arg in ["element", "name", "type", "resname", "mass"]: + if hasattr(self.atoms[0], arg): + args.append(arg) + if len(args) == 0: + yield from [] + return + for at in self.atoms: kwargs = {} - for arg in ["element", "name", "type", "resname", "mass"]: - if hasattr(at, arg): - kwargs[arg] = getattr(at, arg) + for arg in args: + kwargs[arg] = getattr(at, arg) # the first out of the list above will be the main label (k, main_label) = next(iter(kwargs.items())) kwargs.pop(k) - label = AtomLabel(main_label, **kwargs) - if label not in labels: - labels.append(label) - return labels + yield AtomLabel(main_label, **kwargs) diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/MDFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/MDFileConfigurator.py index d81174c23..8e29acb50 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/MDFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/MDFileConfigurator.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from typing import Iterable + import itertools import re import numpy as np @@ -191,16 +193,12 @@ def close(self): """Closes the file.""" self["instance"].close() - def get_atom_labels(self) -> list[AtomLabel]: + def atom_labels(self) -> Iterable[AtomLabel]: """ - Returns - ------- - list[AtomLabel] - An ordered list of atom labels. + Yields + ------ + AtomLabel + An atom label. """ - labels = [] for atm_label, _ in self["atoms"]: - label = AtomLabel(atm_label) - if label not in labels: - labels.append(label) - return labels + yield AtomLabel(atm_label) diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/MDTrajTimeStepConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/MDTrajTimeStepConfigurator.py new file mode 100644 index 000000000..e3bb891d5 --- /dev/null +++ b/MDANSE/Src/MDANSE/Framework/Configurators/MDTrajTimeStepConfigurator.py @@ -0,0 +1,58 @@ +# This file is part of MDANSE. +# +# MDANSE is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +import mdtraj as md + +from MDANSE.Framework.Configurators.FloatConfigurator import FloatConfigurator + + +class MDTrajTimeStepConfigurator(FloatConfigurator): + + _default = 0.0 + + def configure(self, value): + # if the value is not valid then we use the MDTraj + # default values which maybe the time step in the inputted + # files or 1 ps + try: + value = float(value) + except (TypeError, ValueError): + pass + + if value is None or value == "" or value == 0.0: + coord_conf = self._configurable[self._dependencies["coordinate_files"]] + top_conf = self._configurable[self._dependencies["topology_file"]] + if coord_conf._valid and top_conf._valid: + traj_files = coord_conf["filenames"] + top_file = top_conf["filename"] + try: + if top_file: + traj = md.load(traj_files, top=top_file) + else: + traj = md.load(traj_files) + if traj.n_frames == 1: + value = self._default + else: + value = float(traj.timestep) + except Exception as e: + self.error_status = ( + f"Unable to determine a time step from MDTraj: {e}" + ) + return + else: + self.error_status = f"Unable to determine a time step from MDTraj" + return + + super().configure(value) diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/MDTrajTopologyFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/MDTrajTopologyFileConfigurator.py new file mode 100644 index 000000000..af4f3ce03 --- /dev/null +++ b/MDANSE/Src/MDANSE/Framework/Configurators/MDTrajTopologyFileConfigurator.py @@ -0,0 +1,101 @@ +# This file is part of MDANSE. +# +# MDANSE is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from typing import Optional, Iterable +import traceback +from pathlib import Path + +import mdtraj as md +from mdtraj.core.trajectory import _TOPOLOGY_EXTS + +from MDANSE.Framework.AtomMapping import AtomLabel +from .FileWithAtomDataConfigurator import FileWithAtomDataConfigurator + + +class MDTrajTopologyFileConfigurator(FileWithAtomDataConfigurator): + + def configure(self, value: Optional[str]): + """ + Parameters + ---------- + value : str or None + The path of the MDTraj topology file can be None if + topology information is contained in the trajectory files. + """ + if not self._configurable[self._dependencies["coordinate_files"]].valid: + self.error_status = "Trajectory file not valid" + return + + if not value: + self.error_status = "OK" + self["filename"] = value + + extension = self._configurable[ + self._dependencies["coordinate_files"] + ].extension + + supported = list(i[1:] for i in _TOPOLOGY_EXTS) + if extension not in supported: + self.error_status = ( + f"Trajectory file does not contain topology information. " + f"File '{extension}' not support should be one of the following: {supported}" + ) + return + + try: + self.parse() + except Exception as e: + self.error_status = f"File parsing error {e}: {traceback.format_exc()}" + return + + self.labels = self.unique_labels() + if len(self.labels) == 0: + self.error_status = f"Unable to generate atom labels" + + else: + extension = "".join(Path(value).suffixes)[1:] + supported = list(i[1:] for i in _TOPOLOGY_EXTS) + if extension not in supported: + self.error_status = f"File '{extension}' not support should be one of the following: {supported}" + return + super().configure(value) + + def parse(self) -> None: + coord_files = self._configurable[self._dependencies["coordinate_files"]][ + "filenames" + ] + if self["filename"]: + self.atoms = [ + at for at in md.load(coord_files, top=self["filename"]).topology.atoms + ] + + else: + self.atoms = [at for at in md.load(coord_files).topology.atoms] + + def atom_labels(self) -> Iterable[AtomLabel]: + """ + Yields + ------ + AtomLabel + An atom label. + """ + for at in self.atoms: + yield AtomLabel( + at.name, + symbol=at.element.symbol, + residue=at.residue.name, + number=at.element.number, + mass=at.element.mass, + ) diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/MDTrajTrajectoryFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/MDTrajTrajectoryFileConfigurator.py new file mode 100644 index 000000000..06632d17f --- /dev/null +++ b/MDANSE/Src/MDANSE/Framework/Configurators/MDTrajTrajectoryFileConfigurator.py @@ -0,0 +1,37 @@ +# This file is part of MDANSE. +# +# MDANSE is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from pathlib import Path + +from mdtraj.formats.registry import FormatRegistry + +from .MultiInputFileConfigurator import MultiInputFileConfigurator + + +class MDTrajTrajectoryFileConfigurator(MultiInputFileConfigurator): + + def configure(self, value): + super().configure(value) + + extensions = {"".join(Path(value).suffixes)[1:] for value in self["values"]} + if len(extensions) != 1: + self.error_status = f"Files should be of a single format." + return + self.extension = next(iter(extensions)) + + supported = list(i[1:] for i in FormatRegistry.loaders.keys()) + if self.extension not in supported: + self.error_status = f"File '{self.extension}' not support should be one of the following: {supported}" + return diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/MultiInputFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/MultiInputFileConfigurator.py new file mode 100644 index 000000000..59497623f --- /dev/null +++ b/MDANSE/Src/MDANSE/Framework/Configurators/MultiInputFileConfigurator.py @@ -0,0 +1,102 @@ +# This file is part of MDANSE. +# +# MDANSE is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +import ast +import os +from typing import Union + +from MDANSE import PLATFORM +from MDANSE.Framework.Configurators.IConfigurator import IConfigurator + + +class MultiInputFileConfigurator(IConfigurator): + + _default = "" + + def __init__(self, name, wildcard="All files (*)", **kwargs): + IConfigurator.__init__(self, name, **kwargs) + self._wildcard = wildcard + + def configure(self, setting: Union[str, list]): + """ + Parameters + ---------- + setting : Union[str, list] + A list of file names or a string of the list which can be + converted to a list using literal_eval and a string of the + coordinate file format. + """ + values = setting + self["values"] = self._default + self._original_input = values + + if type(values) is str: + if values: + try: + # some issues when \ is used in the path as this + # can be interpreted as an escape character by + # literal_eval, on windows we can use \ or / so lets + # just swap them here + values = ast.literal_eval(values.replace("\\", "/")) + except (SyntaxError, ValueError) as e: + self.error_status = f"Unable to evaluate string: {e}" + return + if type(values) is not list: + self.error_status = ( + f"Input values should be able to be evaluated as a list" + ) + return + else: + values = [] + + if type(values) is list: + if not all([type(value) is str for value in values]): + self.error_status = f"Input values should be a list of str" + return + else: + self.error_status = f"Input values should be able to be evaluated as a list" + return + + values = [PLATFORM.get_path(value) for value in values] + + none_exist = [] + for value in values: + if not os.path.isfile(value): + none_exist.append(value) + + if none_exist: + self.error_status = f"The files {', '.join(none_exist)} do not exist." + return + + self["values"] = values + self["filenames"] = values + self.error_status = "OK" + + @property + def wildcard(self): + return self._wildcard + + def get_information(self) -> str: + """ + Returns + ------- + str + Input file names. + """ + try: + filenames = self["value"] + except KeyError: + filenames = ["None"] + return f"Input files: {', '.join(filenames)}.\n" diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/XDATCARFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/XDATCARFileConfigurator.py index 0d857d11c..85d889e3e 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/XDATCARFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/XDATCARFileConfigurator.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from typing import Iterable + import numpy as np from MDANSE.Core.Error import Error @@ -133,16 +135,12 @@ def read_step(self, step): def close(self): self["instance"].close() - def get_atom_labels(self) -> list[AtomLabel]: + def atom_labels(self) -> Iterable[AtomLabel]: """ - Returns - ------- - list[AtomLabel] - An ordered list of atom labels. + Yields + ------ + AtomLabel + An atom label. """ - labels = [] for symbol in self["atoms"]: - label = AtomLabel(symbol) - if label not in labels: - labels.append(label) - return labels + yield AtomLabel(symbol) diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/XTDFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/XTDFileConfigurator.py index 9c714ae6b..7ddc7b18b 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/XTDFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/XTDFileConfigurator.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from typing import Iterable import collections import xml.etree.ElementTree as ElementTree @@ -186,19 +187,15 @@ def build_chemical_system(self, aliases): realConf.fold_coordinates() self._chemicalSystem.configuration = realConf - def get_atom_labels(self) -> list[AtomLabel]: + def atom_labels(self) -> Iterable[AtomLabel]: """ - Returns - ------- - list[AtomLabel] - An ordered list of atom labels. + Yields + ------ + AtomLabel + An atom label. """ - labels = [] for info in self._atoms.values(): - label = AtomLabel(info["element"], type=info["atom_name"]) - if label not in labels: - labels.append(label) - return labels + yield AtomLabel(info["element"], type=info["atom_name"]) def get_atom_charges(self) -> np.ndarray: """Returns an array of partial electric charges diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/XYZFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/XYZFileConfigurator.py index b56452f7c..ec5ff900f 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/XYZFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/XYZFileConfigurator.py @@ -13,7 +13,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from typing import Iterable import re + import numpy as np from MDANSE.Core.Error import Error @@ -140,16 +142,12 @@ def close(self): """Closes the file that was, until now, open for reading.""" self["instance"].close() - def get_atom_labels(self) -> list[AtomLabel]: + def atom_labels(self) -> Iterable[AtomLabel]: """ - Returns - ------- - list[AtomLabel] - An ordered list of atom labels. + Yields + ------ + AtomLabel + An atom label. """ - labels = [] for atm_label in self["atoms"]: - label = AtomLabel(atm_label) - if label not in labels: - labels.append(label) - return labels + yield AtomLabel(atm_label) diff --git a/MDANSE/Src/MDANSE/Framework/Converters/MDAnalysis.py b/MDANSE/Src/MDANSE/Framework/Converters/MDAnalysis.py index 9efd2b9dc..c9368e624 100644 --- a/MDANSE/Src/MDANSE/Framework/Converters/MDAnalysis.py +++ b/MDANSE/Src/MDANSE/Framework/Converters/MDAnalysis.py @@ -42,7 +42,7 @@ class MDAnalysis(Converter): label = "MDAnalysis" settings = collections.OrderedDict() settings["topology_file"] = ( - "TopologyFileConfigurator", + "MDAnalysisTopologyFileConfigurator", { "wildcard": "All files (*)", "default": "INPUT_FILENAME", @@ -50,7 +50,7 @@ class MDAnalysis(Converter): }, ) settings["coordinate_files"] = ( - "CoordinateFileConfigurator", + "MDAnalysisCoordinateFileConfigurator", { "wildcard": "All files (*)", "default": "", diff --git a/MDANSE/Src/MDANSE/Framework/Converters/MDTraj.py b/MDANSE/Src/MDANSE/Framework/Converters/MDTraj.py new file mode 100644 index 000000000..cdd1b9c57 --- /dev/null +++ b/MDANSE/Src/MDANSE/Framework/Converters/MDTraj.py @@ -0,0 +1,210 @@ +# This file is part of MDANSE. +# +# MDANSE is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +import collections + +import mdtraj as md + +from MDANSE.MolecularDynamics.Trajectory import TrajectoryWriter +from MDANSE.Framework.Converters.Converter import Converter +from MDANSE.Chemistry.ChemicalEntity import ChemicalSystem, Atom +from MDANSE.Framework.AtomMapping import get_element_from_mapping +from MDANSE.MolecularDynamics.Configuration import ( + PeriodicRealConfiguration, + RealConfiguration, +) +from MDANSE.MolecularDynamics.UnitCell import UnitCell + + +class MDTraj(Converter): + """Using MDTraj, read the MD trajectory and write the data out to + the MDT file. MDTraj reads MD trajectories by specifying + trajectory files and optionally a topology file. Multiple files can + be used for the trajectory files so that trajectories will be + stitched together. + """ + + label = "MDTraj" + settings = collections.OrderedDict() + + settings["coordinate_files"] = ( + "MDTrajTrajectoryFileConfigurator", + { + "wildcard": "All files (*)", + "default": '["INPUT_FILENAME"]', + "label": "Trajectory files", + }, + ) + settings["topology_file"] = ( + "MDTrajTopologyFileConfigurator", + { + "wildcard": "All files (*)", + "default": "", + "label": "Topology file (optional)", + "dependencies": {"coordinate_files": "coordinate_files"}, + }, + ) + settings["atom_aliases"] = ( + "AtomMappingConfigurator", + { + "default": "{}", + "label": "Atom mapping", + "dependencies": {"input_file": "topology_file"}, + }, + ) + settings["time_step"] = ( + "MDTrajTimeStepConfigurator", + { + "label": "Time step (ps)", + "default": 0.0, + "mini": 0.0, + "dependencies": { + "coordinate_files": "coordinate_files", + "topology_file": "topology_file", + }, + }, + ) + settings["fold"] = ( + "BooleanConfigurator", + {"default": False, "label": "Fold coordinates into box"}, + ) + settings["discard_overlapping_frames"] = ( + "BooleanConfigurator", + {"default": False, "label": "Discard overlapping frames"}, + ) + settings["output_files"] = ( + "OutputTrajectoryConfigurator", + { + "label": "MDANSE trajectory (filename, format)", + "formats": ["MDTFormat"], + "root": "config_file", + }, + ) + + def initialize(self): + """Load the trajectory using MDTraj and create the + trajectory writer. + """ + coord_files = self.configuration["coordinate_files"]["filenames"] + top_file = self.configuration["topology_file"]["filename"] + if top_file: + self.traj = md.load( + coord_files, + top=top_file, + discard_overlapping_frames=self.configuration[ + "discard_overlapping_frames" + ]["value"], + ) + else: + self.traj = md.load( + coord_files, + discard_overlapping_frames=self.configuration[ + "discard_overlapping_frames" + ]["value"], + ) + + self.numberOfSteps = self.traj.n_frames + + self._chemical_system = ChemicalSystem() + for at in self.traj.topology.atoms: + element = get_element_from_mapping( + self.configuration["atom_aliases"]["value"], + at.name, + symbol=at.element.symbol, + residue=at.residue.name, + number=at.element.number, + mass=at.element.mass, + ) + self._chemical_system.add_chemical_entity( + Atom(symbol=element, name=at.name) + ) + + kwargs = { + "positions_dtype": self.configuration["output_files"]["dtype"], + "chunking_limit": self.configuration["output_files"]["chunk_size"], + "compression": self.configuration["output_files"]["compression"], + } + self._trajectory = TrajectoryWriter( + self.configuration["output_files"]["file"], + self._chemical_system, + self.numberOfSteps, + **kwargs + ) + super().initialize() + + def run_step(self, index: int): + """For the given frame, read the MDTraj trajectory data, + convert and write it out to the MDT file. + + Parameters + ---------- + index : int + The frame index. + + Returns + ------- + tuple[int, None] + A tuple of the job index and None. + """ + if self.traj.unitcell_vectors is None: + conf = RealConfiguration( + self._trajectory._chemical_system, + self.traj.xyz[index], + ) + else: + conf = PeriodicRealConfiguration( + self._trajectory._chemical_system, + self.traj.xyz[index], + UnitCell( + self.traj.unitcell_vectors[index], + ), + ) + if self.configuration["fold"]["value"]: + conf.fold_coordinates() + + self._trajectory._chemical_system.configuration = conf + + # TODO as of 11/12/2024 MDTraj does not read velocity data + # there is a discussion about this on GitHub + # (https://github.com/mdtraj/mdtraj/issues/1824). + # It doesn't look like they have any plans to add this but if + # they change their minds then we should update our code to + # support this. + + if self.numberOfSteps == 1: + time = 0 + else: + if float(self.configuration["time_step"]["value"]) == 0.0: + time = index * self.traj.timestep + else: + time = index * float(self.configuration["time_step"]["value"]) + + self._trajectory.dump_configuration( + time, + units={ + "time": "ps", + "unit_cell": "nm", + "coordinates": "nm", + }, + ) + + return index, None + + def combine(self, index, x): + pass + + def finalize(self): + self._trajectory.close() + super(MDTraj, self).finalize() diff --git a/MDANSE/Tests/UnitTests/AtomMapping/test_atom_mapping.py b/MDANSE/Tests/UnitTests/AtomMapping/test_atom_mapping.py index f5d2a12a5..5ab015fcd 100644 --- a/MDANSE/Tests/UnitTests/AtomMapping/test_atom_mapping.py +++ b/MDANSE/Tests/UnitTests/AtomMapping/test_atom_mapping.py @@ -127,12 +127,48 @@ def test_fill_remaining_labels_fill_mapping_correctly_missing_mapping(): assert mapping == {"molecule=mol1": {"label1": "C", "C1": "C"}} -def test_check_mapping_valid_as_elements_are_correct(): +def test_check_mapping_valid_as_elements_are_correct_1(): labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol1")] mapping = {"molecule=mol1": {"label1": "C", "C1": "C"}} assert check_mapping_valid(mapping, labels) +def test_check_mapping_valid_as_elements_are_correct_2(): + labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol1")] + mapping = {"molecule=mol1": {"C1": "C", "label1": "C"}} + assert check_mapping_valid(mapping, labels) + + +def test_check_mapping_valid_as_elements_are_correct_3(): + labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol2")] + mapping = {"molecule=mol1": {"label1": "C"}, "molecule=mol2": {"C1": "C"}} + assert check_mapping_valid(mapping, labels) + + +def test_check_mapping_valid_as_elements_are_correct_4(): + labels = [AtomLabel("label1", type="1*"), AtomLabel("C1", type="2*")] + mapping = {"type=1*": {"label1": "C"}, "type=2*": {"C1": "C"}} + assert check_mapping_valid(mapping, labels) + + +def test_check_mapping_valid_as_elements_are_correct_5(): + labels = [AtomLabel("label1", mass="12"), AtomLabel("C1", mass="13")] + mapping = {"mass=12": {"label1": "C"}, "mass=13": {"C1": "C"}} + assert check_mapping_valid(mapping, labels) + + +def test_check_mapping_valid_as_elements_are_correct_6(): + labels = [AtomLabel("label=1", molecule="mol=1"), AtomLabel("C=1", molecule="mol=2")] + mapping = {"molecule=mol1": {"label1": "C"}, "molecule=mol2": {"C1": "C"}} + assert check_mapping_valid(mapping, labels) + + +def test_check_mapping_valid_as_elements_are_correct_7(): + labels = [AtomLabel("label=1"), AtomLabel("C=1")] + mapping = {"": {"label1": "C", "C1": "C"}} + assert check_mapping_valid(mapping, labels) + + def test_check_mapping_not_valid_as_elements_not_correct(): labels = [ AtomLabel("label1", molecule="mol1"), @@ -142,10 +178,76 @@ def test_check_mapping_not_valid_as_elements_not_correct(): assert not check_mapping_valid(mapping, labels) -def test_check_mapping_not_valid_due_to_missing_mappings(): +def test_check_mapping_not_valid_due_to_missing_mappings_1(): labels = [ AtomLabel("label1", molecule="mol1"), AtomLabel("label2", molecule="mol1"), ] mapping = {"molecule=mol1": {"label1": "C"}} assert not check_mapping_valid(mapping, labels) + + +def test_check_mapping_not_valid_due_to_missing_mappings_2(): + labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol2")] + mapping = {"molecule=mol1": {"label1": "C", "C1": "C"}} + assert not check_mapping_valid(mapping, labels) + + +def test_check_mapping_not_valid_due_to_incorrect_keys_1(): + labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol1")] + mapping = {"molecule=mol1": {"label1": "C", "label2": "C"}} + assert not check_mapping_valid(mapping, labels) + + +def test_check_mapping_not_valid_due_to_incorrect_keys_2(): + labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol1")] + mapping = {"molecule=mol1": {"label1": "C", "C1": "C", "label2": "C"}} + assert not check_mapping_valid(mapping, labels) + + +def test_check_mapping_not_valid_due_to_bad_keys_1(): + labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol1")] + mapping = {"molecule==mol1": {"label1": "C", "C1": "C"}} + assert not check_mapping_valid(mapping, labels) + + +def test_check_mapping_not_valid_due_to_bad_keys_2(): + labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol1")] + mapping = {"molecule===mol1": {"label1": "C", "C1": "C"}} + assert not check_mapping_valid(mapping, labels) + + +def test_check_mapping_not_valid_due_to_bad_keys_3(): + labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol1")] + mapping = {"molecule=mol1;": {"label1": "C", "C1": "C"}} + assert not check_mapping_valid(mapping, labels) + + +def test_check_mapping_not_valid_due_to_bad_keys_4(): + labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol1")] + mapping = {";molecule=mol1": {"label1": "C", "C1": "C"}} + assert not check_mapping_valid(mapping, labels) + + +def test_check_mapping_not_valid_due_to_bad_keys_5(): + labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol1")] + mapping = {"molecule;=mol1": {"label1": "C", "C1": "C"}} + assert not check_mapping_valid(mapping, labels) + + +def test_check_mapping_not_valid_due_to_bad_keys_6(): + labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol1")] + mapping = {"mole;cule=mol1": {"label1": "C", "C1": "C"}} + assert not check_mapping_valid(mapping, labels) + + +def test_check_mapping_not_valid_due_to_bad_keys_7(): + labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol1")] + mapping = {"molecule=mo;l1": {"label1": "C", "C1": "C"}} + assert not check_mapping_valid(mapping, labels) + + +def test_check_mapping_not_valid_due_to_bad_keys_8(): + labels = [AtomLabel("label1", molecule="mol1"), AtomLabel("C1", molecule="mol2")] + mapping = {"molecule=mol1": {"label1": "C"}, "molec;ule=mol2": {"C1": "C"}} + assert not check_mapping_valid(mapping, labels) diff --git a/MDANSE/Tests/UnitTests/test_converter.py b/MDANSE/Tests/UnitTests/test_converter.py index f494d5292..b540e3df3 100644 --- a/MDANSE/Tests/UnitTests/test_converter.py +++ b/MDANSE/Tests/UnitTests/test_converter.py @@ -544,3 +544,26 @@ def test_mdanalysis_conversion_file_exists_and_loads_up_successfully(compression assert os.path.exists(temp_name + ".log") assert os.path.isfile(temp_name + ".log") os.remove(temp_name + ".log") + + +@pytest.mark.parametrize("compression", ["none", "gzip", "lzf"]) +def test_mdtraj_conversion_file_exists_and_loads_up_successfully(compression): + temp_name = tempfile.mktemp() + + parameters = { + "topology_file": hem_cam_pdb, + "coordinate_files": [hem_cam_dcd], + "output_files": (temp_name, 64, 128, compression, "INFO"), + } + + mdanalysis = Converter.create("MDTraj") + mdanalysis.run(parameters, status=True) + + HDFTrajectoryConfigurator("trajectory").configure(temp_name + ".mdt") + + assert os.path.exists(temp_name + ".mdt") + assert os.path.isfile(temp_name + ".mdt") + os.remove(temp_name + ".mdt") + assert os.path.exists(temp_name + ".log") + assert os.path.isfile(temp_name + ".log") + os.remove(temp_name + ".log") diff --git a/MDANSE/Tests/UnitTests/test_ijob.py b/MDANSE/Tests/UnitTests/test_ijob.py index 04b4c3549..598ecfaf3 100644 --- a/MDANSE/Tests/UnitTests/test_ijob.py +++ b/MDANSE/Tests/UnitTests/test_ijob.py @@ -60,6 +60,7 @@ "ImprovedASE", "LAMMPS", "MDAnalysis", + "MDTraj", "VASP", "CHARMM", "NAMD", diff --git a/MDANSE/pyproject.toml b/MDANSE/pyproject.toml index c9899f3a3..34a2d630f 100644 --- a/MDANSE/pyproject.toml +++ b/MDANSE/pyproject.toml @@ -35,7 +35,8 @@ dependencies = [ "h5py", "ase", "rdkit", - "MDAnalysis" + "MDAnalysis", + "mdtraj" ] # dynamic = ["version", "description"] diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/AtomMappingWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/AtomMappingWidget.py index 912af9b1a..bc36997af 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/AtomMappingWidget.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/AtomMappingWidget.py @@ -64,7 +64,6 @@ def __init__( self._field = field self._file_widget = field_widget - self._file_widget.value_changed.connect(self.update_helper) self.layout = QVBoxLayout() buffer_0 = QWidget(self) @@ -109,11 +108,14 @@ def update_helper(self) -> None: if not self._file_widget._configurator.valid: return - self.labels = self._file_widget._configurator.get_atom_labels() + self.labels = self._file_widget._configurator.labels for i, label in enumerate(self.labels): w0 = QLabel(label.grp_label.replace(";", "\n")) w1 = QLabel(label.atm_label) w2 = QComboBox() + # this combobox can be slow without this policy since we + # are adding alot of items + w2.setSizeAdjustPolicy(w2.AdjustToMinimumContentsLengthWithIcon) w2.addItems(self.all_symbols) self.mapping_widgets.append((w0, w1, w2)) self.mapping_layout.addWidget(w0, i, 0) @@ -202,9 +204,13 @@ def update_helper_button(self) -> None: """ if self._file_widget._configurator.valid: self.helper_button.setEnabled(True) + self.helper.update_helper() + self.helper.apply() else: self.helper_button.setEnabled(False) - self.helper.apply() + self.helper.close() + self.helper.clear_panel() + self.helper.apply() @Slot() def helper_dialog(self) -> None: diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDAnalysisCoordinateFileWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDAnalysisCoordinateFileWidget.py new file mode 100644 index 000000000..699ff6c46 --- /dev/null +++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDAnalysisCoordinateFileWidget.py @@ -0,0 +1,41 @@ +# This file is part of MDANSE_GUI. +# +# MDANSE_GUI is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +import MDAnalysis as mda +from qtpy.QtWidgets import QFileDialog + +from .MDAnalysisTopologyFileWidget import MDAnalysisTopologyFileWidget +from .MultiInputFileWidget import MultiInputFileWidget + + +class MDAnalysisCoordinateFileWidget( + MultiInputFileWidget, MDAnalysisTopologyFileWidget +): + + def __init__(self, *args, file_dialog=QFileDialog.getOpenFileNames, **kwargs): + super().__init__( + *args, + file_dialog=file_dialog, + format_options=sorted(mda._READERS.keys()), + **kwargs, + ) + for widget in self.parent()._widgets: + if ( + widget._configurator + is self._configurator._configurable[ + self._configurator._dependencies["input_file"] + ] + ): + widget.value_changed.connect(self.updateValue) diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDAnalysisTimeStepWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDAnalysisMDTrajTimeStepWidget.py similarity index 54% rename from MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDAnalysisTimeStepWidget.py rename to MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDAnalysisMDTrajTimeStepWidget.py index 6779c4555..0457e089c 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDAnalysisTimeStepWidget.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDAnalysisMDTrajTimeStepWidget.py @@ -13,13 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import MDAnalysis as mda - -from MDANSE.MLogging import LOG from .FloatWidget import FloatWidget -class MDAnalysisTimeStepWidget(FloatWidget): +class MDAnalysisMDTrajTimeStepWidget(FloatWidget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -50,36 +47,8 @@ def update_from_files(self) -> None: """Updates the time step field from the topology and coordinates files if possible else set it back to the default value. """ - if ( - self._topology_file_widget._configurator.valid - and self._coordinates_file_widget._configurator.valid - ): - try: - coord_format = self._coordinates_file_widget._configurator["format"] - coord_files = self._coordinates_file_widget._configurator["filenames"] - - if len(coord_files) <= 1 or coord_format is None: - value = mda.Universe( - self._topology_file_widget._configurator["filename"], - *coord_files, - format=coord_format, - topology_format=self._topology_file_widget._configurator[ - "format" - ], - ).trajectory.ts.dt - else: - coord_files = [(i, coord_format) for i in coord_files] - value = mda.Universe( - self._topology_file_widget._configurator["filename"], - coord_files, - topology_format=self._topology_file_widget._configurator[ - "format" - ], - ).trajectory.ts.dt - - self._field.setText(str(value)) - return - except Exception as e: - LOG.warning(f"Failed to determine time step from MDAnalysis: {e}") - - self._field.setText(str(self._default_value)) + self._configurator.configure(None) + if self._configurator._valid: + self._field.setText(str(self._configurator["value"])) + else: + self._field.setText(str(self._default_value)) diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/TopologyFileWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDAnalysisTopologyFileWidget.py similarity index 97% rename from MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/TopologyFileWidget.py rename to MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDAnalysisTopologyFileWidget.py index c2826f46b..2f3ea8880 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/TopologyFileWidget.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDAnalysisTopologyFileWidget.py @@ -20,7 +20,7 @@ from .InputFileWidget import InputFileWidget -class TopologyFileWidget(InputFileWidget): +class MDAnalysisTopologyFileWidget(InputFileWidget): def __init__(self, *args, format_options=sorted(mda._PARSERS.keys()), **kwargs): self.format_options = ["AUTO"] + list(format_options) diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDTrajTopologyFileWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDTrajTopologyFileWidget.py new file mode 100644 index 000000000..a85634f9f --- /dev/null +++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MDTrajTopologyFileWidget.py @@ -0,0 +1,33 @@ +# This file is part of MDANSE_GUI. +# +# MDANSE_GUI is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from .InputFileWidget import InputFileWidget + + +class MDTrajTopologyFileWidget(InputFileWidget): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # file input widgets should be loaded into the _widgets list + # before this one + for widget in self.parent()._widgets: + if ( + widget._configurator + is self._configurator._configurable[ + self._configurator._dependencies["coordinate_files"] + ] + ): + widget.value_changed.connect(self.updateValue) diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/CoordinateFileWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MultiInputFileWidget.py similarity index 81% rename from MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/CoordinateFileWidget.py rename to MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MultiInputFileWidget.py index 3d7b1a38e..3fb641da9 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/CoordinateFileWidget.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/MultiInputFileWidget.py @@ -16,31 +16,21 @@ import os from pathlib import PurePath -import MDAnalysis as mda -from qtpy.QtWidgets import QFileDialog from qtpy.QtCore import Slot +from qtpy.QtWidgets import QFileDialog from MDANSE.MLogging import LOG -from .TopologyFileWidget import TopologyFileWidget +from .InputFileWidget import InputFileWidget -class CoordinateFileWidget(TopologyFileWidget): +class MultiInputFileWidget(InputFileWidget): def __init__(self, *args, file_dialog=QFileDialog.getOpenFileNames, **kwargs): super().__init__( *args, file_dialog=file_dialog, - format_options=sorted(mda._READERS.keys()), **kwargs, ) - for widget in self.parent()._widgets: - if ( - widget._configurator - is self._configurator._configurable[ - self._configurator._dependencies["input_file"] - ] - ): - widget.value_changed.connect(self.updateValue) @Slot() def valueFromDialog(self): diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py index 25ab46e8e..4fd9291c4 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py @@ -56,9 +56,9 @@ "ASEFileConfigurator": InputFileWidget, "AseInputFileConfigurator": AseInputFileWidget, "ConfigFileConfigurator": InputFileWidget, - "CoordinateFileConfigurator": CoordinateFileWidget, + "MDAnalysisCoordinateFileConfigurator": MDAnalysisCoordinateFileWidget, "InputFileConfigurator": InputFileWidget, - "TopologyFileConfigurator": TopologyFileWidget, + "MDAnalysisTopologyFileConfigurator": MDAnalysisTopologyFileWidget, "MDFileConfigurator": InputFileWidget, "FieldFileConfigurator": InputFileWidget, "XDATCARFileConfigurator": InputFileWidget, @@ -82,7 +82,10 @@ "InstrumentResolutionConfigurator": InstrumentResolutionWidget, "PartialChargeConfigurator": PartialChargeWidget, "UnitCellConfigurator": UnitCellWidget, - "MDAnalysisTimeStepConfigurator": MDAnalysisTimeStepWidget, + "MDAnalysisTimeStepConfigurator": MDAnalysisMDTrajTimeStepWidget, + "MDTrajTimeStepConfigurator": MDAnalysisMDTrajTimeStepWidget, + "MDTrajTrajectoryFileConfigurator": MultiInputFileWidget, + "MDTrajTopologyFileConfigurator": MDTrajTopologyFileWidget, }