Skip to content

Commit 49d063f

Browse files
committed
Merge branch 'main' into develop
# Conflicts: # doc/locale/fr/LC_MESSAGES/contributing/changelog.po
2 parents 07d0590 + f231248 commit 49d063f

File tree

8 files changed

+79
-95
lines changed

8 files changed

+79
-95
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ See DataLab [roadmap page](https://datalab-platform.com/en/contributing/roadmap.
2828

2929
🛠️ Bug fixes:
3030

31+
* Fixed [Issue #233](https://github.com/DataLab-Platform/DataLab/issues/233) - Hard crash when trying to activate the curve stats tool on a zero signal
3132
* Fixed [Issue #184](https://github.com/DataLab-Platform/DataLab/issues/184) - Curve marker style unexpectedly changes to "Square" after validating "Parameters…" dialog
33+
* Fixed [Issue #117](https://github.com/DataLab-Platform/DataLab/issues/117) - DataLab's signal moving median crashes on Linux with `mode='mirror'`: `free(): invalid next size (normal)` (this is a bug in SciPy v1.15.0 to v1.15.2, which was fixed in SciPy v1.15.3)
3234

3335
ℹ️ Other changes:
3436

cdl/core/gui/docks.py

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -63,26 +63,58 @@
6363
from plotpy.styles import BaseImageParam
6464

6565

66-
def fwhm_info(x, y):
67-
"""Return FWHM information string"""
68-
try:
69-
with warnings.catch_warnings(record=True) as w:
70-
x0, _y0, x1, _y1 = fwhm((x, y), "zero-crossing")
71-
wstr = " ⚠️" if w else ""
72-
except ValueError:
73-
return "🛑"
74-
return f"{x1 - x0:g}{wstr}"
75-
76-
77-
CURVESTATSTOOL_LABELFUNCS = (
78-
("%g < x < %g", lambda *args: (np.nanmin(args[0]), np.nanmax(args[0]))),
79-
("%g < y < %g", lambda *args: (np.nanmin(args[1]), np.nanmax(args[1]))),
80-
("<y>=%g", lambda *args: np.nanmean(args[1])),
81-
("σ(y)=%g", lambda *args: np.nanstd(args[1])),
82-
("∑(y)=%g", lambda *args: spt.trapezoid(args[1])),
83-
("∫ydx=%g<br>", lambda *args: spt.trapezoid(args[1], args[0])),
84-
("FWHM = %s", fwhm_info),
85-
)
66+
class CurveStatsToolFunctions:
67+
"""Statistical functions for CurveStatsTool"""
68+
69+
@classmethod
70+
def set_labelfuncs(cls, statstool: CurveStatsTool) -> None:
71+
"""Set label functions for CurveStatsTool"""
72+
labelfuncs = (
73+
("%g &lt; x &lt; %g", lambda *args: cls.nan_min_max(args[0])),
74+
("%g &lt; y &lt; %g", lambda *args: cls.nan_min_max(args[1])),
75+
("&lt;y&gt;=%g", lambda *args: cls.nan_mean(args[1])),
76+
("σ(y)=%g", lambda *args: cls.nan_std(args[1])),
77+
("∑(y)=%g", lambda *args: spt.trapezoid(args[1])),
78+
("∫ydx=%g<br>", lambda *args: spt.trapezoid(args[1], args[0])),
79+
("FWHM = %s", cls.fwhm_info),
80+
)
81+
statstool.set_labelfuncs(labelfuncs)
82+
83+
@staticmethod
84+
def nan_min_max(arr: np.ndarray) -> tuple[float, float]:
85+
"""Return min/max tuple"""
86+
with warnings.catch_warnings():
87+
warnings.simplefilter("ignore", RuntimeWarning)
88+
min_val = np.nanmin(arr)
89+
max_val = np.nanmax(arr)
90+
return (min_val, max_val)
91+
92+
@staticmethod
93+
def nan_mean(arr: np.ndarray) -> float:
94+
"""Return mean value, ignoring NaNs"""
95+
with warnings.catch_warnings():
96+
warnings.simplefilter("ignore", RuntimeWarning)
97+
mean_val = np.nanmean(arr)
98+
return mean_val
99+
100+
@staticmethod
101+
def nan_std(arr: np.ndarray) -> float:
102+
"""Return standard deviation, ignoring NaNs"""
103+
with warnings.catch_warnings():
104+
warnings.simplefilter("ignore", RuntimeWarning)
105+
std_val = np.nanstd(arr)
106+
return std_val
107+
108+
@staticmethod
109+
def fwhm_info(x, y):
110+
"""Return FWHM information string"""
111+
try:
112+
with warnings.catch_warnings(record=True) as w:
113+
x0, _y0, x1, _y1 = fwhm((x, y), "zero-crossing")
114+
wstr = " ⚠️" if w else ""
115+
except (ValueError, ZeroDivisionError):
116+
return "🛑"
117+
return f"{x1 - x0:g}{wstr}"
86118

87119

88120
def get_more_image_stats(
@@ -245,7 +277,7 @@ def __register_other_tools(self) -> None:
245277
if self.options.type == PlotType.CURVE:
246278
mgr.register_curve_tools()
247279
statstool = mgr.get_tool(CurveStatsTool)
248-
statstool.set_labelfuncs(CURVESTATSTOOL_LABELFUNCS)
280+
CurveStatsToolFunctions.set_labelfuncs(statstool)
249281
else:
250282
mgr.register_image_tools()
251283
# Customizing the ImageStatsTool

cdl/core/remote.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import guidata.dataset as gds
2828
import numpy as np
2929
from guidata.io import JSONReader, JSONWriter
30+
from packaging.version import Version
3031
from qtpy import QtCore as QC
3132

3233
import cdl
@@ -36,7 +37,6 @@
3637
from cdl.core.model.image import ImageObj, create_image
3738
from cdl.core.model.signal import SignalObj, create_signal
3839
from cdl.env import execenv
39-
from cdl.utils.misc import is_version_at_least
4040

4141
if TYPE_CHECKING:
4242
from cdl.core.gui.main import CDLMainWindow
@@ -798,14 +798,14 @@ def __connect_to_server(self) -> None:
798798
version = self.get_version()
799799
except ConnectionRefusedError as exc:
800800
raise ConnectionRefusedError("DataLab is currently not running") from exc
801-
# If DataLab version is not compatible with this client, show a warning using
802-
# standard `warnings` module:
803-
minor_version = ".".join(cdl.__version__.split(".")[:2])
804-
if not is_version_at_least(version, minor_version):
801+
# If DataLab version is not compatible with this client, show a warning
802+
server_ver = Version(version)
803+
client_ver = Version(cdl.__version__)
804+
if server_ver < client_ver:
805805
warnings.warn(
806-
f"DataLab server version ({version}) may not be fully compatible with "
807-
f"this DataLab client version ({cdl.__version__}).\n"
808-
f"Please upgrade the server to {minor_version} or higher."
806+
f"DataLab server version ({server_ver}) may not be fully compatible "
807+
f"with this DataLab client version ({client_ver}).\n"
808+
f"Please upgrade the server to {client_ver} or higher."
809809
)
810810

811811
def connect(

cdl/tests/features/common/stats_tools_unit_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import numpy as np
1616
import plotpy
1717
from guidata.qthelpers import exec_dialog, qt_app_context
18+
from packaging.version import Version
1819
from plotpy.constants import PlotType
1920
from plotpy.tests.unit.utils import drag_mouse
2021
from plotpy.tools import CurveStatsTool, ImageStatsTool
@@ -23,7 +24,6 @@
2324
import cdl.obj
2425
from cdl.core.gui.docks import DataLabPlotWidget
2526
from cdl.tests.data import create_multigauss_image, create_paracetamol_signal
26-
from cdl.utils.misc import compare_versions
2727

2828

2929
def simulate_stats_tool(
@@ -42,7 +42,7 @@ def simulate_stats_tool(
4242
klass = CurveStatsTool if plot_type == PlotType.CURVE else ImageStatsTool
4343
stattool = widget.get_manager().get_tool(klass)
4444
stattool.activate()
45-
if compare_versions(plotpy.__version__, "<", "2.4"):
45+
if Version(plotpy.__version__) < Version("2.4"):
4646
qapp = QW.QApplication.instance()
4747
drag_mouse(widget, qapp, x_path, y_path)
4848
else:

cdl/tests/features/signals/processing_unit_test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121

2222
import numpy as np
2323
import pytest
24+
import scipy
2425
import scipy.ndimage as spi
2526
import scipy.signal as sps
27+
from packaging.version import Version
2628

2729
import cdl.algorithms.coordinates
2830
import cdl.computation.signal as cps
@@ -316,6 +318,10 @@ def test_signal_moving_average() -> None:
316318

317319

318320
@pytest.mark.validation
321+
@pytest.mark.skipif(
322+
Version("1.15.0") <= Version(scipy.__version__) <= Version("1.15.2"),
323+
reason="Skipping test: scipy median_filter is broken in 1.15.0-1.15.2",
324+
)
319325
def test_signal_moving_median() -> None:
320326
"""Validation test for the signal moving median processing."""
321327
src = get_test_signal("paracetamol.txt")

cdl/utils/misc.py

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -39,66 +39,3 @@ def go_to_error(text: str) -> None:
3939
args = Conf.console.external_editor_args.get().format(**fdict).split(" ")
4040
editor_path = Conf.console.external_editor_path.get()
4141
subprocess.run([editor_path] + args, shell=True, check=False)
42-
43-
44-
def is_version_at_least(version1: str, version2: str) -> bool:
45-
"""
46-
Compare two version strings to check if the first version is at least
47-
equal to the second. Limit the comparison to the minor version (e.g. 1.2.3 -> 1.2).
48-
49-
Args:
50-
version1 (str): The first version string.
51-
version2 (str): The second version string.
52-
53-
Returns:
54-
bool: True if version1 is greater than or equal to version2, False otherwise.
55-
56-
.. note::
57-
58-
Development, alpha, beta, and rc versions are considered to be equal
59-
to the corresponding release version.
60-
"""
61-
# Split the version strings into parts
62-
parts1 = [part.strip() for part in version1.split(".")]
63-
parts2 = [part.strip() for part in version2.split(".")]
64-
65-
for part1, part2 in zip(parts1, parts2):
66-
if part1.isdigit() and part2.isdigit():
67-
if int(part1) > int(part2):
68-
return True
69-
if int(part1) < int(part2):
70-
return False
71-
elif part1 > part2:
72-
return True
73-
elif part1 < part2:
74-
return False
75-
76-
return len(parts1) >= len(parts2)
77-
78-
79-
def compare_versions(version1: str, operator: str, version2: str) -> bool:
80-
"""Compare module version with the given version.
81-
82-
Args:
83-
version1: Version to compare (e.g., "1.2.3")
84-
operator: Comparison operator (e.g., "==", "<", ">", "<=", ">=")
85-
version2: Version to compare with (e.g., "1.2.3")
86-
87-
Returns:
88-
True if the comparison is successful, False otherwise.
89-
"""
90-
specs1, specs2 = version1.split("."), version2.split(".")
91-
assert len(specs2) <= len(specs1)
92-
specs2 = specs2[: len(specs1)]
93-
tuple1, tuple2 = tuple(map(int, specs1)), tuple(map(int, specs2))
94-
if operator == "==":
95-
return tuple1 == tuple2
96-
if operator == "<":
97-
return tuple1 < tuple2
98-
if operator == ">":
99-
return tuple1 > tuple2
100-
if operator == "<=":
101-
return tuple1 <= tuple2
102-
if operator == ">=":
103-
return tuple1 >= tuple2
104-
raise ValueError("Invalid operator")

doc/locale/fr/LC_MESSAGES/contributing/changelog.po

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,15 @@ msgstr "Ces méthodes sont internes et utilisées par des développeurs avancés
8484
msgid "This closes [Issue #180](https://github.com/DataLab-Platform/DataLab/issues/180) - Rationalize `BaseProcessor` method names for core processing types"
8585
msgstr "Ceci corrige l'[Issue #180](https://github.com/DataLab-Platform/DataLab/issues/180) - Rationaliser les noms de méthodes `BaseProcessor` pour les types de traitement de base"
8686

87+
msgid "Fixed [Issue #233](https://github.com/DataLab-Platform/DataLab/issues/233) - Hard crash when trying to activate the curve stats tool on a zero signal"
88+
msgstr "Correction de l'[Issue #233](https://github.com/DataLab-Platform/DataLab/issues/233) - Plantage sévère lors de l'activation de l'outil de statistiques de courbe sur un signal nul"
89+
8790
msgid "Fixed [Issue #184](https://github.com/DataLab-Platform/DataLab/issues/184) - Curve marker style unexpectedly changes to \"Square\" after validating \"Parameters…\" dialog"
8891
msgstr "Correction de l'[Issue #184](https://github.com/DataLab-Platform/DataLab/issues/184) - Le style du marqueur de courbe change de manière inattendue en \"Carré\" après validation de la boîte de dialogue \"Paramètres…\""
8992

93+
msgid "Fixed [Issue #117](https://github.com/DataLab-Platform/DataLab/issues/117) - DataLab's signal moving median crashes on Linux with `mode='mirror'`: `free(): invalid next size (normal)` (this is a bug in SciPy v1.15.0 to v1.15.2, which was fixed in SciPy v1.15.3)"
94+
msgstr "Correction de l'[Issue #117](https://github.com/DataLab-Platform/DataLab/issues/117) - Le filtre médian mobile de DataLab plante sur Linux avec `mode='mirror'` : `free(): invalid next size (normal)` (il s'agit d'un bug dans SciPy v1.15.0 à v1.15.2, qui a été corrigé dans SciPy v1.15.3)"
95+
9096
msgid "ℹ️ Other changes:"
9197
msgstr "ℹ️ Autres changements :"
9298

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ requires-python = ">=3.9, <4"
4242
dependencies = [
4343
"guidata >= 3.10",
4444
"PlotPy >= 2.7.4",
45-
"SciPy >= 1.5, < 1.15.0",
45+
"SciPy >= 1.5",
4646
"scikit-image >= 0.18",
4747
"pandas >= 1.2",
4848
"PyWavelets >= 1.1",
4949
"psutil >= 5.7",
50+
"packaging >= 20.0",
5051
]
5152
dynamic = ["version"]
5253

0 commit comments

Comments
 (0)