datalab-platform 1.0.1__py3-none-any.whl → 1.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. datalab/__init__.py +1 -1
  2. datalab/adapters_metadata/common.py +2 -2
  3. datalab/adapters_plotpy/converters.py +3 -1
  4. datalab/adapters_plotpy/coordutils.py +157 -0
  5. datalab/adapters_plotpy/roi/image.py +35 -6
  6. datalab/adapters_plotpy/roi/signal.py +8 -1
  7. datalab/config.py +88 -26
  8. datalab/control/baseproxy.py +70 -0
  9. datalab/control/proxy.py +33 -0
  10. datalab/control/remote.py +35 -0
  11. datalab/data/doc/DataLab_en.pdf +0 -0
  12. datalab/data/doc/DataLab_fr.pdf +0 -0
  13. datalab/data/icons/create/linear_chirp.svg +1 -1
  14. datalab/data/icons/create/logistic.svg +1 -1
  15. datalab/gui/actionhandler.py +16 -2
  16. datalab/gui/h5io.py +25 -0
  17. datalab/gui/macroeditor.py +37 -6
  18. datalab/gui/main.py +62 -5
  19. datalab/gui/newobject.py +7 -0
  20. datalab/gui/objectview.py +18 -3
  21. datalab/gui/panel/base.py +89 -16
  22. datalab/gui/panel/macro.py +26 -0
  23. datalab/gui/plothandler.py +20 -2
  24. datalab/gui/processor/base.py +72 -26
  25. datalab/gui/processor/image.py +6 -2
  26. datalab/gui/processor/signal.py +10 -0
  27. datalab/gui/roieditor.py +2 -2
  28. datalab/locale/fr/LC_MESSAGES/datalab.mo +0 -0
  29. datalab/locale/fr/LC_MESSAGES/datalab.po +3288 -0
  30. datalab/objectmodel.py +1 -1
  31. datalab/tests/features/common/auto_analysis_recompute_unit_test.py +81 -0
  32. datalab/tests/features/common/coordutils_unit_test.py +212 -0
  33. datalab/tests/features/common/result_deletion_unit_test.py +121 -1
  34. datalab/tests/features/common/roi_plotitem_unit_test.py +4 -2
  35. datalab/tests/features/common/update_tree_robustness_test.py +65 -0
  36. datalab/tests/features/control/remoteclient_unit.py +10 -0
  37. datalab/tests/features/hdf5/h5workspace_unit_test.py +133 -0
  38. datalab/tests/features/image/roigrid_unit_test.py +75 -0
  39. datalab/tests/features/macro/macroeditor_unit_test.py +104 -3
  40. datalab/tests/features/signal/custom_signal_bug_unit_test.py +96 -0
  41. datalab/widgets/imagebackground.py +13 -4
  42. datalab/widgets/instconfviewer.py +2 -2
  43. datalab/widgets/signalcursor.py +7 -2
  44. datalab/widgets/signaldeltax.py +4 -1
  45. {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.3.dist-info}/METADATA +3 -3
  46. {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.3.dist-info}/RECORD +50 -43
  47. {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.3.dist-info}/WHEEL +0 -0
  48. {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.3.dist-info}/entry_points.txt +0 -0
  49. {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.3.dist-info}/licenses/LICENSE +0 -0
  50. {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.3.dist-info}/top_level.txt +0 -0
@@ -55,7 +55,16 @@ if TYPE_CHECKING:
55
55
 
56
56
 
57
57
  def calc_data_hash(obj: SignalObj | ImageObj) -> str:
58
- """Calculate a hash for a SignalObj | ImageObj object's data"""
58
+ """Calculate a hash for a SignalObj | ImageObj object's data
59
+
60
+ For signals, this includes both X and Y data to detect axis changes.
61
+ For images, this includes only the Z data.
62
+ """
63
+ if isinstance(obj, SignalObj):
64
+ # For signals, hash both X and Y data to detect axis changes
65
+ # (e.g., when xmin/xmax is modified without changing Y values)
66
+ return hashlib.sha1(np.ascontiguousarray(obj.xydata)).hexdigest()
67
+ # For images, hash only the image data
59
68
  return hashlib.sha1(np.ascontiguousarray(obj.data)).hexdigest()
60
69
 
61
70
 
@@ -153,6 +162,7 @@ class BasePlotHandler(Generic[TypeObj, TypePlotItem]): # type: ignore
153
162
  self.__merged_result_adapters = {}
154
163
  self.cleanup_dataview()
155
164
  self.remove_all_shape_items()
165
+ self.plot.replot()
156
166
 
157
167
  def add_shapes(self, oid: str, do_autoscale: bool = False) -> None:
158
168
  """Add geometric shape items associated to computed results and annotations,
@@ -382,7 +392,10 @@ class BasePlotHandler(Generic[TypeObj, TypePlotItem]): # type: ignore
382
392
  if what == "selected":
383
393
  # Refresh selected objects
384
394
  oids = self.panel.objview.get_sel_object_uuids(include_groups=True)
385
- if len(oids) == 1:
395
+ if len(oids) <= 1:
396
+ # Cleanup data view when there is 0 or 1 selected object.
397
+ # This removes stray plot items (like XRangeSelection, DataInfoLabel)
398
+ # that were created by PlotPy tools but are not managed by DataLab.
386
399
  self.cleanup_dataview()
387
400
  self.remove_all_shape_items()
388
401
  for item in self:
@@ -797,4 +810,9 @@ class ImagePlotHandler(BasePlotHandler[ImageObj, MaskedXYImageItem]):
797
810
  options = super().get_plot_options()
798
811
  options.zlabel = self.plot.get_axis_title("right")
799
812
  options.zunit = self.plot.get_axis_unit("right")
813
+ # Include aspect ratio configuration so that separate plot dialogs
814
+ # (e.g. "View in a new window", ROI editors, profile dialogs) use the same
815
+ # settings as the integrated plot handler:
816
+ options.aspect_ratio = self.plot.get_aspect_ratio()
817
+ options.lock_aspect_ratio = self.plot.lock_aspect_ratio
800
818
  return options
@@ -42,6 +42,7 @@ from datalab.adapters_metadata import (
42
42
  TableAdapter,
43
43
  show_resultdata,
44
44
  )
45
+ from datalab.adapters_plotpy import coordutils
45
46
  from datalab.config import Conf, _
46
47
  from datalab.gui.processor.catcher import CompOut, wng_err_func
47
48
  from datalab.objectmodel import get_short_id, get_uuid, patch_title_with_ids
@@ -194,6 +195,38 @@ def insert_processing_parameters(
194
195
  obj.set_metadata_option(PROCESSING_PARAMETERS_OPTION, pp.to_dict())
195
196
 
196
197
 
198
+ def clear_analysis_parameters(obj: SignalObj | ImageObj) -> None:
199
+ """Clear analysis parameters from object metadata.
200
+
201
+ This removes the stored analysis parameters (1-to-0 operations) from the object.
202
+ Should be called when all analysis results are deleted to prevent the
203
+ auto_recompute_analysis function from attempting to recompute deleted analyses.
204
+
205
+ Args:
206
+ obj: Signal or Image object
207
+ """
208
+ key = f"__{ANALYSIS_PARAMETERS_OPTION}"
209
+ if key in obj.metadata:
210
+ del obj.metadata[key]
211
+
212
+
213
+ def run_with_env(func: Callable, args: tuple, env_json: str) -> CompOut:
214
+ """Wrapper to apply environment config before calling func
215
+
216
+ Args:
217
+ func: function to call
218
+ args: function arguments
219
+ env_json: JSON string with environment configuration
220
+
221
+ Returns:
222
+ Computation output object containing the result, error message,
223
+ or warning message.
224
+ """
225
+ sigima_options.set_env(env_json)
226
+ sigima_options.ensure_loaded_from_env() # recharge depuis l'env
227
+ return wng_err_func(func, args)
228
+
229
+
197
230
  # Enable multiprocessing support for Windows, with frozen executable (e.g. PyInstaller)
198
231
  multiprocessing.freeze_support()
199
232
 
@@ -220,22 +253,6 @@ COMPUTATION_TIP = _(
220
253
  POOL: Pool | None = None
221
254
 
222
255
 
223
- def run_with_env(func: Callable, args: tuple, env_json: str) -> CompOut:
224
- """Wrapper to apply environment config before calling func
225
-
226
- Args:
227
- func: function to call
228
- args: function arguments
229
-
230
- Returns:
231
- Computation output object containing the result, error message,
232
- or warning message.
233
- """
234
- sigima_options.set_env(env_json)
235
- sigima_options.ensure_loaded_from_env() # recharge depuis l'env
236
- return wng_err_func(func, args)
237
-
238
-
239
256
  class WorkerState(Enum):
240
257
  """Worker states for computation lifecycle."""
241
258
 
@@ -943,7 +960,9 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
943
960
  TableAdapter.remove_all_from(result_obj)
944
961
  GeometryAdapter.remove_all_from(result_obj)
945
962
 
946
- def auto_recompute_analysis(self, obj: SignalObj | ImageObj) -> None:
963
+ def auto_recompute_analysis(
964
+ self, obj: SignalObj | ImageObj, refresh_plot: bool = True
965
+ ) -> None:
947
966
  """Automatically recompute analysis (1-to-0) operations after data changes.
948
967
 
949
968
  This method checks if the object has 1-to-0 analysis parameters (analysis
@@ -960,6 +979,7 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
960
979
 
961
980
  Args:
962
981
  obj: The object whose data was modified
982
+ refresh_plot: Whether to refresh the plot after recomputation
963
983
  """
964
984
  # Check if object has 1-to-0 analysis parameters (analysis operations)
965
985
  proc_params = extract_analysis_parameters(obj)
@@ -972,14 +992,16 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
972
992
  # Get the actual function from the function name
973
993
  feature = self.get_feature(proc_params.func_name)
974
994
 
975
- # Recompute the analysis operation silently
995
+ # Recompute the analysis operation silently, only for this specific object
996
+ # (not all selected objects, to avoid O(n²) behavior when called in a loop)
976
997
  with Conf.proc.show_result_dialog.temp(False):
977
- self.compute_1_to_0(feature.function, param, edit=False)
998
+ self.compute_1_to_0(feature.function, param, edit=False, target_objs=[obj])
978
999
 
979
1000
  # Update the view
980
1001
  obj_uuid = get_uuid(obj)
981
1002
  self.panel.objview.update_item(obj_uuid)
982
- self.panel.refresh_plot(obj_uuid, update_items=True, force=True)
1003
+ if refresh_plot:
1004
+ self.panel.refresh_plot(obj_uuid, update_items=True, force=True)
983
1005
 
984
1006
  def __exec_func(
985
1007
  self,
@@ -1121,6 +1143,9 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
1121
1143
  )
1122
1144
  insert_processing_parameters(new_obj, pp)
1123
1145
 
1146
+ # Mark object as freshly processed to show Processing tab
1147
+ self.panel.objprop.mark_as_freshly_processed(new_obj)
1148
+
1124
1149
  new_gid = None
1125
1150
  if grps:
1126
1151
  # If groups are selected, then it means that there is no
@@ -1327,12 +1352,14 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
1327
1352
  title: str | None = None,
1328
1353
  comment: str | None = None,
1329
1354
  edit: bool | None = None,
1355
+ target_objs: list[SignalObj | ImageObj] | None = None,
1330
1356
  ) -> ResultData:
1331
1357
  """Generic processing method: 1 object in → no object out.
1332
1358
 
1333
- Applies a function to each selected object, returning metadata or measurement
1334
- results (e.g. peak coordinates, statistical properties) without generating
1335
- new objects. Results are stored in the object's metadata and returned as a
1359
+ Applies a function to each selected object (or specified target objects),
1360
+ returning metadata or measurement results (e.g. peak coordinates, statistical
1361
+ properties) without generating new objects. Results are stored in the object's
1362
+ metadata and returned as a
1336
1363
  ResultData instance.
1337
1364
 
1338
1365
  Args:
@@ -1344,6 +1371,8 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
1344
1371
  title: Optional progress bar title.
1345
1372
  comment: Optional comment for parameter dialog.
1346
1373
  edit: Whether to open the parameter editor before execution.
1374
+ target_objs: Optional list of specific objects to process. If None,
1375
+ processes all currently selected objects.
1347
1376
 
1348
1377
  Returns:
1349
1378
  ResultData instance containing the results for all processed objects.
@@ -1360,7 +1389,11 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
1360
1389
  if param is not None:
1361
1390
  if edit and not param.edit(parent=self.mainwindow):
1362
1391
  return None
1363
- objs = self.panel.objview.get_sel_objects(include_groups=True)
1392
+ objs = (
1393
+ target_objs
1394
+ if target_objs is not None
1395
+ else self.panel.objview.get_sel_objects(include_groups=True)
1396
+ )
1364
1397
  current_obj = self.panel.objview.get_current_object()
1365
1398
  title = func.__name__ if title is None else title
1366
1399
  refresh_needed = False
@@ -1416,6 +1449,8 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
1416
1449
  rdata.append(adapter, obj)
1417
1450
 
1418
1451
  if obj is current_obj:
1452
+ # Mark object as having fresh analysis results to show Analysis tab
1453
+ self.panel.objprop.mark_as_fresh_analysis(obj)
1419
1454
  self.panel.selection_changed(update_items=True)
1420
1455
  else:
1421
1456
  self.panel.refresh_plot(get_uuid(obj), True, False)
@@ -2374,8 +2409,13 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
2374
2409
  )
2375
2410
  # Auto-recompute analysis operations for objects with modified ROIs
2376
2411
  if mode == "apply":
2377
- for obj_i in objs:
2378
- self.auto_recompute_analysis(obj_i)
2412
+ with create_progress_bar(
2413
+ self.panel, _("Recomputing..."), max_=len(objs)
2414
+ ) as progress:
2415
+ for idx, obj_i in enumerate(objs):
2416
+ progress.setValue(idx)
2417
+ self.auto_recompute_analysis(obj_i, refresh_plot=False)
2418
+ self.panel.manual_refresh()
2379
2419
  return edited_roi
2380
2420
 
2381
2421
  def edit_roi_numerically(self) -> TypeROI:
@@ -2390,6 +2430,12 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
2390
2430
  obj = self.panel.objview.get_sel_objects()[0]
2391
2431
  assert obj.roi is not None, _("No ROI selected for editing.")
2392
2432
  params = obj.roi.to_params(obj)
2433
+ # Round coordinates to appropriate precision before displaying
2434
+ for param in params:
2435
+ if isinstance(obj, SignalObj):
2436
+ coordutils.round_signal_roi_param(obj, param)
2437
+ elif isinstance(obj, ImageObj):
2438
+ coordutils.round_image_roi_param(obj, param)
2393
2439
  group = gds.DataSetGroup(params, title=_("Regions of Interest"))
2394
2440
  if group.edit(parent=self.mainwindow):
2395
2441
  edited_roi = obj.roi.__class__.from_params(obj, params)
@@ -779,7 +779,8 @@ class ImageProcessor(BaseProcessor[ImageROI, ROI2DParam]):
779
779
  == QW.QMessageBox.No
780
780
  ):
781
781
  return
782
- editor = ImageGridROIEditor(parent=self.parent(), obj=obj0)
782
+ options = self.panel.plothandler.get_plot_options()
783
+ editor = ImageGridROIEditor(parent=self.parent(), obj=obj0, options=options)
783
784
  if exec_dialog(editor):
784
785
  for obj in self.panel.objview.get_sel_objects():
785
786
  obj.roi = editor.get_roi()
@@ -1014,7 +1015,10 @@ class ImageProcessor(BaseProcessor[ImageROI, ROI2DParam]):
1014
1015
  with :py:func:`sigima.proc.image.offset_correction`"""
1015
1016
  obj = self.panel.objview.get_sel_objects(include_groups=True)[0]
1016
1017
  if param is None:
1017
- dlg = imagebackground.ImageBackgroundDialog(obj, parent=self.mainwindow)
1018
+ options = self.panel.plothandler.get_plot_options()
1019
+ dlg = imagebackground.ImageBackgroundDialog(
1020
+ obj, parent=self.mainwindow, options=options
1021
+ )
1018
1022
  if exec_dialog(dlg):
1019
1023
  x0, y0, x1, y1 = dlg.get_rect_coords()
1020
1024
  param = ROI2DParam.create(
@@ -164,6 +164,16 @@ class SignalProcessor(BaseProcessor[SignalROI, ROI1DParam]):
164
164
  icon_name="convolution.svg",
165
165
  obj2_name=_("signal to convolve with"),
166
166
  )
167
+ self.register_2_to_1(
168
+ sips.replace_x_by_other_y,
169
+ _("Replace X by other signal's Y"),
170
+ comment=_(
171
+ "Replace X coordinates using Y values from another signal.\n"
172
+ "Useful for calibration: plot data vs wavelength scale."
173
+ ),
174
+ obj2_name=_("signal providing Y values for X axis"),
175
+ skip_xarray_compat=True,
176
+ )
167
177
  self.register_2_to_1(
168
178
  sips.deconvolution,
169
179
  _("Deconvolution"),
datalab/gui/roieditor.py CHANGED
@@ -186,7 +186,7 @@ class ROIPolygonTool(PolygonTool):
186
186
 
187
187
  def __init__(self, manager: PlotManager, obj: ImageObj) -> None:
188
188
  super().__init__(manager, switch_to_default_tool=False, toolbar_id=None)
189
- self.roi = PolygonalROI([[0, 0], [1, 0], [1, 1], [0, 1]], False)
189
+ self.roi = PolygonalROI([0, 0, 1, 0, 1, 1, 0, 1], False)
190
190
  self.obj = obj
191
191
 
192
192
  def activate(self):
@@ -385,7 +385,7 @@ class BaseROIEditor(
385
385
  """Parent dialog was accepted: updating ROI Editor data"""
386
386
  self.__roi.empty()
387
387
  for roi_item in self.roi_items:
388
- self.__roi.add_roi(plotitem_to_singleroi(roi_item))
388
+ self.__roi.add_roi(plotitem_to_singleroi(roi_item, self.obj))
389
389
  if self.singleobj_btn is not None:
390
390
  Conf.proc.extract_roi_singleobj.set(self.singleobj_btn.isChecked())
391
391
 
Binary file