datalab-platform 1.0.1__py3-none-any.whl → 1.0.2__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.
- datalab/__init__.py +1 -1
- datalab/adapters_plotpy/converters.py +3 -1
- datalab/adapters_plotpy/coordutils.py +157 -0
- datalab/adapters_plotpy/roi/image.py +35 -6
- datalab/adapters_plotpy/roi/signal.py +8 -1
- datalab/config.py +2 -0
- datalab/data/doc/DataLab_en.pdf +0 -0
- datalab/data/doc/DataLab_fr.pdf +0 -0
- datalab/gui/actionhandler.py +3 -2
- datalab/gui/macroeditor.py +18 -1
- datalab/gui/main.py +2 -0
- datalab/gui/newobject.py +7 -0
- datalab/gui/panel/base.py +80 -13
- datalab/gui/plothandler.py +10 -1
- datalab/gui/processor/base.py +29 -16
- datalab/gui/processor/signal.py +10 -0
- datalab/gui/roieditor.py +2 -2
- datalab/tests/features/common/coordutils_unit_test.py +212 -0
- datalab/tests/features/common/roi_plotitem_unit_test.py +4 -2
- datalab/tests/features/macro/macroeditor_unit_test.py +102 -1
- datalab/tests/features/signal/custom_signal_bug_unit_test.py +96 -0
- {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.2.dist-info}/METADATA +3 -3
- {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.2.dist-info}/RECORD +27 -24
- {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.2.dist-info}/WHEEL +0 -0
- {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.2.dist-info}/entry_points.txt +0 -0
- {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.2.dist-info}/top_level.txt +0 -0
datalab/__init__.py
CHANGED
|
@@ -24,7 +24,7 @@ except RuntimeError:
|
|
|
24
24
|
# this module is imported more than once, e.g. when running tests)
|
|
25
25
|
pass
|
|
26
26
|
|
|
27
|
-
__version__ = "1.0.
|
|
27
|
+
__version__ = "1.0.2"
|
|
28
28
|
__docurl__ = __homeurl__ = "https://datalab-platform.com/"
|
|
29
29
|
__supporturl__ = "https://github.com/DataLab-Platform/DataLab/issues/new/choose"
|
|
30
30
|
|
|
@@ -39,11 +39,13 @@ def plotitem_to_singleroi(
|
|
|
39
39
|
| AnnotatedRectangle
|
|
40
40
|
| AnnotatedCircle
|
|
41
41
|
| AnnotatedPolygon,
|
|
42
|
+
obj: SignalObj | ImageObj | None = None,
|
|
42
43
|
) -> SegmentROI | RectangularROI | CircularROI | PolygonalROI:
|
|
43
44
|
"""Create a single ROI from the given PlotPy item to integrate with DataLab
|
|
44
45
|
|
|
45
46
|
Args:
|
|
46
47
|
plot_item: The PlotPy item for which to create a single ROI
|
|
48
|
+
obj: Optional signal or image object for coordinate rounding
|
|
47
49
|
|
|
48
50
|
Returns:
|
|
49
51
|
A single ROI instance
|
|
@@ -66,7 +68,7 @@ def plotitem_to_singleroi(
|
|
|
66
68
|
adapter = PolygonalROIPlotPyAdapter
|
|
67
69
|
else:
|
|
68
70
|
raise TypeError(f"Unsupported PlotPy item type: {type(plot_item)}")
|
|
69
|
-
return adapter.from_plot_item(plot_item)
|
|
71
|
+
return adapter.from_plot_item(plot_item, obj)
|
|
70
72
|
|
|
71
73
|
|
|
72
74
|
def singleroi_to_plotitem(
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
ROI Coordinate Utilities
|
|
5
|
+
=========================
|
|
6
|
+
|
|
7
|
+
This module provides utility functions for rounding ROI coordinates to appropriate
|
|
8
|
+
precision based on the sampling characteristics of signals and images.
|
|
9
|
+
|
|
10
|
+
These functions are used when converting interactive PlotPy shapes to ROI objects
|
|
11
|
+
to ensure coordinates are displayed with reasonable precision.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from sigima.objects import ImageObj, ROI1DParam, ROI2DParam, SignalObj
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def round_signal_coords(
|
|
21
|
+
obj: SignalObj, coords: list[float], precision_factor: float = 0.1
|
|
22
|
+
) -> list[float]:
|
|
23
|
+
"""Round signal coordinates to appropriate precision based on sampling period.
|
|
24
|
+
|
|
25
|
+
Rounds to a fraction of the median sampling period to avoid excessive decimal
|
|
26
|
+
places while maintaining reasonable precision.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
obj: signal object
|
|
30
|
+
coords: coordinates to round
|
|
31
|
+
precision_factor: fraction of sampling period to use as rounding precision.
|
|
32
|
+
Default is 0.1 (1/10th of sampling period).
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Rounded coordinates
|
|
36
|
+
"""
|
|
37
|
+
if len(obj.x) < 2:
|
|
38
|
+
# Cannot compute sampling period, return coords as-is
|
|
39
|
+
return coords
|
|
40
|
+
# Compute median sampling period
|
|
41
|
+
sampling_period = float(np.median(np.diff(obj.x)))
|
|
42
|
+
if sampling_period == 0:
|
|
43
|
+
# Avoid division by zero for constant x arrays
|
|
44
|
+
return coords
|
|
45
|
+
# Round to specified fraction of sampling period
|
|
46
|
+
precision = sampling_period * precision_factor
|
|
47
|
+
# Determine number of decimal places
|
|
48
|
+
if precision > 0:
|
|
49
|
+
decimals = max(0, int(-np.floor(np.log10(precision))))
|
|
50
|
+
return [round(c, decimals) for c in coords]
|
|
51
|
+
return coords
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def round_image_coords(
|
|
55
|
+
obj: ImageObj, coords: list[float], precision_factor: float = 0.1
|
|
56
|
+
) -> list[float]:
|
|
57
|
+
"""Round image coordinates to appropriate precision based on pixel spacing.
|
|
58
|
+
|
|
59
|
+
Rounds to a fraction of the pixel spacing to avoid excessive decimal places
|
|
60
|
+
while maintaining reasonable precision. Uses separate precision for X and Y.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
obj: image object
|
|
64
|
+
coords: flat list of coordinates [x0, y0, x1, y1, ...] to round
|
|
65
|
+
precision_factor: fraction of pixel spacing to use as rounding precision.
|
|
66
|
+
Default is 0.1 (1/10th of pixel spacing).
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Rounded coordinates
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: if coords does not contain an even number of elements
|
|
73
|
+
"""
|
|
74
|
+
if len(coords) % 2 != 0:
|
|
75
|
+
raise ValueError("coords must contain an even number of elements (x, y pairs).")
|
|
76
|
+
if len(coords) == 0:
|
|
77
|
+
return coords
|
|
78
|
+
|
|
79
|
+
rounded = list(coords)
|
|
80
|
+
if obj.is_uniform_coords:
|
|
81
|
+
# Use dx, dy for uniform coordinates
|
|
82
|
+
precision_x = abs(obj.dx) * precision_factor
|
|
83
|
+
precision_y = abs(obj.dy) * precision_factor
|
|
84
|
+
else:
|
|
85
|
+
# Compute average spacing for non-uniform coordinates
|
|
86
|
+
if len(obj.xcoords) > 1:
|
|
87
|
+
avg_dx = float(np.mean(np.abs(np.diff(obj.xcoords))))
|
|
88
|
+
precision_x = avg_dx * precision_factor
|
|
89
|
+
else:
|
|
90
|
+
precision_x = 0
|
|
91
|
+
if len(obj.ycoords) > 1:
|
|
92
|
+
avg_dy = float(np.mean(np.abs(np.diff(obj.ycoords))))
|
|
93
|
+
precision_y = avg_dy * precision_factor
|
|
94
|
+
else:
|
|
95
|
+
precision_y = 0
|
|
96
|
+
|
|
97
|
+
# Round X coordinates (even indices)
|
|
98
|
+
if precision_x > 0:
|
|
99
|
+
decimals_x = max(0, int(-np.floor(np.log10(precision_x))))
|
|
100
|
+
for i in range(0, len(rounded), 2):
|
|
101
|
+
rounded[i] = round(rounded[i], decimals_x)
|
|
102
|
+
|
|
103
|
+
# Round Y coordinates (odd indices)
|
|
104
|
+
if precision_y > 0:
|
|
105
|
+
decimals_y = max(0, int(-np.floor(np.log10(precision_y))))
|
|
106
|
+
for i in range(1, len(rounded), 2):
|
|
107
|
+
rounded[i] = round(rounded[i], decimals_y)
|
|
108
|
+
|
|
109
|
+
return rounded
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def round_signal_roi_param(
|
|
113
|
+
obj: SignalObj, param: ROI1DParam, precision_factor: float = 0.1
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Round signal ROI parameter coordinates in-place.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
obj: signal object
|
|
119
|
+
param: ROI parameter to round (modified in-place)
|
|
120
|
+
precision_factor: fraction of sampling period to use as rounding precision
|
|
121
|
+
"""
|
|
122
|
+
coords = round_signal_coords(obj, [param.xmin, param.xmax], precision_factor)
|
|
123
|
+
param.xmin, param.xmax = coords
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def round_image_roi_param(
|
|
127
|
+
obj: ImageObj, param: ROI2DParam, precision_factor: float = 0.1
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Round image ROI parameter coordinates in-place.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
obj: image object
|
|
133
|
+
param: ROI parameter to round (modified in-place)
|
|
134
|
+
precision_factor: fraction of pixel spacing to use as rounding precision
|
|
135
|
+
"""
|
|
136
|
+
if param.geometry == "rectangle":
|
|
137
|
+
# Round x0, y0, dx, dy
|
|
138
|
+
x0, y0, x1, y1 = param.x0, param.y0, param.x0 + param.dx, param.y0 + param.dy
|
|
139
|
+
coords = round_image_coords(obj, [x0, y0, x1, y1], precision_factor)
|
|
140
|
+
param.x0, param.y0 = coords[0], coords[1]
|
|
141
|
+
# Round dx and dy to avoid floating-point errors in subtraction
|
|
142
|
+
dx_dy_rounded = round_image_coords(
|
|
143
|
+
obj, [coords[2] - coords[0], coords[3] - coords[1]], precision_factor
|
|
144
|
+
)
|
|
145
|
+
param.dx = dx_dy_rounded[0]
|
|
146
|
+
param.dy = dx_dy_rounded[1]
|
|
147
|
+
elif param.geometry == "circle":
|
|
148
|
+
# Round xc, yc, r
|
|
149
|
+
coords = round_image_coords(obj, [param.xc, param.yc], precision_factor)
|
|
150
|
+
param.xc, param.yc = coords
|
|
151
|
+
# Round radius using X precision
|
|
152
|
+
r_rounded = round_image_coords(obj, [param.r, 0], precision_factor)[0]
|
|
153
|
+
param.r = r_rounded
|
|
154
|
+
elif param.geometry == "polygon":
|
|
155
|
+
# Round polygon points
|
|
156
|
+
rounded = round_image_coords(obj, param.points.tolist(), precision_factor)
|
|
157
|
+
param.points = np.array(rounded)
|
|
@@ -13,6 +13,7 @@ from plotpy.items import AnnotatedCircle, AnnotatedPolygon, AnnotatedRectangle
|
|
|
13
13
|
from sigima.objects import CircularROI, ImageObj, ImageROI, PolygonalROI, RectangularROI
|
|
14
14
|
from sigima.tools import coordinates
|
|
15
15
|
|
|
16
|
+
from datalab.adapters_plotpy.coordutils import round_image_coords
|
|
16
17
|
from datalab.adapters_plotpy.roi.base import (
|
|
17
18
|
BaseROIPlotPyAdapter,
|
|
18
19
|
BaseSingleROIPlotPyAdapter,
|
|
@@ -64,14 +65,21 @@ class PolygonalROIPlotPyAdapter(
|
|
|
64
65
|
return item
|
|
65
66
|
|
|
66
67
|
@classmethod
|
|
67
|
-
def from_plot_item(
|
|
68
|
+
def from_plot_item(
|
|
69
|
+
cls, item: AnnotatedPolygon, obj: ImageObj | None = None
|
|
70
|
+
) -> PolygonalROI:
|
|
68
71
|
"""Create ROI from plot item
|
|
69
72
|
|
|
70
73
|
Args:
|
|
71
74
|
item: plot item
|
|
75
|
+
obj: image object for coordinate rounding (optional)
|
|
72
76
|
"""
|
|
77
|
+
coords = item.get_points().flatten().tolist()
|
|
78
|
+
# Round coordinates to appropriate precision
|
|
79
|
+
if obj is not None:
|
|
80
|
+
coords = round_image_coords(obj, coords)
|
|
73
81
|
title = str(item.title().text())
|
|
74
|
-
return PolygonalROI(
|
|
82
|
+
return PolygonalROI(coords, False, title)
|
|
75
83
|
|
|
76
84
|
|
|
77
85
|
class RectangularROIPlotPyAdapter(
|
|
@@ -116,15 +124,22 @@ class RectangularROIPlotPyAdapter(
|
|
|
116
124
|
return item
|
|
117
125
|
|
|
118
126
|
@classmethod
|
|
119
|
-
def from_plot_item(
|
|
127
|
+
def from_plot_item(
|
|
128
|
+
cls, item: AnnotatedRectangle, obj: ImageObj | None = None
|
|
129
|
+
) -> RectangularROI:
|
|
120
130
|
"""Create ROI from plot item
|
|
121
131
|
|
|
122
132
|
Args:
|
|
123
133
|
item: plot item
|
|
134
|
+
obj: image object for coordinate rounding (optional)
|
|
124
135
|
"""
|
|
125
136
|
rect = item.get_rect()
|
|
137
|
+
coords = RectangularROI.rect_to_coords(*rect)
|
|
138
|
+
# Round coordinates to appropriate precision
|
|
139
|
+
if obj is not None:
|
|
140
|
+
coords = round_image_coords(obj, coords)
|
|
126
141
|
title = str(item.title().text())
|
|
127
|
-
return RectangularROI(
|
|
142
|
+
return RectangularROI(coords, False, title)
|
|
128
143
|
|
|
129
144
|
|
|
130
145
|
class CircularROIPlotPyAdapter(
|
|
@@ -166,15 +181,29 @@ class CircularROIPlotPyAdapter(
|
|
|
166
181
|
return item
|
|
167
182
|
|
|
168
183
|
@classmethod
|
|
169
|
-
def from_plot_item(
|
|
184
|
+
def from_plot_item(
|
|
185
|
+
cls, item: AnnotatedCircle, obj: ImageObj | None = None
|
|
186
|
+
) -> CircularROI:
|
|
170
187
|
"""Create ROI from plot item
|
|
171
188
|
|
|
172
189
|
Args:
|
|
173
190
|
item: plot item
|
|
191
|
+
obj: image object for coordinate rounding (optional)
|
|
174
192
|
"""
|
|
175
193
|
rect = item.get_rect()
|
|
194
|
+
coords = CircularROI.rect_to_coords(*rect)
|
|
195
|
+
# Round coordinates to appropriate precision
|
|
196
|
+
# For circular ROI: [xc, yc, r] - round center (xc, yc) as pair, then radius
|
|
197
|
+
if obj is not None:
|
|
198
|
+
xc, yc, r = coords
|
|
199
|
+
# Round center coordinates
|
|
200
|
+
xc_rounded, yc_rounded = round_image_coords(obj, [xc, yc])
|
|
201
|
+
# Round radius using average of X and Y precision
|
|
202
|
+
# For radius, we use the X precision (could also average X and Y)
|
|
203
|
+
r_rounded = round_image_coords(obj, [r, 0])[0]
|
|
204
|
+
coords = [xc_rounded, yc_rounded, r_rounded]
|
|
176
205
|
title = str(item.title().text())
|
|
177
|
-
return CircularROI(
|
|
206
|
+
return CircularROI(coords, False, title)
|
|
178
207
|
|
|
179
208
|
|
|
180
209
|
class ImageROIPlotPyAdapter(BaseROIPlotPyAdapter[ImageROI]):
|
|
@@ -11,6 +11,7 @@ from plotpy.builder import make
|
|
|
11
11
|
from plotpy.items import AnnotatedXRange
|
|
12
12
|
from sigima.objects import SegmentROI, SignalObj, SignalROI
|
|
13
13
|
|
|
14
|
+
from datalab.adapters_plotpy.coordutils import round_signal_coords
|
|
14
15
|
from datalab.adapters_plotpy.roi.base import (
|
|
15
16
|
BaseROIPlotPyAdapter,
|
|
16
17
|
BaseSingleROIPlotPyAdapter,
|
|
@@ -36,11 +37,14 @@ class SegmentROIPlotPyAdapter(BaseSingleROIPlotPyAdapter[SegmentROI, AnnotatedXR
|
|
|
36
37
|
return item
|
|
37
38
|
|
|
38
39
|
@classmethod
|
|
39
|
-
def from_plot_item(
|
|
40
|
+
def from_plot_item(
|
|
41
|
+
cls, item: AnnotatedXRange, obj: SignalObj | None = None
|
|
42
|
+
) -> SegmentROI:
|
|
40
43
|
"""Create ROI from plot item
|
|
41
44
|
|
|
42
45
|
Args:
|
|
43
46
|
item: plot item
|
|
47
|
+
obj: signal object for coordinate rounding (optional)
|
|
44
48
|
|
|
45
49
|
Returns:
|
|
46
50
|
ROI
|
|
@@ -48,6 +52,9 @@ class SegmentROIPlotPyAdapter(BaseSingleROIPlotPyAdapter[SegmentROI, AnnotatedXR
|
|
|
48
52
|
if not isinstance(item, AnnotatedXRange):
|
|
49
53
|
raise TypeError("Invalid plot item type")
|
|
50
54
|
coords = sorted(item.get_range())
|
|
55
|
+
# Round coordinates to appropriate precision
|
|
56
|
+
if obj is not None:
|
|
57
|
+
coords = round_signal_coords(obj, coords)
|
|
51
58
|
title = str(item.title().text())
|
|
52
59
|
return SegmentROI(coords, False, title)
|
|
53
60
|
|
datalab/config.py
CHANGED
|
@@ -301,6 +301,7 @@ class ViewSection(conf.Section, metaclass=conf.SectionMeta):
|
|
|
301
301
|
ima_def_interpolation = conf.Option()
|
|
302
302
|
ima_def_alpha = conf.Option()
|
|
303
303
|
ima_def_alpha_function = conf.Option()
|
|
304
|
+
ima_def_keep_lut_range = conf.Option()
|
|
304
305
|
|
|
305
306
|
# Annotated shape and marker visualization settings for signals
|
|
306
307
|
sig_shape_param = conf.DataSetOption()
|
|
@@ -459,6 +460,7 @@ def initialize():
|
|
|
459
460
|
Conf.view.ima_def_interpolation.get(5)
|
|
460
461
|
Conf.view.ima_def_alpha.get(1.0)
|
|
461
462
|
Conf.view.ima_def_alpha_function.get(LUTAlpha.NONE.value)
|
|
463
|
+
Conf.view.ima_def_keep_lut_range.get(False)
|
|
462
464
|
|
|
463
465
|
# Datetime format strings: % must be escaped as %% for ConfigParser
|
|
464
466
|
Conf.view.sig_datetime_format_s.get("%%H:%%M:%%S")
|
datalab/data/doc/DataLab_en.pdf
CHANGED
|
Binary file
|
datalab/data/doc/DataLab_fr.pdf
CHANGED
|
Binary file
|
datalab/gui/actionhandler.py
CHANGED
|
@@ -1269,7 +1269,9 @@ class SignalActionHandler(BaseActionHandler):
|
|
|
1269
1269
|
with self.new_menu(_("Axis transformation")):
|
|
1270
1270
|
self.action_for("transpose")
|
|
1271
1271
|
self.action_for("reverse_x")
|
|
1272
|
-
self.action_for("
|
|
1272
|
+
self.action_for("replace_x_by_other_y")
|
|
1273
|
+
self.action_for("xy_mode")
|
|
1274
|
+
self.action_for("to_cartesian", separator=True)
|
|
1273
1275
|
self.action_for("to_polar")
|
|
1274
1276
|
with self.new_menu(_("Frequency filters"), icon_name="highpass.svg"):
|
|
1275
1277
|
self.action_for("lowpass")
|
|
@@ -1364,7 +1366,6 @@ class SignalActionHandler(BaseActionHandler):
|
|
|
1364
1366
|
separator=True,
|
|
1365
1367
|
tip=_("Compute all stability features"),
|
|
1366
1368
|
)
|
|
1367
|
-
self.action_for("xy_mode", separator=True)
|
|
1368
1369
|
|
|
1369
1370
|
# MARK: ANALYSIS
|
|
1370
1371
|
with self.new_category(ActionCategory.ANALYSIS):
|
datalab/gui/macroeditor.py
CHANGED
|
@@ -93,6 +93,7 @@ print("All done!")
|
|
|
93
93
|
self.set_code(self.MACRO_SAMPLE)
|
|
94
94
|
self.editor.modificationChanged.connect(self.modification_changed)
|
|
95
95
|
self.process = None
|
|
96
|
+
self.__last_exit_code = None
|
|
96
97
|
|
|
97
98
|
@property
|
|
98
99
|
def title(self) -> str:
|
|
@@ -259,7 +260,14 @@ print("All done!")
|
|
|
259
260
|
self.process = QC.QProcess()
|
|
260
261
|
code = self.get_code().replace('"', "'")
|
|
261
262
|
datalab_path = osp.abspath(osp.join(osp.dirname(datalab.__file__), os.pardir))
|
|
262
|
-
|
|
263
|
+
# Reconfigure stdout/stderr to use UTF-8 encoding to avoid UnicodeEncodeError
|
|
264
|
+
# on Windows with locales that don't support all Unicode characters
|
|
265
|
+
# (e.g., cp1252)
|
|
266
|
+
code = (
|
|
267
|
+
f"import sys; sys.path.append(r'{datalab_path}'); "
|
|
268
|
+
f"sys.stdout.reconfigure(encoding='utf-8'); "
|
|
269
|
+
f"sys.stderr.reconfigure(encoding='utf-8'){os.linesep}{code}"
|
|
270
|
+
)
|
|
263
271
|
env = QC.QProcessEnvironment()
|
|
264
272
|
env.insert(execenv.XMLRPCPORT_ENV, str(execenv.xmlrpcport))
|
|
265
273
|
sysenv = env.systemEnvironment()
|
|
@@ -305,6 +313,15 @@ print("All done!")
|
|
|
305
313
|
exit_code: Exit code
|
|
306
314
|
exit_status: Exit status
|
|
307
315
|
"""
|
|
316
|
+
self.__last_exit_code = exit_code
|
|
308
317
|
self.print(_("# <== '%s' macro has finished") % self.title, eol_before=False)
|
|
309
318
|
self.FINISHED.emit()
|
|
310
319
|
self.process = None
|
|
320
|
+
|
|
321
|
+
def get_exit_code(self) -> int | None:
|
|
322
|
+
"""Return last exit code of the macro process
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Last exit code or None if process has not finished yet
|
|
326
|
+
"""
|
|
327
|
+
return self.__last_exit_code
|
datalab/gui/main.py
CHANGED
|
@@ -1617,6 +1617,8 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
1617
1617
|
)
|
|
1618
1618
|
if answer == QW.QMessageBox.Yes:
|
|
1619
1619
|
reset_all = True
|
|
1620
|
+
elif answer == QW.QMessageBox.No:
|
|
1621
|
+
reset_all = False
|
|
1620
1622
|
elif answer == QW.QMessageBox.Ignore:
|
|
1621
1623
|
Conf.io.h5_clear_workspace_ask.set(False)
|
|
1622
1624
|
if h5files is None:
|
datalab/gui/newobject.py
CHANGED
|
@@ -122,6 +122,13 @@ def create_signal_gui(
|
|
|
122
122
|
param = NewSignalParam()
|
|
123
123
|
edit = True # Default to editing if no parameters provided
|
|
124
124
|
|
|
125
|
+
# CustomSignalParam requires edit mode to initialize the xyarray.
|
|
126
|
+
# Without this, if edit=False (the default in new_object), the setup_array
|
|
127
|
+
# call would be skipped, leaving xyarray as None, which would cause an
|
|
128
|
+
# AttributeError when trying to access param.xyarray.T later.
|
|
129
|
+
if isinstance(param, OrigCustomSignalParam):
|
|
130
|
+
edit = True
|
|
131
|
+
|
|
125
132
|
if isinstance(param, OrigCustomSignalParam) and edit:
|
|
126
133
|
p_init = NewSignalParam(_("Custom signal"))
|
|
127
134
|
p_init.size = 10 # Set smaller default size for initial input
|
datalab/gui/panel/base.py
CHANGED
|
@@ -216,6 +216,13 @@ class ObjectProp(QW.QWidget):
|
|
|
216
216
|
self.analysis_parameters.setReadOnly(True)
|
|
217
217
|
self.analysis_parameters.setFont(font)
|
|
218
218
|
|
|
219
|
+
# Track newly created objects to show Creation tab only once
|
|
220
|
+
self._newly_created_obj_uuid: str | None = None
|
|
221
|
+
# Track when analysis results were just computed
|
|
222
|
+
self._fresh_analysis_obj_uuid: str | None = None
|
|
223
|
+
# Track when object was just processed (1-to-1)
|
|
224
|
+
self._fresh_processing_obj_uuid: str | None = None
|
|
225
|
+
|
|
219
226
|
self.tabwidget.addTab(
|
|
220
227
|
self.processing_history, get_icon("history.svg"), _("History")
|
|
221
228
|
)
|
|
@@ -370,11 +377,16 @@ class ObjectProp(QW.QWidget):
|
|
|
370
377
|
self.properties.get()
|
|
371
378
|
self.properties.apply_button.setEnabled(False)
|
|
372
379
|
|
|
373
|
-
def update_properties_from(
|
|
380
|
+
def update_properties_from(
|
|
381
|
+
self,
|
|
382
|
+
obj: SignalObj | ImageObj | None = None,
|
|
383
|
+
force_tab: Literal["creation", "processing", "analysis", None] | None = None,
|
|
384
|
+
) -> None:
|
|
374
385
|
"""Update properties panel (properties, creation, processing) from object.
|
|
375
386
|
|
|
376
387
|
Args:
|
|
377
388
|
obj: Signal or Image object
|
|
389
|
+
force_tab: Force a specific tab to be current
|
|
378
390
|
"""
|
|
379
391
|
self.properties.setDisabled(obj is None)
|
|
380
392
|
if obj is None:
|
|
@@ -411,30 +423,56 @@ class ObjectProp(QW.QWidget):
|
|
|
411
423
|
self.processing_scroll = None
|
|
412
424
|
|
|
413
425
|
# Setup Creation and Processing tabs (if applicable)
|
|
414
|
-
has_creation_tab =
|
|
426
|
+
has_creation_tab = False
|
|
427
|
+
has_processing_tab = False
|
|
415
428
|
if obj is not None:
|
|
416
429
|
has_creation_tab = self.setup_creation_tab(obj)
|
|
417
|
-
has_processing_tab = self.setup_processing_tab(obj)
|
|
430
|
+
has_processing_tab = self.setup_processing_tab(obj) # Processing tab setup
|
|
418
431
|
|
|
419
432
|
# Trigger visibility update for History and Analysis parameters tabs
|
|
420
433
|
# (will be called via textChanged signals, but we call explicitly
|
|
421
434
|
# here to ensure initial state is correct)
|
|
422
435
|
self._update_tab_visibility()
|
|
423
436
|
|
|
424
|
-
#
|
|
425
|
-
#
|
|
426
|
-
#
|
|
427
|
-
#
|
|
428
|
-
#
|
|
429
|
-
if
|
|
430
|
-
self.tabwidget.setCurrentWidget(self.analysis_parameters)
|
|
431
|
-
elif has_creation_tab:
|
|
437
|
+
# Determine which tab to show based on force_tab parameter:
|
|
438
|
+
# - If force_tab="creation" and Creation tab exists, show it
|
|
439
|
+
# - If force_tab="processing" and Processing tab exists, show it
|
|
440
|
+
# - If force_tab="analysis" and Analysis tab has content, show it
|
|
441
|
+
# - Otherwise, always show Properties tab (default behavior)
|
|
442
|
+
if force_tab == "creation" and has_creation_tab:
|
|
432
443
|
self.tabwidget.setCurrentWidget(self.creation_scroll)
|
|
433
|
-
elif has_processing_tab:
|
|
444
|
+
elif force_tab == "processing" and has_processing_tab:
|
|
434
445
|
self.tabwidget.setCurrentWidget(self.processing_scroll)
|
|
446
|
+
elif force_tab == "analysis" and has_analysis_parameters:
|
|
447
|
+
self.tabwidget.setCurrentWidget(self.analysis_parameters)
|
|
435
448
|
else:
|
|
449
|
+
# Default: always show Properties tab when switching objects
|
|
436
450
|
self.tabwidget.setCurrentWidget(self.properties)
|
|
437
451
|
|
|
452
|
+
def mark_as_newly_created(self, obj: SignalObj | ImageObj) -> None:
|
|
453
|
+
"""Mark object to show Creation tab on next selection.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
obj: Object to mark
|
|
457
|
+
"""
|
|
458
|
+
self._newly_created_obj_uuid = get_uuid(obj)
|
|
459
|
+
|
|
460
|
+
def mark_as_freshly_processed(self, obj: SignalObj | ImageObj) -> None:
|
|
461
|
+
"""Mark object to show Processing tab on next selection.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
obj: Object to mark
|
|
465
|
+
"""
|
|
466
|
+
self._fresh_processing_obj_uuid = get_uuid(obj)
|
|
467
|
+
|
|
468
|
+
def mark_as_fresh_analysis(self, obj: SignalObj | ImageObj) -> None:
|
|
469
|
+
"""Mark object to show Analysis tab on next selection.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
obj: Object to mark
|
|
473
|
+
"""
|
|
474
|
+
self._fresh_analysis_obj_uuid = get_uuid(obj)
|
|
475
|
+
|
|
438
476
|
def get_changed_properties(self) -> dict[str, Any]:
|
|
439
477
|
"""Get dictionary of properties that have changed from original values.
|
|
440
478
|
|
|
@@ -1490,6 +1528,16 @@ class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]):
|
|
|
1490
1528
|
obj.check_data()
|
|
1491
1529
|
self.objmodel.add_object(obj, group_id)
|
|
1492
1530
|
|
|
1531
|
+
# Mark this object as newly created to show Creation tab on first selection
|
|
1532
|
+
# BUT: Don't overwrite if this object is already marked as freshly processed
|
|
1533
|
+
# or has fresh analysis results (those take precedence)
|
|
1534
|
+
obj_uuid = get_uuid(obj)
|
|
1535
|
+
if (
|
|
1536
|
+
obj_uuid != self.objprop._fresh_processing_obj_uuid
|
|
1537
|
+
and obj_uuid != self.objprop._fresh_analysis_obj_uuid
|
|
1538
|
+
):
|
|
1539
|
+
self.objprop.mark_as_newly_created(obj)
|
|
1540
|
+
|
|
1493
1541
|
# Block signals to avoid updating the plot (unnecessary refresh)
|
|
1494
1542
|
self.objview.blockSignals(True)
|
|
1495
1543
|
self.objview.add_object_item(obj, group_id, set_current=set_current)
|
|
@@ -2419,7 +2467,26 @@ class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]):
|
|
|
2419
2467
|
"""
|
|
2420
2468
|
selected_objects = self.objview.get_sel_objects(include_groups=True)
|
|
2421
2469
|
selected_groups = self.objview.get_sel_groups()
|
|
2422
|
-
|
|
2470
|
+
|
|
2471
|
+
# Determine which tab to show based on object state
|
|
2472
|
+
current_obj = self.objview.get_current_object()
|
|
2473
|
+
force_tab = None
|
|
2474
|
+
if current_obj is not None:
|
|
2475
|
+
obj_uuid = get_uuid(current_obj)
|
|
2476
|
+
# Show Creation tab for newly created objects (only once)
|
|
2477
|
+
if obj_uuid == self.objprop._newly_created_obj_uuid:
|
|
2478
|
+
force_tab = "creation"
|
|
2479
|
+
self.objprop._newly_created_obj_uuid = None
|
|
2480
|
+
# Show Processing tab for freshly processed objects (only once)
|
|
2481
|
+
elif obj_uuid == self.objprop._fresh_processing_obj_uuid:
|
|
2482
|
+
force_tab = "processing"
|
|
2483
|
+
self.objprop._fresh_processing_obj_uuid = None
|
|
2484
|
+
# Show Analysis tab for objects with fresh analysis results
|
|
2485
|
+
elif obj_uuid == self.objprop._fresh_analysis_obj_uuid:
|
|
2486
|
+
force_tab = "analysis"
|
|
2487
|
+
self.objprop._fresh_analysis_obj_uuid = None
|
|
2488
|
+
|
|
2489
|
+
self.objprop.update_properties_from(current_obj, force_tab=force_tab)
|
|
2423
2490
|
self.acthandler.selected_objects_changed(selected_groups, selected_objects)
|
|
2424
2491
|
self.refresh_plot("selected", update_items, False)
|
|
2425
2492
|
|
datalab/gui/plothandler.py
CHANGED
|
@@ -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
|
|
datalab/gui/processor/base.py
CHANGED
|
@@ -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,23 @@ def insert_processing_parameters(
|
|
|
194
195
|
obj.set_metadata_option(PROCESSING_PARAMETERS_OPTION, pp.to_dict())
|
|
195
196
|
|
|
196
197
|
|
|
198
|
+
def run_with_env(func: Callable, args: tuple, env_json: str) -> CompOut:
|
|
199
|
+
"""Wrapper to apply environment config before calling func
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
func: function to call
|
|
203
|
+
args: function arguments
|
|
204
|
+
env_json: JSON string with environment configuration
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Computation output object containing the result, error message,
|
|
208
|
+
or warning message.
|
|
209
|
+
"""
|
|
210
|
+
sigima_options.set_env(env_json)
|
|
211
|
+
sigima_options.ensure_loaded_from_env() # recharge depuis l'env
|
|
212
|
+
return wng_err_func(func, args)
|
|
213
|
+
|
|
214
|
+
|
|
197
215
|
# Enable multiprocessing support for Windows, with frozen executable (e.g. PyInstaller)
|
|
198
216
|
multiprocessing.freeze_support()
|
|
199
217
|
|
|
@@ -220,22 +238,6 @@ COMPUTATION_TIP = _(
|
|
|
220
238
|
POOL: Pool | None = None
|
|
221
239
|
|
|
222
240
|
|
|
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
241
|
class WorkerState(Enum):
|
|
240
242
|
"""Worker states for computation lifecycle."""
|
|
241
243
|
|
|
@@ -1121,6 +1123,9 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
|
|
|
1121
1123
|
)
|
|
1122
1124
|
insert_processing_parameters(new_obj, pp)
|
|
1123
1125
|
|
|
1126
|
+
# Mark object as freshly processed to show Processing tab
|
|
1127
|
+
self.panel.objprop.mark_as_freshly_processed(new_obj)
|
|
1128
|
+
|
|
1124
1129
|
new_gid = None
|
|
1125
1130
|
if grps:
|
|
1126
1131
|
# If groups are selected, then it means that there is no
|
|
@@ -1416,6 +1421,8 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
|
|
|
1416
1421
|
rdata.append(adapter, obj)
|
|
1417
1422
|
|
|
1418
1423
|
if obj is current_obj:
|
|
1424
|
+
# Mark object as having fresh analysis results to show Analysis tab
|
|
1425
|
+
self.panel.objprop.mark_as_fresh_analysis(obj)
|
|
1419
1426
|
self.panel.selection_changed(update_items=True)
|
|
1420
1427
|
else:
|
|
1421
1428
|
self.panel.refresh_plot(get_uuid(obj), True, False)
|
|
@@ -2390,6 +2397,12 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
|
|
|
2390
2397
|
obj = self.panel.objview.get_sel_objects()[0]
|
|
2391
2398
|
assert obj.roi is not None, _("No ROI selected for editing.")
|
|
2392
2399
|
params = obj.roi.to_params(obj)
|
|
2400
|
+
# Round coordinates to appropriate precision before displaying
|
|
2401
|
+
for param in params:
|
|
2402
|
+
if isinstance(obj, SignalObj):
|
|
2403
|
+
coordutils.round_signal_roi_param(obj, param)
|
|
2404
|
+
elif isinstance(obj, ImageObj):
|
|
2405
|
+
coordutils.round_image_roi_param(obj, param)
|
|
2393
2406
|
group = gds.DataSetGroup(params, title=_("Regions of Interest"))
|
|
2394
2407
|
if group.edit(parent=self.mainwindow):
|
|
2395
2408
|
edited_roi = obj.roi.__class__.from_params(obj, params)
|