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..677510c02 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: @@ -572,15 +574,20 @@ 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: 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.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( diff --git a/datalab/gui/h5io.py b/datalab/gui/h5io.py index 120ea32be..473b37dc2 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,29 +272,36 @@ 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: """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, @@ -314,27 +320,52 @@ 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: - self.uint32_wng = False for idx, node in enumerate(nodes): prog.setLabelText(self.__progbartitle(node.h5file.filename)) prog.setValue(idx + 1) QW.QApplication.processEvents() if prog.wasCanceled(): break - self.__add_object_from_node(node) - self.__eventually_show_warnings() - h5browser.cleanup() + 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) + + 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) 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() + finally: + h5importer.close() diff --git a/datalab/gui/main.py b/datalab/gui/main.py index a92da45f5..9c114a0a0 100644 --- a/datalab/gui/main.py +++ b/datalab/gui/main.py @@ -25,6 +25,7 @@ import sys import time import traceback +from concurrent.futures import Future from contextlib import ExitStack from datetime import datetime from typing import TYPE_CHECKING @@ -75,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 +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 @@ -172,6 +177,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 @@ -509,15 +518,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 +686,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 +698,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 @@ -1143,11 +1161,24 @@ 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 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: - 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) ) @@ -1158,15 +1189,33 @@ 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.""" + """Close all plugin instrument windows, and wait for their instruments, + 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) + 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() + self.__instrument_closings.clear() def __unregister_plugins(self) -> None: """Unregister all plugins and let them cleanup their hooks""" @@ -2272,7 +2321,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/gui/plugins/instrument.py b/datalab/gui/plugins/instrument.py index dd66eeb80..0d271e5b3 100644 --- a/datalab/gui/plugins/instrument.py +++ b/datalab/gui/plugins/instrument.py @@ -4,13 +4,20 @@ 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, Iterable from guidata.configtools import get_icon from guidata.dataset.qtwidgets import DataSetEditLayout 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 @@ -27,26 +34,97 @@ PluginInstrument, ) -__all__ = ["InstrumentWindow"] +__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: + """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; 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 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) 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 + self, + main: DLMainWindow, + instrument: PluginInstrument, + title: str, + executor: SerialWorker | None = None, ) -> None: super().__init__(main) win32_fix_title_bar_background(self) @@ -59,6 +137,13 @@ def __init__( self._plot_kind: str | None = None self._plot_signature: tuple | None = None self._value_range: tuple[float, float] | None = None + 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._close_future: Future | None = None + self.SIG_CALL_DONE.connect(self._call_done) splitter = QW.QSplitter(QC.Qt.Horizontal) self._stage = QW.QWidget() @@ -132,7 +217,121 @@ def __init__( min(self.width(), screen.width()), min(self.height(), screen.height()) ) splitter.setSizes([650, 450]) - self._refresh_timer.start(0) + # 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") + + 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 opening the instrument failed, it is opened again first.""" + 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 not self._opened: + self._busy = True + self._pending = method + self._executor.submit(self._call, "open") + return + if method == "acquire": + 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) + # 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 + 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": + 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) + + @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.""" @@ -150,9 +349,20 @@ 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") + 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.""" @@ -164,13 +374,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 +450,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/locale/fr/LC_MESSAGES/datalab.po b/datalab/locale/fr/LC_MESSAGES/datalab.po index 411bfacdf..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 ?" @@ -1325,8 +1331,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/plugins/instruments.py b/datalab/plugins/instruments.py index 660391d9f..0b38ca366 100644 --- a/datalab/plugins/instruments.py +++ b/datalab/plugins/instruments.py @@ -94,6 +94,15 @@ 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, 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. + Args: settings: instrument settings, edited by DataLab """ @@ -106,6 +115,23 @@ 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: 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: + """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/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() 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..da67038a0 --- /dev/null +++ b/datalab/tests/features/hdf5/nexus_import_app_test.py @@ -0,0 +1,149 @@ +# 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: 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 + +from __future__ import annotations + +import json +import os +import os.path as osp + +import h5py +import numpy as np +import pytest +from sigima.objects import ImageObj, 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, 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" + 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]) + 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 _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: + 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") + h5io.import_dataset_from_file(nxsname, "/entry/detector") + signal = win.signalpanel.objmodel.get_all_objects()[0] + 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" + 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, 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]) + 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 + assert source["entry"] == "/entry/spectrum" + assert source["selection"] == {"signal": "I0", "axes": ["E"]} + assert get_experimental_source(image)["selection"] == { + "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"] + 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, and the file is closed.""" + 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) + 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__": + test_nexus_import_and_workspace() + test_import_continues_after_error() 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..af487ad07 --- /dev/null +++ b/datalab/tests/features/plugins/instrument_window_unit_test.py @@ -0,0 +1,252 @@ +# 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, 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. 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. When DataLab +closes, the instruments are waited for within a common time limit. +""" + +# guitest: show + +from __future__ import annotations + +import threading +import time +from concurrent.futures import Future + +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, + SerialWorker, + wait_instruments_closed, +) +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.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._record("open") + + def preview(self) -> InstrumentFrame: + 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._record("close") + + def acquire(self) -> InstrumentAcquisition: + self._record("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() + + +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_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(): + 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() + + +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() + + +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() + test_reopened_window_waits_for_closing() + test_live_mode_after_acquisition() + test_hidden_window_closes_instrument() + test_open_failure_is_shown_and_retried() + test_instruments_close_within_common_time_limit() diff --git a/datalab/tests/features/plugins/plugin_tools_app_test.py b/datalab/tests/features/plugins/plugin_tools_app_test.py index 79d9dd4cf..9e45d386c 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,28 +198,56 @@ 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 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 + + # 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", + "open", + "close", + ] reloaded = PluginRegistry.get_plugin("org.example.tools-app") assert reloaded.launch_tool("generator") is not window win.close_plugin_instruments() diff --git a/doc/features/advanced/plugins.rst b/doc/features/advanced/plugins.rst index 349d745ce..07299e404 100644 --- a/doc/features/advanced/plugins.rst +++ b/doc/features/advanced/plugins.rst @@ -792,6 +792,10 @@ 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, 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. + 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..8f614f4c3 100644 --- a/doc/features/common/h5browser.rst +++ b/doc/features/common/h5browser.rst @@ -28,3 +28,78 @@ 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, 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 +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, 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 +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 +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. 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 (``"."`` 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. + +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 bbdc39765..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" @@ -633,6 +626,12 @@ 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, 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." + 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." @@ -1579,12 +1578,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, 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" 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: 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)." + +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/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/features/common/h5browser.po b/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po index e61715288..1dc912e6d 100644 --- a/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po +++ b/doc/locale/fr/LC_MESSAGES/features/common/h5browser.po @@ -2,7 +2,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" @@ -42,3 +41,43 @@ 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, 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 `_." + +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. 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." + +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. 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 (``\".\"`` 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/intro/installation.po b/doc/locale/fr/LC_MESSAGES/intro/installation.po index 07af2216c..59f7c9d05 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,21 +244,30 @@ 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." -msgstr "" +msgstr "Moteur de calcul scientifique pour signaux 1D et images 2D, faisant partie de la plateforme open-source DataLab." 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 "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 "" +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 "É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/release_notes/release_1.04.po b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po index d2f3b1487..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" @@ -90,6 +83,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. 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" @@ -285,6 +281,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, 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" + +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" @@ -324,8 +341,26 @@ 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 :**" + +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; 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" + +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" @@ -348,6 +383,21 @@ 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. 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 "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 :**" diff --git a/doc/locale/fr/LC_MESSAGES/requirements.po b/doc/locale/fr/LC_MESSAGES/requirements.po index ea443c002..e4415f609 100644 --- a/doc/locale/fr/LC_MESSAGES/requirements.po +++ b/doc/locale/fr/LC_MESSAGES/requirements.po @@ -34,8 +34,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 +52,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,11 +61,11 @@ 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 "" +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 "" diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index 17660131e..71f4c01b4 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. 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 @@ -111,6 +112,17 @@ 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 + +**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, 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 + ### 🔄 Changes ### **Synthetic images in physical coordinates:** @@ -132,7 +144,16 @@ 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 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 + +See the [HDF5 browser guide](../features/common/h5browser.rst) for scientific object import, experimental origin and preview limits. ### 🛠️ Bug Fixes ### @@ -146,6 +167,13 @@ 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. 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:** * Adding, pasting or deleting metadata now marks the workspace as modified, so DataLab asks to save these changes before closing 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