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,