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,
}