Skip to content

Commit b724686

Browse files
authored
Merge pull request #4258 from t20100/prepare-2.2.2
Release: Prepare 2.2.2
2 parents c3db339 + 850444e commit b724686

File tree

10 files changed

+130
-63
lines changed

10 files changed

+130
-63
lines changed

CHANGELOG.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
Release Notes
22
=============
33

4+
2.2.2: 2025/04/07
5+
-----------------
6+
7+
* `silx.gui.plot.PlotWidget`:
8+
9+
* Fixed matplotlib backend issue with plot axes limits (PR #4256)
10+
* Fixed OpenGL backend axes (PR #4246)
11+
12+
* `silx.io.h5py_utils`: Fixed support of libhdf5 1.14.x (PR #4242)
13+
* `silx.opencl`: Fixed context creation through `PYOPENCL_CTX environment` variable (PR #4245)
14+
415
2.2.1: 2025/02/27
516
-----------------
617

src/silx/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969

7070
MAJOR = 2
7171
MINOR = 2
72-
MICRO = 1
72+
MICRO = 2
7373
RELEV = "final" # <16
7474
SERIAL = 0 # <16
7575

src/silx/gui/plot/_utils/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ def addMarginsToLimits(
8686
y2Min = pow(10.0, yMinLog - yMinMargin * yRangeLog)
8787
y2Max = pow(10.0, yMaxLog + yMaxMargin * yRangeLog)
8888

89+
xMin, xMax = checkAxisLimits(xMin, xMax, isXLog)
90+
yMin, yMax = checkAxisLimits(yMin, yMax, isYLog)
91+
8992
if y2Min is None or y2Max is None:
9093
return xMin, xMax, yMin, yMax
9194
else:
95+
y2Min, y2Max = checkAxisLimits(y2Min, y2Max, isYLog)
9296
return xMin, xMax, yMin, yMax, y2Min, y2Max

src/silx/gui/plot/backends/BackendMatplotlib.py

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# /*##########################################################################
22
#
3-
# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
3+
# Copyright (c) 2004-2025 European Synchrotron Radiation Facility
44
#
55
# Permission is hereby granted, free of charge, to any person obtaining a copy
66
# of this software and associated documentation files (the "Software"), to deal
@@ -552,7 +552,8 @@ def __init__(self, plot, parent=None):
552552
for axis in (self.ax.yaxis, self.ax.xaxis, self.ax2.yaxis, self.ax2.xaxis):
553553
axis.set_major_formatter(DefaultTickFormatter())
554554

555-
self.ax2.set_autoscaley_on(True)
555+
self.ax.set_autoscaley_on(False)
556+
self.ax2.set_autoscaley_on(False)
556557

557558
# this works but the figure color is left
558559
if self._matplotlibVersion < Version("2"):
@@ -1192,17 +1193,10 @@ def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None):
11921193
self._dirtyLimits = True
11931194
self.ax.set_xlim(min(xmin, xmax), max(xmin, xmax))
11941195

1195-
keepRatio = self.isKeepDataAspectRatio()
11961196
if y2min is not None and y2max is not None:
1197-
if not self.isYAxisInverted():
1198-
self.ax2.set_ylim(min(y2min, y2max), max(y2min, y2max), auto=keepRatio)
1199-
else:
1200-
self.ax2.set_ylim(max(y2min, y2max), min(y2min, y2max), auto=keepRatio)
1197+
self.ax2.set_ybound(min(y2min, y2max), max(y2min, y2max))
12011198

1202-
if not self.isYAxisInverted():
1203-
self.ax.set_ylim(min(ymin, ymax), max(ymin, ymax), auto=keepRatio)
1204-
else:
1205-
self.ax.set_ylim(max(ymin, ymax), min(ymin, ymax), auto=keepRatio)
1199+
self.ax.set_ybound(min(ymin, ymax), max(ymin, ymax))
12061200

12071201
self._updateMarkers()
12081202

@@ -1251,11 +1245,7 @@ def setGraphYLimits(self, ymin, ymax, axis):
12511245
xcenter = 0.5 * (xmin + xmax)
12521246
ax.set_xlim(xcenter - 0.5 * newXRange, xcenter + 0.5 * newXRange)
12531247

1254-
keepRatio = self.isKeepDataAspectRatio()
1255-
if not self.isYAxisInverted():
1256-
ax.set_ylim(ymin, ymax, auto=keepRatio)
1257-
else:
1258-
ax.set_ylim(ymax, ymin, auto=keepRatio)
1248+
ax.set_ybound(ymin, ymax)
12591249

12601250
self._updateMarkers()
12611251

@@ -1317,7 +1307,7 @@ def setYAxisLogarithmic(self, flag):
13171307
dataRange = self._plot.getDataRange()[dataRangeIndex]
13181308
if dataRange is None:
13191309
dataRange = 1, 100 # Fallback
1320-
axis.set_ylim(*dataRange, auto=self.isKeepDataAspectRatio())
1310+
axis.set_ybound(*dataRange)
13211311
redraw = True
13221312
if redraw:
13231313
self.draw()
@@ -1623,6 +1613,8 @@ def resizeEvent(self, event):
16231613
self.ax.get_ybound(),
16241614
self.ax2.get_ybound(),
16251615
)
1616+
self.ax.set_autoscaley_on(True)
1617+
self.ax2.set_autoscaley_on(True)
16261618

16271619
FigureCanvasQTAgg.resizeEvent(self, event)
16281620
if self.isKeepDataAspectRatio() or self._hasOverlays():
@@ -1673,6 +1665,9 @@ def draw(self):
16731665
if xLimits != self.ax.get_xbound() or yLimits != self.ax.get_ybound():
16741666
self._updateMarkers()
16751667

1668+
self.ax.set_autoscaley_on(False)
1669+
self.ax2.set_autoscaley_on(False)
1670+
16761671
if xLimits != self.ax.get_xbound():
16771672
self._plot.getXAxis()._emitLimitsChanged()
16781673
if yLimits != self.ax.get_ybound():

src/silx/gui/plot/backends/BackendOpenGL.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1538,21 +1538,16 @@ def _setPlotBounds(self, xRange=None, yRange=None, y2Range=None, keepDim=None):
15381538
self._ensureAspectRatio(keepDim)
15391539

15401540
def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None):
1541-
assert xmin < xmax
1542-
assert ymin < ymax
1543-
15441541
if y2min is None or y2max is None:
15451542
y2Range = None
15461543
else:
1547-
assert y2min < y2max
15481544
y2Range = y2min, y2max
15491545
self._setPlotBounds((xmin, xmax), (ymin, ymax), y2Range)
15501546

15511547
def getGraphXLimits(self):
15521548
return self._plotFrame.dataRanges.x
15531549

15541550
def setGraphXLimits(self, xmin, xmax):
1555-
assert xmin < xmax
15561551
self._setPlotBounds(xRange=(xmin, xmax), keepDim="x")
15571552

15581553
def getGraphYLimits(self, axis):
@@ -1563,7 +1558,6 @@ def getGraphYLimits(self, axis):
15631558
return self._plotFrame.dataRanges.y2
15641559

15651560
def setGraphYLimits(self, ymin, ymax, axis):
1566-
assert ymin < ymax
15671561
assert axis in ("left", "right")
15681562

15691563
if axis == "left":

src/silx/io/h5py_utils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# /*##########################################################################
2-
# Copyright (C) 2016-2024 European Synchrotron Radiation Facility
2+
# Copyright (C) 2016-2025 European Synchrotron Radiation Facility
33
#
44
# Permission is hereby granted, free of charge, to any person obtaining a copy
55
# of this software and associated documentation files (the "Software"), to deal
@@ -173,7 +173,11 @@ def retry_h5py_error(e):
173173
elif isinstance(e, KeyError):
174174
# For example this needs to be retried:
175175
# KeyError: 'Unable to open object (bad object header version number)'
176-
return "Unable to open object" in str(e)
176+
message = str(e)
177+
return (
178+
"Unable to open object" in message
179+
or "Unable to synchronously open object" in message
180+
)
177181
elif isinstance(e, retry_mod.RetryError):
178182
return True
179183
return False

src/silx/io/test/test_h5py_utils.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# /*##########################################################################
2-
# Copyright (C) 2016-2024 European Synchrotron Radiation Facility
2+
# Copyright (C) 2016-2025 European Synchrotron Radiation Facility
33
#
44
# Permission is hereby granted, free of charge, to any person obtaining a copy
55
# of this software and associated documentation files (the "Software"), to deal
@@ -37,6 +37,9 @@
3737
import multiprocessing
3838
from contextlib import contextmanager
3939

40+
import h5py
41+
import pytest
42+
4043
from .. import h5py_utils
4144
from ...utils.retry import RetryError, RetryTimeoutError
4245

@@ -486,3 +489,30 @@ def iter_data(filename, name, start_index=0):
486489
kw = {"retry_timeout": insufficient_timeout}
487490
with self.assertRaises(RetryTimeoutError):
488491
list(iter_data(filename, "/check", **kw))
492+
493+
494+
def test_retry_timeout(tmp_path):
495+
filepath = tmp_path / "test.h5"
496+
with h5py.File(filepath, "w"):
497+
pass
498+
499+
@h5py_utils.retry(retry_timeout=1, retry_period=0.1)
500+
def read():
501+
with h5py.File(filepath, locking=False) as f:
502+
group = f["group"]
503+
return group["data"][()]
504+
505+
with pytest.raises(RetryTimeoutError):
506+
read()
507+
508+
with h5py.File(filepath, "w") as f:
509+
f.create_group("group")
510+
511+
with pytest.raises(RetryTimeoutError):
512+
read()
513+
514+
with h5py.File(filepath, "w") as f:
515+
f["group/data"] = 1
516+
517+
data = read()
518+
assert data == 1

src/silx/opencl/common.py

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,43 @@ class mf(object):
110110

111111
AMD_FLOP_PER_CORE = 160 # Measured on a M7820 10 core, 700MHz 1120GFlops
112112

113+
def get_pyopencl_ctx_tuple(pyopencl_ctx_str, cache=None):
114+
"""
115+
Converts a PYOPENCL_CTX environment variable into a tuple (platform, device)
116+
117+
:param cache: dict with the already created contexts
118+
"""
119+
120+
def _convert_to_int(val, default_val=0):
121+
try:
122+
ret = int(val)
123+
except ValueError:
124+
ret = default_val
125+
return ret
126+
127+
if pyopencl_ctx_str in ["", ":"]:
128+
return (0, 0)
129+
if ":" in pyopencl_ctx_str:
130+
platform, device = pyopencl_ctx_str.split(":")
131+
platform_id = _convert_to_int(platform)
132+
device_id = _convert_to_int(device)
133+
elif pyopencl_ctx_str.isdigit():
134+
platform_id = _convert_to_int(pyopencl_ctx_str)
135+
device_id = 0
136+
else:
137+
if "choose_devices" in dir(pyopencl):
138+
device = pyopencl.choose_devices(interactive=False)
139+
device_id = device_instance.platform.get_devices().index(device_instance)
140+
platform_id = pyopencl.get_platforms().index(device.platform)
141+
else: #Fallback for elder PyOpenCL
142+
ctx = pyopencl.create_some_context(interactive=False)
143+
device = device_instance = ctx.devices[0]
144+
device_id = device_instance.platform.get_devices().index(device_instance)
145+
platform_id = pyopencl.get_platforms().index(device.platform)
146+
if cache is not None:
147+
cache[(platform_id, device_id)] = ctx
148+
return (platform_id, device_id)
149+
113150

114151
class Device(object):
115152
"""
@@ -603,25 +640,8 @@ def create_context(
603640
platformid = int(platformid)
604641
deviceid = int(deviceid)
605642
elif "PYOPENCL_CTX" in os.environ:
606-
ctx = pyopencl.create_some_context()
607-
# try:
608-
device = ctx.devices[0]
609-
platforms = [
610-
i
611-
for i, p in enumerate(self.platforms)
612-
if device.platform.name == p.name
613-
]
614-
if platforms:
615-
platformid = platforms[0]
616-
devices = [
617-
i
618-
for i, d in enumerate(self.platforms[platformid].devices)
619-
if device.name == d.name
620-
]
621-
if devices:
622-
deviceid = devices[0]
623-
if cached:
624-
self.context_cache[(platformid, deviceid)] = ctx
643+
platformid, deviceid = get_pyopencl_ctx_tuple(os.environ["PYOPENCL_CTX"],
644+
cache=self.context_cache if cached else None)
625645
else:
626646
ids = self.select_device(type=devicetype, extensions=extensions)
627647
if ids:
@@ -632,19 +652,11 @@ def create_context(
632652
ctx = self.context_cache[(platformid, deviceid)]
633653
else:
634654
try:
635-
ctx = pyopencl.Context(
636-
devices=[
637-
pyopencl.get_platforms()[platformid].get_devices()[deviceid]
638-
]
639-
)
655+
device = pyopencl.get_platforms()[platformid].get_devices()[deviceid]
656+
ctx = pyopencl.Context(devices=[device])
640657
except pyopencl._cl.LogicError as error:
641658
self.platforms[platformid].devices[deviceid].set_unavailable()
642-
logger.warning(
643-
"Unable to create context on %s/%s: %s",
644-
platformid,
645-
deviceid,
646-
error,
647-
)
659+
logger.warning(f"Unable to create context for ({platformid}:{deviceid}): {error}")
648660
ctx = None
649661
else:
650662
if cached:

src/silx/opencl/test/test_addition.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,8 @@ def test_measurement(self):
131131
for platform in ocl.platforms:
132132
for did, device in enumerate(platform.devices):
133133
meas = _measure_workgroup_size((platform.id, device.id))
134-
self.assertEqual(
135-
meas,
136-
device.max_work_group_size,
137-
"Workgroup size for %s/%s: %s == %s"
138-
% (platform, device, meas, device.max_work_group_size),
139-
)
134+
self.assertEqual(meas, device.max_work_group_size,
135+
f"Workgroup size for {platform}/{device}: {meas} == {device.max_work_group_size}")
140136

141137
def test_query(self):
142138
"""
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import os
2+
import pytest
3+
4+
from silx.opencl.common import ocl
5+
6+
if ocl:
7+
from silx.opencl.processing import OpenclProcessing
8+
9+
10+
@pytest.mark.skipif(ocl is None, reason="PyOpenCl is missing")
11+
def test_context_cache():
12+
13+
op1 = OpenclProcessing()
14+
op2 = OpenclProcessing()
15+
assert op1.ctx is op2.ctx, "Context should be the same"
16+
17+
os.environ["PYOPENCL_CTX"] = "0:0"
18+
op3 = OpenclProcessing()
19+
op4 = OpenclProcessing()
20+
21+
assert op3.ctx is op4.ctx, "context should be the same"

0 commit comments

Comments
 (0)