From bf350e183007700cc01f76262dced7e0190526a4 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:07:58 +0200 Subject: [PATCH 01/22] feat(remote): add_group returns the UUID of the new group Assisted-by: Claude Opus 5.5 --- datalab/control/baseproxy.py | 12 +++++++++--- datalab/control/remote.py | 7 ++++++- datalab/gui/main.py | 17 +++++++++++++---- datalab/tests/backbone/main_app_test.py | 3 ++- .../control/remote_requests_unit_test.py | 14 ++++++++++++++ 5 files changed, 44 insertions(+), 9 deletions(-) diff --git a/datalab/control/baseproxy.py b/datalab/control/baseproxy.py index c41d45d30..13198bf58 100644 --- a/datalab/control/baseproxy.py +++ b/datalab/control/baseproxy.py @@ -401,13 +401,16 @@ def set_object(self, obj: SignalObj | ImageObj) -> None: @abc.abstractmethod def add_group( self, title: str, panel: str | None = None, select: bool = False - ) -> None: + ) -> str: """Add group to DataLab. Args: title: Group title panel: Panel name (valid values: "signal", "image"). Defaults to None. select: Select the group after creation. Defaults to False. + + Returns: + UUID of the created group """ @abc.abstractmethod @@ -923,15 +926,18 @@ def get_current_object_uuid(self) -> str | None: def add_group( self, title: str, panel: str | None = None, select: bool = False - ) -> None: + ) -> str: """Add group to DataLab. Args: title: Group title panel: Panel name (valid values: "signal", "image"). Defaults to None. select: Select the group after creation. Defaults to False. + + Returns: + UUID of the created group """ - self._datalab.add_group(title, panel, select) + return self._datalab.add_group(title, panel, select) def select_objects( self, diff --git a/datalab/control/remote.py b/datalab/control/remote.py index 40275fb69..0dbe31811 100644 --- a/datalab/control/remote.py +++ b/datalab/control/remote.py @@ -126,7 +126,6 @@ def __init__(self, win: DLMainWindow) -> None: for signal, command in ( (self.SIG_ADD_OBJECT, win.add_object), (self.SIG_SET_OBJECT, win.set_object), - (self.SIG_ADD_GROUP, win.add_group), (self.SIG_LOAD_FROM_FILES, win.load_from_files), (self.SIG_LOAD_FROM_DIRECTORY, win.load_from_directory), (self.SIG_SELECT_OBJECTS, win.select_objects), @@ -149,6 +148,9 @@ def __init__(self, win: DLMainWindow) -> None: (self.SIG_IMPORT_MACRO_FROM_FILE, win.import_macro_from_file), ): signal.connect(functools.partial(win.run_remote_command, command)) + self.SIG_ADD_GROUP.connect( + functools.partial(win.run_remote_command, win.add_group, return_result=True) + ) self.SIG_CALL_METHOD.connect(win.call_method_slot) def serve(self) -> None: @@ -579,6 +581,9 @@ def add_group( title: Group title panel: Panel name (valid values: "signal", "image"). Defaults to None. select: Select the group after creation. Defaults to False. + + Returns: + UUID of the created group, set by the main window slot """ self.SIG_ADD_GROUP.emit(title, panel, select) diff --git a/datalab/gui/main.py b/datalab/gui/main.py index a92da45f5..f464c40ed 100644 --- a/datalab/gui/main.py +++ b/datalab/gui/main.py @@ -509,15 +509,18 @@ def add_group( title: str, panel: Literal["signal", "image"] | None = None, select: bool = False, - ) -> None: + ) -> str: """Add group to DataLab. Args: title: Group title panel: Panel name. Defaults to None. select: Select the group after creation. Defaults to False. + + Returns: + UUID of the created group """ - self.__get_datapanel(panel).add_group(title, select) + return get_uuid(self.__get_datapanel(panel).add_group(title, select)) @remote_controlled def select_objects( @@ -674,7 +677,9 @@ def call_method_slot( self.remote_server.exception = exc @remote_controlled - def run_remote_command(self, command: Callable[..., object], *args) -> None: + def run_remote_command( + self, command: Callable[..., object], *args, return_result: bool = False + ) -> None: """Slot to run a command requested by a remote client in GUI thread. The exception raised by the command, if any, is stored in RemoteServer to @@ -684,9 +689,13 @@ def run_remote_command(self, command: Callable[..., object], *args) -> None: Args: command: Main window method running the command args: Command arguments + return_result: If True, store the command result in RemoteServer, to be + returned to the client (it must be XML-RPC serializable) """ try: - command(*args) + result = command(*args) + if return_result: + self.remote_server.result = result except Exception as exc: # pylint: disable=broad-except self.remote_server.exception = exc diff --git a/datalab/tests/backbone/main_app_test.py b/datalab/tests/backbone/main_app_test.py index 288028e1c..a08124f88 100644 --- a/datalab/tests/backbone/main_app_test.py +++ b/datalab/tests/backbone/main_app_test.py @@ -65,7 +65,8 @@ def test_main_app(): panel.remove_object(force=True) win.add_group("Group 4", select=True) panel.remove_object(force=True) - win.add_group("Group 5", panel="signal", select=True) + group_id = win.add_group("Group 5", panel="signal", select=True) + assert panel.objmodel.get_group(group_id).title == "Group 5" panel.remove_object(force=True) # Rename group panel.objview.select_groups([2]) diff --git a/datalab/tests/features/control/remote_requests_unit_test.py b/datalab/tests/features/control/remote_requests_unit_test.py index 01065397e..e943de8ae 100644 --- a/datalab/tests/features/control/remote_requests_unit_test.py +++ b/datalab/tests/features/control/remote_requests_unit_test.py @@ -69,6 +69,19 @@ def test_remote_command_result() -> None: assert proxy.get_current_object_uuid() is None +def test_remote_add_group_returns_uuid() -> None: + """The UUID returned by add_group lets the client fill the new group""" + with datalab_in_background_context() as proxy: + group_id = proxy.add_group("Scan 12.1", panel="signal") + assert isinstance(group_id, str) and group_id + signal = create_signal("I0", np.arange(3.0), np.ones(3)) + proxy.add_object(signal, group_id=group_id) + assert len(proxy.get_object_uuids("signal", group=group_id)) == 1 + # The group UUID must not be returned by the next command + proxy.set_current_panel("image") + assert proxy.get_current_object_uuid() is None + + def test_remote_close_during_call() -> None: """Closing DataLab while a remote call is pending does not hang DataLab""" errors: list[Exception] = [] @@ -98,4 +111,5 @@ def close_datalab(proxy: RemoteProxy) -> None: test_remote_load_at_startup(Path(tmpdir), load_mode) test_remote_command_error() test_remote_command_result() + test_remote_add_group_returns_uuid() test_remote_close_during_call() From 892d056d55a9e0bf9a2cb8afb4abf160b5081630 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:08:16 +0200 Subject: [PATCH 02/22] fix(hdf5): drop the obsolete uint32 clipping warning Assisted-by: Claude Opus 5.5 --- datalab/gui/h5io.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/datalab/gui/h5io.py b/datalab/gui/h5io.py index 120ea32be..200c5d104 100644 --- a/datalab/gui/h5io.py +++ b/datalab/gui/h5io.py @@ -68,7 +68,6 @@ class H5InputOutput: def __init__(self, mainwindow: DLMainWindow) -> None: self.mainwindow = mainwindow - self.uint32_wng: bool = None #: True when the user was already told, for the current opening, that #: the provenance of appended workspaces is not imported (no repeat). self.append_informed = False @@ -273,19 +272,11 @@ def __add_object_from_node(self, node: BaseNode) -> None: obj = node.get_native_object() if obj is None: return - self.uint32_wng = self.uint32_wng or node.uint32_wng if isinstance(obj, SignalObj): self.mainwindow.signalpanel.add_object(obj) else: self.mainwindow.imagepanel.add_object(obj) - def __eventually_show_warnings(self) -> None: - """Eventually show warnings after everything is imported""" - if self.uint32_wng: - QW.QMessageBox.warning( - self.mainwindow, _("Warning"), _("Clipping uint32 data to int32.") - ) - def import_files( self, filenames: list[str], import_all: bool, reset_all: bool ) -> None: @@ -316,7 +307,6 @@ def import_files( self.mainwindow.reset_all() with qt_try_loadsave_file(self.mainwindow, "*.h5", "load"): with create_progress_bar(self.mainwindow, "", len(nodes)) as prog: - self.uint32_wng = False for idx, node in enumerate(nodes): prog.setLabelText(self.__progbartitle(node.h5file.filename)) prog.setValue(idx + 1) @@ -324,7 +314,6 @@ def import_files( if prog.wasCanceled(): break self.__add_object_from_node(node) - self.__eventually_show_warnings() h5browser.cleanup() def import_dataset_from_file(self, filename: str, dsetname: str) -> None: @@ -332,9 +321,7 @@ def import_dataset_from_file(self, filename: str, dsetname: str) -> None: h5importer = H5Importer(filename) try: node = h5importer.get(dsetname) - self.uint32_wng = False self.__add_object_from_node(node) - self.__eventually_show_warnings() except KeyError as exc: raise KeyError(f"Dataset not found: {dsetname}") from exc h5importer.close() From 42ffc080c7e391d3164b18e88d8b46ec785f5d7b Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:08:33 +0200 Subject: [PATCH 03/22] docs: release notes for add_group and HDF5 import Assisted-by: Claude Opus 5.5 --- doc/release_notes/release_1.04.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index 9b2f538ca..6f52e7892 100644 --- a/doc/release_notes/release_1.04.md +++ b/doc/release_notes/release_1.04.md @@ -104,6 +104,10 @@ DataLab now uses stable UUID-based object references across object trees, result * Polynomial and linear fit coefficients are labelled `a`, `b`, `c`... as in the formula and in the metadata, instead of `c2`, `c1`, `c0` * The *Fitting* documentation explains how to derive the peak wavelength, the temperature and the emissivity from the parameters of a Planckian fit +**Remote control and plugins:** + +* `add_group` now returns the UUID of the created group, so that a remote client, a macro or a plugin can add objects to it with `add_object(obj, group_id=...)` (or `add_signal`/`add_image`) instead of looking the group up by its title + ### 🔄 Changes ### **Synthetic images in physical coordinates:** @@ -127,6 +131,11 @@ DataLab now uses stable UUID-based object references across object trees, result * Annotation identifiers, lock state, custom metadata and extension data are preserved across edits, while unknown third-party payloads are retained without modification * DataLab now requires Sigima ≄ 1.3.0, SigimaX ≄ 1.1.0 and DataLab-Capsule 0.1.x +**HDF5 browser:** + +* Browsing an HDF5 file no longer reads its datasets entirely: they are listed and described from their shape and data type, and the value previews read only their first elements. Large files open faster and use less memory; data is read when a signal or an image is previewed or imported +* Dataset attributes with more than 10 000 values are no longer copied to the metadata of imported objects + ### đŸ› ïž Bug Fixes ### **Performance with many objects:** @@ -139,6 +148,10 @@ DataLab now uses stable UUID-based object references across object trees, result * Fixed the width of the signal/image panels not being restored at startup: after narrowing the panels and restarting DataLab, they came back wider (up to their maximum width) instead of keeping the width chosen by the user +**HDF5 import:** + +* Fixed images with unsigned 32-bit values above 2 147 483 647 being clipped when imported from an HDF5 file ("Clipping uint32 data to int32" warning): such images are now imported as 64-bit floating-point images keeping every value exactly, as are other integer images that no supported integer type can hold, which were rounded to 32-bit floating-point values beyond 16 777 216 (requires Sigima ≄ 1.4.0) + **Metadata:** * Adding, pasting or deleting metadata now marks the workspace as modified, so DataLab asks to save these changes before closing From fdea49cfb84c4f5cccec1f63f65fccad21c5be51 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:49:02 +0200 Subject: [PATCH 04/22] feat(hdf5): open NeXus files and keep their origin in workspaces Assisted-by: Claude Opus 5.5 --- datalab/gui/main.py | 5 +- datalab/gui/panel/base.py | 4 +- datalab/locale/fr/LC_MESSAGES/datalab.po | 4 +- .../features/hdf5/nexus_import_app_test.py | 81 +++++++++++++++++++ 4 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 datalab/tests/features/hdf5/nexus_import_app_test.py diff --git a/datalab/gui/main.py b/datalab/gui/main.py index f464c40ed..048d0f843 100644 --- a/datalab/gui/main.py +++ b/datalab/gui/main.py @@ -2281,7 +2281,10 @@ def open_h5_files( self, _("Open"), Conf.base_dir.get(), - _("HDF5 files (*.h5 *.hdf5 *.hdf *.he5);;All files (*)"), + _( + "HDF5 files (*.h5 *.hdf5 *.hdf *.he5 *.nxs *.nx5);;" + "All files (*)" + ), ) if not h5files: return diff --git a/datalab/gui/panel/base.py b/datalab/gui/panel/base.py index bf4c90429..e645c8223 100644 --- a/datalab/gui/panel/base.py +++ b/datalab/gui/panel/base.py @@ -185,7 +185,9 @@ def is_hdf5_file(filename: str, check_content: bool = False) -> bool: True if the file is (likely) an HDF5 file, False otherwise. """ # First, check by extension (fast) - has_hdf5_extension = filename.lower().endswith((".h5", ".hdf5", ".hdf", ".he5")) + has_hdf5_extension = filename.lower().endswith( + (".h5", ".hdf5", ".hdf", ".he5", ".nxs", ".nx5") + ) if not check_content: return has_hdf5_extension diff --git a/datalab/locale/fr/LC_MESSAGES/datalab.po b/datalab/locale/fr/LC_MESSAGES/datalab.po index 84c572f49..6d72fd958 100644 --- a/datalab/locale/fr/LC_MESSAGES/datalab.po +++ b/datalab/locale/fr/LC_MESSAGES/datalab.po @@ -1259,8 +1259,8 @@ msgstr "Ouvrir une capsule" msgid "Open" msgstr "Ouvrir" -msgid "HDF5 files (*.h5 *.hdf5 *.hdf *.he5);;All files (*)" -msgstr "Fichiers HDF5 (*.h5 *.hdf5 *.hdf *.he5);;Tous les fichiers (*)" +msgid "HDF5 files (*.h5 *.hdf5 *.hdf *.he5 *.nxs *.nx5);;All files (*)" +msgstr "Fichiers HDF5 (*.h5 *.hdf5 *.hdf *.he5 *.nxs *.nx5);;Tous les fichiers (*)" #, python-format msgid "Open %d HDF5 files" diff --git a/datalab/tests/features/hdf5/nexus_import_app_test.py b/datalab/tests/features/hdf5/nexus_import_app_test.py new file mode 100644 index 000000000..23210c6c4 --- /dev/null +++ b/datalab/tests/features/hdf5/nexus_import_app_test.py @@ -0,0 +1,81 @@ +# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file. + +""" +NeXus import test +----------------- + +A NeXus ``NXdata`` group is imported with its axes and its origin, and the +workspace saved afterwards is reopened without the NeXus file. +""" + +# guitest: show + +from __future__ import annotations + +import os +import os.path as osp + +import h5py +import numpy as np +from sigima.objects import get_experimental_source + +from datalab.tests import datalab_test_app_context, helpers + + +def _create_nexus_file(fname: str) -> None: + """Create a NeXus file with a spectrum and a map with non-uniform axes""" + with h5py.File(fname, "w") as h5file: + entry = h5file.create_group("entry") + entry.attrs["NX_class"] = "NXentry" + spectrum = entry.create_group("spectrum") + spectrum.attrs.update({"NX_class": "NXdata", "signal": "I0", "axes": "E"}) + spectrum["I0"] = np.array([3, 2**31 + 5, 7], dtype=np.uint32) + spectrum["E"] = np.array([7.1, 7.2, 7.4]) + spectrum["E"].attrs["units"] = "keV" + mapping = entry.create_group("map") + mapping.attrs.update({"NX_class": "NXdata", "signal": "counts"}) + mapping.attrs["axes"] = np.array([b"y", b"x"]) + mapping["counts"] = np.array([[1, 2, 3], [4, 5, 2**32 - 1]], dtype=np.uint32) + mapping["x"] = np.array([0.0, 1.0, 4.0]) + mapping["y"] = np.array([0.0, 2.0]) + + +def test_nexus_import_and_workspace() -> None: + """NXdata import, then workspace round trip without the NeXus file""" + with helpers.WorkdirRestoringTempDir() as tmpdir: + nxsname = osp.join(tmpdir, "scan.nxs") + _create_nexus_file(nxsname) + wsname = osp.join(tmpdir, "workspace.h5") + with datalab_test_app_context(console=False, exec_loop=False) as win: + h5io = win.h5inputoutput + h5io.import_dataset_from_file(nxsname, "/entry/spectrum") + h5io.import_dataset_from_file(nxsname, "/entry/map") + signal = win.signalpanel.objmodel.get_all_objects()[0] + image = win.imagepanel.objmodel.get_all_objects()[0] + assert np.array_equal(signal.x, [7.1, 7.2, 7.4]) + assert signal.y[1] == 2**31 + 5 + assert signal.xunit == "keV" + assert image.data[1, 2] == 2**32 - 1 + assert np.array_equal(image.xcoords, [0.0, 1.0, 4.0]) + win.save_h5_workspace(wsname) + os.remove(nxsname) + + win.load_h5_workspace([wsname], reset_all=True) + signal = win.signalpanel.objmodel.get_all_objects()[0] + image = win.imagepanel.objmodel.get_all_objects()[0] + assert np.array_equal(signal.x, [7.1, 7.2, 7.4]) + assert image.data[1, 2] == 2**32 - 1 + assert np.array_equal(image.xcoords, [0.0, 1.0, 4.0]) + source = get_experimental_source(signal) + assert source["kind"] == "nexus" + assert source["location"] == nxsname + assert source["entry"] == "/entry/spectrum" + assert source["selection"] == {"signal": "I0", "axes": ["E"]} + assert get_experimental_source(image)["selection"] == { + "signal": "counts", + "axes": ["y", "x"], + } + + +if __name__ == "__main__": + test_nexus_import_and_workspace() From 5382084513a30a5b7bc2a369ea655d0f1ff2be20 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:49:22 +0200 Subject: [PATCH 05/22] docs: release notes for NeXus import and HDF5 previews Assisted-by: Claude Opus 5.5 --- doc/release_notes/release_1.04.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index 6f52e7892..902dbf6fb 100644 --- a/doc/release_notes/release_1.04.md +++ b/doc/release_notes/release_1.04.md @@ -108,6 +108,12 @@ DataLab now uses stable UUID-based object references across object trees, result * `add_group` now returns the UUID of the created group, so that a remote client, a macro or a plugin can add objects to it with `add_object(obj, group_id=...)` (or `add_signal`/`add_image`) instead of looking the group up by its title +**NeXus files:** + +* NeXus `NXdata` groups are shown as single items in the HDF5 browser and imported as signals or images with their axes, units, labels and, for signals, uncertainties. Bin edges are converted to bin centers, and non-uniform image axes are kept. Invalid axes are reported in the browser instead of being guessed +* Imported signals and images record where they come from (file, group, signal and axes, applied conversions) in their `experimental_source` metadata, which is saved with the workspace: a saved workspace reopens without the NeXus file +* `.nxs` and `.nx5` files are proposed when opening HDF5 files + ### 🔄 Changes ### **Synthetic images in physical coordinates:** @@ -134,6 +140,8 @@ DataLab now uses stable UUID-based object references across object trees, result **HDF5 browser:** * Browsing an HDF5 file no longer reads its datasets entirely: they are listed and described from their shape and data type, and the value previews read only their first elements. Large files open faster and use less memory; data is read when a signal or an image is previewed or imported +* The HDF5 browser stays responsive while a preview is read, and previews datasets of more than 16 million values with a step along each axis +* Datasets with three dimensions or more (image stacks, spectral cubes...) are now listed in the HDF5 browser * Dataset attributes with more than 10 000 values are no longer copied to the metadata of imported objects ### đŸ› ïž Bug Fixes ### From 36f5428cc37e35ecf783d5f161fa572edade4ec7 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Wed, 21 Oct 2026 07:09:15 +0200 Subject: [PATCH 06/22] feat(plugins): run instruments outside the GUI thread, with open/close Assisted-by: Claude Opus 5.5 --- datalab/gui/main.py | 13 +- datalab/gui/plugins/instrument.py | 179 ++++++++++++++++-- datalab/plugins/instruments.py | 22 +++ .../plugins/instrument_window_unit_test.py | 108 +++++++++++ .../features/plugins/plugin_tools_app_test.py | 15 +- 5 files changed, 317 insertions(+), 20 deletions(-) create mode 100644 datalab/tests/features/plugins/instrument_window_unit_test.py diff --git a/datalab/gui/main.py b/datalab/gui/main.py index 048d0f843..b84461d9a 100644 --- a/datalab/gui/main.py +++ b/datalab/gui/main.py @@ -1173,9 +1173,18 @@ def __forget_instrument_window(self, key: tuple[str, str], *_args) -> None: window.deleteLater() def close_plugin_instruments(self) -> None: - """Close all plugin instrument windows.""" - for window in list(self.__instrument_windows.values()): + """Close all plugin instrument windows, and wait for their instruments.""" + windows = list(self.__instrument_windows.values()) + for window in windows: window.close() + window.stop() + for window in windows: + if not window.wait_closed(): + logging.getLogger(__name__).warning( + "Instrument '%s' did not close within %g s", + window.windowTitle(), + window.CLOSE_TIMEOUT, + ) def __unregister_plugins(self) -> None: """Unregister all plugins and let them cleanup their hooks""" diff --git a/datalab/gui/plugins/instrument.py b/datalab/gui/plugins/instrument.py index dd66eeb80..43631fc95 100644 --- a/datalab/gui/plugins/instrument.py +++ b/datalab/gui/plugins/instrument.py @@ -4,7 +4,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import logging +import queue +import threading +import time +from concurrent.futures import Future +from concurrent.futures import wait as wait_futures +from typing import TYPE_CHECKING, Callable from guidata.configtools import get_icon from guidata.dataset.qtwidgets import DataSetEditLayout @@ -30,11 +36,50 @@ __all__ = ["InstrumentWindow"] +class _SerialWorker: + """Run calls one at a time in a daemon thread. + + Unlike a thread pool, a call stuck in third-party code does not prevent + DataLab from quitting. + """ + + def __init__(self) -> None: + self._queue: queue.SimpleQueue = queue.SimpleQueue() + thread = threading.Thread(target=self._run, name="instrument", daemon=True) + thread.start() + + def submit(self, func: Callable, *args) -> Future: + """Schedule a call and return its future.""" + future: Future = Future() + self._queue.put((future, func, args)) + return future + + def shutdown(self) -> None: + """Stop the thread once the scheduled calls are done.""" + self._queue.put(None) + + def _run(self) -> None: + """Run scheduled calls until shutdown.""" + while True: + item = self._queue.get() + if item is None: + return + future, func, args = item + if future.set_running_or_notify_cancel(): + try: + future.set_result(func(*args)) + except BaseException as exc: # pylint: disable=broad-except + future.set_exception(exc) + + class InstrumentWindow(QW.QDialog): """Live view of a plugin instrument, next to its settings. The view is refreshed when settings change, and periodically in live mode. - Each acquisition is added to the workspace in a new group. + Each acquisition is added to the workspace in a new group. The instrument is + opened with the window and closed with it. Its methods are called one at a + time in a worker thread: a request made while a call is running is done + next (an acquisition before a refresh, repeated refreshes only once). Args: main: DataLab main window @@ -45,6 +90,11 @@ class InstrumentWindow(QW.QDialog): #: Delay between a settings change and the refresh of the view (ms) REFRESH_DELAY = 150 + #: Maximum time DataLab waits for an instrument to close (s) + CLOSE_TIMEOUT = 5.0 + + SIG_CALL_DONE = QC.Signal(str, object, object) + def __init__( self, main: DLMainWindow, instrument: PluginInstrument, title: str ) -> None: @@ -59,6 +109,12 @@ def __init__( self._plot_kind: str | None = None self._plot_signature: tuple | None = None self._value_range: tuple[float, float] | None = None + self._executor = _SerialWorker() + self._busy = False + self._pending: str | None = None + self._resume_live = False + self._close_future: Future | None = None + self.SIG_CALL_DONE.connect(self._call_done) splitter = QW.QSplitter(QC.Qt.Horizontal) self._stage = QW.QWidget() @@ -132,8 +188,100 @@ def __init__( min(self.width(), screen.width()), min(self.height(), screen.height()) ) splitter.setSizes([650, 450]) + self._busy = True + self._executor.submit(self._call, "open") self._refresh_timer.start(0) + def _call(self, method: str) -> None: + """Call an instrument method (worker thread).""" + result, error = None, None + try: + result = getattr(self.instrument, method)() + except Exception as exc: # pylint: disable=broad-except + # Instruments are third-party code: keep the window usable + error = exc + if method == "close": + logging.getLogger(__name__).exception("Closing instrument failed") + try: + self.SIG_CALL_DONE.emit(method, result, error) + except RuntimeError: + pass # Window deleted after being closed + + def _request(self, method: str) -> None: + """Call ``preview`` or ``acquire`` now, or after the running call.""" + if self._close_future is not None: + return + if self._busy: + if self._pending != "acquire": + self._pending = method + return + if not self._apply_settings(): + return + if method == "acquire": + self._resume_live = self._live_timer.isActive() + self._live_timer.stop() + self.acquire_button.setEnabled(False) + self._busy = True + self._executor.submit(self._call, method) + + def _call_done(self, method: str, result, error: Exception | None) -> None: + """Show the result of an instrument call, then make the pending one.""" + if method == "close": + return + self._busy = False + if method == "acquire": + self.acquire_button.setEnabled(True) + if self._resume_live and self._close_future is None: + self._live_timer.start() + if self._close_future is not None: + return + if error is not None: + self.status_label.setText(str(error)) + elif method == "preview": + self._show_frame(result) + elif method == "acquire": + self._add_acquired(result) + pending, self._pending = self._pending, None + if pending is not None: + self._request(pending) + + def is_idle(self) -> bool: + """Return True if no instrument call is running, requested or scheduled.""" + return not ( + self._busy or self._pending is not None or self._refresh_timer.isActive() + ) + + def wait_until_idle(self, timeout: float = 10.0) -> bool: + """Process events until no instrument call is running or requested. + + Args: + timeout: maximum waiting time (s) + + Returns: + True if the window is idle, False on timeout + """ + deadline = time.monotonic() + timeout + while not self.is_idle(): + if time.monotonic() > deadline: + return False + QW.QApplication.processEvents() + time.sleep(0.005) + return True + + def wait_closed(self, timeout: float = CLOSE_TIMEOUT) -> bool: + """Wait until the instrument is closed, after the window was closed. + + Args: + timeout: maximum waiting time (s) + + Returns: + True if the instrument is closed, False on timeout + """ + if self._close_future is None: + return False + done, _not_done = wait_futures([self._close_future], timeout) + return bool(done) + def child_title(self, item) -> str: """Supply the usual guidata title for nested editors.""" return f"{self.windowTitle()} - {item.label()}" @@ -150,9 +298,13 @@ def set_live(self, live: bool) -> None: self._live_timer.stop() def stop(self) -> None: - """Stop refreshing the view.""" + """Stop refreshing the view and close the instrument.""" self._refresh_timer.stop() self._live_timer.stop() + if self._close_future is None: + self._pending = None + self._close_future = self._executor.submit(self._call, "close") + self._executor.shutdown() def _apply_settings(self) -> bool: """Write the edited values into the instrument settings.""" @@ -164,13 +316,13 @@ def _apply_settings(self) -> bool: def refresh(self) -> None: """Show a live frame for the current settings.""" - if not self._apply_settings(): - return + self._request("preview") + + def _show_frame(self, frame: InstrumentFrame) -> None: + """Draw a frame returned by the instrument.""" try: - frame = self.instrument.preview() self.render(frame) except Exception as exc: # pylint: disable=broad-except - # Instruments are third-party code: keep the window usable self.status_label.setText(str(exc)) return self.status_label.clear() @@ -240,20 +392,15 @@ def render(self, frame: InstrumentFrame) -> None: def acquire(self) -> None: """Add an acquisition to the workspace, in a new group.""" - if not self._apply_settings(): - return - live = self._live_timer.isActive() - self._live_timer.stop() + self._request("acquire") + + def _add_acquired(self, acquisition: InstrumentAcquisition) -> None: + """Add the objects acquired by the instrument to the workspace.""" try: - acquisition = self.instrument.acquire() self.add_acquisition(acquisition) except Exception as exc: # pylint: disable=broad-except - # Instruments are third-party code: keep the window usable self.status_label.setText(str(exc)) return - finally: - if live: - self._live_timer.start() self.status_label.setText( _("%d objects added to group '%s'") % (len(acquisition.objects), acquisition.group_title) diff --git a/datalab/plugins/instruments.py b/datalab/plugins/instruments.py index 660391d9f..f487e86c0 100644 --- a/datalab/plugins/instruments.py +++ b/datalab/plugins/instruments.py @@ -94,6 +94,14 @@ class PluginInstrument(abc.ABC): keeps one instance per tool during the session, so settings are kept from one opening to the next. + DataLab calls :meth:`open` when the instrument window opens and + :meth:`close` when it closes (including when the plugin is reloaded or + DataLab quits). It calls these methods, :meth:`preview` and :meth:`acquire` + one at a time, outside the GUI thread, and writes :attr:`settings` only + between two calls: a slow instrument does not freeze DataLab. Data received + by the instrument in its own threads (e.g. device events) must be stored + for the next :meth:`preview` or :meth:`acquire` call. + Args: settings: instrument settings, edited by DataLab """ @@ -106,6 +114,20 @@ def __init__(self, settings: gds.DataSet) -> None: raise TypeError("Instrument settings must be a guidata DataSet") self.settings = settings + def open(self) -> None: + """Prepare the instrument when its window opens (e.g. connect to it). + + Raises ``ValueError`` with a user-facing message if the instrument + cannot be used. Does nothing by default. + """ + + def close(self) -> None: + """Release the instrument when its window closes (e.g. disconnect). + + Called even if :meth:`open` failed, and possibly several times: it must + release only what is still held. Does nothing by default. + """ + @abc.abstractmethod def preview(self) -> InstrumentFrame: """Return a live frame for the current settings.""" diff --git a/datalab/tests/features/plugins/instrument_window_unit_test.py b/datalab/tests/features/plugins/instrument_window_unit_test.py new file mode 100644 index 000000000..2711635ba --- /dev/null +++ b/datalab/tests/features/plugins/instrument_window_unit_test.py @@ -0,0 +1,108 @@ +# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file. + +""" +Instrument window unit test +--------------------------- + +Instrument calls run one at a time outside the GUI thread: a slow instrument +does not freeze DataLab, repeated refresh requests are merged, and closing the +window closes the instrument once its running call is done. +""" + +# guitest: show + +from __future__ import annotations + +import threading + +import guidata.dataset as gds +import numpy as np +from guidata.qthelpers import qt_app_context +from qtpy import QtWidgets as QW +from sigima.objects import create_signal + +from datalab.gui.plugins.instrument import InstrumentWindow +from datalab.plugins.instruments import ( + InstrumentAcquisition, + InstrumentFrame, + PluginInstrument, +) + + +class SlowSettings(gds.DataSet): + """Settings of the slow instrument.""" + + gain = gds.FloatItem("Gain", default=1.0) + + +class SlowInstrument(PluginInstrument): + """Instrument whose previews wait for the test to release them.""" + + def __init__(self) -> None: + super().__init__(SlowSettings()) + self.calls: list[str] = [] + self.running = threading.Event() + self.release = threading.Event() + + def open(self) -> None: + self.calls.append("open") + + def preview(self) -> InstrumentFrame: + self.calls.append("preview") + self.running.set() + assert self.release.wait(10.0) + x = np.linspace(0.0, 1.0, 5) + return InstrumentFrame((create_signal("Live", x, self.settings.gain * x),)) + + def close(self) -> None: + self.calls.append("close") + + def acquire(self) -> InstrumentAcquisition: + self.calls.append("acquire") + return InstrumentAcquisition("Slow", self.preview().objects) + + +def test_slow_instrument_calls_are_serialized() -> None: + """Refresh requests made during a slow preview are merged into one.""" + with qt_app_context(): + main = QW.QWidget() + instrument = SlowInstrument() + window = InstrumentWindow(main, instrument, "Slow instrument") + window.show() + while not instrument.running.is_set(): + QW.QApplication.processEvents() + assert not window.is_idle() + for _index in range(3): + window.refresh() + assert instrument.calls == ["open", "preview"] + instrument.release.set() + assert window.wait_until_idle() + assert instrument.calls == ["open", "preview", "preview"] + assert len(window.items) == 1 + window.close() + assert window.wait_closed() + assert instrument.calls[-1] == "close" + main.deleteLater() + + +def test_close_waits_for_running_call() -> None: + """The instrument is closed after its running call, and nothing follows.""" + with qt_app_context(): + main = QW.QWidget() + instrument = SlowInstrument() + window = InstrumentWindow(main, instrument, "Slow instrument") + window.show() + while not instrument.running.is_set(): + QW.QApplication.processEvents() + window.refresh() + window.close() + assert not window.wait_closed(0.2) + instrument.release.set() + assert window.wait_closed() + assert instrument.calls == ["open", "preview", "close"] + main.deleteLater() + + +if __name__ == "__main__": + test_slow_instrument_calls_are_serialized() + test_close_waits_for_running_call() diff --git a/datalab/tests/features/plugins/plugin_tools_app_test.py b/datalab/tests/features/plugins/plugin_tools_app_test.py index 79d9dd4cf..7e41d061e 100644 --- a/datalab/tests/features/plugins/plugin_tools_app_test.py +++ b/datalab/tests/features/plugins/plugin_tools_app_test.py @@ -53,6 +53,13 @@ class SignalGenerator(PluginInstrument): def __init__(self): super().__init__(GeneratorSettings()) self.frames = 0 + self.events = [] + + def open(self): + self.events.append("open") + + def close(self): + self.events.append("close") def preview(self): self.frames += 1 @@ -164,6 +171,9 @@ def test_plugin_tools_in_menus_and_instrument_window( assert isinstance(window, InstrumentWindow) assert plugin.launch_tool("generator") is window window.refresh() + assert window.wait_until_idle() + instrument = plugin.get_instrument("generator") + assert instrument.events == ["open"] assert len(window.items) == 1 assert window.summary_label.text().startswith("Frame") assert window.plotwidget.plot.get_axis_limits("left") == (-2.0, 2.0) @@ -188,17 +198,17 @@ def test_plugin_tools_in_menus_and_instrument_window( assert window._live_timer.isActive() # pylint: disable=protected-access window.live_button.setChecked(False) - instrument = plugin.get_instrument("generator") - def failing_preview(): raise ValueError("Amplitude out of range") monkeypatch.setattr(instrument, "preview", failing_preview) window.refresh() + assert window.wait_until_idle() assert window.status_label.text() == "Amplitude out of range" monkeypatch.undo() window.acquire() + assert window.wait_until_idle() groups = win.signalpanel.objmodel.get_groups() assert groups[-1].title == "Generator acquisition" assert len(groups[-1]) == 3 @@ -210,6 +220,7 @@ def failing_preview(): win.reload_plugins() QW.QApplication.processEvents() assert closed + assert instrument.events == ["open", "close"] reloaded = PluginRegistry.get_plugin("org.example.tools-app") assert reloaded.launch_tool("generator") is not window win.close_plugin_instruments() From cc56a7e0b611380e2fe0c9b1bc25e80e5719eba7 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Wed, 21 Oct 2026 07:09:34 +0200 Subject: [PATCH 07/22] docs: instrument open/close and threading Assisted-by: Claude Opus 5.5 --- doc/features/advanced/plugins.rst | 2 ++ doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po | 3 +++ doc/release_notes/release_1.04.md | 1 + 3 files changed, 6 insertions(+) diff --git a/doc/features/advanced/plugins.rst b/doc/features/advanced/plugins.rst index cb02fdc0c..9f647fc96 100644 --- a/doc/features/advanced/plugins.rst +++ b/doc/features/advanced/plugins.rst @@ -585,6 +585,8 @@ DataLab edits :attr:`~datalab.plugins.instruments.PluginInstrument.settings` in DataLab creates the instrument once per tool and keeps it until the plugins are reloaded, so the settings remain from one opening to the next. +DataLab Desktop calls the instrument outside the user interface thread, one call at a time, and changes its settings only between two calls: a slow instrument, such as a device waiting for its data, does not freeze DataLab. The optional :meth:`~datalab.plugins.instruments.PluginInstrument.open` and :meth:`~datalab.plugins.instruments.PluginInstrument.close` methods are called when the window opens and closes, including when plugins are reloaded or DataLab quits, for example to connect to a device and to release it. ``close`` is called even if ``open`` failed, and must release only what is still held. Data received by the instrument in its own threads, such as device events, is kept by the instrument for its next ``preview`` or ``acquire`` call. + DataLab-Web supports tools too: it lists them in the same places, opens instruments in the same window, and calls launchers in the plugin's Pyodide host. Welcome page tiles diff --git a/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po b/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po index d0a80f388..f31ebf734 100644 --- a/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po +++ b/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po @@ -406,6 +406,9 @@ msgstr "DataLab modifie :attr:`~datalab.plugins.instruments.PluginInstrument.set msgid "DataLab creates the instrument once per tool and keeps it until the plugins are reloaded, so the settings remain from one opening to the next." msgstr "DataLab crĂ©e l'instrument une seule fois par outil et le conserve jusqu'au rechargement des plugins : les rĂ©glages sont ainsi conservĂ©s d'une ouverture Ă  l'autre." +msgid "DataLab Desktop calls the instrument outside the user interface thread, one call at a time, and changes its settings only between two calls: a slow instrument, such as a device waiting for its data, does not freeze DataLab. The optional :meth:`~datalab.plugins.instruments.PluginInstrument.open` and :meth:`~datalab.plugins.instruments.PluginInstrument.close` methods are called when the window opens and closes, including when plugins are reloaded or DataLab quits, for example to connect to a device and to release it. ``close`` is called even if ``open`` failed, and must release only what is still held. Data received by the instrument in its own threads, such as device events, is kept by the instrument for its next ``preview`` or ``acquire`` call." +msgstr "DataLab Desktop appelle l'instrument en dehors du fil d'exĂ©cution de l'interface utilisateur, un appel Ă  la fois, et ne modifie ses rĂ©glages qu'entre deux appels : un instrument lent, comme un appareil attendant ses donnĂ©es, ne fige pas DataLab. Les mĂ©thodes facultatives :meth:`~datalab.plugins.instruments.PluginInstrument.open` et :meth:`~datalab.plugins.instruments.PluginInstrument.close` sont appelĂ©es Ă  l'ouverture et Ă  la fermeture de la fenĂȘtre, y compris lors du rechargement des plugins ou de la fermeture de DataLab, par exemple pour se connecter Ă  un appareil puis le libĂ©rer. ``close`` est appelĂ©e mĂȘme si ``open`` a Ă©chouĂ©, et ne doit libĂ©rer que ce qui est encore dĂ©tenu. Les donnĂ©es reçues par l'instrument dans ses propres fils d'exĂ©cution, comme les Ă©vĂ©nements d'un appareil, sont conservĂ©es par l'instrument jusqu'Ă  son prochain appel de ``preview`` ou ``acquire``." + msgid "DataLab-Web supports tools too: it lists them in the same places, opens instruments in the same window, and calls launchers in the plugin's Pyodide host." msgstr "DataLab-Web prend aussi en charge les outils : il les liste aux mĂȘmes endroits, ouvre les instruments dans la mĂȘme fenĂȘtre et appelle les lanceurs dans l'hĂŽte Pyodide du plugin." diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index 902dbf6fb..562e3a24b 100644 --- a/doc/release_notes/release_1.04.md +++ b/doc/release_notes/release_1.04.md @@ -32,6 +32,7 @@ * Application plugins may also list tools, such as wizards or editors, in the catalog * Plugin tools also appear in the plugin's submenu of the **Plugins** menu, in the Signal or Image panel they work on. A tool that needs a selection is disabled until the right objects are selected, and its catalog button tells what to select * Application plugins may provide instruments, such as simulated cameras or oscilloscopes, without writing any user interface: DataLab shows a window with a live view and the instrument settings, refreshed as settings change or continuously in **Live** mode, and **Acquire** adds the acquired objects to a new group, ready for the application's methods +* Instruments are called outside the user interface thread, one call at a time: a slow instrument, such as a real device waiting for its data, no longer freezes DataLab, and repeated refresh requests made meanwhile are merged. Instruments may also declare `open` and `close` methods, called when their window opens and closes (including when plugins are reloaded or DataLab quits), for example to connect to a device and to release it * Plugin developers describe recipe inputs declaratively (titles, minimum counts, metadata requirements) and may add binding suggestions and fast input checks; every recipe then runs through DataLab's generic launcher, without plugin-specific input dialogs * The catalog is non-modal and remains open after starting a method or opening an example, so users may continue interacting with the DataLab workspace * The methods, tools and datasets of an application form an accordion: one section is open at a time, and a colored dot tells, without opening it, whether each method can run on the current selection From 85091b248c57a50f50c91c25762f0175513c3536 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:14:29 +0200 Subject: [PATCH 08/22] docs: release notes for HDF5 data mappings Assisted-by: Claude Opus 5.5 --- doc/release_notes/release_1.04.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index 562e3a24b..893b75e46 100644 --- a/doc/release_notes/release_1.04.md +++ b/doc/release_notes/release_1.04.md @@ -114,6 +114,7 @@ DataLab now uses stable UUID-based object references across object trees, result * NeXus `NXdata` groups are shown as single items in the HDF5 browser and imported as signals or images with their axes, units, labels and, for signals, uncertainties. Bin edges are converted to bin centers, and non-uniform image axes are kept. Invalid axes are reported in the browser instead of being guessed * Imported signals and images record where they come from (file, group, signal and axes, applied conversions) in their `experimental_source` metadata, which is saved with the workspace: a saved workspace reopens without the NeXus file * `.nxs` and `.nx5` files are proposed when opening HDF5 files +* Plugins can describe the signals and images of files that NeXus `NXdata` groups designate incompletely (no axes, several counters...) with declarative data mappings (JSON) registered with `sigimax.h5.mapping.register_mapping`: the HDF5 browser then shows each described object as an extra item, imported with its axes, labels and units ### 🔄 Changes ### From c41916c662be775e72810b89c8862216ebb7f0b3 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:17:41 +0200 Subject: [PATCH 09/22] docs: French translation of HDF5, NeXus and instrument release notes --- .../LC_MESSAGES/release_notes/release_1.04.po | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po index e784a9f87..a766b0539 100644 --- a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po +++ b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po @@ -84,6 +84,9 @@ msgstr "Les outils de plugin apparaissent aussi dans le sous-menu du plugin du m msgid "Application plugins may provide instruments, such as simulated cameras or oscilloscopes, without writing any user interface: DataLab shows a window with a live view and the instrument settings, refreshed as settings change or continuously in **Live** mode, and **Acquire** adds the acquired objects to a new group, ready for the application's methods" msgstr "Les plugins d'application peuvent fournir des instruments, comme des camĂ©ras ou des oscilloscopes simulĂ©s, sans Ă©crire d'interface utilisateur : DataLab affiche une fenĂȘtre avec une vue en direct et les rĂ©glages de l'instrument, rafraĂźchie Ă  chaque modification des rĂ©glages ou en continu en mode **En direct**, et **AcquĂ©rir** ajoute les objets acquis dans un nouveau groupe, prĂȘts pour les mĂ©thodes de l'application" +msgid "Instruments are called outside the user interface thread, one call at a time: a slow instrument, such as a real device waiting for its data, no longer freezes DataLab, and repeated refresh requests made meanwhile are merged. Instruments may also declare `open` and `close` methods, called when their window opens and closes (including when plugins are reloaded or DataLab quits), for example to connect to a device and to release it" +msgstr "Les instruments sont appelĂ©s en dehors du fil d'exĂ©cution de l'interface utilisateur, un appel Ă  la fois : un instrument lent, comme un appareil rĂ©el attendant ses donnĂ©es, ne fige plus DataLab, et les demandes de rafraĂźchissement rĂ©pĂ©tĂ©es entre-temps sont regroupĂ©es. Les instruments peuvent aussi dĂ©clarer des mĂ©thodes `open` et `close`, appelĂ©es Ă  l'ouverture et Ă  la fermeture de leur fenĂȘtre (y compris lors du rechargement des plugins ou de la fermeture de DataLab), par exemple pour se connecter Ă  un appareil puis le libĂ©rer" + msgid "Plugin developers describe recipe inputs declaratively (titles, minimum counts, metadata requirements) and may add binding suggestions and fast input checks; every recipe then runs through DataLab's generic launcher, without plugin-specific input dialogs" msgstr "Les dĂ©veloppeurs de plugins dĂ©crivent les entrĂ©es des recettes de maniĂšre dĂ©clarative (titres, effectifs minimaux, mĂ©tadonnĂ©es requises) et peuvent ajouter des suggestions d'affectation et des vĂ©rifications rapides des entrĂ©es ; chaque recette s'exĂ©cute alors via le lanceur gĂ©nĂ©rique de DataLab, sans boĂźte de dialogue d'entrĂ©es propre au plugin" @@ -258,6 +261,27 @@ msgstr "Les coefficients des ajustements polynomial et linĂ©aire sont nommĂ©s `a msgid "The *Fitting* documentation explains how to derive the peak wavelength, the temperature and the emissivity from the parameters of a Planckian fit" msgstr "La documentation *Ajustement* explique comment dĂ©duire la longueur d'onde du pic, la tempĂ©rature et l'Ă©missivitĂ© des paramĂštres d'un ajustement planckien" +msgid "**Remote control and plugins:**" +msgstr "**ContrĂŽle Ă  distance et plugins :**" + +msgid "`add_group` now returns the UUID of the created group, so that a remote client, a macro or a plugin can add objects to it with `add_object(obj, group_id=...)` (or `add_signal`/`add_image`) instead of looking the group up by its title" +msgstr "`add_group` renvoie dĂ©sormais l'UUID du groupe créé, de sorte qu'un client distant, une macro ou un plugin peut y ajouter des objets avec `add_object(obj, group_id=...)` (ou `add_signal`/`add_image`) au lieu de rechercher le groupe par son titre" + +msgid "**NeXus files:**" +msgstr "**Fichiers NeXus :**" + +msgid "NeXus `NXdata` groups are shown as single items in the HDF5 browser and imported as signals or images with their axes, units, labels and, for signals, uncertainties. Bin edges are converted to bin centers, and non-uniform image axes are kept. Invalid axes are reported in the browser instead of being guessed" +msgstr "Les groupes NeXus `NXdata` sont prĂ©sentĂ©s comme des Ă©lĂ©ments uniques dans l'explorateur HDF5 et importĂ©s comme signaux ou images avec leurs axes, unitĂ©s, libellĂ©s et, pour les signaux, incertitudes. Les bords de classes sont convertis en centres de classes, et les axes d'image non uniformes sont conservĂ©s. Les axes invalides sont signalĂ©s dans l'explorateur au lieu d'ĂȘtre devinĂ©s" + +msgid "Imported signals and images record where they come from (file, group, signal and axes, applied conversions) in their `experimental_source` metadata, which is saved with the workspace: a saved workspace reopens without the NeXus file" +msgstr "Les signaux et images importĂ©s enregistrent leur origine (fichier, groupe, signal et axes, conversions appliquĂ©es) dans leur mĂ©tadonnĂ©e `experimental_source`, enregistrĂ©e avec l'espace de travail : un espace de travail enregistrĂ© se rouvre sans le fichier NeXus" + +msgid "`.nxs` and `.nx5` files are proposed when opening HDF5 files" +msgstr "Les fichiers `.nxs` et `.nx5` sont proposĂ©s Ă  l'ouverture de fichiers HDF5" + +msgid "Plugins can describe the signals and images of files that NeXus `NXdata` groups designate incompletely (no axes, several counters...) with declarative data mappings (JSON) registered with `sigimax.h5.mapping.register_mapping`: the HDF5 browser then shows each described object as an extra item, imported with its axes, labels and units" +msgstr "Les plugins peuvent dĂ©crire les signaux et images des fichiers que les groupes NeXus `NXdata` dĂ©signent de maniĂšre incomplĂšte (sans axes, plusieurs compteurs...) avec des profils de correspondance dĂ©claratifs (JSON) enregistrĂ©s avec `sigimax.h5.mapping.register_mapping` : l'explorateur HDF5 prĂ©sente alors chaque objet dĂ©crit comme un Ă©lĂ©ment supplĂ©mentaire, importĂ© avec ses axes, libellĂ©s et unitĂ©s" + msgid "🔄 Changes" msgstr "🔄 Changements" @@ -300,6 +324,21 @@ msgstr "Les identifiants d'annotation, l'Ă©tat de verrouillage, les mĂ©tadonnĂ©e msgid "DataLab now requires Sigima ≄ 1.3.0, SigimaX ≄ 1.1.0 and DataLab-Capsule 0.1.x" msgstr "DataLab nĂ©cessite dĂ©sormais Sigima ≄ 1.3.0, SigimaX ≄ 1.1.0 et DataLab-Capsule 0.1.x" +msgid "**HDF5 browser:**" +msgstr "**Explorateur HDF5 :**" + +msgid "Browsing an HDF5 file no longer reads its datasets entirely: they are listed and described from their shape and data type, and the value previews read only their first elements. Large files open faster and use less memory; data is read when a signal or an image is previewed or imported" +msgstr "Parcourir un fichier HDF5 ne lit plus ses jeux de donnĂ©es en entier : ils sont listĂ©s et dĂ©crits Ă  partir de leur forme et de leur type de donnĂ©es, et les aperçus de valeurs ne lisent que leurs premiers Ă©lĂ©ments. Les gros fichiers s'ouvrent plus vite et utilisent moins de mĂ©moire ; les donnĂ©es sont lues lorsqu'un signal ou une image est prĂ©visualisĂ© ou importĂ©" + +msgid "The HDF5 browser stays responsive while a preview is read, and previews datasets of more than 16 million values with a step along each axis" +msgstr "L'explorateur HDF5 reste rĂ©actif pendant la lecture d'un aperçu, et prĂ©visualise les jeux de donnĂ©es de plus de 16 millions de valeurs avec un pas le long de chaque axe" + +msgid "Datasets with three dimensions or more (image stacks, spectral cubes...) are now listed in the HDF5 browser" +msgstr "Les jeux de donnĂ©es Ă  trois dimensions ou plus (piles d'images, cubes spectraux...) sont dĂ©sormais listĂ©s dans l'explorateur HDF5" + +msgid "Dataset attributes with more than 10 000 values are no longer copied to the metadata of imported objects" +msgstr "Les attributs de jeux de donnĂ©es de plus de 10 000 valeurs ne sont plus copiĂ©s dans les mĂ©tadonnĂ©es des objets importĂ©s" + msgid "đŸ› ïž Bug Fixes" msgstr "đŸ› ïž Corrections de bugs" @@ -321,6 +360,12 @@ msgstr "**Disposition de la fenĂȘtre :**" msgid "Fixed the width of the signal/image panels not being restored at startup: after narrowing the panels and restarting DataLab, they came back wider (up to their maximum width) instead of keeping the width chosen by the user" msgstr "Correction de la largeur des panneaux Signal/Image non restaurĂ©e au dĂ©marrage : aprĂšs avoir rĂ©duit les panneaux puis redĂ©marrĂ© DataLab, ils rĂ©apparaissaient plus larges (jusqu'Ă  leur largeur maximale) au lieu de conserver la largeur choisie par l'utilisateur" +msgid "**HDF5 import:**" +msgstr "**Import HDF5 :**" + +msgid "Fixed images with unsigned 32-bit values above 2 147 483 647 being clipped when imported from an HDF5 file (\"Clipping uint32 data to int32\" warning): such images are now imported as 64-bit floating-point images keeping every value exactly, as are other integer images that no supported integer type can hold, which were rounded to 32-bit floating-point values beyond 16 777 216 (requires Sigima ≄ 1.4.0)" +msgstr "Correction de l'Ă©crĂȘtage des images aux valeurs entiĂšres non signĂ©es 32 bits supĂ©rieures Ă  2 147 483 647 lors de leur import depuis un fichier HDF5 (avertissement \"Clipping uint32 data to int32\") : ces images sont dĂ©sormais importĂ©es comme images en virgule flottante 64 bits conservant exactement chaque valeur, comme les autres images entiĂšres qu'aucun type entier pris en charge ne peut contenir, qui Ă©taient arrondies en virgule flottante 32 bits au-delĂ  de 16 777 216 (nĂ©cessite Sigima ≄ 1.4.0)" + msgid "**Metadata:**" msgstr "**MĂ©tadonnĂ©es :**" @@ -392,4 +437,3 @@ msgstr "**SĂ©curitĂ© :**" msgid "Opening a workspace received from someone else can no longer make DataLab load or run other code installed on the computer. The parameters stored with objects, analysis results and History actions name their class, and DataLab imported whatever module they named and called the object named there, which a modified file could use, for example, to make DataLab quit. Only parameter classes of DataLab, Sigima, guidata and installed plugins are now used; other stored parameters are reported as unreadable (fixes [Issue #407](https://github.com/DataLab-Platform/DataLab/issues/407))" msgstr "Ouvrir un espace de travail reçu d'une autre personne ne permet plus de faire charger ou exĂ©cuter par DataLab un autre code installĂ© sur l'ordinateur. Les paramĂštres enregistrĂ©s avec les objets, les rĂ©sultats d'analyse et les actions de l'historique nomment leur classe, et DataLab importait le module ainsi nommĂ© puis appelait l'objet dĂ©signĂ©, ce qu'un fichier modifiĂ© pouvait exploiter, par exemple pour faire quitter DataLab. Seules les classes de paramĂštres de DataLab, Sigima, guidata et des plugins installĂ©s sont dĂ©sormais utilisĂ©es ; les autres paramĂštres enregistrĂ©s sont signalĂ©s comme illisibles (corrige [l'issue #407](https://github.com/DataLab-Platform/DataLab/issues/407))" - From a1d3074e52277517a6bec7b52c3d037036cac994 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:40:37 +0200 Subject: [PATCH 10/22] docs: complete experimental data integration guidance Document NeXus import, experimental origin and preview limits, clarify instrument connector responsibilities, update French translations and require Sigima 1.4. Assisted-by: GPT-6.1 Sol --- doc/features/advanced/plugins.rst | 2 + doc/features/common/h5browser.rst | 65 +++++++++++++++++++ .../LC_MESSAGES/features/advanced/plugins.po | 18 +++++ .../LC_MESSAGES/features/common/h5browser.po | 47 +++++++++++++- .../LC_MESSAGES/release_notes/release_1.04.po | 19 +++--- doc/locale/fr/LC_MESSAGES/requirements.po | 19 ++++-- doc/release_notes/release_1.04.md | 10 +-- doc/requirements.rst | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 10 files changed, 164 insertions(+), 22 deletions(-) diff --git a/doc/features/advanced/plugins.rst b/doc/features/advanced/plugins.rst index efce522d1..2db631a2a 100644 --- a/doc/features/advanced/plugins.rst +++ b/doc/features/advanced/plugins.rst @@ -720,6 +720,8 @@ DataLab creates the instrument once per tool and keeps it until the plugins are DataLab Desktop calls the instrument outside the user interface thread, one call at a time, and changes its settings only between two calls: a slow instrument, such as a device waiting for its data, does not freeze DataLab. The optional :meth:`~datalab.plugins.instruments.PluginInstrument.open` and :meth:`~datalab.plugins.instruments.PluginInstrument.close` methods are called when the window opens and closes, including when plugins are reloaded or DataLab quits, for example to connect to a device and to release it. ``close`` is called even if ``open`` failed, and must release only what is still held. Data received by the instrument in its own threads, such as device events, is kept by the instrument for its next ``preview`` or ``acquire`` call. +Instrument connectors should attach an ``experimental_source`` metadata entry to acquired objects before returning them; DataLab does not fill it automatically. Use Sigima's ``make_experimental_source`` and ``set_experimental_source`` helpers to record the device address, attribute, selection and timestamps as appropriate. This identifies the measurement origin, not a reproducible acquisition. See the `Sigima experimental data guide `_ for an example and the portable metadata format. + DataLab-Web supports tools too: it lists them in the same places, opens instruments in the same window, and calls launchers in the plugin's Pyodide host. Welcome page tiles diff --git a/doc/features/common/h5browser.rst b/doc/features/common/h5browser.rst index 4b3d16167..144f318d0 100644 --- a/doc/features/common/h5browser.rst +++ b/doc/features/common/h5browser.rst @@ -28,3 +28,68 @@ The HDF5 browser is fairly simple to use: .. note:: The HDF5 browser may be used to explore multiple HDF5 files at once, thus allowing to import data from different files into the same DataLab workspace. + +NeXus files and physical axes +---------------------------- + +Open ``.nxs`` and ``.nx5`` files with **Browse HDF5 file**, just like ``.h5`` +files. A NeXus ``NXdata`` group designates a measured dataset and its axes. +Select the group to import the complete scientific object, rather than selecting +its individual datasets: a 1D group produces a signal, and a 2D group an image. +The raw datasets remain available separately in the tree. + +The import preserves axis values, labels and units, including non-uniform image +coordinates. Signal Y uncertainties are imported when supplied; image +uncertainties are not imported. Histogram axes supplied as bin edges are +converted to bin centers, and this conversion is recorded. + +An axis with an invalid shape or length is reported in the group description +and prevents importing the group. An undeclared axis uses point or pixel +indices; the importer does not infer a physical axis from neighbouring datasets. + +Files with several counters or incomplete NeXus descriptions can be interpreted +by a plugin-provided data mapping. Each mapped object appears as an additional +item with its own axes, labels and units. Mappings must be registered before +opening the file; DataLab does not automatically discover JSON files beside it. +Plugin authors can follow the +`SigimaX HDF5 import guide `_. + +Large datasets and previews +-------------------------- + +Listing numeric datasets uses their shape and type without loading all their +values. Textual value previews read only the first elements, and attributes +containing more than 10 000 values are not copied to imported object metadata. +Graphical previews are read in a background thread; only the preview of the +last selected item is displayed. + +Large ordinary 1D arrays and 2D images are previewed with a sampling step above +``2**24`` elements (about 16.8 million). Import still reads every value, not the +sampled preview. This is not a universal memory limit: NeXus and mapped objects, +compound datasets and signals stored as 2D tables still use complete objects +for their graphical previews. Closing a file waits for its preview read to end. + +Datasets with three or more dimensions, such as image stacks and spectral +cubes, are listed but cannot be imported as complete objects. There is no +graphical slice selector yet. Developers can read 1D or 2D views without loading +the whole cube using the selection API described in the SigimaX guide. + +Numerical values and experimental origin +--------------------------------------- + +Unsigned 32-bit detector counts above 2 147 483 647 are imported as ``float64`` +images without clipping or rounding. Other integer arrays are also converted +to an exact supported type when possible. If conversion to floating point +cannot preserve every integer (in particular beyond ``+/-2**53`` for ``float64``), +Sigima emits a warning instead of silently promising exact values. + +NeXus and mapped objects carry an ``experimental_source`` metadata entry +describing the source file, group, measured dataset, axes and recorded +conversions. Mapped objects also identify their mapping. This entry describes +where the measurement came from; it is not the processing provenance shown in +the Properties panel and does not make the experiment reproducible. + +Save the imported objects in a DataLab workspace to keep both their data and +this metadata. Reopening that workspace does not require the original NeXus +file or the mapping plugin. Generic raw dataset imports do not automatically +receive this structured experimental source. diff --git a/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po b/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po index 1b5508444..55254c426 100644 --- a/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po +++ b/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po @@ -558,6 +558,9 @@ msgstr "DataLab crĂ©e l'instrument une seule fois par outil et le conserve jusqu msgid "DataLab Desktop calls the instrument outside the user interface thread, one call at a time, and changes its settings only between two calls: a slow instrument, such as a device waiting for its data, does not freeze DataLab. The optional :meth:`~datalab.plugins.instruments.PluginInstrument.open` and :meth:`~datalab.plugins.instruments.PluginInstrument.close` methods are called when the window opens and closes, including when plugins are reloaded or DataLab quits, for example to connect to a device and to release it. ``close`` is called even if ``open`` failed, and must release only what is still held. Data received by the instrument in its own threads, such as device events, is kept by the instrument for its next ``preview`` or ``acquire`` call." msgstr "DataLab Desktop appelle l'instrument en dehors du fil d'exĂ©cution de l'interface utilisateur, un appel Ă  la fois, et ne modifie ses rĂ©glages qu'entre deux appels : un instrument lent, comme un appareil attendant ses donnĂ©es, ne fige pas DataLab. Les mĂ©thodes facultatives :meth:`~datalab.plugins.instruments.PluginInstrument.open` et :meth:`~datalab.plugins.instruments.PluginInstrument.close` sont appelĂ©es Ă  l'ouverture et Ă  la fermeture de la fenĂȘtre, y compris lors du rechargement des plugins ou de la fermeture de DataLab, par exemple pour se connecter Ă  un appareil puis le libĂ©rer. ``close`` est appelĂ©e mĂȘme si ``open`` a Ă©chouĂ©, et ne doit libĂ©rer que ce qui est encore dĂ©tenu. Les donnĂ©es reçues par l'instrument dans ses propres fils d'exĂ©cution, comme les Ă©vĂ©nements d'un appareil, sont conservĂ©es par l'instrument jusqu'Ă  son prochain appel de ``preview`` ou ``acquire``." +msgid "Instrument connectors should attach an ``experimental_source`` metadata entry to acquired objects before returning them; DataLab does not fill it automatically. Use Sigima's ``make_experimental_source`` and ``set_experimental_source`` helpers to record the device address, attribute, selection and timestamps as appropriate. This identifies the measurement origin, not a reproducible acquisition. See the `Sigima experimental data guide `_ for an example and the portable metadata format." +msgstr "Les connecteurs d'instruments doivent attacher une entrĂ©e de mĂ©tadonnĂ©es ``experimental_source`` aux objets acquis avant de les renvoyer ; DataLab ne la remplit pas automatiquement. Utilisez les fonctions ``make_experimental_source`` et ``set_experimental_source`` de Sigima pour enregistrer, selon le cas, l'adresse de l'appareil, l'attribut, la sĂ©lection et les horodatages. Cela identifie l'origine de la mesure, pas une acquisition reproductible. Le `guide des donnĂ©es expĂ©rimentales de Sigima `_ fournit un exemple et dĂ©crit le format portable des mĂ©tadonnĂ©es." + msgid "DataLab-Web supports tools too: it lists them in the same places, opens instruments in the same window, and calls launchers in the plugin's Pyodide host." msgstr "DataLab-Web prend aussi en charge les outils : il les liste aux mĂȘmes endroits, ouvre les instruments dans la mĂȘme fenĂȘtre et appelle les lanceurs dans l'hĂŽte Pyodide du plugin." @@ -1504,12 +1507,27 @@ msgstr "Instrument ouvert par un outil de plugin." msgid "DataLab writes the edited values into :attr:`settings` before calling :meth:`preview` or :meth:`acquire`. Both methods raise ``ValueError`` with a user-facing message when the settings cannot be used. The plugin keeps one instance per tool during the session, so settings are kept from one opening to the next." msgstr "DataLab Ă©crit les valeurs modifiĂ©es dans :attr:`settings` avant d'appeler :meth:`preview` ou :meth:`acquire`. Les deux mĂ©thodes lĂšvent ``ValueError`` avec un message destinĂ© Ă  l'utilisateur lorsque les rĂ©glages ne peuvent pas ĂȘtre utilisĂ©s. Le plugin conserve une instance par outil pendant la session : les rĂ©glages sont ainsi conservĂ©s d'une ouverture Ă  l'autre." +msgid "DataLab calls :meth:`open` when the instrument window opens and :meth:`close` when it closes (including when the plugin is reloaded or DataLab quits). It calls these methods, :meth:`preview` and :meth:`acquire` one at a time, outside the GUI thread, and writes :attr:`settings` only between two calls: a slow instrument does not freeze DataLab. Data received by the instrument in its own threads (e.g. device events) must be stored for the next :meth:`preview` or :meth:`acquire` call." +msgstr "DataLab appelle :meth:`open` Ă  l'ouverture de la fenĂȘtre de l'instrument et :meth:`close` Ă  sa fermeture (y compris lorsque le plugin est rechargĂ© ou que DataLab se ferme). Il appelle ces mĂ©thodes, :meth:`preview` et :meth:`acquire` une Ă  la fois, en dehors du fil d'exĂ©cution de l'interface graphique, et ne modifie :attr:`settings` qu'entre deux appels : un instrument lent ne fige pas DataLab. Les donnĂ©es reçues par l'instrument dans ses propres fils d'exĂ©cution (par exemple les Ă©vĂ©nements d'un appareil) doivent ĂȘtre conservĂ©es jusqu'au prochain appel de :meth:`preview` ou :meth:`acquire`." + msgid "instrument settings, edited by DataLab" msgstr "rĂ©glages de l'instrument, modifiĂ©s par DataLab" msgid "Interval between two frames in live mode, in milliseconds" msgstr "Intervalle entre deux images en mode direct, en millisecondes" +msgid "Prepare the instrument when its window opens (e.g. connect to it)." +msgstr "PrĂ©parer l'instrument Ă  l'ouverture de sa fenĂȘtre (par exemple s'y connecter)." + +msgid "Raises ``ValueError`` with a user-facing message if the instrument cannot be used. Does nothing by default." +msgstr "LĂšve ``ValueError`` avec un message destinĂ© Ă  l'utilisateur si l'instrument ne peut pas ĂȘtre utilisĂ©. Ne fait rien par dĂ©faut." + +msgid "Release the instrument when its window closes (e.g. disconnect)." +msgstr "LibĂ©rer l'instrument Ă  la fermeture de sa fenĂȘtre (par exemple s'en dĂ©connecter)." + +msgid "Called even if :meth:`open` failed, and possibly several times: it must release only what is still held. Does nothing by default." +msgstr "AppelĂ©e mĂȘme si :meth:`open` a Ă©chouĂ©, et Ă©ventuellement plusieurs fois : elle ne doit libĂ©rer que ce qui est encore dĂ©tenu. Ne fait rien par dĂ©faut." + msgid "Return a live frame for the current settings." msgstr "Renvoie une image en direct pour les rĂ©glages courants." diff --git a/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po b/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po index e61715288..a6dd86e18 100644 --- a/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po +++ b/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po @@ -2,10 +2,16 @@ #, fuzzy msgid "" msgstr "" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2026-10-22 21:34+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" msgid "HDF5 Browser in DataLab, the open-source scientific data analysis and visualization platform" msgstr "Explorateur HDF5 dans DataLab, la plateforme open-source d'analyse et de visualisation de donnĂ©es scientifiques" @@ -42,3 +48,42 @@ msgstr "Valider ensuite en cliquant sur \"OK\"" msgid "The HDF5 browser may be used to explore multiple HDF5 files at once, thus allowing to import data from different files into the same DataLab workspace." msgstr "L'explorateur HDF5 peut ĂȘtre utilisĂ© pour explorer plusieurs fichiers HDF5 Ă  la fois, permettant ainsi d'importer des donnĂ©es de diffĂ©rents fichiers dans le mĂȘme espace de travail de DataLab." + +msgid "NeXus files and physical axes" +msgstr "Fichiers NeXus et axes physiques" + +msgid "Open ``.nxs`` and ``.nx5`` files with **Browse HDF5 file**, just like ``.h5`` files. A NeXus ``NXdata`` group designates a measured dataset and its axes. Select the group to import the complete scientific object, rather than selecting its individual datasets: a 1D group produces a signal, and a 2D group an image. The raw datasets remain available separately in the tree." +msgstr "Ouvrez les fichiers ``.nxs`` et ``.nx5`` avec **Parcourir un fichier HDF5**, comme les fichiers ``.h5``. Un groupe NeXus ``NXdata`` dĂ©signe un jeu de donnĂ©es mesurĂ©es et ses axes. SĂ©lectionnez le groupe pour importer l'objet scientifique complet plutĂŽt que ses jeux de donnĂ©es individuels : un groupe 1D produit un signal, et un groupe 2D une image. Les jeux de donnĂ©es bruts restent accessibles sĂ©parĂ©ment dans l'arbre." + +msgid "The import preserves axis values, labels and units, including non-uniform image coordinates. Signal Y uncertainties are imported when supplied; image uncertainties are not imported. Histogram axes supplied as bin edges are converted to bin centers, and this conversion is recorded." +msgstr "L'import conserve les valeurs, libellĂ©s et unitĂ©s des axes, y compris les coordonnĂ©es d'image non uniformes. Les incertitudes Y des signaux sont importĂ©es lorsqu'elles sont fournies ; les incertitudes des images ne le sont pas. Les axes d'histogramme fournis sous forme de bords de classes sont convertis en centres de classes, et cette conversion est enregistrĂ©e." + +msgid "An axis with an invalid shape or length is reported in the group description and prevents importing the group. An undeclared axis uses point or pixel indices; the importer does not infer a physical axis from neighbouring datasets." +msgstr "Un axe dont la forme ou la longueur est invalide est signalĂ© dans la description du groupe et empĂȘche son import. Un axe non dĂ©clarĂ© utilise les indices des points ou pixels ; l'importeur ne dĂ©duit pas d'axe physique Ă  partir des jeux de donnĂ©es voisins." + +msgid "Files with several counters or incomplete NeXus descriptions can be interpreted by a plugin-provided data mapping. Each mapped object appears as an additional item with its own axes, labels and units. Mappings must be registered before opening the file; DataLab does not automatically discover JSON files beside it. Plugin authors can follow the `SigimaX HDF5 import guide `_." +msgstr "Les fichiers contenant plusieurs compteurs ou des descriptions NeXus incomplĂštes peuvent ĂȘtre interprĂ©tĂ©s par un profil de correspondance fourni par un plugin. Chaque objet dĂ©crit apparaĂźt comme un Ă©lĂ©ment supplĂ©mentaire avec ses propres axes, libellĂ©s et unitĂ©s. Les profils doivent ĂȘtre enregistrĂ©s avant l'ouverture du fichier ; DataLab ne dĂ©couvre pas automatiquement les fichiers JSON placĂ©s Ă  cĂŽtĂ©. Les auteurs de plugins peuvent suivre le `guide d'import HDF5 de SigimaX `_." + +msgid "Large datasets and previews" +msgstr "Grands jeux de donnĂ©es et aperçus" + +msgid "Listing numeric datasets uses their shape and type without loading all their values. Textual value previews read only the first elements, and attributes containing more than 10 000 values are not copied to imported object metadata. Graphical previews are read in a background thread; only the preview of the last selected item is displayed." +msgstr "La liste des jeux de donnĂ©es numĂ©riques utilise leur forme et leur type sans charger toutes leurs valeurs. Les aperçus textuels ne lisent que les premiers Ă©lĂ©ments, et les attributs contenant plus de 10 000 valeurs ne sont pas copiĂ©s dans les mĂ©tadonnĂ©es des objets importĂ©s. Les aperçus graphiques sont lus dans un fil d'exĂ©cution en arriĂšre-plan ; seul l'aperçu du dernier Ă©lĂ©ment sĂ©lectionnĂ© est affichĂ©." + +msgid "Large ordinary 1D arrays and 2D images are previewed with a sampling step above ``2**24`` elements (about 16.8 million). Import still reads every value, not the sampled preview. This is not a universal memory limit: NeXus and mapped objects, compound datasets and signals stored as 2D tables still use complete objects for their graphical previews. Closing a file waits for its preview read to end." +msgstr "Les grands tableaux 1D ordinaires et les images 2D sont prĂ©visualisĂ©s avec un pas d'Ă©chantillonnage au-delĂ  de ``2**24`` Ă©lĂ©ments (environ 16,8 millions). L'import lit toujours toutes les valeurs, pas l'aperçu Ă©chantillonnĂ©. Ce n'est pas une limite mĂ©moire universelle : les objets NeXus et dĂ©crits par un profil, les jeux de donnĂ©es composites et les signaux stockĂ©s dans des tableaux 2D utilisent encore des objets complets pour leurs aperçus graphiques. La fermeture d'un fichier attend la fin de la lecture de son aperçu." + +msgid "Datasets with three or more dimensions, such as image stacks and spectral cubes, are listed but cannot be imported as complete objects. There is no graphical slice selector yet. Developers can read 1D or 2D views without loading the whole cube using the selection API described in the SigimaX guide." +msgstr "Les jeux de donnĂ©es Ă  trois dimensions ou plus, comme les piles d'images et les cubes spectraux, sont listĂ©s mais ne peuvent pas ĂȘtre importĂ©s comme objets complets. Il n'existe pas encore de sĂ©lecteur graphique de coupes. Les dĂ©veloppeurs peuvent lire des vues 1D ou 2D sans charger tout le cube avec l'API de sĂ©lection dĂ©crite dans le guide de SigimaX." + +msgid "Numerical values and experimental origin" +msgstr "Valeurs numĂ©riques et origine expĂ©rimentale" + +msgid "Unsigned 32-bit detector counts above 2 147 483 647 are imported as ``float64`` images without clipping or rounding. Other integer arrays are also converted to an exact supported type when possible. If conversion to floating point cannot preserve every integer (in particular beyond ``+/-2**53`` for ``float64``), Sigima emits a warning instead of silently promising exact values." +msgstr "Les comptages de dĂ©tecteur entiers non signĂ©s sur 32 bits au-delĂ  de 2 147 483 647 sont importĂ©s comme images ``float64`` sans Ă©crĂȘtage ni arrondi. Les autres tableaux entiers sont aussi convertis vers un type pris en charge exact lorsque c'est possible. Si la conversion en flottant ne peut pas prĂ©server chaque entier (notamment au-delĂ  de ``+/-2**53`` pour ``float64``), Sigima Ă©met un avertissement au lieu de promettre silencieusement des valeurs exactes." + +msgid "NeXus and mapped objects carry an ``experimental_source`` metadata entry describing the source file, group, measured dataset, axes and recorded conversions. Mapped objects also identify their mapping. This entry describes where the measurement came from; it is not the processing provenance shown in the Properties panel and does not make the experiment reproducible." +msgstr "Les objets NeXus et dĂ©crits par un profil portent une entrĂ©e de mĂ©tadonnĂ©es ``experimental_source`` dĂ©crivant le fichier source, le groupe, le jeu de donnĂ©es mesurĂ©es, les axes et les conversions enregistrĂ©es. Les objets dĂ©crits par un profil identifient aussi celui-ci. Cette entrĂ©e dĂ©crit l'origine de la mesure ; ce n'est pas la provenance des traitements affichĂ©e dans le panneau PropriĂ©tĂ©s et elle ne rend pas l'expĂ©rience reproductible." + +msgid "Save the imported objects in a DataLab workspace to keep both their data and this metadata. Reopening that workspace does not require the original NeXus file or the mapping plugin. Generic raw dataset imports do not automatically receive this structured experimental source." +msgstr "Enregistrez les objets importĂ©s dans un espace de travail DataLab pour conserver leurs donnĂ©es et ces mĂ©tadonnĂ©es. La rĂ©ouverture de cet espace de travail ne nĂ©cessite ni le fichier NeXus original ni le plugin du profil. Les imports gĂ©nĂ©riques de jeux de donnĂ©es bruts ne reçoivent pas automatiquement cette description structurĂ©e de leur origine expĂ©rimentale." diff --git a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po index 5273f089f..8bc1ea54a 100644 --- a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po +++ b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po @@ -339,8 +339,8 @@ msgstr "Les annotations PlotPy existantes restent lisibles et ne sont converties msgid "Annotation identifiers, lock state, custom metadata and extension data are preserved across edits, while unknown third-party payloads are retained without modification" msgstr "Les identifiants d'annotation, l'Ă©tat de verrouillage, les mĂ©tadonnĂ©es personnalisĂ©es et les donnĂ©es d'extension sont conservĂ©s lors des modifications, tandis que les charges utiles tierces inconnues sont conservĂ©es sans modification" -msgid "DataLab now requires Sigima ≄ 1.3.0, SigimaX ≄ 1.1.0 and DataLab-Capsule 0.1.x" -msgstr "DataLab nĂ©cessite dĂ©sormais Sigima ≄ 1.3.0, SigimaX ≄ 1.1.0 et DataLab-Capsule 0.1.x" +msgid "DataLab now requires Sigima ≄ 1.4.0, SigimaX ≄ 1.1.0 and DataLab-Capsule 0.1.x; HDF5 imports use Sigima's experimental-source API" +msgstr "DataLab nĂ©cessite dĂ©sormais Sigima ≄ 1.4.0, SigimaX ≄ 1.1.0 et DataLab-Capsule 0.1.x ; les imports HDF5 utilisent l'API d'origine expĂ©rimentale de Sigima" msgid "**HDF5 browser:**" msgstr "**Explorateur HDF5 :**" @@ -348,15 +348,18 @@ msgstr "**Explorateur HDF5 :**" msgid "Browsing an HDF5 file no longer reads its datasets entirely: they are listed and described from their shape and data type, and the value previews read only their first elements. Large files open faster and use less memory; data is read when a signal or an image is previewed or imported" msgstr "Parcourir un fichier HDF5 ne lit plus ses jeux de donnĂ©es en entier : ils sont listĂ©s et dĂ©crits Ă  partir de leur forme et de leur type de donnĂ©es, et les aperçus de valeurs ne lisent que leurs premiers Ă©lĂ©ments. Les gros fichiers s'ouvrent plus vite et utilisent moins de mĂ©moire ; les donnĂ©es sont lues lorsqu'un signal ou une image est prĂ©visualisĂ© ou importĂ©" -msgid "The HDF5 browser stays responsive while a preview is read, and previews datasets of more than 16 million values with a step along each axis" -msgstr "L'explorateur HDF5 reste rĂ©actif pendant la lecture d'un aperçu, et prĂ©visualise les jeux de donnĂ©es de plus de 16 millions de valeurs avec un pas le long de chaque axe" +msgid "The HDF5 browser stays responsive while a preview is read. Ordinary 1D arrays and 2D images above 2ÂČ⁎ values (about 16.8 million) are previewed with a step along each axis; NeXus and mapped objects, compound datasets and signals stored in 2D tables still use complete objects for graphical previews" +msgstr "L'explorateur HDF5 reste rĂ©actif pendant la lecture d'un aperçu. Les tableaux 1D ordinaires et les images 2D au-delĂ  de 2ÂČ⁎ valeurs (environ 16,8 millions) sont prĂ©visualisĂ©s avec un pas le long de chaque axe ; les objets NeXus et dĂ©crits par un profil, les jeux de donnĂ©es composites et les signaux stockĂ©s dans des tableaux 2D utilisent encore des objets complets pour leurs aperçus graphiques" -msgid "Datasets with three dimensions or more (image stacks, spectral cubes...) are now listed in the HDF5 browser" -msgstr "Les jeux de donnĂ©es Ă  trois dimensions ou plus (piles d'images, cubes spectraux...) sont dĂ©sormais listĂ©s dans l'explorateur HDF5" +msgid "Datasets with three dimensions or more (image stacks, spectral cubes...) are now listed in the HDF5 browser, but are not directly importable and have no graphical slice selector" +msgstr "Les jeux de donnĂ©es Ă  trois dimensions ou plus (piles d'images, cubes spectraux...) sont dĂ©sormais listĂ©s dans l'explorateur HDF5, mais ne sont pas directement importables et n'ont pas de sĂ©lecteur graphique de coupes" msgid "Dataset attributes with more than 10 000 values are no longer copied to the metadata of imported objects" msgstr "Les attributs de jeux de donnĂ©es de plus de 10 000 valeurs ne sont plus copiĂ©s dans les mĂ©tadonnĂ©es des objets importĂ©s" +msgid "See the [HDF5 browser guide](../features/common/h5browser.rst) for scientific object import, experimental origin and preview limits." +msgstr "Le [guide de l'explorateur HDF5](../features/common/h5browser.rst) dĂ©crit l'import d'objets scientifiques, l'origine expĂ©rimentale et les limites des aperçus." + msgid "đŸ› ïž Bug Fixes" msgstr "đŸ› ïž Corrections de bugs" @@ -381,8 +384,8 @@ msgstr "Correction de la largeur des panneaux Signal/Image non restaurĂ©e au dĂ© msgid "**HDF5 import:**" msgstr "**Import HDF5 :**" -msgid "Fixed images with unsigned 32-bit values above 2 147 483 647 being clipped when imported from an HDF5 file (\"Clipping uint32 data to int32\" warning): such images are now imported as 64-bit floating-point images keeping every value exactly, as are other integer images that no supported integer type can hold, which were rounded to 32-bit floating-point values beyond 16 777 216 (requires Sigima ≄ 1.4.0)" -msgstr "Correction de l'Ă©crĂȘtage des images aux valeurs entiĂšres non signĂ©es 32 bits supĂ©rieures Ă  2 147 483 647 lors de leur import depuis un fichier HDF5 (avertissement \"Clipping uint32 data to int32\") : ces images sont dĂ©sormais importĂ©es comme images en virgule flottante 64 bits conservant exactement chaque valeur, comme les autres images entiĂšres qu'aucun type entier pris en charge ne peut contenir, qui Ă©taient arrondies en virgule flottante 32 bits au-delĂ  de 16 777 216 (nĂ©cessite Sigima ≄ 1.4.0)" +msgid "Fixed images with unsigned 32-bit values above 2 147 483 647 being clipped when imported from an HDF5 file (\"Clipping uint32 data to int32\" warning): such images are now imported as 64-bit floating-point images keeping every value exactly. Other integer images that no supported integer type can hold also use a sufficiently precise floating-point type when possible, instead of rounding to 32-bit floating-point values beyond 16 777 216; a warning is emitted when no supported type guarantees exact conversion (in particular beyond ±2⁔³ for float64)" +msgstr "Correction de l'Ă©crĂȘtage des images contenant des valeurs entiĂšres non signĂ©es sur 32 bits supĂ©rieures Ă  2 147 483 647 lors de leur import depuis un fichier HDF5 (avertissement « Clipping uint32 data to int32 ») : ces images sont dĂ©sormais importĂ©es en flottants sur 64 bits en conservant chaque valeur exactement. Les autres images entiĂšres qu'aucun type entier pris en charge ne peut contenir utilisent aussi, lorsque c'est possible, un type flottant suffisamment prĂ©cis, au lieu d'ĂȘtre arrondies en flottants sur 32 bits au-delĂ  de 16 777 216 ; un avertissement est Ă©mis lorsqu'aucun type pris en charge ne garantit une conversion exacte (notamment au-delĂ  de ±2⁔³ pour float64)" msgid "**Metadata:**" msgstr "**MĂ©tadonnĂ©es :**" diff --git a/doc/locale/fr/LC_MESSAGES/requirements.po b/doc/locale/fr/LC_MESSAGES/requirements.po index ea443c002..6b1d869fa 100644 --- a/doc/locale/fr/LC_MESSAGES/requirements.po +++ b/doc/locale/fr/LC_MESSAGES/requirements.po @@ -6,9 +6,16 @@ #, fuzzy msgid "" msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2026-10-22 21:34+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" msgid "The `datalab-platform` package requires the following Python modules:" msgstr "Le paquet `datalab-platform` nĂ©cessite les modules Python suivants :" @@ -34,8 +41,8 @@ msgstr "Langage de programmation Python" msgid "guidata" msgstr "" -msgid ">= 3.15.1" -msgstr "" +msgid ">= 3.16.0" +msgstr ">= 3.16.0" msgid "Automatic GUI generation for easy dataset editing and display" msgstr "GĂ©nĂ©ration automatique d'interface graphique pour l'Ă©dition et l'affichage de jeux de donnĂ©es" @@ -52,8 +59,8 @@ msgstr "Outils de tracĂ© de courbes et d'images pour les applications Python/Qt" msgid "Sigima" msgstr "" -msgid ">= 1.2.0" -msgstr "" +msgid ">= 1.4.0" +msgstr ">= 1.4.0" msgid "Scientific computing engine for 1D signals and 2D images, part of the DataLab open-source platform." msgstr "Moteur de calcul scientifique pour signaux 1D et images 2D, faisant partie de la plateforme open-source DataLab." @@ -61,8 +68,8 @@ msgstr "Moteur de calcul scientifique pour signaux 1D et images 2D, faisant part msgid "SigimaX" msgstr "" -msgid ">= 1.0.2" -msgstr "" +msgid ">= 1.1.0" +msgstr ">= 1.1.0" msgid "Reusable GUI components and application utilities, part of the DataLab platform for signal and image processing" msgstr "" diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index b05617d26..e1b482f22 100644 --- a/doc/release_notes/release_1.04.md +++ b/doc/release_notes/release_1.04.md @@ -141,15 +141,17 @@ DataLab now uses stable UUID-based object references across object trees, result * Plot annotations are now stored in a renderer-independent format shared with DataLab-Web, so annotations in workspaces and `.dlabann` files are no longer tied to PlotPy * Existing PlotPy annotations remain readable and are converted only after an annotation edit is accepted; simply opening a workspace or cancelling the editor leaves its data unchanged * Annotation identifiers, lock state, custom metadata and extension data are preserved across edits, while unknown third-party payloads are retained without modification -* DataLab now requires Sigima ≄ 1.3.0, SigimaX ≄ 1.1.0 and DataLab-Capsule 0.1.x +* DataLab now requires Sigima ≄ 1.4.0, SigimaX ≄ 1.1.0 and DataLab-Capsule 0.1.x; HDF5 imports use Sigima's experimental-source API **HDF5 browser:** * Browsing an HDF5 file no longer reads its datasets entirely: they are listed and described from their shape and data type, and the value previews read only their first elements. Large files open faster and use less memory; data is read when a signal or an image is previewed or imported -* The HDF5 browser stays responsive while a preview is read, and previews datasets of more than 16 million values with a step along each axis -* Datasets with three dimensions or more (image stacks, spectral cubes...) are now listed in the HDF5 browser +* The HDF5 browser stays responsive while a preview is read. Ordinary 1D arrays and 2D images above 2ÂČ⁎ values (about 16.8 million) are previewed with a step along each axis; NeXus and mapped objects, compound datasets and signals stored in 2D tables still use complete objects for graphical previews +* Datasets with three dimensions or more (image stacks, spectral cubes...) are now listed in the HDF5 browser, but are not directly importable and have no graphical slice selector * Dataset attributes with more than 10 000 values are no longer copied to the metadata of imported objects +See the [HDF5 browser guide](../features/common/h5browser.rst) for scientific object import, experimental origin and preview limits. + ### đŸ› ïž Bug Fixes ### **Performance with many objects:** @@ -164,7 +166,7 @@ DataLab now uses stable UUID-based object references across object trees, result **HDF5 import:** -* Fixed images with unsigned 32-bit values above 2 147 483 647 being clipped when imported from an HDF5 file ("Clipping uint32 data to int32" warning): such images are now imported as 64-bit floating-point images keeping every value exactly, as are other integer images that no supported integer type can hold, which were rounded to 32-bit floating-point values beyond 16 777 216 (requires Sigima ≄ 1.4.0) +* Fixed images with unsigned 32-bit values above 2 147 483 647 being clipped when imported from an HDF5 file ("Clipping uint32 data to int32" warning): such images are now imported as 64-bit floating-point images keeping every value exactly. Other integer images that no supported integer type can hold also use a sufficiently precise floating-point type when possible, instead of rounding to 32-bit floating-point values beyond 16 777 216; a warning is emitted when no supported type guarantees exact conversion (in particular beyond ±2⁔³ for float64) **Metadata:** diff --git a/doc/requirements.rst b/doc/requirements.rst index ea4eeb420..a6bc882e1 100644 --- a/doc/requirements.rst +++ b/doc/requirements.rst @@ -17,7 +17,7 @@ The `datalab-platform` package requires the following Python modules: - >= 2.11.0 - Curve and image plotting tools for Python/Qt applications * - Sigima - - >= 1.3.0 + - >= 1.4.0 - Scientific computing engine for 1D signals and 2D images, part of the DataLab open-source platform. * - SigimaX - >= 1.1.0 diff --git a/pyproject.toml b/pyproject.toml index 8def24563..4ee45eed5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ requires-python = ">=3.9, <4" dependencies = [ "guidata >= 3.16.0", "PlotPy >= 2.11.0", - "Sigima >= 1.3.0", + "Sigima >= 1.4.0", "SigimaX >= 1.1.0", "datalab-capsule >= 0.1.0, < 0.2", "NumPy >= 1.22, < 2.6", diff --git a/requirements.txt b/requirements.txt index 236c847cc..45c12f572 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ PlotPy >= 2.11.0 PyQt5 >= 5.15.6 PyWavelets >= 1.2, < 2.0 SciPy >= 1.10.1, < 1.19 -Sigima >= 1.3.0 +Sigima >= 1.4.0 SigimaX >= 1.1.0 babel build From ec20645a5ac8b9a9e37edaece77b7eab4c823f7e Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:02:25 +0200 Subject: [PATCH 11/22] fix(instruments): serialize instrument calls across window reopenings DataLab forgot an instrument window as soon as it was closed, while its worker could still be running a slow preview or acquisition. Opening the tool again created a second worker calling the same instrument in parallel (open and preview during the running call), and the old worker then closed the instrument the new window had just opened. - DLMainWindow keeps one worker per instrument, shared by its successive windows: a reopened window opens the instrument once the closed one has closed it, in the same thread. - Reloading plugins or quitting waits for the instruments of windows already closed by the user, then stops the workers. - Closing a window that was never shown now closes its instrument. Assisted-by: Claude Opus 5.5 --- datalab/gui/main.py | 50 +++++++++++---- datalab/gui/plugins/instrument.py | 32 ++++++++-- datalab/plugins/instruments.py | 3 +- .../plugins/instrument_window_unit_test.py | 64 +++++++++++++++++-- .../features/plugins/plugin_tools_app_test.py | 11 +++- 5 files changed, 135 insertions(+), 25 deletions(-) diff --git a/datalab/gui/main.py b/datalab/gui/main.py index b84461d9a..349eff245 100644 --- a/datalab/gui/main.py +++ b/datalab/gui/main.py @@ -25,6 +25,8 @@ import sys import time import traceback +from concurrent.futures import Future +from concurrent.futures import wait as wait_futures from contextlib import ExitStack from datetime import datetime from typing import TYPE_CHECKING @@ -75,7 +77,7 @@ from datalab.gui.panel import base, history, image, macro, signal from datalab.gui.plugins.applications import ApplicationsDialog, get_plugin_icon from datalab.gui.plugins.config import PluginConfigDialog -from datalab.gui.plugins.instrument import InstrumentWindow +from datalab.gui.plugins.instrument import InstrumentWindow, SerialWorker from datalab.gui.processor.base import FeatureNotFoundError from datalab.gui.processor.preview import PreviewExecutorCache from datalab.gui.settings import AI_OPTION_NAMES, edit_settings @@ -172,6 +174,10 @@ def __init__(self, console=None, hide_on_close=False): self.aiassistantpanel = None # type: ignore[assignment] self.__applications_dialog: ApplicationsDialog | None = None self.__instrument_windows: dict[tuple[str, str], InstrumentWindow] = {} + #: Worker of each plugin instrument, shared by its successive windows + self.__instrument_workers: dict[tuple[str, str], SerialWorker] = {} + #: Title and closing call of the last closed window of each instrument + self.__instrument_closings: dict[tuple[str, str], tuple[str, Future]] = {} self.welcomepanel: WelcomePanel | None = None self.signalpanel_toolbar: QW.QToolBar | None = None @@ -1152,11 +1158,20 @@ def __launch_plugin_tool(self, plugin: PluginBase, tool_id: str) -> None: def open_plugin_instrument( self, plugin: PluginBase, tool: PluginTool ) -> InstrumentWindow: - """Show the instrument window of a plugin tool, creating it if needed.""" + """Show the instrument window of a plugin tool, creating it if needed. + + The successive windows of an instrument share its worker: a window + opened again calls the instrument once the closed one has closed it. + """ key = (plugin.plugin_id, tool.id) window = self.__instrument_windows.get(key) if window is None: - window = InstrumentWindow(self, plugin.get_instrument(tool.id), tool.title) + worker = self.__instrument_workers.get(key) + if worker is None: + worker = self.__instrument_workers[key] = SerialWorker() + window = InstrumentWindow( + self, plugin.get_instrument(tool.id), tool.title, worker + ) window.finished.connect( functools.partial(self.__forget_instrument_window, key) ) @@ -1167,24 +1182,35 @@ def open_plugin_instrument( return window def __forget_instrument_window(self, key: tuple[str, str], *_args) -> None: - """Release a closed instrument window.""" + """Release a closed instrument window, keeping track of its instrument + until it is closed.""" window = self.__instrument_windows.pop(key, None) if window is not None: + window.stop() + closing = (window.windowTitle(), window.close_future) + self.__instrument_closings[key] = closing window.deleteLater() def close_plugin_instruments(self) -> None: - """Close all plugin instrument windows, and wait for their instruments.""" - windows = list(self.__instrument_windows.values()) - for window in windows: + """Close all plugin instrument windows, and wait for their instruments, + including those of windows closed before.""" + for window in list(self.__instrument_windows.values()): window.close() - window.stop() - for window in windows: - if not window.wait_closed(): + # Windows that were not visible do not emit `finished` when closed + for key in list(self.__instrument_windows): + self.__forget_instrument_window(key) + for title, future in self.__instrument_closings.values(): + done, _not_done = wait_futures([future], InstrumentWindow.CLOSE_TIMEOUT) + if not done: logging.getLogger(__name__).warning( "Instrument '%s' did not close within %g s", - window.windowTitle(), - window.CLOSE_TIMEOUT, + title, + InstrumentWindow.CLOSE_TIMEOUT, ) + for worker in self.__instrument_workers.values(): + worker.shutdown() + self.__instrument_workers.clear() + self.__instrument_closings.clear() def __unregister_plugins(self) -> None: """Unregister all plugins and let them cleanup their hooks""" diff --git a/datalab/gui/plugins/instrument.py b/datalab/gui/plugins/instrument.py index 43631fc95..ce8e636e2 100644 --- a/datalab/gui/plugins/instrument.py +++ b/datalab/gui/plugins/instrument.py @@ -17,6 +17,7 @@ from guidata.qthelpers import win32_fix_title_bar_background from plotpy.plot import PlotOptions, PlotWidget from qtpy import QtCore as QC +from qtpy import QtGui as QG from qtpy import QtWidgets as QW from sigima.objects import ImageObj, SignalObj from sigimax.adapters_plotpy.objects.signal import CURVESTYLES @@ -33,10 +34,10 @@ PluginInstrument, ) -__all__ = ["InstrumentWindow"] +__all__ = ["InstrumentWindow", "SerialWorker"] -class _SerialWorker: +class SerialWorker: """Run calls one at a time in a daemon thread. Unlike a thread pool, a call stuck in third-party code does not prevent @@ -85,6 +86,10 @@ class InstrumentWindow(QW.QDialog): main: DataLab main window instrument: instrument opened by a plugin tool title: window title + executor: worker running the calls of the instrument, shared by the + successive windows of the instrument so that a window opened again runs + its calls after those of the closed one (default: a worker of its own, + stopped when the window is closed) """ #: Delay between a settings change and the refresh of the view (ms) @@ -96,7 +101,11 @@ class InstrumentWindow(QW.QDialog): SIG_CALL_DONE = QC.Signal(str, object, object) def __init__( - self, main: DLMainWindow, instrument: PluginInstrument, title: str + self, + main: DLMainWindow, + instrument: PluginInstrument, + title: str, + executor: SerialWorker | None = None, ) -> None: super().__init__(main) win32_fix_title_bar_background(self) @@ -109,7 +118,8 @@ def __init__( self._plot_kind: str | None = None self._plot_signature: tuple | None = None self._value_range: tuple[float, float] | None = None - self._executor = _SerialWorker() + self._owns_executor = executor is None + self._executor = SerialWorker() if executor is None else executor self._busy = False self._pending: str | None = None self._resume_live = False @@ -282,6 +292,11 @@ def wait_closed(self, timeout: float = CLOSE_TIMEOUT) -> bool: done, _not_done = wait_futures([self._close_future], timeout) return bool(done) + @property + def close_future(self) -> Future | None: + """Call closing the instrument, once the window was closed (else None)""" + return self._close_future + def child_title(self, item) -> str: """Supply the usual guidata title for nested editors.""" return f"{self.windowTitle()} - {item.label()}" @@ -304,7 +319,14 @@ def stop(self) -> None: if self._close_future is None: self._pending = None self._close_future = self._executor.submit(self._call, "close") - self._executor.shutdown() + if self._owns_executor: + self._executor.shutdown() + + def closeEvent(self, event: QG.QCloseEvent) -> None: # pylint: disable=invalid-name + """Close the instrument, even if the window was never shown (Qt only + emits ``finished`` when a visible dialog is closed).""" + self.stop() + super().closeEvent(event) def _apply_settings(self) -> bool: """Write the edited values into the instrument settings.""" diff --git a/datalab/plugins/instruments.py b/datalab/plugins/instruments.py index f487e86c0..d70fe826a 100644 --- a/datalab/plugins/instruments.py +++ b/datalab/plugins/instruments.py @@ -97,7 +97,8 @@ class PluginInstrument(abc.ABC): DataLab calls :meth:`open` when the instrument window opens and :meth:`close` when it closes (including when the plugin is reloaded or DataLab quits). It calls these methods, :meth:`preview` and :meth:`acquire` - one at a time, outside the GUI thread, and writes :attr:`settings` only + one at a time, outside the GUI thread, even when the window is closed and + opened again while a call is running, and writes :attr:`settings` only between two calls: a slow instrument does not freeze DataLab. Data received by the instrument in its own threads (e.g. device events) must be stored for the next :meth:`preview` or :meth:`acquire` call. diff --git a/datalab/tests/features/plugins/instrument_window_unit_test.py b/datalab/tests/features/plugins/instrument_window_unit_test.py index 2711635ba..42e016c89 100644 --- a/datalab/tests/features/plugins/instrument_window_unit_test.py +++ b/datalab/tests/features/plugins/instrument_window_unit_test.py @@ -6,7 +6,9 @@ Instrument calls run one at a time outside the GUI thread: a slow instrument does not freeze DataLab, repeated refresh requests are merged, and closing the -window closes the instrument once its running call is done. +window closes the instrument once its running call is done, even if the window +was never shown. A window opened again with the worker of a closed one calls +the instrument once it is closed. """ # guitest: show @@ -21,7 +23,7 @@ from qtpy import QtWidgets as QW from sigima.objects import create_signal -from datalab.gui.plugins.instrument import InstrumentWindow +from datalab.gui.plugins.instrument import InstrumentWindow, SerialWorker from datalab.plugins.instruments import ( InstrumentAcquisition, InstrumentFrame, @@ -41,24 +43,30 @@ class SlowInstrument(PluginInstrument): def __init__(self) -> None: super().__init__(SlowSettings()) self.calls: list[str] = [] + self.threads: set[int] = set() self.running = threading.Event() self.release = threading.Event() + def _record(self, method: str) -> None: + """Record a call and the thread running it.""" + self.calls.append(method) + self.threads.add(threading.get_ident()) + def open(self) -> None: - self.calls.append("open") + self._record("open") def preview(self) -> InstrumentFrame: - self.calls.append("preview") + self._record("preview") self.running.set() assert self.release.wait(10.0) x = np.linspace(0.0, 1.0, 5) return InstrumentFrame((create_signal("Live", x, self.settings.gain * x),)) def close(self) -> None: - self.calls.append("close") + self._record("close") def acquire(self) -> InstrumentAcquisition: - self.calls.append("acquire") + self._record("acquire") return InstrumentAcquisition("Slow", self.preview().objects) @@ -103,6 +111,50 @@ def test_close_waits_for_running_call() -> None: main.deleteLater() +def test_reopened_window_waits_for_closing() -> None: + """A window opened again while the closed one still runs a call (sharing its + worker, as DataLab does) calls the instrument once it is closed, in the same + thread.""" + with qt_app_context(): + main = QW.QWidget() + instrument = SlowInstrument() + worker = SerialWorker() + first = InstrumentWindow(main, instrument, "Slow instrument", worker) + first.show() + while not instrument.running.is_set(): + QW.QApplication.processEvents() + first.close() + second = InstrumentWindow(main, instrument, "Slow instrument", worker) + second.show() + QW.QApplication.processEvents() + assert instrument.calls == ["open", "preview"] + instrument.release.set() + assert first.wait_closed() + assert second.wait_until_idle() + assert instrument.calls == ["open", "preview", "close", "open", "preview"] + assert len(instrument.threads) == 1 + second.close() + assert second.wait_closed() + assert instrument.calls[-1] == "close" + worker.shutdown() + main.deleteLater() + + +def test_hidden_window_closes_instrument() -> None: + """Closing a window that was never shown closes its instrument.""" + with qt_app_context(): + main = QW.QWidget() + instrument = SlowInstrument() + instrument.release.set() + window = InstrumentWindow(main, instrument, "Slow instrument") + window.close() + assert window.wait_closed() + assert instrument.calls[0] == "open" and instrument.calls[-1] == "close" + main.deleteLater() + + if __name__ == "__main__": test_slow_instrument_calls_are_serialized() test_close_waits_for_running_call() + test_reopened_window_waits_for_closing() + test_hidden_window_closes_instrument() diff --git a/datalab/tests/features/plugins/plugin_tools_app_test.py b/datalab/tests/features/plugins/plugin_tools_app_test.py index 7e41d061e..9bbbda960 100644 --- a/datalab/tests/features/plugins/plugin_tools_app_test.py +++ b/datalab/tests/features/plugins/plugin_tools_app_test.py @@ -215,12 +215,21 @@ def failing_preview(): assert win.get_current_panel() == "signal" assert "3 objects" in window.status_label.text() + # A window opened again calls the instrument after the closed one + window.close() + QW.QApplication.processEvents() + reopened = plugin.launch_tool("generator") + assert reopened is not window + assert reopened.wait_until_idle() + assert instrument.events == ["open", "close", "open"] + window = reopened + closed: list[int] = [] window.finished.connect(closed.append) win.reload_plugins() QW.QApplication.processEvents() assert closed - assert instrument.events == ["open", "close"] + assert instrument.events == ["open", "close", "open", "close"] reloaded = PluginRegistry.get_plugin("org.example.tools-app") assert reloaded.launch_tool("generator") is not window win.close_plugin_instruments() From f11336c432c4c5e117acae64eeb9cdc31418f6e4 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:06:18 +0200 Subject: [PATCH 12/22] fix(instruments): show open() errors and retry opening before the next call The first refresh of an instrument window was scheduled independently of open(): when open() failed, preview() was still called on the instrument, and its result cleared the message of open() (or its own error replaced it), although PluginInstrument.open documents a user-facing error. - The first preview is requested once open() succeeded. - When open() fails, its message stays in view and the instrument is not called; the next request (e.g. after a settings change, or in live mode) opens it again before calling preview() or acquire(). Assisted-by: Claude Opus 5.5 --- datalab/gui/plugins/instrument.py | 30 ++++++++++--- datalab/plugins/instruments.py | 5 ++- .../plugins/instrument_window_unit_test.py | 45 ++++++++++++++++++- 3 files changed, 73 insertions(+), 7 deletions(-) diff --git a/datalab/gui/plugins/instrument.py b/datalab/gui/plugins/instrument.py index ce8e636e2..aa8fb6dc9 100644 --- a/datalab/gui/plugins/instrument.py +++ b/datalab/gui/plugins/instrument.py @@ -78,9 +78,11 @@ class InstrumentWindow(QW.QDialog): The view is refreshed when settings change, and periodically in live mode. Each acquisition is added to the workspace in a new group. The instrument is - opened with the window and closed with it. Its methods are called one at a - time in a worker thread: a request made while a call is running is done - next (an acquisition before a refresh, repeated refreshes only once). + opened with the window and closed with it; if opening it fails, the reason + is shown, and it is opened again before the next call. Its methods are + called one at a time in a worker thread: a request made while a call is + running is done next (an acquisition before a refresh, repeated refreshes + only once). Args: main: DataLab main window @@ -121,6 +123,7 @@ def __init__( self._owns_executor = executor is None self._executor = SerialWorker() if executor is None else executor self._busy = False + self._opened = False self._pending: str | None = None self._resume_live = False self._close_future: Future | None = None @@ -198,9 +201,12 @@ def __init__( min(self.width(), screen.width()), min(self.height(), screen.height()) ) splitter.setSizes([650, 450]) + # The first view is requested once the instrument is opened (filling the + # settings editor reported changes, which do not need another view) + self._refresh_timer.stop() self._busy = True + self._pending = "preview" self._executor.submit(self._call, "open") - self._refresh_timer.start(0) def _call(self, method: str) -> None: """Call an instrument method (worker thread).""" @@ -218,7 +224,9 @@ def _call(self, method: str) -> None: pass # Window deleted after being closed def _request(self, method: str) -> None: - """Call ``preview`` or ``acquire`` now, or after the running call.""" + """Call ``preview`` or ``acquire`` now, or after the running call. + + If opening the instrument failed, it is opened again first.""" if self._close_future is not None: return if self._busy: @@ -227,6 +235,11 @@ def _request(self, method: str) -> None: return if not self._apply_settings(): return + if not self._opened: + self._busy = True + self._pending = method + self._executor.submit(self._call, "open") + return if method == "acquire": self._resume_live = self._live_timer.isActive() self._live_timer.stop() @@ -245,8 +258,15 @@ def _call_done(self, method: str, result, error: Exception | None) -> None: self._live_timer.start() if self._close_future is not None: return + if method == "open": + self._opened = error is None if error is not None: self.status_label.setText(str(error)) + if method == "open": + # Keep the reason in view: the instrument is not called until + # the next request opens it again (e.g. after a settings change) + self._pending = None + return elif method == "preview": self._show_frame(result) elif method == "acquire": diff --git a/datalab/plugins/instruments.py b/datalab/plugins/instruments.py index d70fe826a..0b38ca366 100644 --- a/datalab/plugins/instruments.py +++ b/datalab/plugins/instruments.py @@ -119,7 +119,10 @@ def open(self) -> None: """Prepare the instrument when its window opens (e.g. connect to it). Raises ``ValueError`` with a user-facing message if the instrument - cannot be used. Does nothing by default. + cannot be used: DataLab shows this message, and does not call + :meth:`preview` or :meth:`acquire` until a new request (e.g. after a + settings change) opens the instrument successfully. Does nothing by + default. """ def close(self) -> None: diff --git a/datalab/tests/features/plugins/instrument_window_unit_test.py b/datalab/tests/features/plugins/instrument_window_unit_test.py index 42e016c89..7b7a08924 100644 --- a/datalab/tests/features/plugins/instrument_window_unit_test.py +++ b/datalab/tests/features/plugins/instrument_window_unit_test.py @@ -8,7 +8,8 @@ does not freeze DataLab, repeated refresh requests are merged, and closing the window closes the instrument once its running call is done, even if the window was never shown. A window opened again with the worker of a closed one calls -the instrument once it is closed. +the instrument once it is closed. When opening the instrument fails, the reason +stays in view and the instrument is opened again before the next call. """ # guitest: show @@ -153,8 +154,50 @@ def test_hidden_window_closes_instrument() -> None: main.deleteLater() +class FailingInstrument(SlowInstrument): + """Instrument that cannot be opened until the test allows it.""" + + def __init__(self) -> None: + super().__init__() + self.fail = True + self.release.set() + + def open(self) -> None: + self._record("open") + if self.fail: + raise ValueError("Device unreachable") + + +def test_open_failure_is_shown_and_retried() -> None: + """When opening fails, the reason stays in view and the instrument is not + called: the next request opens it again.""" + with qt_app_context(): + main = QW.QWidget() + instrument = FailingInstrument() + window = InstrumentWindow(main, instrument, "Failing instrument") + window.show() + assert window.wait_until_idle() + assert instrument.calls == ["open"] + assert window.status_label.text() == "Device unreachable" + window.refresh() + assert window.wait_until_idle() + assert instrument.calls == ["open", "open"] + assert window.status_label.text() == "Device unreachable" + instrument.fail = False + window.refresh() + assert window.wait_until_idle() + assert instrument.calls == ["open", "open", "open", "preview"] + assert window.status_label.text() == "" + assert len(window.items) == 1 + window.close() + assert window.wait_closed() + assert instrument.calls[-1] == "close" + main.deleteLater() + + if __name__ == "__main__": test_slow_instrument_calls_are_serialized() test_close_waits_for_running_call() test_reopened_window_waits_for_closing() test_hidden_window_closes_instrument() + test_open_failure_is_shown_and_retried() From 6c03a4f0f58128ccbece95eacdd3bf1a92e6f279 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:13:42 +0200 Subject: [PATCH 13/22] test(hdf5): check experimental_source types and NeXus axes after workspace reload - The NeXus workspace test imports a detector image with a decreasing axis and a dimension without axis, and checks after reloading the workspace that the image is reversed, that "." is kept for the missing axis, and that the experimental source keeps its JSON types. - Documentation and release notes (English and French): instrument calls across window reopenings and open() failures, NeXus axis handling, decimated NeXus/mapped previews, and the complex64 HDF5 import fix. Assisted-by: Claude Opus 5.5 --- .../features/hdf5/nexus_import_app_test.py | 29 ++++++++++++++--- doc/features/advanced/plugins.rst | 2 +- doc/features/common/h5browser.rst | 31 ++++++++++++------- .../LC_MESSAGES/features/advanced/plugins.po | 12 +++---- .../LC_MESSAGES/features/common/h5browser.po | 20 ++++++------ .../LC_MESSAGES/release_notes/release_1.04.po | 15 +++++---- doc/release_notes/release_1.04.md | 7 +++-- 7 files changed, 74 insertions(+), 42 deletions(-) diff --git a/datalab/tests/features/hdf5/nexus_import_app_test.py b/datalab/tests/features/hdf5/nexus_import_app_test.py index 23210c6c4..33c39fa01 100644 --- a/datalab/tests/features/hdf5/nexus_import_app_test.py +++ b/datalab/tests/features/hdf5/nexus_import_app_test.py @@ -5,13 +5,15 @@ ----------------- A NeXus ``NXdata`` group is imported with its axes and its origin, and the -workspace saved afterwards is reopened without the NeXus file. +workspace saved afterwards is reopened without the NeXus file: the origin keeps +its JSON types, and dimensions without axis or decreasing axes are preserved. """ # guitest: show from __future__ import annotations +import json import os import os.path as osp @@ -23,7 +25,8 @@ def _create_nexus_file(fname: str) -> None: - """Create a NeXus file with a spectrum and a map with non-uniform axes""" + """Create a NeXus file with a spectrum, a map with non-uniform axes, and a + detector image with a decreasing axis and a dimension without axis""" with h5py.File(fname, "w") as h5file: entry = h5file.create_group("entry") entry.attrs["NX_class"] = "NXentry" @@ -38,6 +41,11 @@ def _create_nexus_file(fname: str) -> None: mapping["counts"] = np.array([[1, 2, 3], [4, 5, 2**32 - 1]], dtype=np.uint32) mapping["x"] = np.array([0.0, 1.0, 4.0]) mapping["y"] = np.array([0.0, 2.0]) + detector = entry.create_group("detector") + detector.attrs.update({"NX_class": "NXdata", "signal": "frame"}) + detector.attrs["axes"] = np.array([b".", b"x"]) + detector["frame"] = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.uint16) + detector["x"] = np.array([4.0, 2.0, 0.0]) def test_nexus_import_and_workspace() -> None: @@ -50,22 +58,27 @@ def test_nexus_import_and_workspace() -> None: h5io = win.h5inputoutput h5io.import_dataset_from_file(nxsname, "/entry/spectrum") h5io.import_dataset_from_file(nxsname, "/entry/map") + h5io.import_dataset_from_file(nxsname, "/entry/detector") signal = win.signalpanel.objmodel.get_all_objects()[0] - image = win.imagepanel.objmodel.get_all_objects()[0] + image, detector = win.imagepanel.objmodel.get_all_objects() assert np.array_equal(signal.x, [7.1, 7.2, 7.4]) assert signal.y[1] == 2**31 + 5 assert signal.xunit == "keV" assert image.data[1, 2] == 2**32 - 1 assert np.array_equal(image.xcoords, [0.0, 1.0, 4.0]) + # Decreasing axis: reversed with the image, as PlotPy needs + assert (detector.x0, detector.dx) == (0.0, 2.0) + assert np.array_equal(detector.data, [[3, 2, 1], [6, 5, 4]]) win.save_h5_workspace(wsname) os.remove(nxsname) win.load_h5_workspace([wsname], reset_all=True) signal = win.signalpanel.objmodel.get_all_objects()[0] - image = win.imagepanel.objmodel.get_all_objects()[0] + image, detector = win.imagepanel.objmodel.get_all_objects() assert np.array_equal(signal.x, [7.1, 7.2, 7.4]) assert image.data[1, 2] == 2**32 - 1 assert np.array_equal(image.xcoords, [0.0, 1.0, 4.0]) + assert np.array_equal(detector.data, [[3, 2, 1], [6, 5, 4]]) source = get_experimental_source(signal) assert source["kind"] == "nexus" assert source["location"] == nxsname @@ -75,6 +88,14 @@ def test_nexus_import_and_workspace() -> None: "signal": "counts", "axes": ["y", "x"], } + source = get_experimental_source(detector) + assert source["selection"] == {"signal": "frame", "axes": [".", "x"]} + assert source["conversions"] == ["axis 'x': reversed to increasing order"] + # The reloaded origin keeps its JSON types, so it can be stored again + for obj in (signal, image, detector): + stored = obj.metadata["experimental_source"] + assert type(stored["version"]) is int + json.dumps(stored) if __name__ == "__main__": diff --git a/doc/features/advanced/plugins.rst b/doc/features/advanced/plugins.rst index abd12e938..07299e404 100644 --- a/doc/features/advanced/plugins.rst +++ b/doc/features/advanced/plugins.rst @@ -792,7 +792,7 @@ DataLab edits :attr:`~datalab.plugins.instruments.PluginInstrument.settings` in DataLab creates the instrument once per tool and keeps it until the plugins are reloaded, so the settings remain from one opening to the next. -DataLab Desktop calls the instrument outside the user interface thread, one call at a time, and changes its settings only between two calls: a slow instrument, such as a device waiting for its data, does not freeze DataLab. The optional :meth:`~datalab.plugins.instruments.PluginInstrument.open` and :meth:`~datalab.plugins.instruments.PluginInstrument.close` methods are called when the window opens and closes, including when plugins are reloaded or DataLab quits, for example to connect to a device and to release it. ``close`` is called even if ``open`` failed, and must release only what is still held. Data received by the instrument in its own threads, such as device events, is kept by the instrument for its next ``preview`` or ``acquire`` call. +DataLab Desktop calls the instrument outside the user interface thread, one call at a time, even when its window is closed and opened again while a call is running, and changes its settings only between two calls: a slow instrument, such as a device waiting for its data, does not freeze DataLab. The optional :meth:`~datalab.plugins.instruments.PluginInstrument.open` and :meth:`~datalab.plugins.instruments.PluginInstrument.close` methods are called when the window opens and closes, including when plugins are reloaded or DataLab quits, for example to connect to a device and to release it. If ``open`` raises an error, its message stays in the window and the instrument is not called: the next refresh or acquisition, for example after a settings change, calls ``open`` again first. ``close`` is called even if ``open`` failed, and must release only what is still held. Data received by the instrument in its own threads, such as device events, is kept by the instrument for its next ``preview`` or ``acquire`` call. Instrument connectors should attach an ``experimental_source`` metadata entry to acquired objects before returning them; DataLab does not fill it automatically. Use Sigima's ``make_experimental_source`` and ``set_experimental_source`` helpers to record the device address, attribute, selection and timestamps as appropriate. This identifies the measurement origin, not a reproducible acquisition. See the `Sigima experimental data guide `_ for an example and the portable metadata format. diff --git a/doc/features/common/h5browser.rst b/doc/features/common/h5browser.rst index 144f318d0..18f7cdd1c 100644 --- a/doc/features/common/h5browser.rst +++ b/doc/features/common/h5browser.rst @@ -43,9 +43,13 @@ coordinates. Signal Y uncertainties are imported when supplied; image uncertainties are not imported. Histogram axes supplied as bin edges are converted to bin centers, and this conversion is recorded. -An axis with an invalid shape or length is reported in the group description -and prevents importing the group. An undeclared axis uses point or pixel -indices; the importer does not infer a physical axis from neighbouring datasets. +An axis with an invalid shape or length, an ``axes`` attribute that does not +give one axis per dimension, or an axis that does not hold numbers is reported +in the group description and prevents importing the group. An undeclared axis +uses point or pixel indices; the importer does not infer a physical axis from +neighbouring datasets. Images need increasing coordinates: a decreasing image +axis is reversed together with the image, and this conversion is recorded, +while an image axis that is not strictly monotonic prevents the import. Files with several counters or incomplete NeXus descriptions can be interpreted by a plugin-provided data mapping. Each mapped object appears as an additional @@ -61,13 +65,14 @@ Listing numeric datasets uses their shape and type without loading all their values. Textual value previews read only the first elements, and attributes containing more than 10 000 values are not copied to imported object metadata. Graphical previews are read in a background thread; only the preview of the -last selected item is displayed. +last selected item is displayed, and items selected meanwhile are not read. -Large ordinary 1D arrays and 2D images are previewed with a sampling step above -``2**24`` elements (about 16.8 million). Import still reads every value, not the -sampled preview. This is not a universal memory limit: NeXus and mapped objects, -compound datasets and signals stored as 2D tables still use complete objects -for their graphical previews. Closing a file waits for its preview read to end. +Large ordinary 1D arrays and 2D images, NeXus and mapped objects are previewed +with a sampling step above ``2**24`` elements (about 16.8 million). Import still +reads every value, not the sampled preview. This is not a universal memory +limit: compound datasets and signals stored as 2D tables still use complete +objects for their graphical previews. Closing a file or the browser waits for +its preview read to end. Datasets with three or more dimensions, such as image stacks and spectral cubes, are listed but cannot be imported as complete objects. There is no @@ -81,11 +86,13 @@ Unsigned 32-bit detector counts above 2 147 483 647 are imported as ``float64`` images without clipping or rounding. Other integer arrays are also converted to an exact supported type when possible. If conversion to floating point cannot preserve every integer (in particular beyond ``+/-2**53`` for ``float64``), -Sigima emits a warning instead of silently promising exact values. +Sigima emits a warning instead of silently promising exact values. Complex +signals keep their imaginary part, including single-precision ones. NeXus and mapped objects carry an ``experimental_source`` metadata entry -describing the source file, group, measured dataset, axes and recorded -conversions. Mapped objects also identify their mapping. This entry describes +describing the source file, group, measured dataset, axes (``"."`` for a +dimension without axis) and recorded conversions. Mapped objects also identify +their mapping. This entry describes where the measurement came from; it is not the processing provenance shown in the Properties panel and does not make the experiment reproducible. diff --git a/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po b/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po index 6b8c14a6e..41dc1b423 100644 --- a/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po +++ b/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po @@ -633,8 +633,8 @@ msgstr "DataLab modifie :attr:`~datalab.plugins.instruments.PluginInstrument.set msgid "DataLab creates the instrument once per tool and keeps it until the plugins are reloaded, so the settings remain from one opening to the next." msgstr "DataLab crĂ©e l'instrument une seule fois par outil et le conserve jusqu'au rechargement des plugins : les rĂ©glages sont ainsi conservĂ©s d'une ouverture Ă  l'autre." -msgid "DataLab Desktop calls the instrument outside the user interface thread, one call at a time, and changes its settings only between two calls: a slow instrument, such as a device waiting for its data, does not freeze DataLab. The optional :meth:`~datalab.plugins.instruments.PluginInstrument.open` and :meth:`~datalab.plugins.instruments.PluginInstrument.close` methods are called when the window opens and closes, including when plugins are reloaded or DataLab quits, for example to connect to a device and to release it. ``close`` is called even if ``open`` failed, and must release only what is still held. Data received by the instrument in its own threads, such as device events, is kept by the instrument for its next ``preview`` or ``acquire`` call." -msgstr "DataLab Desktop appelle l'instrument en dehors du fil d'exĂ©cution de l'interface utilisateur, un appel Ă  la fois, et ne modifie ses rĂ©glages qu'entre deux appels : un instrument lent, comme un appareil attendant ses donnĂ©es, ne fige pas DataLab. Les mĂ©thodes facultatives :meth:`~datalab.plugins.instruments.PluginInstrument.open` et :meth:`~datalab.plugins.instruments.PluginInstrument.close` sont appelĂ©es Ă  l'ouverture et Ă  la fermeture de la fenĂȘtre, y compris lors du rechargement des plugins ou de la fermeture de DataLab, par exemple pour se connecter Ă  un appareil puis le libĂ©rer. ``close`` est appelĂ©e mĂȘme si ``open`` a Ă©chouĂ©, et ne doit libĂ©rer que ce qui est encore dĂ©tenu. Les donnĂ©es reçues par l'instrument dans ses propres fils d'exĂ©cution, comme les Ă©vĂ©nements d'un appareil, sont conservĂ©es par l'instrument jusqu'Ă  son prochain appel de ``preview`` ou ``acquire``." +msgid "DataLab Desktop calls the instrument outside the user interface thread, one call at a time, even when its window is closed and opened again while a call is running, and changes its settings only between two calls: a slow instrument, such as a device waiting for its data, does not freeze DataLab. The optional :meth:`~datalab.plugins.instruments.PluginInstrument.open` and :meth:`~datalab.plugins.instruments.PluginInstrument.close` methods are called when the window opens and closes, including when plugins are reloaded or DataLab quits, for example to connect to a device and to release it. If ``open`` raises an error, its message stays in the window and the instrument is not called: the next refresh or acquisition, for example after a settings change, calls ``open`` again first. ``close`` is called even if ``open`` failed, and must release only what is still held. Data received by the instrument in its own threads, such as device events, is kept by the instrument for its next ``preview`` or ``acquire`` call." +msgstr "DataLab Desktop appelle l'instrument en dehors du fil d'exĂ©cution de l'interface utilisateur, un appel Ă  la fois, mĂȘme lorsque sa fenĂȘtre est fermĂ©e puis rouverte pendant un appel, et ne modifie ses rĂ©glages qu'entre deux appels : un instrument lent, comme un appareil attendant ses donnĂ©es, ne fige pas DataLab. Les mĂ©thodes facultatives :meth:`~datalab.plugins.instruments.PluginInstrument.open` et :meth:`~datalab.plugins.instruments.PluginInstrument.close` sont appelĂ©es Ă  l'ouverture et Ă  la fermeture de la fenĂȘtre, y compris lors du rechargement des plugins ou de la fermeture de DataLab, par exemple pour se connecter Ă  un appareil puis le libĂ©rer. Si ``open`` lĂšve une erreur, son message reste affichĂ© dans la fenĂȘtre et l'instrument n'est pas appelĂ© : le rafraĂźchissement ou l'acquisition suivant, par exemple aprĂšs une modification des rĂ©glages, appelle d'abord ``open`` Ă  nouveau. ``close`` est appelĂ©e mĂȘme si ``open`` a Ă©chouĂ©, et ne doit libĂ©rer que ce qui est encore dĂ©tenu. Les donnĂ©es reçues par l'instrument dans ses propres fils d'exĂ©cution, comme les Ă©vĂ©nements d'un appareil, sont conservĂ©es par l'instrument jusqu'Ă  son prochain appel de ``preview`` ou ``acquire``." msgid "Instrument connectors should attach an ``experimental_source`` metadata entry to acquired objects before returning them; DataLab does not fill it automatically. Use Sigima's ``make_experimental_source`` and ``set_experimental_source`` helpers to record the device address, attribute, selection and timestamps as appropriate. This identifies the measurement origin, not a reproducible acquisition. See the `Sigima experimental data guide `_ for an example and the portable metadata format." msgstr "Les connecteurs d'instruments doivent attacher une entrĂ©e de mĂ©tadonnĂ©es ``experimental_source`` aux objets acquis avant de les renvoyer ; DataLab ne la remplit pas automatiquement. Utilisez les fonctions ``make_experimental_source`` et ``set_experimental_source`` de Sigima pour enregistrer, selon le cas, l'adresse de l'appareil, l'attribut, la sĂ©lection et les horodatages. Cela identifie l'origine de la mesure, pas une acquisition reproductible. Le `guide des donnĂ©es expĂ©rimentales de Sigima `_ fournit un exemple et dĂ©crit le format portable des mĂ©tadonnĂ©es." @@ -1585,8 +1585,8 @@ msgstr "Instrument ouvert par un outil de plugin." msgid "DataLab writes the edited values into :attr:`settings` before calling :meth:`preview` or :meth:`acquire`. Both methods raise ``ValueError`` with a user-facing message when the settings cannot be used. The plugin keeps one instance per tool during the session, so settings are kept from one opening to the next." msgstr "DataLab Ă©crit les valeurs modifiĂ©es dans :attr:`settings` avant d'appeler :meth:`preview` ou :meth:`acquire`. Les deux mĂ©thodes lĂšvent ``ValueError`` avec un message destinĂ© Ă  l'utilisateur lorsque les rĂ©glages ne peuvent pas ĂȘtre utilisĂ©s. Le plugin conserve une instance par outil pendant la session : les rĂ©glages sont ainsi conservĂ©s d'une ouverture Ă  l'autre." -msgid "DataLab calls :meth:`open` when the instrument window opens and :meth:`close` when it closes (including when the plugin is reloaded or DataLab quits). It calls these methods, :meth:`preview` and :meth:`acquire` one at a time, outside the GUI thread, and writes :attr:`settings` only between two calls: a slow instrument does not freeze DataLab. Data received by the instrument in its own threads (e.g. device events) must be stored for the next :meth:`preview` or :meth:`acquire` call." -msgstr "DataLab appelle :meth:`open` Ă  l'ouverture de la fenĂȘtre de l'instrument et :meth:`close` Ă  sa fermeture (y compris lorsque le plugin est rechargĂ© ou que DataLab se ferme). Il appelle ces mĂ©thodes, :meth:`preview` et :meth:`acquire` une Ă  la fois, en dehors du fil d'exĂ©cution de l'interface graphique, et ne modifie :attr:`settings` qu'entre deux appels : un instrument lent ne fige pas DataLab. Les donnĂ©es reçues par l'instrument dans ses propres fils d'exĂ©cution (par exemple les Ă©vĂ©nements d'un appareil) doivent ĂȘtre conservĂ©es jusqu'au prochain appel de :meth:`preview` ou :meth:`acquire`." +msgid "DataLab calls :meth:`open` when the instrument window opens and :meth:`close` when it closes (including when the plugin is reloaded or DataLab quits). It calls these methods, :meth:`preview` and :meth:`acquire` one at a time, outside the GUI thread, even when the window is closed and opened again while a call is running, and writes :attr:`settings` only between two calls: a slow instrument does not freeze DataLab. Data received by the instrument in its own threads (e.g. device events) must be stored for the next :meth:`preview` or :meth:`acquire` call." +msgstr "DataLab appelle :meth:`open` Ă  l'ouverture de la fenĂȘtre de l'instrument et :meth:`close` Ă  sa fermeture (y compris lorsque le plugin est rechargĂ© ou que DataLab se ferme). Il appelle ces mĂ©thodes, :meth:`preview` et :meth:`acquire` une Ă  la fois, en dehors du fil d'exĂ©cution de l'interface graphique, mĂȘme lorsque la fenĂȘtre est fermĂ©e puis rouverte pendant un appel, et ne modifie :attr:`settings` qu'entre deux appels : un instrument lent ne fige pas DataLab. Les donnĂ©es reçues par l'instrument dans ses propres fils d'exĂ©cution (par exemple les Ă©vĂ©nements d'un appareil) doivent ĂȘtre conservĂ©es jusqu'au prochain appel de :meth:`preview` ou :meth:`acquire`." msgid "instrument settings, edited by DataLab" msgstr "rĂ©glages de l'instrument, modifiĂ©s par DataLab" @@ -1597,8 +1597,8 @@ msgstr "Intervalle entre deux images en mode direct, en millisecondes" msgid "Prepare the instrument when its window opens (e.g. connect to it)." msgstr "PrĂ©parer l'instrument Ă  l'ouverture de sa fenĂȘtre (par exemple s'y connecter)." -msgid "Raises ``ValueError`` with a user-facing message if the instrument cannot be used. Does nothing by default." -msgstr "LĂšve ``ValueError`` avec un message destinĂ© Ă  l'utilisateur si l'instrument ne peut pas ĂȘtre utilisĂ©. Ne fait rien par dĂ©faut." +msgid "Raises ``ValueError`` with a user-facing message if the instrument cannot be used: DataLab shows this message, and does not call :meth:`preview` or :meth:`acquire` until a new request (e.g. after a settings change) opens the instrument successfully. Does nothing by default." +msgstr "LĂšve ``ValueError`` avec un message destinĂ© Ă  l'utilisateur si l'instrument ne peut pas ĂȘtre utilisĂ© : DataLab affiche ce message, et n'appelle pas :meth:`preview` ou :meth:`acquire` tant qu'une nouvelle demande (par exemple aprĂšs une modification des rĂ©glages) n'a pas ouvert l'instrument avec succĂšs. Ne fait rien par dĂ©faut." msgid "Release the instrument when its window closes (e.g. disconnect)." msgstr "LibĂ©rer l'instrument Ă  la fermeture de sa fenĂȘtre (par exemple s'en dĂ©connecter)." diff --git a/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po b/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po index a6dd86e18..d98642720 100644 --- a/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po +++ b/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po @@ -58,8 +58,8 @@ msgstr "Ouvrez les fichiers ``.nxs`` et ``.nx5`` avec **Parcourir un fichier HDF msgid "The import preserves axis values, labels and units, including non-uniform image coordinates. Signal Y uncertainties are imported when supplied; image uncertainties are not imported. Histogram axes supplied as bin edges are converted to bin centers, and this conversion is recorded." msgstr "L'import conserve les valeurs, libellĂ©s et unitĂ©s des axes, y compris les coordonnĂ©es d'image non uniformes. Les incertitudes Y des signaux sont importĂ©es lorsqu'elles sont fournies ; les incertitudes des images ne le sont pas. Les axes d'histogramme fournis sous forme de bords de classes sont convertis en centres de classes, et cette conversion est enregistrĂ©e." -msgid "An axis with an invalid shape or length is reported in the group description and prevents importing the group. An undeclared axis uses point or pixel indices; the importer does not infer a physical axis from neighbouring datasets." -msgstr "Un axe dont la forme ou la longueur est invalide est signalĂ© dans la description du groupe et empĂȘche son import. Un axe non dĂ©clarĂ© utilise les indices des points ou pixels ; l'importeur ne dĂ©duit pas d'axe physique Ă  partir des jeux de donnĂ©es voisins." +msgid "An axis with an invalid shape or length, an ``axes`` attribute that does not give one axis per dimension, or an axis that does not hold numbers is reported in the group description and prevents importing the group. An undeclared axis uses point or pixel indices; the importer does not infer a physical axis from neighbouring datasets. Images need increasing coordinates: a decreasing image axis is reversed together with the image, and this conversion is recorded, while an image axis that is not strictly monotonic prevents the import." +msgstr "Un axe dont la forme ou la longueur est invalide, un attribut ``axes`` qui ne donne pas un axe par dimension ou un axe qui ne contient pas de nombres est signalĂ© dans la description du groupe et empĂȘche son import. Un axe non dĂ©clarĂ© utilise les indices des points ou pixels ; l'importeur ne dĂ©duit pas d'axe physique Ă  partir des jeux de donnĂ©es voisins. Les images nĂ©cessitent des coordonnĂ©es croissantes : un axe d'image dĂ©croissant est inversĂ© avec l'image, et cette conversion est enregistrĂ©e, tandis qu'un axe d'image qui n'est pas strictement monotone empĂȘche l'import." msgid "Files with several counters or incomplete NeXus descriptions can be interpreted by a plugin-provided data mapping. Each mapped object appears as an additional item with its own axes, labels and units. Mappings must be registered before opening the file; DataLab does not automatically discover JSON files beside it. Plugin authors can follow the `SigimaX HDF5 import guide `_." msgstr "Les fichiers contenant plusieurs compteurs ou des descriptions NeXus incomplĂštes peuvent ĂȘtre interprĂ©tĂ©s par un profil de correspondance fourni par un plugin. Chaque objet dĂ©crit apparaĂźt comme un Ă©lĂ©ment supplĂ©mentaire avec ses propres axes, libellĂ©s et unitĂ©s. Les profils doivent ĂȘtre enregistrĂ©s avant l'ouverture du fichier ; DataLab ne dĂ©couvre pas automatiquement les fichiers JSON placĂ©s Ă  cĂŽtĂ©. Les auteurs de plugins peuvent suivre le `guide d'import HDF5 de SigimaX `_." @@ -67,11 +67,11 @@ msgstr "Les fichiers contenant plusieurs compteurs ou des descriptions NeXus inc msgid "Large datasets and previews" msgstr "Grands jeux de donnĂ©es et aperçus" -msgid "Listing numeric datasets uses their shape and type without loading all their values. Textual value previews read only the first elements, and attributes containing more than 10 000 values are not copied to imported object metadata. Graphical previews are read in a background thread; only the preview of the last selected item is displayed." -msgstr "La liste des jeux de donnĂ©es numĂ©riques utilise leur forme et leur type sans charger toutes leurs valeurs. Les aperçus textuels ne lisent que les premiers Ă©lĂ©ments, et les attributs contenant plus de 10 000 valeurs ne sont pas copiĂ©s dans les mĂ©tadonnĂ©es des objets importĂ©s. Les aperçus graphiques sont lus dans un fil d'exĂ©cution en arriĂšre-plan ; seul l'aperçu du dernier Ă©lĂ©ment sĂ©lectionnĂ© est affichĂ©." +msgid "Listing numeric datasets uses their shape and type without loading all their values. Textual value previews read only the first elements, and attributes containing more than 10 000 values are not copied to imported object metadata. Graphical previews are read in a background thread; only the preview of the last selected item is displayed, and items selected meanwhile are not read." +msgstr "La liste des jeux de donnĂ©es numĂ©riques utilise leur forme et leur type sans charger toutes leurs valeurs. Les aperçus textuels ne lisent que les premiers Ă©lĂ©ments, et les attributs contenant plus de 10 000 valeurs ne sont pas copiĂ©s dans les mĂ©tadonnĂ©es des objets importĂ©s. Les aperçus graphiques sont lus dans un fil d'exĂ©cution en arriĂšre-plan ; seul l'aperçu du dernier Ă©lĂ©ment sĂ©lectionnĂ© est affichĂ©, et les Ă©lĂ©ments sĂ©lectionnĂ©s entre-temps ne sont pas lus." -msgid "Large ordinary 1D arrays and 2D images are previewed with a sampling step above ``2**24`` elements (about 16.8 million). Import still reads every value, not the sampled preview. This is not a universal memory limit: NeXus and mapped objects, compound datasets and signals stored as 2D tables still use complete objects for their graphical previews. Closing a file waits for its preview read to end." -msgstr "Les grands tableaux 1D ordinaires et les images 2D sont prĂ©visualisĂ©s avec un pas d'Ă©chantillonnage au-delĂ  de ``2**24`` Ă©lĂ©ments (environ 16,8 millions). L'import lit toujours toutes les valeurs, pas l'aperçu Ă©chantillonnĂ©. Ce n'est pas une limite mĂ©moire universelle : les objets NeXus et dĂ©crits par un profil, les jeux de donnĂ©es composites et les signaux stockĂ©s dans des tableaux 2D utilisent encore des objets complets pour leurs aperçus graphiques. La fermeture d'un fichier attend la fin de la lecture de son aperçu." +msgid "Large ordinary 1D arrays and 2D images, NeXus and mapped objects are previewed with a sampling step above ``2**24`` elements (about 16.8 million). Import still reads every value, not the sampled preview. This is not a universal memory limit: compound datasets and signals stored as 2D tables still use complete objects for their graphical previews. Closing a file or the browser waits for its preview read to end." +msgstr "Les grands tableaux 1D ordinaires et les images 2D, les objets NeXus et dĂ©crits par un profil sont prĂ©visualisĂ©s avec un pas d'Ă©chantillonnage au-delĂ  de ``2**24`` Ă©lĂ©ments (environ 16,8 millions). L'import lit toujours toutes les valeurs, pas l'aperçu Ă©chantillonnĂ©. Ce n'est pas une limite mĂ©moire universelle : les jeux de donnĂ©es composites et les signaux stockĂ©s dans des tableaux 2D utilisent encore des objets complets pour leurs aperçus graphiques. La fermeture d'un fichier ou de l'explorateur attend la fin de la lecture de son aperçu." msgid "Datasets with three or more dimensions, such as image stacks and spectral cubes, are listed but cannot be imported as complete objects. There is no graphical slice selector yet. Developers can read 1D or 2D views without loading the whole cube using the selection API described in the SigimaX guide." msgstr "Les jeux de donnĂ©es Ă  trois dimensions ou plus, comme les piles d'images et les cubes spectraux, sont listĂ©s mais ne peuvent pas ĂȘtre importĂ©s comme objets complets. Il n'existe pas encore de sĂ©lecteur graphique de coupes. Les dĂ©veloppeurs peuvent lire des vues 1D ou 2D sans charger tout le cube avec l'API de sĂ©lection dĂ©crite dans le guide de SigimaX." @@ -79,11 +79,11 @@ msgstr "Les jeux de donnĂ©es Ă  trois dimensions ou plus, comme les piles d'imag msgid "Numerical values and experimental origin" msgstr "Valeurs numĂ©riques et origine expĂ©rimentale" -msgid "Unsigned 32-bit detector counts above 2 147 483 647 are imported as ``float64`` images without clipping or rounding. Other integer arrays are also converted to an exact supported type when possible. If conversion to floating point cannot preserve every integer (in particular beyond ``+/-2**53`` for ``float64``), Sigima emits a warning instead of silently promising exact values." -msgstr "Les comptages de dĂ©tecteur entiers non signĂ©s sur 32 bits au-delĂ  de 2 147 483 647 sont importĂ©s comme images ``float64`` sans Ă©crĂȘtage ni arrondi. Les autres tableaux entiers sont aussi convertis vers un type pris en charge exact lorsque c'est possible. Si la conversion en flottant ne peut pas prĂ©server chaque entier (notamment au-delĂ  de ``+/-2**53`` pour ``float64``), Sigima Ă©met un avertissement au lieu de promettre silencieusement des valeurs exactes." +msgid "Unsigned 32-bit detector counts above 2 147 483 647 are imported as ``float64`` images without clipping or rounding. Other integer arrays are also converted to an exact supported type when possible. If conversion to floating point cannot preserve every integer (in particular beyond ``+/-2**53`` for ``float64``), Sigima emits a warning instead of silently promising exact values. Complex signals keep their imaginary part, including single-precision ones." +msgstr "Les comptages de dĂ©tecteur entiers non signĂ©s sur 32 bits au-delĂ  de 2 147 483 647 sont importĂ©s comme images ``float64`` sans Ă©crĂȘtage ni arrondi. Les autres tableaux entiers sont aussi convertis vers un type pris en charge exact lorsque c'est possible. Si la conversion en flottant ne peut pas prĂ©server chaque entier (notamment au-delĂ  de ``+/-2**53`` pour ``float64``), Sigima Ă©met un avertissement au lieu de promettre silencieusement des valeurs exactes. Les signaux complexes conservent leur partie imaginaire, y compris en simple prĂ©cision." -msgid "NeXus and mapped objects carry an ``experimental_source`` metadata entry describing the source file, group, measured dataset, axes and recorded conversions. Mapped objects also identify their mapping. This entry describes where the measurement came from; it is not the processing provenance shown in the Properties panel and does not make the experiment reproducible." -msgstr "Les objets NeXus et dĂ©crits par un profil portent une entrĂ©e de mĂ©tadonnĂ©es ``experimental_source`` dĂ©crivant le fichier source, le groupe, le jeu de donnĂ©es mesurĂ©es, les axes et les conversions enregistrĂ©es. Les objets dĂ©crits par un profil identifient aussi celui-ci. Cette entrĂ©e dĂ©crit l'origine de la mesure ; ce n'est pas la provenance des traitements affichĂ©e dans le panneau PropriĂ©tĂ©s et elle ne rend pas l'expĂ©rience reproductible." +msgid "NeXus and mapped objects carry an ``experimental_source`` metadata entry describing the source file, group, measured dataset, axes (``\".\"`` for a dimension without axis) and recorded conversions. Mapped objects also identify their mapping. This entry describes where the measurement came from; it is not the processing provenance shown in the Properties panel and does not make the experiment reproducible." +msgstr "Les objets NeXus et dĂ©crits par un profil portent une entrĂ©e de mĂ©tadonnĂ©es ``experimental_source`` dĂ©crivant le fichier source, le groupe, le jeu de donnĂ©es mesurĂ©es, les axes (``\".\"`` pour une dimension sans axe) et les conversions enregistrĂ©es. Les objets dĂ©crits par un profil identifient aussi celui-ci. Cette entrĂ©e dĂ©crit l'origine de la mesure ; ce n'est pas la provenance des traitements affichĂ©e dans le panneau PropriĂ©tĂ©s et elle ne rend pas l'expĂ©rience reproductible." msgid "Save the imported objects in a DataLab workspace to keep both their data and this metadata. Reopening that workspace does not require the original NeXus file or the mapping plugin. Generic raw dataset imports do not automatically receive this structured experimental source." msgstr "Enregistrez les objets importĂ©s dans un espace de travail DataLab pour conserver leurs donnĂ©es et ces mĂ©tadonnĂ©es. La rĂ©ouverture de cet espace de travail ne nĂ©cessite ni le fichier NeXus original ni le plugin du profil. Les imports gĂ©nĂ©riques de jeux de donnĂ©es bruts ne reçoivent pas automatiquement cette description structurĂ©e de leur origine expĂ©rimentale." diff --git a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po index dc220935b..98b8b3c65 100644 --- a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po +++ b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po @@ -90,8 +90,8 @@ msgstr "Les outils de plugin apparaissent aussi dans le sous-menu du plugin du m msgid "Application plugins may provide instruments, such as simulated cameras or oscilloscopes, without writing any user interface: DataLab shows a window with a live view and the instrument settings, refreshed as settings change or continuously in **Live** mode, and **Acquire** adds the acquired objects to a new group, ready for the application's methods" msgstr "Les plugins d'application peuvent fournir des instruments, comme des camĂ©ras ou des oscilloscopes simulĂ©s, sans Ă©crire d'interface utilisateur : DataLab affiche une fenĂȘtre avec une vue en direct et les rĂ©glages de l'instrument, rafraĂźchie Ă  chaque modification des rĂ©glages ou en continu en mode **En direct**, et **AcquĂ©rir** ajoute les objets acquis dans un nouveau groupe, prĂȘts pour les mĂ©thodes de l'application" -msgid "Instruments are called outside the user interface thread, one call at a time: a slow instrument, such as a real device waiting for its data, no longer freezes DataLab, and repeated refresh requests made meanwhile are merged. Instruments may also declare `open` and `close` methods, called when their window opens and closes (including when plugins are reloaded or DataLab quits), for example to connect to a device and to release it" -msgstr "Les instruments sont appelĂ©s en dehors du fil d'exĂ©cution de l'interface utilisateur, un appel Ă  la fois : un instrument lent, comme un appareil rĂ©el attendant ses donnĂ©es, ne fige plus DataLab, et les demandes de rafraĂźchissement rĂ©pĂ©tĂ©es entre-temps sont regroupĂ©es. Les instruments peuvent aussi dĂ©clarer des mĂ©thodes `open` et `close`, appelĂ©es Ă  l'ouverture et Ă  la fermeture de leur fenĂȘtre (y compris lors du rechargement des plugins ou de la fermeture de DataLab), par exemple pour se connecter Ă  un appareil puis le libĂ©rer" +msgid "Instruments are called outside the user interface thread, one call at a time: a slow instrument, such as a real device waiting for its data, no longer freezes DataLab, and repeated refresh requests made meanwhile are merged. Instruments may also declare `open` and `close` methods, called when their window opens and closes (including when plugins are reloaded or DataLab quits), for example to connect to a device and to release it. Calls stay one at a time when a window is closed and opened again while a call is running. When `open` fails, its message stays in the window and the instrument is opened again before the next refresh or acquisition, for example after a settings change" +msgstr "Les instruments sont appelĂ©s en dehors du fil d'exĂ©cution de l'interface utilisateur, un appel Ă  la fois : un instrument lent, comme un appareil rĂ©el attendant ses donnĂ©es, ne fige plus DataLab, et les demandes de rafraĂźchissement rĂ©pĂ©tĂ©es entre-temps sont regroupĂ©es. Les instruments peuvent aussi dĂ©clarer des mĂ©thodes `open` et `close`, appelĂ©es Ă  l'ouverture et Ă  la fermeture de leur fenĂȘtre (y compris lors du rechargement des plugins ou de la fermeture de DataLab), par exemple pour se connecter Ă  un appareil puis le libĂ©rer. Les appels restent successifs lorsqu'une fenĂȘtre est fermĂ©e puis rouverte pendant un appel. Lorsque `open` Ă©choue, son message reste affichĂ© dans la fenĂȘtre et l'instrument est ouvert Ă  nouveau avant le rafraĂźchissement ou l'acquisition suivant, par exemple aprĂšs une modification des rĂ©glages" msgid "Plugin developers describe recipe inputs declaratively (titles, minimum counts, metadata requirements) and may add binding suggestions and fast input checks; every recipe then runs through DataLab's generic launcher, without plugin-specific input dialogs" msgstr "Les dĂ©veloppeurs de plugins dĂ©crivent les entrĂ©es des recettes de maniĂšre dĂ©clarative (titres, effectifs minimaux, mĂ©tadonnĂ©es requises) et peuvent ajouter des suggestions d'affectation et des vĂ©rifications rapides des entrĂ©es ; chaque recette s'exĂ©cute alors via le lanceur gĂ©nĂ©rique de DataLab, sans boĂźte de dialogue d'entrĂ©es propre au plugin" @@ -297,8 +297,8 @@ msgstr "`add_group` renvoie dĂ©sormais l'UUID du groupe créé, de sorte qu'un c msgid "**NeXus files:**" msgstr "**Fichiers NeXus :**" -msgid "NeXus `NXdata` groups are shown as single items in the HDF5 browser and imported as signals or images with their axes, units, labels and, for signals, uncertainties. Bin edges are converted to bin centers, and non-uniform image axes are kept. Invalid axes are reported in the browser instead of being guessed" -msgstr "Les groupes NeXus `NXdata` sont prĂ©sentĂ©s comme des Ă©lĂ©ments uniques dans l'explorateur HDF5 et importĂ©s comme signaux ou images avec leurs axes, unitĂ©s, libellĂ©s et, pour les signaux, incertitudes. Les bords de classes sont convertis en centres de classes, et les axes d'image non uniformes sont conservĂ©s. Les axes invalides sont signalĂ©s dans l'explorateur au lieu d'ĂȘtre devinĂ©s" +msgid "NeXus `NXdata` groups are shown as single items in the HDF5 browser and imported as signals or images with their axes, units, labels and, for signals, uncertainties. Bin edges are converted to bin centers, non-uniform image axes are kept, and decreasing image axes are reversed with the image. Invalid axes are reported in the browser instead of being guessed" +msgstr "Les groupes NeXus `NXdata` sont prĂ©sentĂ©s comme des Ă©lĂ©ments uniques dans l'explorateur HDF5 et importĂ©s comme signaux ou images avec leurs axes, unitĂ©s, libellĂ©s et, pour les signaux, incertitudes. Les bords de classes sont convertis en centres de classes, les axes d'image non uniformes sont conservĂ©s, et les axes d'image dĂ©croissants sont inversĂ©s avec l'image. Les axes invalides sont signalĂ©s dans l'explorateur au lieu d'ĂȘtre devinĂ©s" msgid "Imported signals and images record where they come from (file, group, signal and axes, applied conversions) in their `experimental_source` metadata, which is saved with the workspace: a saved workspace reopens without the NeXus file" msgstr "Les signaux et images importĂ©s enregistrent leur origine (fichier, groupe, signal et axes, conversions appliquĂ©es) dans leur mĂ©tadonnĂ©e `experimental_source`, enregistrĂ©e avec l'espace de travail : un espace de travail enregistrĂ© se rouvre sans le fichier NeXus" @@ -357,8 +357,8 @@ msgstr "**Explorateur HDF5 :**" msgid "Browsing an HDF5 file no longer reads its datasets entirely: they are listed and described from their shape and data type, and the value previews read only their first elements. Large files open faster and use less memory; data is read when a signal or an image is previewed or imported" msgstr "Parcourir un fichier HDF5 ne lit plus ses jeux de donnĂ©es en entier : ils sont listĂ©s et dĂ©crits Ă  partir de leur forme et de leur type de donnĂ©es, et les aperçus de valeurs ne lisent que leurs premiers Ă©lĂ©ments. Les gros fichiers s'ouvrent plus vite et utilisent moins de mĂ©moire ; les donnĂ©es sont lues lorsqu'un signal ou une image est prĂ©visualisĂ© ou importĂ©" -msgid "The HDF5 browser stays responsive while a preview is read. Ordinary 1D arrays and 2D images above 2ÂČ⁎ values (about 16.8 million) are previewed with a step along each axis; NeXus and mapped objects, compound datasets and signals stored in 2D tables still use complete objects for graphical previews" -msgstr "L'explorateur HDF5 reste rĂ©actif pendant la lecture d'un aperçu. Les tableaux 1D ordinaires et les images 2D au-delĂ  de 2ÂČ⁎ valeurs (environ 16,8 millions) sont prĂ©visualisĂ©s avec un pas le long de chaque axe ; les objets NeXus et dĂ©crits par un profil, les jeux de donnĂ©es composites et les signaux stockĂ©s dans des tableaux 2D utilisent encore des objets complets pour leurs aperçus graphiques" +msgid "The HDF5 browser stays responsive while a preview is read, and items selected meanwhile are not read. Ordinary 1D arrays and 2D images, NeXus and mapped objects above 2ÂČ⁎ values (about 16.8 million) are previewed with a step along each axis; compound datasets and signals stored in 2D tables still use complete objects for graphical previews" +msgstr "L'explorateur HDF5 reste rĂ©actif pendant la lecture d'un aperçu, et les Ă©lĂ©ments sĂ©lectionnĂ©s entre-temps ne sont pas lus. Les tableaux 1D ordinaires et les images 2D, les objets NeXus et dĂ©crits par un profil au-delĂ  de 2ÂČ⁎ valeurs (environ 16,8 millions) sont prĂ©visualisĂ©s avec un pas le long de chaque axe ; les jeux de donnĂ©es composites et les signaux stockĂ©s dans des tableaux 2D utilisent encore des objets complets pour leurs aperçus graphiques" msgid "Datasets with three dimensions or more (image stacks, spectral cubes...) are now listed in the HDF5 browser, but are not directly importable and have no graphical slice selector" msgstr "Les jeux de donnĂ©es Ă  trois dimensions ou plus (piles d'images, cubes spectraux...) sont dĂ©sormais listĂ©s dans l'explorateur HDF5, mais ne sont pas directement importables et n'ont pas de sĂ©lecteur graphique de coupes" @@ -396,6 +396,9 @@ msgstr "**Import HDF5 :**" msgid "Fixed images with unsigned 32-bit values above 2 147 483 647 being clipped when imported from an HDF5 file (\"Clipping uint32 data to int32\" warning): such images are now imported as 64-bit floating-point images keeping every value exactly. Other integer images that no supported integer type can hold also use a sufficiently precise floating-point type when possible, instead of rounding to 32-bit floating-point values beyond 16 777 216; a warning is emitted when no supported type guarantees exact conversion (in particular beyond ±2⁔³ for float64)" msgstr "Correction de l'Ă©crĂȘtage des images contenant des valeurs entiĂšres non signĂ©es sur 32 bits supĂ©rieures Ă  2 147 483 647 lors de leur import depuis un fichier HDF5 (avertissement « Clipping uint32 data to int32 ») : ces images sont dĂ©sormais importĂ©es en flottants sur 64 bits en conservant chaque valeur exactement. Les autres images entiĂšres qu'aucun type entier pris en charge ne peut contenir utilisent aussi, lorsque c'est possible, un type flottant suffisamment prĂ©cis, au lieu d'ĂȘtre arrondies en flottants sur 32 bits au-delĂ  de 16 777 216 ; un avertissement est Ă©mis lorsqu'aucun type pris en charge ne garantit une conversion exacte (notamment au-delĂ  de ±2⁔³ pour float64)" +msgid "Fixed single-precision complex (`complex64`) 1D datasets losing their imaginary part when imported from an HDF5 file: they are now imported as complex signals. Integer signals beyond ±2⁔³ now also emit the precision warning instead of being rounded silently" +msgstr "Correction de la perte de la partie imaginaire des jeux de donnĂ©es 1D complexes en simple prĂ©cision (`complex64`) lors de leur import depuis un fichier HDF5 : ils sont dĂ©sormais importĂ©s comme des signaux complexes. Les signaux entiers au-delĂ  de ±2⁔³ Ă©mettent dĂ©sormais aussi l'avertissement de prĂ©cision au lieu d'ĂȘtre arrondis silencieusement" + msgid "**Metadata:**" msgstr "**MĂ©tadonnĂ©es :**" diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index e102e22df..22797b487 100644 --- a/doc/release_notes/release_1.04.md +++ b/doc/release_notes/release_1.04.md @@ -32,7 +32,7 @@ * Application plugins may also list tools, such as wizards or editors, in the catalog * Plugin tools also appear in the plugin's submenu of the **Plugins** menu, in the Signal or Image panel they work on. A tool that needs a selection is disabled until the right objects are selected, and its catalog button tells what to select * Application plugins may provide instruments, such as simulated cameras or oscilloscopes, without writing any user interface: DataLab shows a window with a live view and the instrument settings, refreshed as settings change or continuously in **Live** mode, and **Acquire** adds the acquired objects to a new group, ready for the application's methods -* Instruments are called outside the user interface thread, one call at a time: a slow instrument, such as a real device waiting for its data, no longer freezes DataLab, and repeated refresh requests made meanwhile are merged. Instruments may also declare `open` and `close` methods, called when their window opens and closes (including when plugins are reloaded or DataLab quits), for example to connect to a device and to release it +* Instruments are called outside the user interface thread, one call at a time: a slow instrument, such as a real device waiting for its data, no longer freezes DataLab, and repeated refresh requests made meanwhile are merged. Instruments may also declare `open` and `close` methods, called when their window opens and closes (including when plugins are reloaded or DataLab quits), for example to connect to a device and to release it. Calls stay one at a time when a window is closed and opened again while a call is running. When `open` fails, its message stays in the window and the instrument is opened again before the next refresh or acquisition, for example after a settings change * Plugin developers describe recipe inputs declaratively (titles, minimum counts, metadata requirements) and may add binding suggestions and fast input checks; every recipe then runs through DataLab's generic launcher, without plugin-specific input dialogs * The catalog is non-modal and remains open after starting a method or opening an example, so users may continue interacting with the DataLab workspace * The methods, tools and datasets of an application form an accordion: one section is open at a time, and a colored dot tells, without opening it, whether each method can run on the current selection @@ -118,7 +118,7 @@ DataLab now uses stable UUID-based object references across object trees, result **NeXus files:** -* NeXus `NXdata` groups are shown as single items in the HDF5 browser and imported as signals or images with their axes, units, labels and, for signals, uncertainties. Bin edges are converted to bin centers, and non-uniform image axes are kept. Invalid axes are reported in the browser instead of being guessed +* NeXus `NXdata` groups are shown as single items in the HDF5 browser and imported as signals or images with their axes, units, labels and, for signals, uncertainties. Bin edges are converted to bin centers, non-uniform image axes are kept, and decreasing image axes are reversed with the image. Invalid axes are reported in the browser instead of being guessed * Imported signals and images record where they come from (file, group, signal and axes, applied conversions) in their `experimental_source` metadata, which is saved with the workspace: a saved workspace reopens without the NeXus file * `.nxs` and `.nx5` files are proposed when opening HDF5 files * Plugins can describe the signals and images of files that NeXus `NXdata` groups designate incompletely (no axes, several counters...) with declarative data mappings (JSON) registered with `sigimax.h5.mapping.register_mapping`: the HDF5 browser then shows each described object as an extra item, imported with its axes, labels and units @@ -149,7 +149,7 @@ DataLab now uses stable UUID-based object references across object trees, result **HDF5 browser:** * Browsing an HDF5 file no longer reads its datasets entirely: they are listed and described from their shape and data type, and the value previews read only their first elements. Large files open faster and use less memory; data is read when a signal or an image is previewed or imported -* The HDF5 browser stays responsive while a preview is read. Ordinary 1D arrays and 2D images above 2ÂČ⁎ values (about 16.8 million) are previewed with a step along each axis; NeXus and mapped objects, compound datasets and signals stored in 2D tables still use complete objects for graphical previews +* The HDF5 browser stays responsive while a preview is read, and items selected meanwhile are not read. Ordinary 1D arrays and 2D images, NeXus and mapped objects above 2ÂČ⁎ values (about 16.8 million) are previewed with a step along each axis; compound datasets and signals stored in 2D tables still use complete objects for graphical previews * Datasets with three dimensions or more (image stacks, spectral cubes...) are now listed in the HDF5 browser, but are not directly importable and have no graphical slice selector * Dataset attributes with more than 10 000 values are no longer copied to the metadata of imported objects @@ -170,6 +170,7 @@ See the [HDF5 browser guide](../features/common/h5browser.rst) for scientific ob **HDF5 import:** * Fixed images with unsigned 32-bit values above 2 147 483 647 being clipped when imported from an HDF5 file ("Clipping uint32 data to int32" warning): such images are now imported as 64-bit floating-point images keeping every value exactly. Other integer images that no supported integer type can hold also use a sufficiently precise floating-point type when possible, instead of rounding to 32-bit floating-point values beyond 16 777 216; a warning is emitted when no supported type guarantees exact conversion (in particular beyond ±2⁔³ for float64) +* Fixed single-precision complex (`complex64`) 1D datasets losing their imaginary part when imported from an HDF5 file: they are now imported as complex signals. Integer signals beyond ±2⁔³ now also emit the precision warning instead of being rounded silently **Metadata:** From 3a49d772644a3c3ec8102a7c2563ce8a80f8467b Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:37:46 +0200 Subject: [PATCH 14/22] fix(h5io): import the other HDF5 datasets when one cannot be imported Importing several datasets from the HDF5 browser stopped at the first dataset that could not be imported (e.g. a NeXus image axis that is not monotonic), so the following datasets were silently skipped. The other datasets are now imported, and the errors are reported in a single message at the end (raised in unattended mode, as before). Assisted-by: Claude Opus 5.5 --- datalab/gui/h5io.py | 31 ++++++++++++++++++- datalab/locale/fr/LC_MESSAGES/datalab.po | 6 ++++ .../features/hdf5/nexus_import_app_test.py | 27 ++++++++++++++++ .../LC_MESSAGES/release_notes/release_1.04.po | 3 ++ doc/release_notes/release_1.04.md | 1 + 5 files changed, 67 insertions(+), 1 deletion(-) diff --git a/datalab/gui/h5io.py b/datalab/gui/h5io.py index 200c5d104..9225d4b46 100644 --- a/datalab/gui/h5io.py +++ b/datalab/gui/h5io.py @@ -305,6 +305,7 @@ def import_files( if nodes is not None: if reset_all: self.mainwindow.reset_all() + errors: list[str] = [] with qt_try_loadsave_file(self.mainwindow, "*.h5", "load"): with create_progress_bar(self.mainwindow, "", len(nodes)) as prog: for idx, node in enumerate(nodes): @@ -313,9 +314,37 @@ def import_files( QW.QApplication.processEvents() if prog.wasCanceled(): break - self.__add_object_from_node(node) + try: + self.__add_object_from_node(node) + except Exception as exc: # pylint: disable=broad-except + # Data of third-party files: import the other + # datasets, then report all errors at once + errors.append(f"{node.id}: {exc}") + self.__report_import_errors(errors) h5browser.cleanup() + def __report_import_errors(self, errors: list[str]) -> None: + """Report the datasets that could not be imported + + Args: + errors: description of each error ("dataset: reason") + + Raises: + ValueError: in unattended mode, if any dataset could not be imported + """ + if not errors: + return + if execenv.unattended: + raise ValueError("\n".join(errors)) + shown = errors[:10] + if len(errors) > len(shown): + shown.append(_("... and %d more") % (len(errors) - len(shown))) + QW.QMessageBox.warning( + self.mainwindow, + _("Warning"), + _("Some datasets could not be imported:") + "\n\n" + "\n".join(shown), + ) + def import_dataset_from_file(self, filename: str, dsetname: str) -> None: """Import dataset from HDF5 file""" h5importer = H5Importer(filename) diff --git a/datalab/locale/fr/LC_MESSAGES/datalab.po b/datalab/locale/fr/LC_MESSAGES/datalab.po index f21648dcb..c811b193e 100644 --- a/datalab/locale/fr/LC_MESSAGES/datalab.po +++ b/datalab/locale/fr/LC_MESSAGES/datalab.po @@ -1076,6 +1076,12 @@ msgstr "Troncature de donnĂ©es uint32 en int32." msgid "No supported data available in HDF5 file(s)." msgstr "Aucune donnĂ©e prise en charge dans le(s) fichier(s) HDF5." +msgid "... and %d more" +msgstr "... et %d autres" + +msgid "Some datasets could not be imported:" +msgstr "Certains jeux de donnĂ©es n'ont pas pu ĂȘtre importĂ©s :" + msgid "A new object was loaded. Start a new history session?" msgstr "Un nouvel objet a Ă©tĂ© chargĂ©. DĂ©marrer une nouvelle session d'historique ?" diff --git a/datalab/tests/features/hdf5/nexus_import_app_test.py b/datalab/tests/features/hdf5/nexus_import_app_test.py index 33c39fa01..327fade4e 100644 --- a/datalab/tests/features/hdf5/nexus_import_app_test.py +++ b/datalab/tests/features/hdf5/nexus_import_app_test.py @@ -19,6 +19,7 @@ import h5py import numpy as np +import pytest from sigima.objects import get_experimental_source from datalab.tests import datalab_test_app_context, helpers @@ -98,5 +99,31 @@ def test_nexus_import_and_workspace() -> None: json.dumps(stored) +def test_import_continues_after_error() -> None: + """A dataset that cannot be imported does not prevent importing the next ones: + errors are reported once all datasets were imported.""" + with helpers.WorkdirRestoringTempDir() as tmpdir: + nxsname = osp.join(tmpdir, "errors.nxs") + with h5py.File(nxsname, "w") as h5file: + bad = h5file.create_group("a_bad") + bad.attrs.update({"NX_class": "NXdata", "signal": "data"}) + bad.attrs["axes"] = np.array([b".", b"x"]) + bad["data"] = np.ones((2, 4)) + bad["x"] = np.array([0.0, 2.0, 1.0, 3.0]) # Not monotonic + good = h5file.create_group("b_good") + good.attrs.update({"NX_class": "NXdata", "signal": "I0"}) + good["I0"] = np.array([1.0, 2.0, 3.0]) + with datalab_test_app_context(console=False, exec_loop=False) as win: + with pytest.raises(ValueError, match="a_bad: Axis 'x' is not strictly"): + win.h5inputoutput.import_files([nxsname], True, False) + entries = [ + get_experimental_source(obj)["entry"] + for obj in win.signalpanel.objmodel.get_all_objects() + if get_experimental_source(obj) is not None + ] + assert entries == ["/b_good"] + + if __name__ == "__main__": test_nexus_import_and_workspace() + test_import_continues_after_error() diff --git a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po index 98b8b3c65..d8563486b 100644 --- a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po +++ b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po @@ -399,6 +399,9 @@ msgstr "Correction de l'Ă©crĂȘtage des images contenant des valeurs entiĂšres no msgid "Fixed single-precision complex (`complex64`) 1D datasets losing their imaginary part when imported from an HDF5 file: they are now imported as complex signals. Integer signals beyond ±2⁔³ now also emit the precision warning instead of being rounded silently" msgstr "Correction de la perte de la partie imaginaire des jeux de donnĂ©es 1D complexes en simple prĂ©cision (`complex64`) lors de leur import depuis un fichier HDF5 : ils sont dĂ©sormais importĂ©s comme des signaux complexes. Les signaux entiers au-delĂ  de ±2⁔³ Ă©mettent dĂ©sormais aussi l'avertissement de prĂ©cision au lieu d'ĂȘtre arrondis silencieusement" +msgid "Fixed the import of several datasets from HDF5 files stopping at the first dataset that could not be imported: the other datasets are now imported, and the datasets that could not be are listed in a single message at the end" +msgstr "Correction de l'import de plusieurs jeux de donnĂ©es depuis des fichiers HDF5, qui s'arrĂȘtait au premier jeu de donnĂ©es impossible Ă  importer : les autres jeux de donnĂ©es sont dĂ©sormais importĂ©s, et ceux qui n'ont pas pu l'ĂȘtre sont listĂ©s dans un message unique Ă  la fin" + msgid "**Metadata:**" msgstr "**MĂ©tadonnĂ©es :**" diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index 22797b487..08478da86 100644 --- a/doc/release_notes/release_1.04.md +++ b/doc/release_notes/release_1.04.md @@ -171,6 +171,7 @@ See the [HDF5 browser guide](../features/common/h5browser.rst) for scientific ob * Fixed images with unsigned 32-bit values above 2 147 483 647 being clipped when imported from an HDF5 file ("Clipping uint32 data to int32" warning): such images are now imported as 64-bit floating-point images keeping every value exactly. Other integer images that no supported integer type can hold also use a sufficiently precise floating-point type when possible, instead of rounding to 32-bit floating-point values beyond 16 777 216; a warning is emitted when no supported type guarantees exact conversion (in particular beyond ±2⁔³ for float64) * Fixed single-precision complex (`complex64`) 1D datasets losing their imaginary part when imported from an HDF5 file: they are now imported as complex signals. Integer signals beyond ±2⁔³ now also emit the precision warning instead of being rounded silently +* Fixed the import of several datasets from HDF5 files stopping at the first dataset that could not be imported: the other datasets are now imported, and the datasets that could not be are listed in a single message at the end **Metadata:** From 00a6674b2b98ec82d1270599f9f7da638f876c12 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:39:08 +0200 Subject: [PATCH 15/22] docs(i18n): synchronize French remote control and installation catalogs - remote.po: translate the new "UUID of the created group" entry (add_group now returns the UUID of the created group). - installation.po: update the requirement versions (guidata, Sigima, SigimaX) and add the datalab-capsule entries. - proxy.po: synchronize the API reference entries (add_group, capsule export/open); this catalog is not translated, as on develop. Assisted-by: Claude Opus 5.5 --- .../fr/LC_MESSAGES/features/advanced/proxy.po | 195 +++++++----------- .../LC_MESSAGES/features/advanced/remote.po | 4 +- .../fr/LC_MESSAGES/intro/installation.po | 15 +- 3 files changed, 89 insertions(+), 125 deletions(-) diff --git a/doc/locale/fr/LC_MESSAGES/features/advanced/proxy.po b/doc/locale/fr/LC_MESSAGES/features/advanced/proxy.po index 60546710e..a38c5ef8a 100644 --- a/doc/locale/fr/LC_MESSAGES/features/advanced/proxy.po +++ b/doc/locale/fr/LC_MESSAGES/features/advanced/proxy.po @@ -6,7 +6,6 @@ #, fuzzy msgid "" msgstr "" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -14,27 +13,19 @@ msgstr "" msgid "Proxy objects (:mod:`datalab.control.proxy`)" msgstr "" -msgid "" -"The :mod:`datalab.control.proxy` module provides a way to access DataLab " -"features from a proxy class." +msgid "The :mod:`datalab.control.proxy` module provides a way to access DataLab features from a proxy class." msgstr "" msgid "Remote proxy" msgstr "" -msgid "" -"The remote proxy is used when DataLab is started from a different process" -" than the proxy. In this case, the proxy connects to DataLab XML-RPC " -"server." +msgid "The remote proxy is used when DataLab is started from a different process than the proxy. In this case, the proxy connects to DataLab XML-RPC server." msgstr "" msgid "DataLab remote proxy class." msgstr "" -msgid "" -"This class provides access to DataLab features from a proxy class. This " -"is the remote version of proxy, which is used when DataLab is started " -"from a different process than the proxy." +msgid "This class provides access to DataLab features from a proxy class. This is the remote version of proxy, which is used when DataLab is started from a different process than the proxy." msgstr "" msgid "Parameters" @@ -58,9 +49,7 @@ msgstr "" msgid "Examples" msgstr "" -msgid "" -"Here is a simple example of how to use RemoteProxy in a Python script or " -"in a Jupyter notebook:" +msgid "Here is a simple example of how to use RemoteProxy in a Python script or in a Jupyter notebook:" msgstr "" msgid "Add object annotations (annotation plot items)." @@ -72,9 +61,7 @@ msgstr "" msgid "refresh plot. Defaults to True." msgstr "" -msgid "" -"panel name (valid values: \"signal\", \"image\"). If None, current panel " -"is used." +msgid "panel name (valid values: \"signal\", \"image\"). If None, current panel is used." msgstr "" msgid "Add group to DataLab." @@ -89,6 +76,12 @@ msgstr "" msgid "Select the group after creation. Defaults to False." msgstr "" +msgid "Returns" +msgstr "" + +msgid "UUID of the created group" +msgstr "" + msgid "Add image data to DataLab." msgstr "" @@ -122,9 +115,6 @@ msgstr "" msgid "if True, set the added image as current" msgstr "" -msgid "Returns" -msgstr "" - msgid "True if image was added successfully, False otherwise" msgstr "" @@ -179,11 +169,7 @@ msgstr "" msgid "Call computation feature ``name``" msgstr "" -msgid "" -"This calls either the processor's ``compute_`` method (if it " -"exists), or the processor's ```` computation feature (if it is " -"registered, using the ``run_feature`` method). It looks for the function " -"in all panels, starting with the current one." +msgid "This calls either the processor's ``compute_`` method (if it exists), or the processor's ```` computation feature (if it is registered, using the ``run_feature`` method). It looks for the function in all panels, starting with the current one." msgstr "" msgid "Compute function name" @@ -198,9 +184,7 @@ msgstr "" msgid "Call a public method on a panel or main window." msgstr "" -msgid "" -"Method resolution order when panel is None: 1. Try main window " -"(DLMainWindow) 2. If not found, try current panel (BaseDataPanel)" +msgid "Method resolution order when panel is None: 1. Try main window (DLMainWindow) 2. If not found, try current panel (BaseDataPanel)" msgstr "" msgid "Name of the method to call" @@ -209,9 +193,7 @@ msgstr "" msgid "Positional arguments to pass to the method" msgstr "" -msgid "" -"Panel name (\"signal\", \"image\", or None for auto-detection). Defaults " -"to None." +msgid "Panel name (\"signal\", \"image\", or None for auto-detection). Defaults to None." msgstr "" msgid "Keyword arguments to pass to the method" @@ -232,19 +214,13 @@ msgstr "" msgid "Try to connect to DataLab XML-RPC server." msgstr "" -msgid "" -"XML-RPC port to connect to. If not specified, the port is automatically " -"retrieved from DataLab configuration." +msgid "XML-RPC port to connect to. If not specified, the port is automatically retrieved from DataLab configuration." msgstr "" -msgid "" -"Maximum time to wait for connection in seconds. Defaults to 5.0. This is " -"the total maximum wait time, not per retry." +msgid "Maximum time to wait for connection in seconds. Defaults to 5.0. This is the total maximum wait time, not per retry." msgstr "" -msgid "" -"Number of retries. Defaults to 10. This parameter is deprecated and will " -"be removed in a future version (kept for backward compatibility)." +msgid "Number of retries. Defaults to 10. This parameter is deprecated and will be removed in a future version (kept for backward compatibility)." msgstr "" msgid "Return a context manager to temporarily disable auto refresh." @@ -268,6 +244,33 @@ msgstr "" msgid "Disconnect from DataLab XML-RPC server." msgstr "" +msgid "Export the workspace as a capsule (workspace and its provenance)." +msgstr "" + +msgid "Capsule file name (``.dlcapsule``)." +msgstr "" + +msgid "Capsule name." +msgstr "" + +msgid "Capsule description." +msgstr "" + +msgid "When the workspace was opened from a capsule, name that capsule as the parent revision. Defaults to True." +msgstr "" + +msgid "Absolute IRI where the workspace will be published: the capsule then references the workspace instead of including it." +msgstr "" + +msgid "File name of the workspace published at *workspace_ref* (default: last segment of the IRI, next to the capsule)." +msgstr "" + +msgid "Light files declared in the capsule (16 MiB at most each), never opened or executed." +msgstr "" + +msgid "Manifest fingerprint of the capsule." +msgstr "" + msgid "Return current object uuid in current panel." msgstr "" @@ -295,9 +298,7 @@ msgstr "" msgid "Get object (signal/image) from index." msgstr "" -msgid "" -"Object number, or object id, or object title. Defaults to None (current " -"object)." +msgid "Object number, or object id, or object title. Defaults to None (current object)." msgstr "" msgid "Panel name. Defaults to None (current panel)." @@ -315,14 +316,10 @@ msgstr "" msgid "List of plot item shapes" msgstr "" -msgid "" -"Get object (signal/image) list for current panel. Objects are sorted by " -"group number and object index in group." +msgid "Get object (signal/image) list for current panel. Objects are sorted by group number and object index in group." msgstr "" -msgid "" -"panel name (valid values: \"signal\", \"image\", \"macro\"). If None, " -"current data panel is used (i.e. signal or image panel)." +msgid "panel name (valid values: \"signal\", \"image\", \"macro\"). If None, current data panel is used (i.e. signal or image panel)." msgstr "" msgid "List of object titles" @@ -331,9 +328,7 @@ msgstr "" msgid "if panel not found" msgstr "" -msgid "" -"Get object (signal/image) uuid list for current panel. Objects are sorted" -" by group number and object index in group." +msgid "Get object (signal/image) uuid list for current panel. Objects are sorted by group number and object index in group." msgstr "" msgid "Group number, or group id, or group title. Defaults to None (all groups)." @@ -402,15 +397,10 @@ msgstr "" msgid "Load native DataLab HDF5 workspace files without any GUI elements." msgstr "" -msgid "" -"This method can be safely called from scripts (e.g., internal console, " -"macros) as it does not create any Qt widgets, dialogs, or progress bars." +msgid "This method can be safely called from scripts (e.g., internal console, macros) as it does not create any Qt widgets, dialogs, or progress bars." msgstr "" -msgid "" -"This method only supports native DataLab HDF5 files. For importing " -"arbitrary HDF5 files (non-native), use :meth:`open_h5_files` or " -":meth:`import_h5_file` instead." +msgid "This method only supports native DataLab HDF5 files. For importing arbitrary HDF5 files (non-native), use :meth:`open_h5_files` or :meth:`import_h5_file` instead." msgstr "" msgid "List of native DataLab HDF5 filenames" @@ -422,6 +412,12 @@ msgstr "" msgid "If a file is not a valid native DataLab HDF5 file" msgstr "" +msgid "Open a capsule as the workspace, replacing the current one." +msgstr "" + +msgid "Workspace file of a capsule that references its workspace instead of including it." +msgstr "" + msgid "Open a DataLab HDF5 file or import from any other HDF5 file." msgstr "" @@ -467,14 +463,10 @@ msgstr "" msgid "Select groups in current panel." msgstr "" -msgid "" -"List of group numbers (1 to N), or list of group uuids, or None to select" -" all groups. Defaults to None." +msgid "List of group numbers (1 to N), or list of group uuids, or None to select all groups. Defaults to None." msgstr "" -msgid "" -"panel name (valid values: \"signal\", \"image\"). If None, current panel " -"is used. Defaults to None." +msgid "panel name (valid values: \"signal\", \"image\"). If None, current panel is used. Defaults to None." msgstr "" msgid "Select objects in current panel." @@ -492,15 +484,10 @@ msgstr "" msgid "Set object data in DataLab." msgstr "" -msgid "" -"Update an existing object in DataLab with new data from ``obj``. The " -"object is identified by its UUID (which is carried by ``obj`` from a " -"previous :meth:`get_object` call)." +msgid "Update an existing object in DataLab with new data from ``obj``. The object is identified by its UUID (which is carried by ``obj`` from a previous :meth:`get_object` call)." msgstr "" -msgid "" -"Signal or image object (must have the same UUID as an existing object in " -"DataLab)" +msgid "Signal or image object (must have the same UUID as an existing object in DataLab)" msgstr "" msgid "if no object with matching UUID is found" @@ -509,9 +496,7 @@ msgstr "" msgid "Set XML-RPC port to connect to." msgstr "" -msgid "" -"XML-RPC port to connect to. If None, the port is automatically retrieved " -"from DataLab configuration." +msgid "XML-RPC port to connect to. If None, the port is automatically retrieved from DataLab configuration." msgstr "" msgid "Start the WebAPI server." @@ -550,19 +535,13 @@ msgstr "" msgid "Local proxy" msgstr "" -msgid "" -"The local proxy is used when DataLab is started from the same process as " -"the proxy. In this case, the proxy is directly connected to DataLab main " -"window instance. The typical use case is high-level scripting." +msgid "The local proxy is used when DataLab is started from the same process as the proxy. In this case, the proxy is directly connected to DataLab main window instance. The typical use case is high-level scripting." msgstr "" msgid "DataLab local proxy class." msgstr "" -msgid "" -"This class provides access to DataLab features from a proxy class. This " -"is the local version of proxy, which is used when DataLab is started from" -" the same process as the proxy." +msgid "This class provides access to DataLab features from a proxy class. This is the local version of proxy, which is used when DataLab is started from the same process as the proxy." msgstr "" msgid "DLMainWindow instance." @@ -592,9 +571,7 @@ msgstr "" msgid "Compute function parameter. Defaults to None" msgstr "" -msgid "" -"Object number, or object id, or object title. Defaults to None (current " -"object)" +msgid "Object number, or object id, or object title. Defaults to None (current object)" msgstr "" msgid "Panel name. Defaults to None (current panel)" @@ -603,18 +580,13 @@ msgstr "" msgid "refresh plot. Defaults to True" msgstr "" -msgid "" -"panel name (valid values: \"signal\", \"image\"). If None, current panel " -"is used" +msgid "panel name (valid values: \"signal\", \"image\"). If None, current panel is used" msgstr "" msgid "Load HDF5 workspace files without showing file dialog." msgstr "" -msgid "" -"This method loads one or more DataLab native HDF5 files directly, " -"bypassing the file dialog. It is safe to call from the internal console " -"or any context where Qt dialogs would cause threading issues." +msgid "This method loads one or more DataLab native HDF5 files directly, bypassing the file dialog. It is safe to call from the internal console or any context where Qt dialogs would cause threading issues." msgstr "" msgid "Path(s) to HDF5 file(s). Can be a single path string or a list of paths" @@ -629,10 +601,7 @@ msgstr "" msgid "Save workspace to HDF5 file without showing file dialog." msgstr "" -msgid "" -"This method saves the current workspace to a DataLab native HDF5 file " -"directly, bypassing the file dialog. It is safe to call from the internal" -" console or any context where Qt dialogs would cause threading issues." +msgid "This method saves the current workspace to a DataLab native HDF5 file directly, bypassing the file dialog. It is safe to call from the internal console or any context where Qt dialogs would cause threading issues." msgstr "" msgid "Path to the output HDF5 file" @@ -662,29 +631,19 @@ msgstr "" msgid "Proxy context manager" msgstr "" -msgid "" -"The proxy context manager is a convenient way to handle proxy creation " -"and destruction. It is used as follows:" +msgid "The proxy context manager is a convenient way to handle proxy creation and destruction. It is used as follows:" msgstr "" -msgid "" -"The proxy type can be \"local\" or \"remote\". For remote proxy, the port" -" can be specified as \"remote:port\"." +msgid "The proxy type can be \"local\" or \"remote\". For remote proxy, the port can be specified as \"remote:port\"." msgstr "" -msgid "" -"The proxy context manager allows to use the proxy in various contexts " -"(Python script, Jupyter notebook, etc.). It also allows to switch " -"seamlessly between local and remote proxy, keeping the same code inside " -"the context." +msgid "The proxy context manager allows to use the proxy in various contexts (Python script, Jupyter notebook, etc.). It also allows to switch seamlessly between local and remote proxy, keeping the same code inside the context." msgstr "" msgid "Context manager handling DL proxy creation and destruction." msgstr "" -msgid "" -"proxy type (\"local\" or \"remote\") For remote proxy, the port can be " -"specified as \"remote:port\"" +msgid "proxy type (\"local\" or \"remote\") For remote proxy, the port can be specified as \"remote:port\"" msgstr "" msgid "Yields" @@ -693,9 +652,7 @@ msgstr "" msgid "proxy" msgstr "" -msgid "" -"LocalProxy if what == \"local\" RemoteProxy if what == \"remote\" or " -"\"remote:port\"" +msgid "LocalProxy if what == \"local\" RemoteProxy if what == \"remote\" or \"remote:port\"" msgstr "" msgid "with proxy_context(\"local\") as proxy:" @@ -707,9 +664,7 @@ msgstr "" msgid "Calling processor methods using proxy objects" msgstr "" -msgid "" -"All the proxy objects provide access to the DataLab computing methods " -"exposed by the processor classes:" +msgid "All the proxy objects provide access to the DataLab computing methods exposed by the processor classes:" msgstr "" msgid ":class:`datalab.gui.processor.signal.SignalProcessor`" @@ -718,7 +673,5 @@ msgstr "" msgid ":class:`datalab.gui.processor.image.ImageProcessor`" msgstr "" -msgid "" -"To run a computation feature associated to a processor, you can use the " -":meth:`calc` method of the proxy object:" +msgid "To run a computation feature associated to a processor, you can use the :meth:`calc` method of the proxy object:" msgstr "" diff --git a/doc/locale/fr/LC_MESSAGES/features/advanced/remote.po b/doc/locale/fr/LC_MESSAGES/features/advanced/remote.po index 3fdc481f5..e908717e8 100644 --- a/doc/locale/fr/LC_MESSAGES/features/advanced/remote.po +++ b/doc/locale/fr/LC_MESSAGES/features/advanced/remote.po @@ -391,6 +391,9 @@ msgstr "Nom du panneau (valeurs valides : \"signal\", \"image\"). Par dĂ©faut No msgid "Select the group after creation. Defaults to False." msgstr "SĂ©lectionner le groupe aprĂšs sa crĂ©ation. Par dĂ©faut False." +msgid "UUID of the created group" +msgstr "UUID du groupe créé" + msgid "Add a label with object title on the associated plot" msgstr "Ajouter un libellĂ© avec le titre de l'objet sur le graphique associĂ©" @@ -636,4 +639,3 @@ msgstr "Basculer l'Ă©tat d'affichage des titres." msgid "Show titles state" msgstr "État d'affichage des titres" - diff --git a/doc/locale/fr/LC_MESSAGES/intro/installation.po b/doc/locale/fr/LC_MESSAGES/intro/installation.po index 07af2216c..f9871af8f 100644 --- a/doc/locale/fr/LC_MESSAGES/intro/installation.po +++ b/doc/locale/fr/LC_MESSAGES/intro/installation.po @@ -226,7 +226,7 @@ msgstr "Langage de programmation Python" msgid "guidata" msgstr "" -msgid ">= 3.15.1" +msgid ">= 3.16.0" msgstr "" msgid "Automatic GUI generation for easy dataset editing and display" @@ -244,7 +244,7 @@ msgstr "Outils de tracĂ© de courbes et d'images pour les applications Python/Qt" msgid "Sigima" msgstr "" -msgid ">= 1.2.0" +msgid ">= 1.4.0" msgstr "" msgid "Scientific computing engine for 1D signals and 2D images, part of the DataLab open-source platform." @@ -253,12 +253,21 @@ msgstr "" msgid "SigimaX" msgstr "" -msgid ">= 1.0.2" +msgid ">= 1.1.0" msgstr "" msgid "Reusable GUI components and application utilities, part of the DataLab platform for signal and image processing" msgstr "" +msgid "datalab-capsule" +msgstr "" + +msgid ">= 0.1.0, < 0.2" +msgstr "" + +msgid "Exchange and verify scientific workspace provenance as open RO-Crate capsules (library and CLI, no DataLab required)" +msgstr "" + msgid "NumPy" msgstr "" From 277c3f1efadfa899fb0fff9d64fb41cbb681e40a Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:36:43 +0200 Subject: [PATCH 16/22] fix(h5io): close HDF5 files when an import fails When datasets could not be imported in unattended mode, or when opening a file in the browser failed, the error was raised before the browser was cleaned up: the HDF5 files stayed open (locked on Windows) and the preview worker kept running. `import_dataset_from_file` did not close its file on errors either. Both now close their files in a `finally` clause. The NeXus import test checks that the file is closed after errors, and no longer triggers Pylint's unbalanced-tuple-unpacking and unidiomatic-typecheck messages. Assisted-by: Claude Opus 5.5 --- datalab/gui/h5io.py | 21 +++++++++++-- .../features/hdf5/nexus_import_app_test.py | 30 +++++++++++++++---- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/datalab/gui/h5io.py b/datalab/gui/h5io.py index 9225d4b46..473b37dc2 100644 --- a/datalab/gui/h5io.py +++ b/datalab/gui/h5io.py @@ -282,11 +282,26 @@ def import_files( ) -> None: """Import HDF5 files""" h5browser = H5BrowserDialog(self.mainwindow) + try: + self.__import_files_from_browser( + h5browser, filenames, import_all, reset_all + ) + finally: + # Close the HDF5 files, even if an import error is raised + h5browser.cleanup() + + def __import_files_from_browser( + self, + h5browser: H5BrowserDialog, + filenames: list[str], + import_all: bool, + reset_all: bool, + ) -> None: + """Open HDF5 files in browser, then import the selected datasets""" for filename in filenames: with qt_try_loadsave_file(self.mainwindow, filename, "load"): h5browser.open_file(filename) if h5browser.is_empty(): - h5browser.cleanup() if not execenv.unattended: QW.QMessageBox.warning( self.mainwindow, @@ -321,7 +336,6 @@ def import_files( # datasets, then report all errors at once errors.append(f"{node.id}: {exc}") self.__report_import_errors(errors) - h5browser.cleanup() def __report_import_errors(self, errors: list[str]) -> None: """Report the datasets that could not be imported @@ -353,4 +367,5 @@ def import_dataset_from_file(self, filename: str, dsetname: str) -> None: self.__add_object_from_node(node) except KeyError as exc: raise KeyError(f"Dataset not found: {dsetname}") from exc - h5importer.close() + finally: + h5importer.close() diff --git a/datalab/tests/features/hdf5/nexus_import_app_test.py b/datalab/tests/features/hdf5/nexus_import_app_test.py index 327fade4e..da67038a0 100644 --- a/datalab/tests/features/hdf5/nexus_import_app_test.py +++ b/datalab/tests/features/hdf5/nexus_import_app_test.py @@ -7,6 +7,8 @@ A NeXus ``NXdata`` group is imported with its axes and its origin, and the workspace saved afterwards is reopened without the NeXus file: the origin keeps its JSON types, and dimensions without axis or decreasing axes are preserved. +Datasets that cannot be imported are reported without preventing the import of +the others, and the NeXus file is closed afterwards. """ # guitest: show @@ -20,7 +22,7 @@ import h5py import numpy as np import pytest -from sigima.objects import get_experimental_source +from sigima.objects import ImageObj, get_experimental_source from datalab.tests import datalab_test_app_context, helpers @@ -49,6 +51,13 @@ def _create_nexus_file(fname: str) -> None: detector["x"] = np.array([4.0, 2.0, 0.0]) +def _two_images(win) -> tuple[ImageObj, ImageObj]: + """Return the two images of the workspace""" + images = win.imagepanel.objmodel.get_all_objects() + assert len(images) == 2 + return images[0], images[1] + + def test_nexus_import_and_workspace() -> None: """NXdata import, then workspace round trip without the NeXus file""" with helpers.WorkdirRestoringTempDir() as tmpdir: @@ -61,7 +70,7 @@ def test_nexus_import_and_workspace() -> None: h5io.import_dataset_from_file(nxsname, "/entry/map") h5io.import_dataset_from_file(nxsname, "/entry/detector") signal = win.signalpanel.objmodel.get_all_objects()[0] - image, detector = win.imagepanel.objmodel.get_all_objects() + image, detector = _two_images(win) assert np.array_equal(signal.x, [7.1, 7.2, 7.4]) assert signal.y[1] == 2**31 + 5 assert signal.xunit == "keV" @@ -75,7 +84,7 @@ def test_nexus_import_and_workspace() -> None: win.load_h5_workspace([wsname], reset_all=True) signal = win.signalpanel.objmodel.get_all_objects()[0] - image, detector = win.imagepanel.objmodel.get_all_objects() + image, detector = _two_images(win) assert np.array_equal(signal.x, [7.1, 7.2, 7.4]) assert image.data[1, 2] == 2**32 - 1 assert np.array_equal(image.xcoords, [0.0, 1.0, 4.0]) @@ -95,13 +104,20 @@ def test_nexus_import_and_workspace() -> None: # The reloaded origin keeps its JSON types, so it can be stored again for obj in (signal, image, detector): stored = obj.metadata["experimental_source"] - assert type(stored["version"]) is int + version = stored["version"] + assert isinstance(version, int) and not isinstance(version, bool) json.dumps(stored) +def _is_open(fname: str) -> bool: + """Return True if an HDF5 file of this name is open in this process""" + names = [fid.name for fid in h5py.h5f.get_obj_ids(types=h5py.h5f.OBJ_FILE)] + return osp.basename(fname) in [osp.basename(os.fsdecode(name)) for name in names] + + def test_import_continues_after_error() -> None: """A dataset that cannot be imported does not prevent importing the next ones: - errors are reported once all datasets were imported.""" + errors are reported once all datasets were imported, and the file is closed.""" with helpers.WorkdirRestoringTempDir() as tmpdir: nxsname = osp.join(tmpdir, "errors.nxs") with h5py.File(nxsname, "w") as h5file: @@ -116,12 +132,16 @@ def test_import_continues_after_error() -> None: with datalab_test_app_context(console=False, exec_loop=False) as win: with pytest.raises(ValueError, match="a_bad: Axis 'x' is not strictly"): win.h5inputoutput.import_files([nxsname], True, False) + assert not _is_open(nxsname) entries = [ get_experimental_source(obj)["entry"] for obj in win.signalpanel.objmodel.get_all_objects() if get_experimental_source(obj) is not None ] assert entries == ["/b_good"] + with pytest.raises(ValueError, match="Axis 'x' is not strictly"): + win.h5inputoutput.import_dataset_from_file(nxsname, "/a_bad") + assert not _is_open(nxsname) if __name__ == "__main__": From 5a131350abd83f933b6f9b8774258c98e39273e8 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:36:45 +0200 Subject: [PATCH 17/22] fix(instruments): keep Live mode off when unchecked during an acquisition Live mode was resumed after an acquisition whenever it was on when the acquisition started, even if the user unchecked the Live button meanwhile. It is now resumed only if the button is still checked. Assisted-by: Claude Opus 5.5 --- datalab/gui/plugins/instrument.py | 5 ++- .../plugins/instrument_window_unit_test.py | 35 +++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/datalab/gui/plugins/instrument.py b/datalab/gui/plugins/instrument.py index aa8fb6dc9..37e574603 100644 --- a/datalab/gui/plugins/instrument.py +++ b/datalab/gui/plugins/instrument.py @@ -125,7 +125,6 @@ def __init__( self._busy = False self._opened = False self._pending: str | None = None - self._resume_live = False self._close_future: Future | None = None self.SIG_CALL_DONE.connect(self._call_done) @@ -241,7 +240,6 @@ def _request(self, method: str) -> None: self._executor.submit(self._call, "open") return if method == "acquire": - self._resume_live = self._live_timer.isActive() self._live_timer.stop() self.acquire_button.setEnabled(False) self._busy = True @@ -254,7 +252,8 @@ def _call_done(self, method: str, result, error: Exception | None) -> None: self._busy = False if method == "acquire": self.acquire_button.setEnabled(True) - if self._resume_live and self._close_future is None: + # Live mode may have been switched off during the acquisition + if self.live_button.isChecked() and self._close_future is None: self._live_timer.start() if self._close_future is not None: return diff --git a/datalab/tests/features/plugins/instrument_window_unit_test.py b/datalab/tests/features/plugins/instrument_window_unit_test.py index 7b7a08924..de648313d 100644 --- a/datalab/tests/features/plugins/instrument_window_unit_test.py +++ b/datalab/tests/features/plugins/instrument_window_unit_test.py @@ -8,8 +8,9 @@ does not freeze DataLab, repeated refresh requests are merged, and closing the window closes the instrument once its running call is done, even if the window was never shown. A window opened again with the worker of a closed one calls -the instrument once it is closed. When opening the instrument fails, the reason -stays in view and the instrument is opened again before the next call. +the instrument once it is closed. Live mode switched off during an acquisition +is not resumed after it. When opening the instrument fails, the reason stays in +view and the instrument is opened again before the next call. """ # guitest: show @@ -141,6 +142,35 @@ def test_reopened_window_waits_for_closing() -> None: main.deleteLater() +def test_live_mode_after_acquisition() -> None: + """Live mode is resumed after an acquisition, unless it was switched off + while the acquisition was running.""" + with qt_app_context(): + main = QW.QWidget() + instrument = SlowInstrument() + instrument.release.set() + window = InstrumentWindow(main, instrument, "Slow instrument") + window.show() + assert window.wait_until_idle() + live_timer = window._live_timer # pylint: disable=protected-access + for keep_live in (False, True): + window.live_button.setChecked(True) + instrument.release.clear() + instrument.running.clear() + window.acquire() + assert instrument.running.wait(10.0) + assert not live_timer.isActive() + window.live_button.setChecked(keep_live) + instrument.release.set() + assert window.wait_until_idle() + assert live_timer.isActive() == keep_live + window.live_button.setChecked(False) + window.close() + assert window.wait_closed() + assert instrument.calls.count("acquire") == 2 + main.deleteLater() + + def test_hidden_window_closes_instrument() -> None: """Closing a window that was never shown closes its instrument.""" with qt_app_context(): @@ -199,5 +229,6 @@ def test_open_failure_is_shown_and_retried() -> None: test_slow_instrument_calls_are_serialized() test_close_waits_for_running_call() test_reopened_window_waits_for_closing() + test_live_mode_after_acquisition() test_hidden_window_closes_instrument() test_open_failure_is_shown_and_retried() From 13c5d32801683211dbdaa6fbaa1f34129e9ac061 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:36:47 +0200 Subject: [PATCH 18/22] fix(instruments): replace an instrument window closed while hidden Qt does not emit `finished` when a hidden dialog is closed, so such a window stayed registered and was shown again, with its instrument already closed, when the tool was launched again. It is now released and replaced by a new window. Assisted-by: Claude Opus 5.5 --- datalab/gui/main.py | 4 ++++ .../features/plugins/plugin_tools_app_test.py | 20 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/datalab/gui/main.py b/datalab/gui/main.py index 349eff245..22a4ba9af 100644 --- a/datalab/gui/main.py +++ b/datalab/gui/main.py @@ -1165,6 +1165,10 @@ def open_plugin_instrument( """ key = (plugin.plugin_id, tool.id) window = self.__instrument_windows.get(key) + if window is not None and window.close_future is not None: + # Closed while hidden: Qt did not emit `finished`, release it now + self.__forget_instrument_window(key) + window = None if window is None: worker = self.__instrument_workers.get(key) if worker is None: diff --git a/datalab/tests/features/plugins/plugin_tools_app_test.py b/datalab/tests/features/plugins/plugin_tools_app_test.py index 9bbbda960..9e45d386c 100644 --- a/datalab/tests/features/plugins/plugin_tools_app_test.py +++ b/datalab/tests/features/plugins/plugin_tools_app_test.py @@ -224,12 +224,30 @@ def failing_preview(): assert instrument.events == ["open", "close", "open"] window = reopened + # A window closed while hidden (Qt does not emit `finished`) is + # replaced by a new one when the tool is launched again + window.hide() + window.close() + assert window.wait_closed() + replaced = plugin.launch_tool("generator") + assert replaced is not window + assert replaced.wait_until_idle() + assert instrument.events == ["open", "close", "open", "close", "open"] + window = replaced + closed: list[int] = [] window.finished.connect(closed.append) win.reload_plugins() QW.QApplication.processEvents() assert closed - assert instrument.events == ["open", "close", "open", "close"] + assert instrument.events == [ + "open", + "close", + "open", + "close", + "open", + "close", + ] reloaded = PluginRegistry.get_plugin("org.example.tools-app") assert reloaded.launch_tool("generator") is not window win.close_plugin_instruments() From 549d857b82093e652abc54ff0a7f0a2de0021bd4 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:36:50 +0200 Subject: [PATCH 19/22] docs: document broken HDF5 links and preview clearing Release notes and HDF5 browser guide, with French translations: files with broken links can be opened, and selecting an item that cannot be previewed clears the preview. Assisted-by: Claude Opus 5.5 --- doc/features/common/h5browser.rst | 3 +++ .../fr/LC_MESSAGES/features/advanced/plugins.po | 7 ------- .../fr/LC_MESSAGES/features/common/h5browser.po | 12 +++--------- .../fr/LC_MESSAGES/release_notes/release_1.04.po | 15 ++++++--------- doc/locale/fr/LC_MESSAGES/requirements.po | 7 ------- doc/release_notes/release_1.04.md | 3 ++- 6 files changed, 14 insertions(+), 33 deletions(-) diff --git a/doc/features/common/h5browser.rst b/doc/features/common/h5browser.rst index 18f7cdd1c..8f614f4c3 100644 --- a/doc/features/common/h5browser.rst +++ b/doc/features/common/h5browser.rst @@ -66,6 +66,9 @@ values. Textual value previews read only the first elements, and attributes containing more than 10 000 values are not copied to imported object metadata. Graphical previews are read in a background thread; only the preview of the last selected item is displayed, and items selected meanwhile are not read. +Selecting an item that cannot be previewed, such as a group, clears the +preview. Broken links, such as external links to missing detector files, are +skipped when the file is opened. Large ordinary 1D arrays and 2D images, NeXus and mapped objects are previewed with a sampling step above ``2**24`` elements (about 16.8 million). Import still diff --git a/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po b/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po index 41dc1b423..8c55c72ce 100644 --- a/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po +++ b/doc/locale/fr/LC_MESSAGES/features/advanced/plugins.po @@ -5,18 +5,11 @@ # msgid "" msgstr "" -"Project-Id-Version: PROJECT VERSION\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-10-22 22:19+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" "Language: fr\n" -"Language-Team: fr \n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.18.0\n" msgid "Plugin system for DataLab, the open-source scientific data analysis and visualization platform" msgstr "SystĂšme de plugins de DataLab, la plateforme open-source d'analyse et de visualisation de donnĂ©es scientifiques" diff --git a/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po b/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po index d98642720..1dc912e6d 100644 --- a/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po +++ b/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po @@ -2,16 +2,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: PROJECT VERSION\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-10-22 21:34+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.18.0\n" msgid "HDF5 Browser in DataLab, the open-source scientific data analysis and visualization platform" msgstr "Explorateur HDF5 dans DataLab, la plateforme open-source d'analyse et de visualisation de donnĂ©es scientifiques" @@ -67,8 +60,8 @@ msgstr "Les fichiers contenant plusieurs compteurs ou des descriptions NeXus inc msgid "Large datasets and previews" msgstr "Grands jeux de donnĂ©es et aperçus" -msgid "Listing numeric datasets uses their shape and type without loading all their values. Textual value previews read only the first elements, and attributes containing more than 10 000 values are not copied to imported object metadata. Graphical previews are read in a background thread; only the preview of the last selected item is displayed, and items selected meanwhile are not read." -msgstr "La liste des jeux de donnĂ©es numĂ©riques utilise leur forme et leur type sans charger toutes leurs valeurs. Les aperçus textuels ne lisent que les premiers Ă©lĂ©ments, et les attributs contenant plus de 10 000 valeurs ne sont pas copiĂ©s dans les mĂ©tadonnĂ©es des objets importĂ©s. Les aperçus graphiques sont lus dans un fil d'exĂ©cution en arriĂšre-plan ; seul l'aperçu du dernier Ă©lĂ©ment sĂ©lectionnĂ© est affichĂ©, et les Ă©lĂ©ments sĂ©lectionnĂ©s entre-temps ne sont pas lus." +msgid "Listing numeric datasets uses their shape and type without loading all their values. Textual value previews read only the first elements, and attributes containing more than 10 000 values are not copied to imported object metadata. Graphical previews are read in a background thread; only the preview of the last selected item is displayed, and items selected meanwhile are not read. Selecting an item that cannot be previewed, such as a group, clears the preview. Broken links, such as external links to missing detector files, are skipped when the file is opened." +msgstr "La liste des jeux de donnĂ©es numĂ©riques utilise leur forme et leur type sans charger toutes leurs valeurs. Les aperçus textuels ne lisent que les premiers Ă©lĂ©ments, et les attributs contenant plus de 10 000 valeurs ne sont pas copiĂ©s dans les mĂ©tadonnĂ©es des objets importĂ©s. Les aperçus graphiques sont lus dans un fil d'exĂ©cution en arriĂšre-plan ; seul l'aperçu du dernier Ă©lĂ©ment sĂ©lectionnĂ© est affichĂ©, et les Ă©lĂ©ments sĂ©lectionnĂ©s entre-temps ne sont pas lus. La sĂ©lection d'un Ă©lĂ©ment qui ne peut pas ĂȘtre prĂ©visualisĂ©, comme un groupe, efface l'aperçu. Les liens rompus, comme les liens externes vers des fichiers de dĂ©tecteur absents, sont ignorĂ©s Ă  l'ouverture du fichier." msgid "Large ordinary 1D arrays and 2D images, NeXus and mapped objects are previewed with a sampling step above ``2**24`` elements (about 16.8 million). Import still reads every value, not the sampled preview. This is not a universal memory limit: compound datasets and signals stored as 2D tables still use complete objects for their graphical previews. Closing a file or the browser waits for its preview read to end." msgstr "Les grands tableaux 1D ordinaires et les images 2D, les objets NeXus et dĂ©crits par un profil sont prĂ©visualisĂ©s avec un pas d'Ă©chantillonnage au-delĂ  de ``2**24`` Ă©lĂ©ments (environ 16,8 millions). L'import lit toujours toutes les valeurs, pas l'aperçu Ă©chantillonnĂ©. Ce n'est pas une limite mĂ©moire universelle : les jeux de donnĂ©es composites et les signaux stockĂ©s dans des tableaux 2D utilisent encore des objets complets pour leurs aperçus graphiques. La fermeture d'un fichier ou de l'explorateur attend la fin de la lecture de son aperçu." @@ -87,3 +80,4 @@ msgstr "Les objets NeXus et dĂ©crits par un profil portent une entrĂ©e de mĂ©tad msgid "Save the imported objects in a DataLab workspace to keep both their data and this metadata. Reopening that workspace does not require the original NeXus file or the mapping plugin. Generic raw dataset imports do not automatically receive this structured experimental source." msgstr "Enregistrez les objets importĂ©s dans un espace de travail DataLab pour conserver leurs donnĂ©es et ces mĂ©tadonnĂ©es. La rĂ©ouverture de cet espace de travail ne nĂ©cessite ni le fichier NeXus original ni le plugin du profil. Les imports gĂ©nĂ©riques de jeux de donnĂ©es bruts ne reçoivent pas automatiquement cette description structurĂ©e de leur origine expĂ©rimentale." + diff --git a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po index d8563486b..475587e2b 100644 --- a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po +++ b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po @@ -5,18 +5,11 @@ # msgid "" msgstr "" -"Project-Id-Version: PROJECT VERSION\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-10-22 22:19+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" "Language: fr\n" -"Language-Team: fr \n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.18.0\n" msgid "Version 1.4" msgstr "Version 1.4" @@ -357,8 +350,8 @@ msgstr "**Explorateur HDF5 :**" msgid "Browsing an HDF5 file no longer reads its datasets entirely: they are listed and described from their shape and data type, and the value previews read only their first elements. Large files open faster and use less memory; data is read when a signal or an image is previewed or imported" msgstr "Parcourir un fichier HDF5 ne lit plus ses jeux de donnĂ©es en entier : ils sont listĂ©s et dĂ©crits Ă  partir de leur forme et de leur type de donnĂ©es, et les aperçus de valeurs ne lisent que leurs premiers Ă©lĂ©ments. Les gros fichiers s'ouvrent plus vite et utilisent moins de mĂ©moire ; les donnĂ©es sont lues lorsqu'un signal ou une image est prĂ©visualisĂ© ou importĂ©" -msgid "The HDF5 browser stays responsive while a preview is read, and items selected meanwhile are not read. Ordinary 1D arrays and 2D images, NeXus and mapped objects above 2ÂČ⁎ values (about 16.8 million) are previewed with a step along each axis; compound datasets and signals stored in 2D tables still use complete objects for graphical previews" -msgstr "L'explorateur HDF5 reste rĂ©actif pendant la lecture d'un aperçu, et les Ă©lĂ©ments sĂ©lectionnĂ©s entre-temps ne sont pas lus. Les tableaux 1D ordinaires et les images 2D, les objets NeXus et dĂ©crits par un profil au-delĂ  de 2ÂČ⁎ valeurs (environ 16,8 millions) sont prĂ©visualisĂ©s avec un pas le long de chaque axe ; les jeux de donnĂ©es composites et les signaux stockĂ©s dans des tableaux 2D utilisent encore des objets complets pour leurs aperçus graphiques" +msgid "The HDF5 browser stays responsive while a preview is read, and items selected meanwhile are not read; selecting an item that cannot be previewed (a group, for example) clears the preview. Ordinary 1D arrays and 2D images, NeXus and mapped objects above 2ÂČ⁎ values (about 16.8 million) are previewed with a step along each axis; compound datasets and signals stored in 2D tables still use complete objects for graphical previews" +msgstr "L'explorateur HDF5 reste rĂ©actif pendant la lecture d'un aperçu, et les Ă©lĂ©ments sĂ©lectionnĂ©s entre-temps ne sont pas lus ; la sĂ©lection d'un Ă©lĂ©ment qui ne peut pas ĂȘtre prĂ©visualisĂ© (un groupe, par exemple) efface l'aperçu. Les tableaux 1D ordinaires et les images 2D, les objets NeXus et dĂ©crits par un profil au-delĂ  de 2ÂČ⁎ valeurs (environ 16,8 millions) sont prĂ©visualisĂ©s avec un pas le long de chaque axe ; les jeux de donnĂ©es composites et les signaux stockĂ©s dans des tableaux 2D utilisent encore des objets complets pour leurs aperçus graphiques" msgid "Datasets with three dimensions or more (image stacks, spectral cubes...) are now listed in the HDF5 browser, but are not directly importable and have no graphical slice selector" msgstr "Les jeux de donnĂ©es Ă  trois dimensions ou plus (piles d'images, cubes spectraux...) sont dĂ©sormais listĂ©s dans l'explorateur HDF5, mais ne sont pas directement importables et n'ont pas de sĂ©lecteur graphique de coupes" @@ -402,6 +395,9 @@ msgstr "Correction de la perte de la partie imaginaire des jeux de donnĂ©es 1D c msgid "Fixed the import of several datasets from HDF5 files stopping at the first dataset that could not be imported: the other datasets are now imported, and the datasets that could not be are listed in a single message at the end" msgstr "Correction de l'import de plusieurs jeux de donnĂ©es depuis des fichiers HDF5, qui s'arrĂȘtait au premier jeu de donnĂ©es impossible Ă  importer : les autres jeux de donnĂ©es sont dĂ©sormais importĂ©s, et ceux qui n'ont pas pu l'ĂȘtre sont listĂ©s dans un message unique Ă  la fin" +msgid "Fixed HDF5 files containing broken links failing to open in the HDF5 browser, for example synchrotron master files whose detector files are not available (external links to a missing file): the broken links are now skipped and the rest of the file can be browsed and imported" +msgstr "Correction de l'Ă©chec d'ouverture dans l'explorateur HDF5 des fichiers HDF5 contenant des liens rompus, par exemple les fichiers maĂźtres de synchrotron dont les fichiers de dĂ©tecteur ne sont pas disponibles (liens externes vers un fichier absent) : les liens rompus sont dĂ©sormais ignorĂ©s et le reste du fichier peut ĂȘtre explorĂ© et importĂ©" + msgid "**Metadata:**" msgstr "**MĂ©tadonnĂ©es :**" @@ -479,3 +475,4 @@ msgstr "**SĂ©curitĂ© :**" msgid "Opening a workspace received from someone else can no longer make DataLab load or run other code installed on the computer. The parameters stored with objects, analysis results and History actions name their class, and DataLab imported whatever module they named and called the object named there, which a modified file could use, for example, to make DataLab quit. Only parameter classes of DataLab, Sigima, guidata and installed plugins are now used; other stored parameters are reported as unreadable (fixes [Issue #407](https://github.com/DataLab-Platform/DataLab/issues/407))" msgstr "Ouvrir un espace de travail reçu d'une autre personne ne permet plus de faire charger ou exĂ©cuter par DataLab un autre code installĂ© sur l'ordinateur. Les paramĂštres enregistrĂ©s avec les objets, les rĂ©sultats d'analyse et les actions de l'historique nomment leur classe, et DataLab importait le module ainsi nommĂ© puis appelait l'objet dĂ©signĂ©, ce qu'un fichier modifiĂ© pouvait exploiter, par exemple pour faire quitter DataLab. Seules les classes de paramĂštres de DataLab, Sigima, guidata et des plugins installĂ©s sont dĂ©sormais utilisĂ©es ; les autres paramĂštres enregistrĂ©s sont signalĂ©s comme illisibles (corrige [l'issue #407](https://github.com/DataLab-Platform/DataLab/issues/407))" + diff --git a/doc/locale/fr/LC_MESSAGES/requirements.po b/doc/locale/fr/LC_MESSAGES/requirements.po index 6b1d869fa..193f76398 100644 --- a/doc/locale/fr/LC_MESSAGES/requirements.po +++ b/doc/locale/fr/LC_MESSAGES/requirements.po @@ -6,16 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: PROJECT VERSION\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-10-22 21:34+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.18.0\n" msgid "The `datalab-platform` package requires the following Python modules:" msgstr "Le paquet `datalab-platform` nĂ©cessite les modules Python suivants :" diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index 08478da86..71f4c01b4 100644 --- a/doc/release_notes/release_1.04.md +++ b/doc/release_notes/release_1.04.md @@ -149,7 +149,7 @@ DataLab now uses stable UUID-based object references across object trees, result **HDF5 browser:** * Browsing an HDF5 file no longer reads its datasets entirely: they are listed and described from their shape and data type, and the value previews read only their first elements. Large files open faster and use less memory; data is read when a signal or an image is previewed or imported -* The HDF5 browser stays responsive while a preview is read, and items selected meanwhile are not read. Ordinary 1D arrays and 2D images, NeXus and mapped objects above 2ÂČ⁎ values (about 16.8 million) are previewed with a step along each axis; compound datasets and signals stored in 2D tables still use complete objects for graphical previews +* The HDF5 browser stays responsive while a preview is read, and items selected meanwhile are not read; selecting an item that cannot be previewed (a group, for example) clears the preview. Ordinary 1D arrays and 2D images, NeXus and mapped objects above 2ÂČ⁎ values (about 16.8 million) are previewed with a step along each axis; compound datasets and signals stored in 2D tables still use complete objects for graphical previews * Datasets with three dimensions or more (image stacks, spectral cubes...) are now listed in the HDF5 browser, but are not directly importable and have no graphical slice selector * Dataset attributes with more than 10 000 values are no longer copied to the metadata of imported objects @@ -172,6 +172,7 @@ See the [HDF5 browser guide](../features/common/h5browser.rst) for scientific ob * Fixed images with unsigned 32-bit values above 2 147 483 647 being clipped when imported from an HDF5 file ("Clipping uint32 data to int32" warning): such images are now imported as 64-bit floating-point images keeping every value exactly. Other integer images that no supported integer type can hold also use a sufficiently precise floating-point type when possible, instead of rounding to 32-bit floating-point values beyond 16 777 216; a warning is emitted when no supported type guarantees exact conversion (in particular beyond ±2⁔³ for float64) * Fixed single-precision complex (`complex64`) 1D datasets losing their imaginary part when imported from an HDF5 file: they are now imported as complex signals. Integer signals beyond ±2⁔³ now also emit the precision warning instead of being rounded silently * Fixed the import of several datasets from HDF5 files stopping at the first dataset that could not be imported: the other datasets are now imported, and the datasets that could not be are listed in a single message at the end +* Fixed HDF5 files containing broken links failing to open in the HDF5 browser, for example synchrotron master files whose detector files are not available (external links to a missing file): the broken links are now skipped and the rest of the file can be browsed and imported **Metadata:** From 2cdd3a54cdf154fa0052dde044d8013a3d86bfa5 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:31:49 +0200 Subject: [PATCH 20/22] fix(remote): annotate RemoteServer.add_group as returning the group UUID Assisted-by: Claude Opus 5.5 --- datalab/control/remote.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/datalab/control/remote.py b/datalab/control/remote.py index 0dbe31811..677510c02 100644 --- a/datalab/control/remote.py +++ b/datalab/control/remote.py @@ -574,7 +574,7 @@ def get_current_object_uuid(self) -> str | None: @remote_call def add_group( self, title: str, panel: str | None = None, select: bool = False - ) -> None: + ) -> str: """Add group to DataLab. Args: @@ -583,9 +583,11 @@ def add_group( select: Select the group after creation. Defaults to False. Returns: - UUID of the created group, set by the main window slot + UUID of the created group """ self.SIG_ADD_GROUP.emit(title, panel, select) + # The decorator waits for the main window slot, which sets the result + return self.result @remote_call def select_objects( From 67846e0bb9b2e0c45bbad0b425568e6f93f2d9f0 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:31:52 +0200 Subject: [PATCH 21/22] fix(instruments): bound the total wait for instruments on close Instruments are waited for together when plugins are reloaded or DataLab quits, so that several stuck instruments no longer add up their time limits. Assisted-by: Claude Opus 5.5 --- datalab/gui/main.py | 23 ++++++++++--------- datalab/gui/plugins/instrument.py | 21 +++++++++++++++-- .../plugins/instrument_window_unit_test.py | 22 ++++++++++++++++-- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/datalab/gui/main.py b/datalab/gui/main.py index 22a4ba9af..9c114a0a0 100644 --- a/datalab/gui/main.py +++ b/datalab/gui/main.py @@ -26,7 +26,6 @@ import time import traceback from concurrent.futures import Future -from concurrent.futures import wait as wait_futures from contextlib import ExitStack from datetime import datetime from typing import TYPE_CHECKING @@ -77,7 +76,11 @@ from datalab.gui.panel import base, history, image, macro, signal from datalab.gui.plugins.applications import ApplicationsDialog, get_plugin_icon from datalab.gui.plugins.config import PluginConfigDialog -from datalab.gui.plugins.instrument import InstrumentWindow, SerialWorker +from datalab.gui.plugins.instrument import ( + InstrumentWindow, + SerialWorker, + wait_instruments_closed, +) from datalab.gui.processor.base import FeatureNotFoundError from datalab.gui.processor.preview import PreviewExecutorCache from datalab.gui.settings import AI_OPTION_NAMES, edit_settings @@ -1197,20 +1200,18 @@ def __forget_instrument_window(self, key: tuple[str, str], *_args) -> None: def close_plugin_instruments(self) -> None: """Close all plugin instrument windows, and wait for their instruments, - including those of windows closed before.""" + including those of windows closed before, within a common time limit.""" for window in list(self.__instrument_windows.values()): window.close() # Windows that were not visible do not emit `finished` when closed for key in list(self.__instrument_windows): self.__forget_instrument_window(key) - for title, future in self.__instrument_closings.values(): - done, _not_done = wait_futures([future], InstrumentWindow.CLOSE_TIMEOUT) - if not done: - logging.getLogger(__name__).warning( - "Instrument '%s' did not close within %g s", - title, - InstrumentWindow.CLOSE_TIMEOUT, - ) + timeout = InstrumentWindow.CLOSE_TIMEOUT + closings = self.__instrument_closings.values() + for title in wait_instruments_closed(closings, timeout): + logging.getLogger(__name__).warning( + "Instrument '%s' did not close within %g s", title, timeout + ) for worker in self.__instrument_workers.values(): worker.shutdown() self.__instrument_workers.clear() diff --git a/datalab/gui/plugins/instrument.py b/datalab/gui/plugins/instrument.py index 37e574603..0d271e5b3 100644 --- a/datalab/gui/plugins/instrument.py +++ b/datalab/gui/plugins/instrument.py @@ -10,7 +10,7 @@ import time from concurrent.futures import Future from concurrent.futures import wait as wait_futures -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Callable, Iterable from guidata.configtools import get_icon from guidata.dataset.qtwidgets import DataSetEditLayout @@ -34,7 +34,24 @@ PluginInstrument, ) -__all__ = ["InstrumentWindow", "SerialWorker"] +__all__ = ["InstrumentWindow", "SerialWorker", "wait_instruments_closed"] + + +def wait_instruments_closed( + closings: Iterable[tuple[str, Future]], timeout: float +) -> list[str]: + """Wait for instruments being closed, within a common time limit. + + Args: + closings: title of each instrument, with the call closing it + timeout: maximum waiting time for all instruments (s) + + Returns: + Titles of the instruments that were not closed in time + """ + closings = list(closings) + _done, not_done = wait_futures([future for _title, future in closings], timeout) + return [title for title, future in closings if future in not_done] class SerialWorker: diff --git a/datalab/tests/features/plugins/instrument_window_unit_test.py b/datalab/tests/features/plugins/instrument_window_unit_test.py index de648313d..af487ad07 100644 --- a/datalab/tests/features/plugins/instrument_window_unit_test.py +++ b/datalab/tests/features/plugins/instrument_window_unit_test.py @@ -10,7 +10,8 @@ was never shown. A window opened again with the worker of a closed one calls the instrument once it is closed. Live mode switched off during an acquisition is not resumed after it. When opening the instrument fails, the reason stays in -view and the instrument is opened again before the next call. +view and the instrument is opened again before the next call. When DataLab +closes, the instruments are waited for within a common time limit. """ # guitest: show @@ -18,6 +19,8 @@ from __future__ import annotations import threading +import time +from concurrent.futures import Future import guidata.dataset as gds import numpy as np @@ -25,7 +28,11 @@ from qtpy import QtWidgets as QW from sigima.objects import create_signal -from datalab.gui.plugins.instrument import InstrumentWindow, SerialWorker +from datalab.gui.plugins.instrument import ( + InstrumentWindow, + SerialWorker, + wait_instruments_closed, +) from datalab.plugins.instruments import ( InstrumentAcquisition, InstrumentFrame, @@ -225,6 +232,16 @@ def test_open_failure_is_shown_and_retried() -> None: main.deleteLater() +def test_instruments_close_within_common_time_limit() -> None: + """Instruments are waited for together: their time limits do not add up.""" + closed: Future = Future() + closed.set_result(None) + closings = [("Stuck 1", Future()), ("Closed", closed), ("Stuck 2", Future())] + start = time.perf_counter() + assert wait_instruments_closed(closings, 0.5) == ["Stuck 1", "Stuck 2"] + assert time.perf_counter() - start < 0.9 + + if __name__ == "__main__": test_slow_instrument_calls_are_serialized() test_close_waits_for_running_call() @@ -232,3 +249,4 @@ def test_open_failure_is_shown_and_retried() -> None: test_live_mode_after_acquisition() test_hidden_window_closes_instrument() test_open_failure_is_shown_and_retried() + test_instruments_close_within_common_time_limit() From dce5cced167a61777c3cf3f978a9c9223ef97a3a Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:32:21 +0200 Subject: [PATCH 22/22] docs(i18n): translate dependency descriptions in French catalogs Assisted-by: Claude Opus 5.5 --- doc/locale/fr/LC_MESSAGES/intro/installation.po | 6 +++--- doc/locale/fr/LC_MESSAGES/requirements.po | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/locale/fr/LC_MESSAGES/intro/installation.po b/doc/locale/fr/LC_MESSAGES/intro/installation.po index f9871af8f..59f7c9d05 100644 --- a/doc/locale/fr/LC_MESSAGES/intro/installation.po +++ b/doc/locale/fr/LC_MESSAGES/intro/installation.po @@ -248,7 +248,7 @@ msgid ">= 1.4.0" msgstr "" msgid "Scientific computing engine for 1D signals and 2D images, part of the DataLab open-source platform." -msgstr "" +msgstr "Moteur de calcul scientifique pour signaux 1D et images 2D, faisant partie de la plateforme open-source DataLab." msgid "SigimaX" msgstr "" @@ -257,7 +257,7 @@ msgid ">= 1.1.0" msgstr "" msgid "Reusable GUI components and application utilities, part of the DataLab platform for signal and image processing" -msgstr "" +msgstr "Composants d'interface graphique rĂ©utilisables et utilitaires applicatifs, faisant partie de la plateforme DataLab de traitement du signal et des images" msgid "datalab-capsule" msgstr "" @@ -266,7 +266,7 @@ msgid ">= 0.1.0, < 0.2" msgstr "" msgid "Exchange and verify scientific workspace provenance as open RO-Crate capsules (library and CLI, no DataLab required)" -msgstr "" +msgstr "Échange et vĂ©rification de la provenance d'espaces de travail scientifiques sous forme de capsules RO-Crate ouvertes (bibliothĂšque et ligne de commande, sans DataLab)" msgid "NumPy" msgstr "" diff --git a/doc/locale/fr/LC_MESSAGES/requirements.po b/doc/locale/fr/LC_MESSAGES/requirements.po index 193f76398..e4415f609 100644 --- a/doc/locale/fr/LC_MESSAGES/requirements.po +++ b/doc/locale/fr/LC_MESSAGES/requirements.po @@ -65,7 +65,7 @@ msgid ">= 1.1.0" msgstr ">= 1.1.0" msgid "Reusable GUI components and application utilities, part of the DataLab platform for signal and image processing" -msgstr "" +msgstr "Composants d'interface graphique rĂ©utilisables et utilitaires applicatifs, faisant partie de la plateforme DataLab de traitement du signal et des images" msgid "datalab-capsule" msgstr ""