diff --git a/Doc/pages/analysis.rst b/Doc/pages/analysis.rst
index d7a2d7f725..c755826c50 100644
--- a/Doc/pages/analysis.rst
+++ b/Doc/pages/analysis.rst
@@ -8,8 +8,9 @@ Analysis: Other
This section contains background theory for following plugins:
-- :ref:`infrared`
- :ref:`dipole-autocorrelation-function`
+- :ref:`infraredbulk`
+- :ref:`infraredmolecular`
- :ref:`density`
- :ref:`temperature`
- :ref:`center-of-masses-trajectory`
@@ -18,23 +19,6 @@ This section contains background theory for following plugins:
Infrared
^^^^^^^^
-.. _infrared:
-
-Infrared
-''''''''
-Calculates the molecular infrared spectrum averaged over all molecules
-in the trajectory. The infrared spectrum is calculated from the Fourier
-transform of the autocorrelation of the time-derivative of the
-molecular dipole:
-
-.. math::
- :label: ir1
-
- I(\omega) \propto \frac{1}{N_{m}}\sum_{m} \frac{1}{6\pi} \int \mathrm{d}t \, \left\langle \dot{\vec{\mu}}_{m}(0) \cdot \dot{\vec{\mu}}_{m}(t) \right\rangle e^{-i\omega t}
-
-where :math:`N_{m}` is the number of molecules and :math:`\dot{\vec{\mu}}_{m}(t)` is
-the time-derivative of the molecular dipole moment of molecule :math:`m`.
-
.. _dipole-autocorrelation-function:
Dipole Autocorrelation Function
@@ -44,12 +28,70 @@ Calculates the molecular dipole autocorrelation function which is closely
related to the molecular infrared spectrum
.. math::
- :label: ir2
+ :label: ir1
\mathrm{DACF}(t) = \frac{1}{3 N_{m}}\sum_{m} \left\langle \vec{\mu}_{m}(0) \cdot \vec{\mu}_{m}(t) \right\rangle
where :math:`N_{m}` is the number of molecules :math:`m` and :math:`\vec{\mu}(t)` is
-the molecular dipole moment of molecule :math:`m`.
+the molecular dipole moment of molecule :math:`m`. The electric dipole of a
+given molecule is calculated relative to the molecules COM
+
+.. math::
+ :label: ir2
+
+ \vec{\mu}_{m}(t) = \sum_{i \in m} q_{i}(t)(\mathbf{r}_{i}(t) - \mathbf{r}_{\mathrm{COM},m}).
+
+.. _infraredbulk:
+
+InfraredBulk
+''''''''''''
+Calculates the infrared spectrum, should usually be used for systems where
+molecules are not defined and the system is amorphous. The infrared spectrum
+is calculated following the equation
+
+.. math::
+ :label: ir3
+
+ I(\omega) = \frac{1}{N}\sum_{i \in \mathrm{u.c.}} \sum_{\substack{j \\ \vert \mathbf{r}_j-\mathbf{r}_i \vert < r}} \frac{1}{6\pi} \int \mathrm{d}t \, \left\langle \dot{\vec{\mu}}_{i}(0) \cdot \dot{\vec{\mu}}_{j}(t) \right\rangle e^{-i\omega t}
+
+where
+
+.. math::
+ :label: ir4
+
+ \dot{\vec{\mu}}_{i}(t) = \frac{\mathrm{d}}{\mathrm{d}t} [q(t)\mathbf{r}(t)]
+
+and the summation over :math:`i` goes over all atoms in the unit cell
+and the summation over :math:`j` goes over all atoms which have approached
+atom :math:`i`. We define atom :math:`j` to have approached :math:`i` when
+it has reached a distance less than the user defined cutoff, :math:`r`, at
+any point during the trajectory. We therefore assume that the correlation function
+
+.. math::
+ :label: ir5
+
+ \Big\langle \dot{\vec{\mu}}_{i}(0) \cdot \dot{\vec{\mu}}_{j}(t) \Big\rangle
+
+is small when atoms :math:`i` and :math:`j` are separeted by large distances
+which may not be true for periodic systems where the motion of atoms
+separeted by large distances may still be correlated.
+
+.. _infraredmolecular:
+
+InfraredMolecular
+'''''''''''''''''
+Calculates the molecular infrared spectrum averaged over all molecules
+in the trajectory. The infrared spectrum is calculated from the Fourier
+transform of the autocorrelation of the time-derivative of the
+molecular dipole:
+
+.. math::
+ :label: ir6
+
+ I(\omega) = \frac{1}{N_{m}}\sum_{m} \frac{1}{6\pi} \int \mathrm{d}t \, \left\langle \dot{\vec{\mu}}_{m}(0) \cdot \dot{\vec{\mu}}_{m}(t) \right\rangle e^{-i\omega t}
+
+where :math:`N_{m}` is the number of molecules and :math:`\dot{\vec{\mu}}_{m}(t)` is
+the time-derivative of the molecular dipole moment of molecule :math:`m`.
Thermodynamics
diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/DistCutoffConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/DistCutoffConfigurator.py
new file mode 100644
index 0000000000..d1585f3c8e
--- /dev/null
+++ b/MDANSE/Src/MDANSE/Framework/Configurators/DistCutoffConfigurator.py
@@ -0,0 +1,89 @@
+# This file is part of MDANSE.
+#
+# 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 __future__ import annotations
+
+import numpy as np
+
+from .FloatConfigurator import FloatConfigurator
+
+
+def get_largest_cutoff(traj_config) -> float:
+ """Get the largest cutoff value for the given trajectories
+ unit cells.
+
+ Returns
+ -------
+ traj_config
+ The trajectory configuration object
+ """
+ try:
+ trajectory_array = np.array(
+ [
+ traj_config.unit_cell(frame)._unit_cell
+ for frame in range(len(traj_config))
+ ]
+ )
+ except Exception:
+ return np.linalg.norm(traj_config.min_span)
+
+ if np.allclose(trajectory_array, 0.0):
+ return np.linalg.norm(traj_config.min_span)
+
+ # calculated the radius of the largest sphere that can
+ # fit into the unit cell
+ min_d = np.min(trajectory_array, axis=0)
+ vec_a, vec_b, vec_c = min_d
+
+ cross_bc = np.cross(vec_b, vec_c)
+ cross_ca = np.cross(vec_c, vec_a)
+ cross_ab = np.cross(vec_a, vec_b)
+
+ if any(np.allclose(vec, 0.0) for vec in (cross_bc, cross_ca, cross_ab)):
+ raise ValueError("Trajectory contains invalid unit cell.")
+
+ h_1 = abs(np.dot(vec_a, cross_bc)) / np.linalg.norm(cross_bc)
+ h_2 = abs(np.dot(vec_b, cross_ca)) / np.linalg.norm(cross_ca)
+ h_3 = abs(np.dot(vec_c, cross_ab)) / np.linalg.norm(cross_ab)
+
+ return 0.5 * min(h_1, h_2, h_3)
+
+
+class DistCutoffConfigurator(FloatConfigurator):
+ """.
+
+ It does not allow distances large enough to include
+ the periodic image of any atom in the system.
+ """
+
+ def configure(self, value):
+ """Configure the distance histogram cutoff configurator.
+
+ Parameters
+ ----------
+ value : tuple
+ A tuple of the range parameters.
+ """
+ super().configure(value)
+
+ if float(value) > round(self.get_max_cutoff(), 2):
+ self.error_status = (
+ "The cutoff distance goes into the simulation box periodic images."
+ )
+ return
+
+ def get_max_cutoff(self):
+ traj_config = self.configurable[self.dependencies["trajectory"]]["instance"]
+ return get_largest_cutoff(traj_config)
diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/DistHistCutoffConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/DistHistCutoffConfigurator.py
index e1355029b8..bc82a7281b 100644
--- a/MDANSE/Src/MDANSE/Framework/Configurators/DistHistCutoffConfigurator.py
+++ b/MDANSE/Src/MDANSE/Framework/Configurators/DistHistCutoffConfigurator.py
@@ -1,9 +1,21 @@
+# This file is part of MDANSE.
+#
+# 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 __future__ import annotations
-from math import floor
-
-import numpy as np
-
+from .DistCutoffConfigurator import get_largest_cutoff
from .IConfigurator import PredictionSettings
from .RangeConfigurator import RangeConfigurator
@@ -34,7 +46,7 @@ def configure(self, value):
if not self.update_needed(value):
return
- if self._max_value and value[1] > floor(self.get_largest_cutoff() * 100) / 100:
+ if self._max_value and value[1] > round(self.get_max_cutoff(), 2):
self.error_status = (
"The cutoff distance goes into the simulation box periodic images."
)
@@ -42,47 +54,6 @@ def configure(self, value):
super().configure(value)
- def get_largest_cutoff(self) -> float:
- """Get the largest cutoff value for the given trajectories
- unit cells.
-
- Returns
- -------
- float
- The maximum cutoff for the distance histogram job.
- """
+ def get_max_cutoff(self):
traj_config = self.configurable[self.dependencies["trajectory"]]["instance"]
- try:
- trajectory_array = np.array(
- [
- traj_config.unit_cell(frame)._unit_cell
- for frame in range(len(traj_config))
- ]
- )
- except Exception:
- return np.linalg.norm(traj_config.min_span)
- else:
- if np.allclose(trajectory_array, 0.0):
- return np.linalg.norm(traj_config.min_span)
- else:
- # calculated the radius of the largest sphere that can
- # fit into the unit cell
- min_d = np.min(trajectory_array, axis=0)
- vec_a, vec_b, vec_c = min_d
-
- cross_bc = np.cross(vec_b, vec_c)
- cross_ca = np.cross(vec_c, vec_a)
- cross_ab = np.cross(vec_a, vec_b)
-
- if (
- np.allclose(cross_bc, 0.0)
- or np.allclose(cross_ca, 0.0)
- or np.allclose(cross_ab, 0.0)
- ):
- raise ValueError("Trajectory contains invalid unit cell.")
-
- h_1 = abs(np.dot(vec_a, cross_bc)) / np.linalg.norm(cross_bc)
- h_2 = abs(np.dot(vec_b, cross_ca)) / np.linalg.norm(cross_ca)
- h_3 = abs(np.dot(vec_c, cross_ab)) / np.linalg.norm(cross_ab)
-
- return 0.5 * min(h_1, h_2, h_3)
+ return get_largest_cutoff(traj_config)
diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/__init__.py b/MDANSE/Src/MDANSE/Framework/Configurators/__init__.py
index 48bef31dd5..444232593f 100644
--- a/MDANSE/Src/MDANSE/Framework/Configurators/__init__.py
+++ b/MDANSE/Src/MDANSE/Framework/Configurators/__init__.py
@@ -35,6 +35,9 @@
from .DerivativeOrderConfigurator import (
DerivativeOrderConfigurator as DerivativeOrderConfigurator,
)
+from .DistCutoffConfigurator import (
+ DistCutoffConfigurator as DistCutoffConfigurator,
+)
from .DistHistCutoffConfigurator import (
DistHistCutoffConfigurator as DistHistCutoffConfigurator,
)
diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/InfraredBulk.py b/MDANSE/Src/MDANSE/Framework/Jobs/InfraredBulk.py
new file mode 100644
index 0000000000..aa564407df
--- /dev/null
+++ b/MDANSE/Src/MDANSE/Framework/Jobs/InfraredBulk.py
@@ -0,0 +1,289 @@
+# 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 __future__ import annotations
+
+import collections
+from itertools import compress
+
+import numpy as np
+from scipy.signal import correlate
+
+from MDANSE.Framework.Jobs.IJob import IJob
+from MDANSE.Mathematics.Signal import differentiate, get_spectrum
+
+
+class InfraredBulk(IJob):
+ """Calculates the infrared spectrum of a system bulk system.
+
+ The infrared spectrum is calculated as the autocorrelation of the derivative
+ the dipole moments.
+
+ This analysis requires molecules to be defined in the system,
+ and partial charges to be set to non-zero values.
+ """
+
+ enabled = True
+
+ label = "Infrared Spectrum Bulk"
+
+ category = (
+ "Analysis",
+ "Infrared",
+ )
+ PREDICTORS = ("instrument_resolution",)
+
+ ancestor = ["hdf_trajectory", "molecular_viewer"]
+
+ settings = collections.OrderedDict()
+ settings["trajectory"] = ("HDFTrajectoryConfigurator", {})
+ settings["frames"] = (
+ "CorrelationFramesConfigurator",
+ {"dependencies": {"trajectory": "trajectory"}},
+ )
+ settings["instrument_resolution"] = (
+ "InstrumentResolutionConfigurator",
+ {
+ "dependencies": {"trajectory": "trajectory", "frames": "frames"},
+ },
+ )
+ settings["derivative_order"] = (
+ "DerivativeOrderConfigurator",
+ {
+ "label": "d/dt dipole numerical derivative",
+ "dependencies": {"frames": "frames"},
+ },
+ )
+ settings["distance_cutoff"] = (
+ "DistCutoffConfigurator",
+ {
+ "label": "cutoff (nm)",
+ "mini": 1e-8,
+ "dependencies": {"trajectory": "trajectory"},
+ },
+ )
+ settings["atom_charges"] = (
+ "PartialChargeConfigurator",
+ {
+ "dependencies": {"trajectory": "trajectory"},
+ "default": {},
+ },
+ )
+ settings["output_files"] = ("OutputFilesConfigurator", {})
+ settings["running_mode"] = ("RunningModeConfigurator", {})
+
+ def initialize(self):
+ super().initialize()
+
+ self.numberOfSteps = len(self.trajectory.atom_indices)
+ instrResolution = self.configuration["instrument_resolution"]
+
+ self.add_ideal_results = (
+ self.configuration["instrument_resolution"]["kernel"].lower() != "ideal"
+ )
+ self._outputData.add(
+ "ir/axes/time",
+ "LineOutputVariable",
+ self.configuration["frames"]["duration"],
+ units="ps",
+ )
+ self._outputData.add(
+ "ir/res/time_window",
+ "LineOutputVariable",
+ instrResolution["time_window_positive"],
+ axis="ir/axes/time",
+ units="au",
+ )
+
+ self._outputData.add(
+ "ir/axes/omega",
+ "LineOutputVariable",
+ instrResolution["omega"],
+ units="rad/ps",
+ )
+ self._outputData.add(
+ "ir/axes/romega",
+ "LineOutputVariable",
+ instrResolution["romega"],
+ units="rad/ps",
+ )
+ self._outputData.add(
+ "ir/res/omega_window",
+ "LineOutputVariable",
+ instrResolution["omega_window"],
+ axis="ir/axes/omega",
+ units="au",
+ )
+
+ self._outputData.add(
+ "ddacf/ddacf",
+ "LineOutputVariable",
+ (self.configuration["frames"]["n_frames"],),
+ axis="ir/axes/time",
+ )
+ self._outputData.add(
+ "ir/ir",
+ "LineOutputVariable",
+ (instrResolution["n_romegas"],),
+ axis="ir/axes/romega",
+ main_result=True,
+ )
+ if self.add_ideal_results:
+ self._outputData.add(
+ "ir/ideal",
+ "LineOutputVariable",
+ (instrResolution["n_romegas"],),
+ axis="ir/axes/romega",
+ )
+
+ def run_step(self, index: int) -> tuple[int, np.ndarray]:
+ """Runs a single step of the job.
+
+ Parameters
+ ----------
+ index : int
+ The index of the atom.
+
+ Returns
+ -------
+ tuple[int, np.ndarray]
+ The index of the step and the calculated d/dt dipole
+ auto-correlation function for a molecule.
+ """
+ n_configs = self.configuration["frames"]["n_configs"]
+ first_frame = self.configuration["frames"]["first"]
+ step_frame = self.configuration["frames"]["step"]
+ last_frame = self.configuration["frames"]["last"]
+ n_frames = self.configuration["frames"]["number"]
+ n_atms = self.trajectory.get_total_natoms()
+
+ series_i = self.trajectory.read_atomic_trajectory(
+ index,
+ first=first_frame,
+ last=last_frame + 1,
+ step=step_frame,
+ )
+ try:
+ q_i = self.configuration["atom_charges"]["charges"][index]
+ except KeyError:
+ q_i = np.array(
+ [
+ self.trajectory.charges(t)[index]
+ for t in range(first_frame, last_frame + 1, step_frame)
+ ]
+ )[:, np.newaxis]
+ ddipole_i = q_i * series_i
+
+ for axis in range(3):
+ ddipole_i[:, axis] = differentiate(
+ ddipole_i[:, axis],
+ order=self.configuration["derivative_order"]["value"],
+ dt=step_frame,
+ )
+
+ cutoff = np.zeros(n_atms, dtype=bool)
+ for frame_index in range(
+ first_frame,
+ last_frame + 1,
+ step_frame,
+ ):
+ configuration = self.trajectory.configuration(frame_index)
+ coords = configuration["coordinates"]
+ coords_ref = coords[index]
+ cell = configuration.unit_cell.direct
+ inverse_cell = configuration.unit_cell.inverse
+ diff_frac = (coords - coords_ref) @ inverse_cell
+ diff_frac -= np.round(diff_frac)
+ diff_coords = diff_frac @ cell
+ r = np.sqrt(np.sum(diff_coords * diff_coords, axis=1))
+
+ # smaller cutoffs reproduce molecular calculation
+ # all atoms that have approached the reference atom across
+ # the entire trajectory
+ cutoff = np.logical_or(
+ cutoff, r < self.configuration["distance_cutoff"]["value"]
+ )
+
+ ddipole_j = np.zeros((n_frames, 3))
+ for j in compress(range(n_atms), cutoff):
+ series_j = self.trajectory.read_atomic_trajectory(
+ j,
+ first=first_frame,
+ last=last_frame + 1,
+ step=step_frame,
+ )
+ try:
+ q_j = self.configuration["atom_charges"]["charges"][j]
+ except KeyError:
+ q_j = np.array(
+ [
+ self.trajectory.charges(t)[j]
+ for t in range(first_frame, last_frame + 1, step_frame)
+ ]
+ )[:, np.newaxis]
+ ddipole_j += q_j * series_j
+
+ for axis in range(3):
+ ddipole_j[:, axis] = differentiate(
+ ddipole_j[:, axis],
+ order=self.configuration["derivative_order"]["value"],
+ dt=step_frame,
+ )
+
+ ddipole_ij = correlate(ddipole_i, ddipole_j[:n_configs], mode="valid") / (
+ 3 * n_configs
+ )
+ return index, ddipole_ij.T[0]
+
+ def combine(self, index: int, x: np.ndarray):
+ """Add the d/dt dipole correlation function to the results.
+
+ Parameters
+ ----------
+ index : int
+ The index of the molecule.
+ x : np.ndarray
+ d/dt dipole correlation function
+ """
+ self._outputData["ddacf/ddacf"] += x
+
+ def finalize(self):
+ """Average the d/dt dipole auto-correlation function and
+ fourier transform to get the IR spectrum and save the results.
+ """
+ self._outputData["ddacf/ddacf"] /= self.numberOfSteps
+ self._outputData["ir/ir"][:] = get_spectrum(
+ self._outputData["ddacf/ddacf"],
+ self.configuration["instrument_resolution"]["time_window"],
+ self.configuration["instrument_resolution"]["time_step"],
+ fft="rfft",
+ )
+ if self.add_ideal_results:
+ self._outputData["ir/ideal"][:] = get_spectrum(
+ self._outputData["ddacf/ddacf"],
+ None,
+ self.configuration["instrument_resolution"]["time_step"],
+ fft="rfft",
+ )
+
+ self._outputData.write(
+ self.configuration["output_files"]["root"],
+ self.configuration["output_files"]["formats"],
+ str(self),
+ self,
+ )
+
+ self.trajectory.close()
+ super().finalize()
diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/Infrared.py b/MDANSE/Src/MDANSE/Framework/Jobs/InfraredMolecular.py
similarity index 99%
rename from MDANSE/Src/MDANSE/Framework/Jobs/Infrared.py
rename to MDANSE/Src/MDANSE/Framework/Jobs/InfraredMolecular.py
index cf9db32b70..b7ae7cfe34 100644
--- a/MDANSE/Src/MDANSE/Framework/Jobs/Infrared.py
+++ b/MDANSE/Src/MDANSE/Framework/Jobs/InfraredMolecular.py
@@ -23,7 +23,7 @@
from MDANSE.Mathematics.Signal import differentiate, get_spectrum
-class Infrared(IJob):
+class InfraredMolecular(IJob):
"""Calculates the infrared spectrum of a system of molecules.
The infrared spectrum is calculated as the autocorrelation of the derivative
@@ -35,7 +35,7 @@ class Infrared(IJob):
enabled = True
- label = "Infrared Spectrum"
+ label = "Infrared Spectrum Molecular"
category = (
"Analysis",
diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/__init__.py b/MDANSE/Src/MDANSE/Framework/Jobs/__init__.py
index 38aec532ef..7e7baf9e27 100644
--- a/MDANSE/Src/MDANSE/Framework/Jobs/__init__.py
+++ b/MDANSE/Src/MDANSE/Framework/Jobs/__init__.py
@@ -48,7 +48,8 @@
GaussianDynamicIncoherentStructureFactor as GaussianDynamicIncoherentStructureFactor,
)
from .IJob import IJob as IJob
-from .Infrared import Infrared as Infrared
+from .InfraredBulk import InfraredBulk as InfraredBulk
+from .InfraredMolecular import InfraredMolecular as InfraredMolecular
from .JobStatus import JobStatus as JobStatus
from .MeanSquareDisplacement import MeanSquareDisplacement as MeanSquareDisplacement
from .MolecularTrace import MolecularTrace as MolecularTrace
diff --git a/MDANSE/Tests/UnitTests/Analysis/test_infrared.py b/MDANSE/Tests/UnitTests/Analysis/test_infrared.py
index 810ca67642..eb1d7cfce4 100644
--- a/MDANSE/Tests/UnitTests/Analysis/test_infrared.py
+++ b/MDANSE/Tests/UnitTests/Analysis/test_infrared.py
@@ -83,7 +83,7 @@ def test_ir_analysis(generate_benchmarks, tmp_path):
"molecule_name": "C1_O2",
}
- job = IJob.create("Infrared")
+ job = IJob.create("InfraredMolecular")
job.run(parameters, status=True)
if generate_benchmarks:
diff --git a/MDANSE/Tests/UnitTests/test_ijob.py b/MDANSE/Tests/UnitTests/test_ijob.py
index 165ff4d800..04c2c77f52 100644
--- a/MDANSE/Tests/UnitTests/test_ijob.py
+++ b/MDANSE/Tests/UnitTests/test_ijob.py
@@ -60,7 +60,8 @@
"NAMD",
"XPLOR",
"DFTB",
- "Infrared",
+ "InfraredBulk",
+ "InfraredMolecular",
]
diff --git a/MDANSE/dftb_molecule_specification.mdt b/MDANSE/dftb_molecule_specification.mdt
new file mode 100644
index 0000000000..38ecc15e2e
Binary files /dev/null and b/MDANSE/dftb_molecule_specification.mdt differ
diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/DistCutoffWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/DistCutoffWidget.py
new file mode 100644
index 0000000000..def11772a4
--- /dev/null
+++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/DistCutoffWidget.py
@@ -0,0 +1,26 @@
+# 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 __future__ import annotations
+
+from .FloatWidget import FloatWidget
+
+
+class DistCutoffWidget(FloatWidget):
+ def setup_field(self, *args, **kwargs):
+ mini = 0.0
+ default = round(self._configurator.get_max_cutoff(), 2)
+ maxi = default
+ super().setup_field(mini=mini, default=default, maxi=maxi)
diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/DistHistCutoffWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/DistHistCutoffWidget.py
index 4b7bdf01a4..0ba2fc8d7f 100644
--- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/DistHistCutoffWidget.py
+++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/DistHistCutoffWidget.py
@@ -26,6 +26,6 @@ def __init__(self, *args, **kwargs):
def setup_fields(self, *args, **kwargs):
start = 0.0
- end = floor(self._configurator.get_largest_cutoff() * 100) / 100
+ end = floor(self._configurator.get_max_cutoff() * 100) / 100
step = 0.01
super().setup_fields(*args, default=(start, end, step), **kwargs)
diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/FloatWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/FloatWidget.py
index 29604e06a6..01c5a5d8fb 100644
--- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/FloatWidget.py
+++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/FloatWidget.py
@@ -24,8 +24,20 @@
class FloatWidget(WidgetBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+ self.setup_field(*args, **kwargs)
+
+ def setup_field(
+ self,
+ *args,
+ default: float | None = None,
+ mini: float | None = None,
+ maxi: float | None = None,
+ **kwargs,
+ ):
try:
- default_option = float(self._configurator.default)
+ default_option = (
+ default if default is not None else self._configurator.default
+ )
except ValueError:
default_option = 0.0
if self._configurator.choices:
@@ -40,7 +52,8 @@ def __init__(self, *args, **kwargs):
else:
field = QLineEdit(self._base)
validator = QDoubleValidator(field)
- minval, maxval = self._configurator.mini, self._configurator.maxi
+ minval = mini if mini is not None else self._configurator.mini
+ maxval = maxi if maxi is not None else self._configurator.maxi
if minval is not None:
validator.setBottom(minval)
if maxval is not None:
@@ -78,3 +91,7 @@ def get_widget_value(self):
else:
self._empty = False
return strval
+
+ def configure_using_default(self):
+ """Configure with the default value."""
+ self._configurator.configure(self._default_value)
diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/__init__.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/__init__.py
index 686da70d61..46d16e75a7 100644
--- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/__init__.py
+++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/__init__.py
@@ -25,6 +25,7 @@
from .ComboWidget import ComboWidget as ComboWidget
from .CorrelationFramesWidget import CorrelationFramesWidget as CorrelationFramesWidget
from .DerivativeOrderWidget import DerivativeOrderWidget as DerivativeOrderWidget
+from .DistCutoffWidget import DistCutoffWidget as DistCutoffWidget
from .DistHistCutoffWidget import DistHistCutoffWidget as DistHistCutoffWidget
from .FloatWidget import FloatWidget as FloatWidget
from .FramesWidget import FramesWidget as FramesWidget
diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py
index b18c72c265..8407cc1486 100644
--- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py
+++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py
@@ -48,6 +48,7 @@
ComboWidget,
CorrelationFramesWidget,
DerivativeOrderWidget,
+ DistCutoffWidget,
DistHistCutoffWidget,
FloatWidget,
FramesWidget,
@@ -92,6 +93,7 @@
"FramesConfigurator": FramesWidget,
"RangeConfigurator": RangeWidget,
"QRangeConfigurator": RangeWidget,
+ "DistCutoffConfigurator": DistCutoffWidget,
"DistHistCutoffConfigurator": DistHistCutoffWidget,
"VectorConfigurator": VectorWidget,
"HDFInputFileConfigurator": InputFileWidget,