Skip to content

Commit 21b6c15

Browse files
committed
Merge branch 'release' into develop
2 parents 6b25402 + ab6da97 commit 21b6c15

61 files changed

Lines changed: 6355 additions & 3328 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test_pyqt5.yml

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ name: Install and Test on Ubuntu (latest) with PyQt5
77

88
on:
99
push:
10-
branches: [ "main", "develop" ]
10+
branches: [ "main", "develop", "release" ]
1111
pull_request:
12-
branches: [ "main", "develop" ]
12+
branches: [ "main", "develop", "release" ]
1313

1414
jobs:
1515
build:
@@ -55,6 +55,29 @@ jobs:
5555
pip install -r deps.txt
5656
# Install DataLab without dependencies
5757
pip install --no-deps .
58+
elif [ "${{ github.ref_name }}" = "release" ]; then
59+
# Clone dependencies from release branches (with fallback to main/master)
60+
cd ..
61+
# Try cloning PythonQwt from main or master
62+
git clone --depth 1 https://github.com/PlotPyStack/PythonQwt.git || git clone --depth 1 --branch master https://github.com/PlotPyStack/PythonQwt.git
63+
# Try cloning guidata from release, fallback to main or master
64+
git clone --depth 1 --branch release https://github.com/PlotPyStack/guidata.git || git clone --depth 1 https://github.com/PlotPyStack/guidata.git || git clone --depth 1 --branch master https://github.com/PlotPyStack/guidata.git
65+
# Try cloning plotpy from release, fallback to main or master
66+
git clone --depth 1 --branch release https://github.com/PlotPyStack/plotpy.git || git clone --depth 1 https://github.com/PlotPyStack/plotpy.git || git clone --depth 1 --branch master https://github.com/PlotPyStack/plotpy.git
67+
# Try cloning sigima from release, fallback to main
68+
git clone --depth 1 --branch release https://github.com/DataLab-Platform/sigima.git || git clone --depth 1 https://github.com/DataLab-Platform/sigima.git
69+
cd DataLab
70+
pip install -e ../guidata
71+
pip install -e ../PythonQwt
72+
pip install -e ../plotpy
73+
pip install -e ../sigima
74+
# Install tomli for TOML parsing (safe if already present)
75+
pip install tomli
76+
# Extract dependencies and save to file, then install
77+
python -c "import tomli; f=open('pyproject.toml','rb'); data=tomli.load(f); deps=[d for d in data['project']['dependencies'] if not any(p in d for p in ['guidata','PlotPy','Sigima'])]; open('deps.txt','w').write('\n'.join(deps))"
78+
pip install -r deps.txt
79+
# Install DataLab without dependencies
80+
pip install --no-deps .
5881
else
5982
# Install from PyPI normally for main branch
6083
pip install .

CHANGELOG.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,77 @@ See DataLab [roadmap page](https://datalab-platform.com/en/contributing/roadmap.
1717

1818
### 🛠️ Bug Fixes ###
1919

20+
**Signal axis calibration - Replace X by other signal's Y:**
21+
22+
* Added new "Replace X by other signal's Y" operation in Processing > Axis transformation menu for signal calibration workflows
23+
* Addresses critical missing functionality reported by users who needed to apply wavelength calibration or similar transformations to spectroscopy data
24+
* The operation combines two signals: uses Y values from one signal as the new X coordinates for another signal's Y values
25+
* Unlike X-Y mode (which resamples and interpolates), this operation directly uses Y arrays without interpolation, preserving exact calibration values
26+
* Requires both signals to have the same number of points - raises clear error message if sizes don't match
27+
* Automatically transfers metadata: X axis label/unit taken from calibration signal's Y label/unit
28+
* Menu location: Processing > Axis transformation > "Replace X by other signal's Y"
29+
* This closes [Issue #273](https://github.com/datalab-platform/datalab/issues/273) - Missing signal axis calibration: no way to replace X with Y from another signal
30+
31+
**X-Y mode:**
32+
33+
* The X-Y mode processing operation for signals has been moved to Processing > Axis transformation > "X-Y mode" for better discoverability
34+
* The nuance between X-Y mode (which resamples/interpolates) and the new "Replace X by other signal's Y" operation has been clarified in documentation
35+
36+
**Lock LUT setting persistence:**
37+
38+
* Fixed "Lock LUT range when updating data" setting not persisting in Settings > Visualization > Images > Default Image visualization settings
39+
* The `keep_lut_range` parameter was not being saved to configuration, causing the checkbox to systematically uncheck itself after validation (added missing `ima_def_keep_lut_range` option in configuration)
40+
* This closes [Issue #270](https://github.com/datalab-platform/datalab/issues/270) - Lock LUT setting not persisting in image visualization defaults
41+
42+
**Custom signal creation:**
43+
44+
* Fixed `AttributeError: 'NoneType' object has no attribute 'T'` error when creating a custom signal from the menu
45+
* This closes [Issue #269](https://github.com/datalab-platform/datalab/issues/269) - Custom Signal Creation: `AttributeError` when creating signal from menu
46+
2047
**Macro execution:**
2148

2249
* Fixed `UnicodeEncodeError` when executing macros that print Unicode characters (e.g., arrows ``) on Windows systems with certain locales, e.g. cp1252 (closes [Issue #263](https://github.com/datalab-platform/datalab/issues/263))
2350
* The macro subprocess now automatically uses UTF-8 encoding for stdout and stderr, eliminating the need to manually add `sys.stdout.reconfigure(encoding='utf-8')` at the beginning of each macro.
2451

52+
**ROI coordinate precision:**
53+
54+
* ROI coordinates are now automatically rounded to appropriate precision when defining ROIs interactively from geometrical shapes, avoiding excessive decimal places while maintaining reasonable precision relative to data sampling (1/10th of sampling period for signals, 1/10th of pixel spacing for images)
55+
* ROI coordinates are also rounded when displaying them in the "Edit numerically" dialog, preventing floating-point arithmetic errors from showing excessive decimal places (e.g., 172.29999999999995 is now displayed as 172.3)
56+
* This closes [Issue #266](https://github.com/datalab-platform/datalab/issues/266) - Excessive decimal places in ROI coordinates
57+
58+
**Polygonal ROI handling:**
59+
60+
* Fixed `ValueError: Buffer has wrong number of dimensions` error when creating masks from polygonal ROIs in the ROI editor
61+
* The PolygonalTool was incorrectly initializing ROI coordinates as a nested list instead of a flat list, causing mask computation to fail
62+
63+
**HDF5 file opening dialog:**
64+
65+
* Fixed bug where user's choice in the "clear workspace" confirmation dialog was ignored when opening HDF5 files
66+
* When the user clicked "No" in the dialog, the default configuration setting was applied instead of respecting the user's choice
67+
* This closes [Issue #267](https://github.com/datalab-platform/datalab/issues/267) - HDF5 file opening dialog ignores user's choice
68+
69+
**Creation tab axis update:**
70+
71+
* Fixed plot not updating when modifying only xmin/xmax parameters for distribution signals (Zero, Normal, Poisson, Uniform) in the Creation tab
72+
* The issue occurred because the data hash calculation only considered Y values, so changes to X axis bounds were not detected
73+
* Plot now properly refreshes when any axis parameter changes, even if Y values remain identical
74+
* This closes [Issue #268](https://github.com/datalab-platform/datalab/issues/268) - Creation tab axis not updating for distribution signals
75+
76+
**ROI statistics with out-of-bounds ROI:**
77+
78+
* Fixed `ValueError: zero-size array to reduction operation minimum which has no identity` error when computing statistics on images with ROI extending beyond canvas boundaries
79+
* The issue occurred when a ROI partially or completely extended outside the image bounds, resulting in empty array slices during statistics computation
80+
* ROI bounding boxes are now properly clipped to image boundaries, and fully out-of-bounds ROIs return NaN statistics values
81+
* This fix is implemented in Sigima library (see [Issue #1](https://github.com/DataLab-Platform/Sigima/issues/1) - `ValueError` when computing statistics on ROI extending beyond image boundaries)
82+
83+
**Object property panel tab selection:**
84+
85+
* Fixed tab selection behavior in object properties panel to be more predictable and user-friendly
86+
* Properties tab is now always shown by default when switching between objects, providing consistent navigation
87+
* Creation, Processing, and Analysis tabs now appear automatically only once after their respective triggering events (object creation, 1-to-1 processing, or analysis computation), then revert to Properties tab for subsequent selections
88+
* This eliminates the confusing behavior where the tab would arbitrarily persist or change based on previous selections
89+
* This closes [Issue #271](https://github.com/datalab-platform/datalab/issues/271) - Improve object property panel tab selection behavior
90+
2591
## DataLab Version 1.0.1 ##
2692

2793
This major release represents a significant milestone for DataLab with numerous enhancements across all areas. The changes are organized by category for easier navigation.

MANIFEST.in

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ graft resources
33
graft macros
44
graft plugins
55
include conftest.py
6-
include CHANGELOG.md
76
include CONTRIBUTING.md
87
include requirements.txt
98
include *.desktop

datalab/adapters_plotpy/converters.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@ def plotitem_to_singleroi(
3939
| AnnotatedRectangle
4040
| AnnotatedCircle
4141
| AnnotatedPolygon,
42+
obj: SignalObj | ImageObj | None = None,
4243
) -> SegmentROI | RectangularROI | CircularROI | PolygonalROI:
4344
"""Create a single ROI from the given PlotPy item to integrate with DataLab
4445
4546
Args:
4647
plot_item: The PlotPy item for which to create a single ROI
48+
obj: Optional signal or image object for coordinate rounding
4749
4850
Returns:
4951
A single ROI instance
@@ -66,7 +68,7 @@ def plotitem_to_singleroi(
6668
adapter = PolygonalROIPlotPyAdapter
6769
else:
6870
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)
7072

7173

7274
def singleroi_to_plotitem(
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

datalab/adapters_plotpy/roi/image.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from sigima.objects import CircularROI, ImageObj, ImageROI, PolygonalROI, RectangularROI
1414
from sigima.tools import coordinates
1515

16+
from datalab.adapters_plotpy.coordutils import round_image_coords
1617
from datalab.adapters_plotpy.roi.base import (
1718
BaseROIPlotPyAdapter,
1819
BaseSingleROIPlotPyAdapter,
@@ -64,14 +65,21 @@ def info_callback(item: AnnotatedPolygon) -> str:
6465
return item
6566

6667
@classmethod
67-
def from_plot_item(cls, item: AnnotatedPolygon) -> PolygonalROI:
68+
def from_plot_item(
69+
cls, item: AnnotatedPolygon, obj: ImageObj | None = None
70+
) -> PolygonalROI:
6871
"""Create ROI from plot item
6972
7073
Args:
7174
item: plot item
75+
obj: image object for coordinate rounding (optional)
7276
"""
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)
7381
title = str(item.title().text())
74-
return PolygonalROI(item.get_points().flatten(), False, title)
82+
return PolygonalROI(coords, False, title)
7583

7684

7785
class RectangularROIPlotPyAdapter(
@@ -116,15 +124,22 @@ def info_callback(item: AnnotatedRectangle) -> str:
116124
return item
117125

118126
@classmethod
119-
def from_plot_item(cls, item: AnnotatedRectangle) -> RectangularROI:
127+
def from_plot_item(
128+
cls, item: AnnotatedRectangle, obj: ImageObj | None = None
129+
) -> RectangularROI:
120130
"""Create ROI from plot item
121131
122132
Args:
123133
item: plot item
134+
obj: image object for coordinate rounding (optional)
124135
"""
125136
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)
126141
title = str(item.title().text())
127-
return RectangularROI(RectangularROI.rect_to_coords(*rect), False, title)
142+
return RectangularROI(coords, False, title)
128143

129144

130145
class CircularROIPlotPyAdapter(
@@ -166,15 +181,29 @@ def info_callback(item: AnnotatedCircle) -> str:
166181
return item
167182

168183
@classmethod
169-
def from_plot_item(cls, item: AnnotatedCircle) -> CircularROI:
184+
def from_plot_item(
185+
cls, item: AnnotatedCircle, obj: ImageObj | None = None
186+
) -> CircularROI:
170187
"""Create ROI from plot item
171188
172189
Args:
173190
item: plot item
191+
obj: image object for coordinate rounding (optional)
174192
"""
175193
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]
176205
title = str(item.title().text())
177-
return CircularROI(CircularROI.rect_to_coords(*rect), False, title)
206+
return CircularROI(coords, False, title)
178207

179208

180209
class ImageROIPlotPyAdapter(BaseROIPlotPyAdapter[ImageROI]):

0 commit comments

Comments
 (0)