diff --git a/CHANGELOG b/CHANGELOG index f8e2c0634f..f331ae7d6a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,15 @@ +version 2.0.0b1 - 2024-10-15 +-------------------------- +* ADDED van Hove functions (self function and distinct function) +* FIXED Eccentricity analysis to give correct results +* ADDED Infrared analysis (only for molecules at the moment) +* FIXED MolecularTrace analysis data axes to make data plottable +* CHANGED the plotting interface to allow overplotting curves +* ADDED instrument profiles for storing user parameters +* ADDED logging in the code and a logger tab in the GUI +* ADDED charge information to the stored trajectory data +* ADDED support for H5MD trajectories + version 2.0.0 - 2024-02-15 -------------------------- * CHANGED programming language to Python 3 diff --git a/Doc/conf.py b/Doc/conf.py index 36052104e0..6050cdf304 100644 --- a/Doc/conf.py +++ b/Doc/conf.py @@ -41,7 +41,7 @@ # project = 'MDANSE' # copyright = '2015-2022, Eric Pellegrini' author = 'Eric Pellegrini' -release = '2.0.0a1' +release = '2.0.0b1' version = '2.0' # -- General configuration --------------------------------------------------- diff --git a/Doc/index.rst b/Doc/index.rst index 2a3b78f79b..3bf3e8fb0a 100644 --- a/Doc/index.rst +++ b/Doc/index.rst @@ -9,6 +9,7 @@ Welcome to MDANSE's documentation! .. note:: This is the documentation of the MDANSE 2.0 release. The documentation, just like the code itself, is still under development. + MDANSE 2 has currently (October 2024) just reached the first beta release. **Useful links**: `MDANSE Project Website `_ | `MDANSE GitHub Page `_ diff --git a/Doc/pages/H_start.rst b/Doc/pages/H_start.rst index fadd26d96d..594b9b524e 100644 --- a/Doc/pages/H_start.rst +++ b/Doc/pages/H_start.rst @@ -57,6 +57,9 @@ Use `pip` to install the MDANSE package from the specified GitHub repository: pip install MDANSE +The MDANSE package contains all the code needed to perform trajectory conversion +and analysis using MDANSE, but none of the visualisation tools. + Install MDANSE_GUI Package ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -66,14 +69,9 @@ Similarly, install the MDANSE_GUI package using `pip`: pip install MDANSE_GUI -Run MDANSE -~~~~~~~~~~ - -You can now start using MDANSE by running the following command: - -.. code-block:: bash - - mdanse_gui +From now on, the `mdanse_gui` command will be available to start +the graphical interface of MDANSE, which makes it easier to create +valid inputs for different analysis types. Run MDANSE ~~~~~~~~~~ @@ -84,13 +82,20 @@ You can now start using MDANSE by running the following command: mdanse_gui -This will launch the MDANSE Graphical User Interface (GUI), and you can start using MDANSE for your -analysis. - -Note for Windows Users: On Windows, the command to run MDANSE may need to be: - -.. code-block:: bash - - python3 mdanse_gui - -That's it! You have successfully installed MDANSE and are ready to use it for your data analysis needs. +This will launch the MDANSE Graphical User Interface (GUI), +and you can start using MDANSE for your analysis. + +MDANSE Scripts +~~~~~~~~~~~~~~ + +If you intend to run your analysis on a remote platform +(e.g. a cluster), most likely you will have limited options +of using the GUI there. However, you can still prepare +a script using MDANSE_GUI on your own computer, save it +and transfer it to the other computer to run the analysis +there. You will need to change the file paths in the script, +but all the other parameters should be transferable. One +of the design principles of MDANSE 2 is that the scripts +should not depend on any settings stored locally on +a specific computer, but should instead contain all the +information needed to run a specific analysis type. diff --git a/MDANSE/Doc/_static/mdanse_logo.png b/MDANSE/Doc/_static/mdanse_logo.png deleted file mode 100644 index f41ec6f500..0000000000 Binary files a/MDANSE/Doc/_static/mdanse_logo.png and /dev/null differ diff --git a/MDANSE/Doc/conf_api.py b/MDANSE/Doc/conf_api.py index da79f2b508..25a1841831 100644 --- a/MDANSE/Doc/conf_api.py +++ b/MDANSE/Doc/conf_api.py @@ -70,7 +70,7 @@ # The full version, including alpha/beta/rc tags. release = "1.0" -html_logo = "_static/mdanse_logo.png" +# html_logo = "_static/mdanse_logo.png" inheritance_graph_attrs = dict(size='""') diff --git a/MDANSE/Doc/conf_help.py b/MDANSE/Doc/conf_help.py index be7b917af7..09322c09d3 100644 --- a/MDANSE/Doc/conf_help.py +++ b/MDANSE/Doc/conf_help.py @@ -66,7 +66,7 @@ # The full version, including alpha/beta/rc tags. release = "1.0" -html_logo = "_static/mdanse_logo.png" +# html_logo = "_static/mdanse_logo.png" inheritance_graph_attrs = dict(size='""') diff --git a/MDANSE/Doc/mdanse_logo.png b/MDANSE/Doc/mdanse_logo.png deleted file mode 100644 index f41ec6f500..0000000000 Binary files a/MDANSE/Doc/mdanse_logo.png and /dev/null differ diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/ConfigFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/ConfigFileConfigurator.py index b4109744de..9c507f0193 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/ConfigFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/ConfigFileConfigurator.py @@ -140,15 +140,42 @@ def parse(self): self["bonds"].append([at1, at2]) self["bonds"] = np.array(self["bonds"], dtype=np.int32) - if re.match("^\s*Atoms\s*$", line): + if re.match("^\s*Atoms\s*$", line.split("#")[0]): + if not "#" in line: + num_of_columns = len(lines[i + 2].split()) + if num_of_columns <= 5: + type_index = 1 + charge_index = None + line_limit = 6 + else: + type_index = 2 + charge_index = 3 + line_limit = 7 + else: + if "charge" in line.split("#")[-1]: + type_index = 1 + charge_index = 2 + line_limit = 6 + elif "atomic" in line.split("#")[-1]: + type_index = 1 + charge_index = None + line_limit = 6 + elif "full" in line.split("#")[-1]: + type_index = 2 + charge_index = 3 + line_limit = 7 + else: + type_index = 2 + charge_index = 3 + line_limit = 7 if self["n_atoms"] is not None: self["atom_types"] = self["n_atoms"] * [0] self["charges"] = self["n_atoms"] * [0.0] for j in range(self["n_atoms"]): atoks = lines[i + j + 1].split() - self["atom_types"][j] = int(atoks[2]) - if len(atoks) >= 7: - self["charges"][j] = float(atoks[3]) + self["atom_types"][j] = int(atoks[type_index]) + if len(atoks) >= line_limit and charge_index is not None: + self["charges"][j] = float(atoks[charge_index]) if np.trace(np.abs(self["unit_cell"])) < 1e-8: # print(f"Concatenated: {np.concatenate([x_inputs, y_inputs, z_inputs])}") diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/FileWithAtomDataConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/FileWithAtomDataConfigurator.py index 348a3ffdc0..5e29ab12cc 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/FileWithAtomDataConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/FileWithAtomDataConfigurator.py @@ -14,6 +14,7 @@ # along with this program. If not, see . # from abc import abstractmethod +import traceback from MDANSE.Framework.AtomMapping import AtomLabel from .InputFileConfigurator import InputFileConfigurator @@ -35,7 +36,7 @@ def configure(self, filepath: str) -> None: try: self.parse() except Exception as e: - self.error_status = "File parsing error" + self.error_status = f"File parsing error {e}: {traceback.format_exc()}" @abstractmethod def parse(self) -> None: diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/GroupingLevelConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/GroupingLevelConfigurator.py index 922719a416..7437419a81 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/GroupingLevelConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/GroupingLevelConfigurator.py @@ -131,11 +131,13 @@ def configure(self, value): elements = [] masses = [] names = [] + group_indices = [] for i, v in enumerate(groups.values()): names.append("group_%d" % i) elements.append(v["elements"]) indexes.append(v["indexes"]) masses.append(v["masses"]) + group_indices.append(i) atomSelectionConfig["indexes"] = indexes atomSelectionConfig["elements"] = elements @@ -145,6 +147,7 @@ def configure(self, value): atomSelectionConfig["unique_names"] = sorted(set(atomSelectionConfig["names"])) self["level"] = value + self["group_indices"] = group_indices @staticmethod def find_parent(atom, level): diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/ProjectionConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/ProjectionConfigurator.py index fdc60d1afa..1c9aa15747 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/ProjectionConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/ProjectionConfigurator.py @@ -14,6 +14,7 @@ # along with this program. If not, see . # +import numpy as np from MDANSE.Framework.Configurators.IConfigurator import IConfigurator from MDANSE.Framework.Projectors.IProjector import IProjector @@ -62,13 +63,26 @@ def configure(self, value): self.error_status = f"the projector {mode} is unknown" return else: + if mode == "NullProjector": + self.error_status = "OK" + return try: - self["projector"].set_axis(axis) + vector = [float(x) for x in axis] + except ValueError: + self.error_status = f"Could not convert {axis} to numbers" + return + else: + if np.allclose(vector, 0): + self.error_status = f"Vector of 0 length does not define projection" + return + try: + self["projector"].set_axis(vector) except ProjectorError: - self.error_status = f"Axis {axis} is wrong for this projector" + self.error_status = f"Axis {vector} is wrong for this projector" return else: self["axis"] = self["projector"].axis + self.error_status = "OK" def get_information(self): diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/QVectorsConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/QVectorsConfigurator.py index 13f4341e8f..5d4c468a69 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/QVectorsConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/QVectorsConfigurator.py @@ -72,10 +72,14 @@ def configure(self, value): return try: - generator.generate() + generator_success = generator.generate() except: self.error_status = "Q Vector parameters were parsed correctly, but caused an error. Invalid values?" return + else: + if not generator_success: + self.error_status = "Q Vector parameters were parsed correctly, but caused an error. Invalid values?" + return if not "q_vectors" in generator.configuration: self.error_status = "Wrong inputs for q-vector generation. At the moment there are no valid Q points." diff --git a/MDANSE/Src/MDANSE/Framework/Converters/LAMMPS.py b/MDANSE/Src/MDANSE/Framework/Converters/LAMMPS.py index 0f8ab026dc..4e46db5e4e 100644 --- a/MDANSE/Src/MDANSE/Framework/Converters/LAMMPS.py +++ b/MDANSE/Src/MDANSE/Framework/Converters/LAMMPS.py @@ -159,8 +159,18 @@ def parse_first_step(self, aliases, config): elif line.startswith("ITEM: ATOMS"): keywords = line.split()[2:] - self._id = keywords.index("id") - self._type = keywords.index("type") + if "id" in keywords: + self._id = keywords.index("id") + else: + self._id = None + if "type" in keywords: + self._type = keywords.index("type") + else: + self._type = None + if "element" in keywords: + self._element = keywords.index("element") + else: + self._element = None try: self._charge = keywords.index("q") except ValueError: @@ -194,12 +204,28 @@ def parse_first_step(self, aliases, config): self._itemsPosition["ATOMS"] = [comp + 1, comp + self._nAtoms + 1] for i in range(self._nAtoms): temp = self._file.readline().split() - idx = int(temp[self._id]) - 1 - ty = int(temp[self._type]) - 1 + if self._id is not None: + idx = int(temp[self._id]) - 1 + else: + idx = int(i) + if self._type is not None: + ty = int(temp[self._type]) - 1 + else: + try: + ty = int(config["atom_types"][i]) - 1 + except IndexError: + LOG.error( + f"Failed to find index [{i}] in list of len {len(config['atom_types'])}" + ) label = str(config["elements"][ty][0]) mass = str(config["elements"][ty][1]) name = "{:s}_{:d}".format(str(config["elements"][ty][0]), idx) - self._rankToName[int(temp[0]) - 1] = name + try: + temp_index = int(temp[0]) + except ValueError: + self._rankToName[i] = name + else: + self._rankToName[temp_index - 1] = name g.add_node(idx, label=label, mass=mass, atomName=name) if config["n_bonds"] is not None: @@ -335,7 +361,12 @@ def run_step(self, index): range(self._itemsPosition["ATOMS"][0], self._itemsPosition["ATOMS"][1]) ): temp = self._file.readline().split() - idx = self._nameToIndex[self._rankToName[int(temp[0]) - 1]] + try: + temp_index = int(temp[0]) + except ValueError: + idx = i + else: + idx = self._nameToIndex[self._rankToName[temp_index - 1]] coords[idx, :] = np.array( [temp[self._x], temp[self._y], temp[self._z]], dtype=np.float64 ) diff --git a/MDANSE/Src/MDANSE/Framework/InputData/HDFTrajectoryInputData.py b/MDANSE/Src/MDANSE/Framework/InputData/HDFTrajectoryInputData.py index d4a0b55099..42e66c4602 100644 --- a/MDANSE/Src/MDANSE/Framework/InputData/HDFTrajectoryInputData.py +++ b/MDANSE/Src/MDANSE/Framework/InputData/HDFTrajectoryInputData.py @@ -107,7 +107,7 @@ def put_into_dict(name, obj): try: string = obj[:][0].decode() except: - LOG.warning(f"Decode failed for {name}: {obj}") + LOG.debug(f"Decode failed for {name}: {obj}") else: try: meta_dict[name] = json_decoder.decode(string) diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/AverageStructure.py b/MDANSE/Src/MDANSE/Framework/Jobs/AverageStructure.py index 60874678f5..ed4caf0546 100644 --- a/MDANSE/Src/MDANSE/Framework/Jobs/AverageStructure.py +++ b/MDANSE/Src/MDANSE/Framework/Jobs/AverageStructure.py @@ -97,6 +97,26 @@ def initialize(self): self._ase_atoms = Atoms() + trajectory = self.configuration["trajectory"]["instance"] + + frame_range = range( + self.configuration["frames"]["first"], + self.configuration["frames"]["last"] + 1, + self.configuration["frames"]["step"], + ) + + try: + unit_cells = [ + trajectory.unit_cell(frame)._unit_cell for frame in frame_range + ] + except: + raise ValueError( + "Unit cell needs to be defined for the AverageStructure analysis. " + "You can add a unit cell using TrajectoryEditor." + ) + else: + self._unit_cells = unit_cells + def run_step(self, index): """ Runs a single step of the job. @@ -135,7 +155,10 @@ def combine(self, index, x): # The symbol of the atom. element = self.configuration["atom_selection"]["names"][index] - the_atom = Atom(element, x) + try: + the_atom = Atom(element, x) + except KeyError: + the_atom = Atom(str(element).strip("0123456789"), x) self._ase_atoms.append(the_atom) @@ -152,17 +175,7 @@ def finalize(self): self.configuration["frames"]["step"], ) - try: - unit_cells = [ - trajectory.unit_cell(frame)._unit_cell for frame in frame_range - ] - except: - raise ValueError( - "Unit cell needs to be defined for the AverageStructure analysis. " - "You can add a unit cell using TrajectoryEditor." - ) - - average_unit_cell = np.mean(unit_cells, axis=0) * self._conversion_factor + average_unit_cell = np.mean(self._unit_cells, axis=0) * self._conversion_factor self._ase_atoms.set_cell(average_unit_cell) diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/DensityProfile.py b/MDANSE/Src/MDANSE/Framework/Jobs/DensityProfile.py index 37c2046cef..a6f2b51d72 100644 --- a/MDANSE/Src/MDANSE/Framework/Jobs/DensityProfile.py +++ b/MDANSE/Src/MDANSE/Framework/Jobs/DensityProfile.py @@ -137,6 +137,7 @@ def run_step(self, index): conf = self.configuration["trajectory"]["instance"].configuration(frame_index) box_coords = conf.to_box_coordinates() + box_coords = box_coords - np.floor(box_coords) axis_index = self.configuration["axis"]["index"] axis = conf.unit_cell.direct[axis_index, :] @@ -146,7 +147,7 @@ def run_step(self, index): for k, v in self._indexes_per_element.items(): h = np.histogram( - box_coords[v, axis_index], bins=self._n_bins, range=[-0.5, 0.5] + box_coords[v, axis_index], bins=self._n_bins, range=[0.0, 1.0] ) dp_per_frame[k] = h[0] diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/Infrared.py b/MDANSE/Src/MDANSE/Framework/Jobs/Infrared.py index c39ff9da63..52d6eaecb5 100644 --- a/MDANSE/Src/MDANSE/Framework/Jobs/Infrared.py +++ b/MDANSE/Src/MDANSE/Framework/Jobs/Infrared.py @@ -100,7 +100,10 @@ def initialize(self): ) self._outputData.add( - "omega", "LineOutputVariable", instrResolution["romega"], units="rad/ps" + "omega", "LineOutputVariable", instrResolution["omega"], units="rad/ps" + ) + self._outputData.add( + "romega", "LineOutputVariable", instrResolution["romega"], units="rad/ps" ) self._outputData.add( "omega_window", @@ -120,7 +123,7 @@ def initialize(self): "ir", "LineOutputVariable", (instrResolution["n_romegas"],), - axis="omega", + axis="romega", main_result=True, ) diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/RootMeanSquareFluctuation.py b/MDANSE/Src/MDANSE/Framework/Jobs/RootMeanSquareFluctuation.py index 8f89252688..f161da2b67 100644 --- a/MDANSE/Src/MDANSE/Framework/Jobs/RootMeanSquareFluctuation.py +++ b/MDANSE/Src/MDANSE/Framework/Jobs/RootMeanSquareFluctuation.py @@ -77,7 +77,14 @@ def initialize(self): for idxs in self.configuration["atom_selection"]["indexes"] for idx in idxs ] - self._outputData.add("indexes", "LineOutputVariable", indexes) + if self.configuration["grouping_level"]["value"] == "atom": + self._outputData.add("indexes", "LineOutputVariable", indexes) + else: + self._outputData.add( + "indexes", + "LineOutputVariable", + self.configuration["grouping_level"]["group_indices"], + ) # Will store the mean square fluctuation evolution. self._outputData.add( diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/Temperature.py b/MDANSE/Src/MDANSE/Framework/Jobs/Temperature.py index 3dfd1ba583..740c0a5581 100644 --- a/MDANSE/Src/MDANSE/Framework/Jobs/Temperature.py +++ b/MDANSE/Src/MDANSE/Framework/Jobs/Temperature.py @@ -64,6 +64,7 @@ class Temperature(IJob): "OutputFilesConfigurator", {"formats": ["MDAFormat", "TextFormat"]}, ) + settings["running_mode"] = ("RunningModeConfigurator", {}) def initialize(self): """ diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/UnfoldedTrajectory.py b/MDANSE/Src/MDANSE/Framework/Jobs/UnfoldedTrajectory.py index 4885470e58..0d04038f65 100644 --- a/MDANSE/Src/MDANSE/Framework/Jobs/UnfoldedTrajectory.py +++ b/MDANSE/Src/MDANSE/Framework/Jobs/UnfoldedTrajectory.py @@ -30,6 +30,8 @@ class UnfoldedTrajectory(IJob): label = "Unfolded Trajectory" + enabled = False + category = ( "Analysis", "Trajectory", diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/VanHoveFunctionSelf.py b/MDANSE/Src/MDANSE/Framework/Jobs/VanHoveFunctionSelf.py index 1ff62d42a8..056c78b9c1 100644 --- a/MDANSE/Src/MDANSE/Framework/Jobs/VanHoveFunctionSelf.py +++ b/MDANSE/Src/MDANSE/Framework/Jobs/VanHoveFunctionSelf.py @@ -74,6 +74,14 @@ class VanHoveFunctionSelf(IJob): ) settings["running_mode"] = ("RunningModeConfigurator", {}) + def detailed_unit_cell_error(self): + raise ValueError( + "This analysis job requires a unit cell (simulation box) to be defined. " + "The box will be used for calculating density in the analysis. " + "You can add a simulation box to the trajectory using the TrajectoryEditor job. " + "Be careful adding the simulation box, as the wrong dimensions can render the results meaningless." + ) + def initialize(self): super().initialize() @@ -86,6 +94,17 @@ def initialize(self): self.n_mid_points = len(self.configuration["r_values"]["mid_points"]) + conf = self.configuration["trajectory"]["instance"].configuration( + self.configuration["frames"]["first"] + ) + try: + cell_volume = conf.unit_cell.volume + except: + self.detailed_unit_cell_error() + else: + if cell_volume < 1e-9: + self.detailed_unit_cell_error() + self._outputData.add( "r", "LineOutputVariable", diff --git a/MDANSE/Src/MDANSE/Framework/QVectors/IQVectors.py b/MDANSE/Src/MDANSE/Framework/QVectors/IQVectors.py index 5d7d342856..47588cabd3 100644 --- a/MDANSE/Src/MDANSE/Framework/QVectors/IQVectors.py +++ b/MDANSE/Src/MDANSE/Framework/QVectors/IQVectors.py @@ -39,16 +39,18 @@ def __init__(self, chemical_system, status=None): def _generate(self): pass - def generate(self): + def generate(self) -> bool: if self._configured: self._generate() if self._status is not None: self._status.finish() + return True else: LOG.error( f"Cannot generate vectors: q vector generator is not configured correctly." ) + return False def setStatus(self, status): self._status = status diff --git a/MDANSE/pyproject.toml b/MDANSE/pyproject.toml index 9b64c52ed1..4c834d00fc 100644 --- a/MDANSE/pyproject.toml +++ b/MDANSE/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "MDANSE" -version = "2.0.0-dev" +version = "2.0.0b1" description = 'MDANSE Core package - Molecular Dynamics trajectory handling and analysis code' readme = "README.md" requires-python = ">=3.9" @@ -18,7 +18,7 @@ maintainers = [ {name = "Sanghamitra Mukhopadhyay", email = "sanghamitra.mukhopadhyay@stfc.ac.uk"} ] classifiers = [ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Intended Audience :: Science/Research', 'Topic :: Scientific/Engineering :: Chemistry', diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/ProjectionWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/ProjectionWidget.py index 8623d86982..d9431c7665 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/ProjectionWidget.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/ProjectionWidget.py @@ -51,6 +51,10 @@ def __init__(self, *args, **kwargs): wid.setToolTip(tooltip_text) for wid in self._button_group.buttons(): wid.setToolTip(tooltip_text) + self.button_switched(0) + self._button_group.buttonClicked.connect(self.updateValue) + for vfield in self._vector_fields: + vfield.textChanged.connect(self.updateValue) def configure_using_default(self): """This is too complex to have a default value""" @@ -76,7 +80,7 @@ def get_widget_value(self): """Collect the results from the input widgets and return the value.""" if self._mode == 0: return ("NullProjector", []) - vector = [float(x.text()) for x in self._vector_fields] + vector = [x.text() for x in self._vector_fields] if self._mode == 1: return ("AxialProjector", vector) else: diff --git a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/QVectorsWidget.py b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/QVectorsWidget.py index 238488c82c..517f94c5d2 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/QVectorsWidget.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/InputWidgets/QVectorsWidget.py @@ -69,7 +69,8 @@ def params_summary(self) -> dict: try: params[name] = self.parse_vtype(vtype, value, name) except ValueError: - params[name] = self._defaults[rownum] + params[name] = "failed" + if params[name] == "failed": self.item(rownum, 1).setData( QBrush(Qt.GlobalColor.red), role=Qt.ItemDataRole.BackgroundRole ) @@ -83,11 +84,19 @@ def parse_vtype(self, vtype: str, value: str, vname: str): if vtype == "RangeConfigurator": inner_type = self._generator.settings[vname][1]["valueType"] tempstring = value.strip("()[] ") - return [inner_type(x) for x in tempstring.split(",")] + result = [inner_type(x) for x in tempstring.split(",")] + if len(result) == 3: + return result + else: + return "failed" elif vtype == "VectorConfigurator": inner_type = self._generator.settings[vname][1]["valueType"] tempstring = value.strip("()[] ") - return [inner_type(x) for x in tempstring.split(",")] + result = [inner_type(x) for x in tempstring.split(",")] + if len(result) == 3: + return result + else: + return "failed" elif vtype == "FloatConfigurator": return float(value) elif vtype == "IntegerConfigurator": diff --git a/MDANSE_GUI/Src/MDANSE_GUI/MolecularViewer/MolecularViewer.py b/MDANSE_GUI/Src/MDANSE_GUI/MolecularViewer/MolecularViewer.py index a9c51c8ab7..dfc1169493 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/MolecularViewer/MolecularViewer.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/MolecularViewer/MolecularViewer.py @@ -76,6 +76,11 @@ def __init__(self): self._iren = QVTKRenderWindowInteractor(self) + def dummy_method(self, ev=None): + pass + + setattr(self._iren, "keyPressEvent", dummy_method) + self._renderer = vtk.vtkRenderer() self._iren.GetRenderWindow().AddRenderer(self._renderer) diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Resources/splash.png b/MDANSE_GUI/Src/MDANSE_GUI/Resources/splash.png index b32edf100a..d250ddffb3 100644 Binary files a/MDANSE_GUI/Src/MDANSE_GUI/Resources/splash.png and b/MDANSE_GUI/Src/MDANSE_GUI/Resources/splash.png differ diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/GeneralTab.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/GeneralTab.py index ce77f01f6e..e7a0277554 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/GeneralTab.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/GeneralTab.py @@ -105,7 +105,7 @@ def grouped_settings(self): def connect_units(self): if self._visualiser is not None: if self._visualiser._unit_lookup is None: - print(f"Visualiser {self._visualiser} has no unit lookup") + LOG.debug(f"Visualiser {self._visualiser} has no unit lookup") self._visualiser._unit_lookup = self def conversion_factor(self, input_unit: str) -> Tuple[float, str]: diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Models/PlottingContext.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Models/PlottingContext.py index 80fa5374cb..14b0b1f222 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Models/PlottingContext.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Models/PlottingContext.py @@ -75,12 +75,12 @@ def __init__(self, name: str, source: "h5py.File"): try: self._data = source[name][:] except KeyError: - LOG.error(f"{name} is not a data set in the file") + LOG.debug(f"{name} is not a data set in the file") self._valid = False return except TypeError: self._valid = False - LOG.error(f"{name} is not plottable") + LOG.debug(f"{name} is not plottable") return self._data_unit = source[name].attrs["units"] self._n_dim = len(self._data.shape) @@ -422,7 +422,7 @@ def add_dataset(self, new_dataset: SingleDataset): new_dataset._filename, new_dataset._data.shape, new_dataset._data_unit, - "", + new_dataset.longest_axis()[-1], "", self.next_colour(), "-", @@ -455,7 +455,7 @@ def add_dataset(self, new_dataset: SingleDataset): elif len(curves) == 1: LOG.debug("A single curve output from PlottingContext.") else: - LOG.error("No curves!") + LOG.debug("No curves!") self.appendRow(items) def set_axes(self): diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Plotters/Grid.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Plotters/Grid.py index 3f6b4677a7..284b8b5e7f 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Plotters/Grid.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Plotters/Grid.py @@ -77,9 +77,9 @@ def plot( break dataset, colour, linestyle, marker, ds_num, axis_label = databundle try: - best_unit, best_axis = ds._axes_units[axis_label], axis_label + best_unit, best_axis = dataset._axes_units[axis_label], axis_label except KeyError: - best_unit, best_axis = ds.longest_axis() + best_unit, best_axis = dataset.longest_axis() xaxis_unit = plotting_context.get_conversion_factor(best_unit) for key, curve in dataset._curves.items(): if counter > self._plot_limit: diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Plotters/Heatmap.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Plotters/Heatmap.py index 724f9f317c..658cf29f08 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Plotters/Heatmap.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Plotters/Heatmap.py @@ -186,9 +186,9 @@ def plot( primary_axis_number = number all_numbers = [0] if primary_axis_number == 0: - all_datasets = [dataset._data] - else: all_datasets = [dataset._data.T] + else: + all_datasets = [dataset._data] transposed = True all_labels = [dataset._name] limits = [] diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Plotters/__init__.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Plotters/__init__.py index edbe3d778b..d6cc074c4e 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Plotters/__init__.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Plotters/__init__.py @@ -30,12 +30,13 @@ globdict = globals() for name in modnames: - if name in ["__init__"]: + if name in ["__init__", "PlotterTemplate"]: continue try: tempmod = importlib.import_module("." + name, "MDANSE_GUI.Tabs.Plotters") except ModuleNotFoundError: LOG.error(f"Could not find {name} in MDANSE_GUI.Tabs.Plotters") - tempobject = getattr(tempmod, name) - globdict[name] = tempobject - del tempmod # optionally delete the reference to the parent module + else: + tempobject = getattr(tempmod, name) + globdict[name] = tempobject + del tempmod # optionally delete the reference to the parent module diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Views/Delegates.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Views/Delegates.py index a12aad60a4..7b72b38a57 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Views/Delegates.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Views/Delegates.py @@ -14,9 +14,6 @@ # along with this program. If not, see . # -from PyQt6.QtCore import QModelIndex -from PyQt6.QtGui import QPainter -from PyQt6.QtWidgets import QStyleOptionViewItem from qtpy.QtWidgets import ( QDoubleSpinBox, QComboBox, @@ -30,8 +27,6 @@ from qtpy.QtCore import Signal, Slot, Qt from qtpy.QtGui import QColor -from MDANSE_GUI.Tabs.Models.PlottingContext import get_mpl_lines, get_mpl_markers - class ColourPicker(QStyledItemDelegate): @@ -154,7 +149,7 @@ class ProgressDelegate(QItemDelegate): def paint(self, painter, option, index): progress = index.data(self.progress_role) - progress_max = max(index.data(self.progress_role + 1), 1) + progress_max = max(index.data(self.progress_role + 1) - 1, 1) try: int(progress) except: @@ -164,6 +159,7 @@ def paint(self, painter, option, index): opt.minimum = 0 opt.maximum = progress_max opt.progress = progress - opt.text = "{}%".format(round(100 * progress / progress_max, 2)) + percentage = min(round(100 * progress / progress_max, 2), 100.0) + opt.text = "{}%".format(percentage) opt.textVisible = True QApplication.style().drawControl(QStyle.CE_ProgressBar, opt, painter) diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py index 763939d80f..236e7af59e 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/Action.py @@ -162,9 +162,9 @@ def update_panel(self, job_name: str) -> None: try: job_instance = IJob.create(job_name) except ValueError as e: - LOG.error( + LOG.debug( f"Failed to create IJob {job_name};\n" - f"error {e};\n" + f"reason {e};\n" f"traceback {traceback.format_exc()}" ) return diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/InstrumentDetails.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/InstrumentDetails.py index 26cc13bc44..ef0c42a5ae 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/InstrumentDetails.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/InstrumentDetails.py @@ -72,10 +72,12 @@ def create_widget(self, entry): instance = QLineEdit(self) if value == "float": instance.setValidator(QDoubleValidator()) - instance.setText("0.0") + instance.setText("1.0") + instance.setPlaceholderText("1.0") elif value == "int": instance.setValidator(QIntValidator()) - instance.setText("0") + instance.setText("1") + instance.setPlaceholderText("1") else: instance.setText("text") instance.textChanged.connect(self.update_values) @@ -116,6 +118,11 @@ def update_values(self): new_val = value.currentText() except AttributeError: new_val = None + if len(new_val) == 0: + try: + new_val = value.placeholderText() + except AttributeError: + new_val = "" self._values[key] = new_val self.commit_changes() self.toggle_axis_fields() diff --git a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/PlotHolder.py b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/PlotHolder.py index f4384e426a..32bc9f7d1e 100644 --- a/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/PlotHolder.py +++ b/MDANSE_GUI/Src/MDANSE_GUI/Tabs/Visualisers/PlotHolder.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from qtpy.QtWidgets import QVBoxLayout, QTabWidget +from qtpy.QtWidgets import QVBoxLayout, QTabWidget, QTabBar from qtpy.QtCore import Slot, Signal from MDANSE.MLogging import LOG @@ -45,6 +45,13 @@ def __init__(self, *args, unit_lookup=None, **kwargs): self._protected_id = int(self._current_id) self.setTabsClosable(True) self.tabCloseRequested.connect(self.clean_up_closed_tab) + # remove the close button on the protected tab + tabbar = self.tabBar() + close_button = tabbar.tabButton( + self._protected_id, QTabBar.ButtonPosition.RightSide + ) + close_button.deleteLater() + tabbar.setTabButton(self._protected_id, QTabBar.ButtonPosition.RightSide, None) @Slot(str) def new_plot(self, tab_name: str) -> int: diff --git a/MDANSE_GUI/pyproject.toml b/MDANSE_GUI/pyproject.toml index 5f91325e13..188737a3e8 100644 --- a/MDANSE_GUI/pyproject.toml +++ b/MDANSE_GUI/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "MDANSE_GUI" -version = "0.1.0-dev" +version = "0.1.0b1" description = 'MDANSE GUI package - the graphical interface for MDANSE' readme = "README.md" requires-python = ">=3.9" @@ -18,7 +18,7 @@ maintainers = [ {name = "Sanghamitra Mukhopadhyay", email = "sanghamitra.mukhopadhyay@stfc.ac.uk"} ] classifiers = [ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Intended Audience :: Science/Research', 'Topic :: Scientific/Engineering :: Chemistry',