Maskscatter into roi tool layout#1223
Conversation
|
Some summary:
Extra:
Things I failed to do better:
|
|
Actually, to keep padding in one place it can go deep to Do you think it is cleaner logic?
please also let me know your thoughts about this one. After these two resolved i think this PR can be finalized. |
|
The last commit:
If you do not like it we can remove the last commit. |
There was a problem hiding this comment.
I added some comments. In addition, just like I started doing for silx , I think it is very helpful to reviewer to produce a MWE, especially when it comes to GUI features.
I do not see what you see in the screenshot of ROI maging:
Screencast.from.2026-06-21.14-16-12.webm
This is the demo code. AI can help you generate code fast. Useful for unit tests as well.
import sys
import h5py
import numpy
from silx.gui import qt
from PyMca5.PyMcaGui.pymca.PyMcaPostBatch import PyMcaPostBatch
from PyMca5.PyMcaGui.pymca.QStackWidget import QStackWidget
from PyMca5.PyMcaIO.HDF5Stack1D import HDF5Stack1D
def create_hdf5(filename="demo_scan.h5"):
nx, ny = 20, 15
npoints = nx * ny
nchannels = 200
order = "C" # X fast axis
# order = "F" # Y fast axis
x = numpy.linspace(0, 10, nx)
y = numpy.linspace(0, 5, ny)
x, y = numpy.meshgrid(x, y)
x = x.ravel(order=order)
y = y.ravel(order=order)
energy = numpy.linspace(0, 10, nchannels)
mca = numpy.zeros((npoints, nchannels), dtype=numpy.float32)
# Data which makes it clear if the user gets the shape wrong
for i in range(npoints):
xv = x[i]
yv = y[i]
center = 3.0 + 0.4 * xv + 0.8 * yv
peak1 = numpy.exp(-((energy - center) ** 2) / (0.8 + 0.02 * xv))
peak2 = 0.4 * numpy.exp(-((energy - (7.5 - 0.3 * yv)) ** 2) / 1.5)
amplitude = 1.0 + 0.5 * numpy.sin(xv * 0.8) + 0.2 * yv
drift = 0.02 * (xv - yv)
mca[i] = amplitude * (peak1 + peak2) + drift
# CTRL-C of scan: datasets have different lengths
x = random_trim(x)
y = random_trim(y)
mca = random_trim(mca)
with h5py.File(filename, "w") as f:
pymcaroitool = f.create_group("pymcaroitool")
pymcaroitool.attrs["NX_class"] = "NXdata"
pymcaroitool.attrs["mca00"] = "mca"
pymcaroitool.attrs["axes"] = ["y", "x"] # ESRF scatter convention, not NeXus
pymcaroitool.create_dataset("x", data=x)
pymcaroitool.create_dataset("y", data=y)
pymcaroitool.create_dataset("energy", data=energy)
pymcaroitool.create_dataset("mca00", data=mca)
pymcapostbatch = f.create_group("pymcapostbatch")
pymcapostbatch.attrs["NX_class"] = "NXdata"
pymcapostbatch.attrs["signal"] = "roi00"
pymcapostbatch.attrs["auxiliary_signals"] = ["roi01", "roi02"]
pymcapostbatch.attrs["axes"] = ["y", "x"] # ESRF scatter convention, not NeXus
pymcapostbatch.create_dataset("x", data=x)
pymcapostbatch.create_dataset("y", data=y)
pymcapostbatch.create_dataset("roi00", data=mca[:, 100])
pymcapostbatch.create_dataset("roi01", data=mca[:, 120])
pymcapostbatch.create_dataset("roi02", data=mca[:, 130])
f.attrs["default"] = "pymcapostbatch"
if order == "C":
message = f"""
HDF5 created: {filename}
Number of rows: {ny}
Number of columns: {nx}
X is fast axis
"""
else:
message = f"""
HDF5 created: {filename}
Number of rows: {nx}
Number of columns: {ny}
Y is fast axis
"""
print(message)
def random_trim(array):
max_trim = max(1, int(0.05 * len(array)))
ntrim = numpy.random.randint(1, max_trim + 1)
return array[:-ntrim]
def launch_pymcaroitool(filename):
app = qt.QApplication.instance() or qt.QApplication(sys.argv)
selection = {
"x": [],
"y": ["/pymcaroitool/mca00"],
"index": -1,
"scatter": True,
"allowPadding": True,
}
stack = HDF5Stack1D([filename], selection)
w = QStackWidget()
w.setStack(stack)
w.resize(900, 700)
w.move(100, 100)
w.show()
app.exec()
def launch_pymcapostbatch(filename):
app = qt.QApplication.instance() or qt.QApplication(sys.argv)
w = PyMcaPostBatch()
w.addFileList([filename])
w.resize(900, 700)
w.move(100, 100)
w.show()
app.exec()
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO)
filename = "demo_scan.h5"
create_hdf5(filename)
launch_pymcaroitool(filename)
launch_pymcapostbatch(filename)|
|
||
|
|
||
| except Exception: | ||
| print("WARNING: Fail to identify number of scan points and/or axes sizes") |
There was a problem hiding this comment.
| print("WARNING: Fail to identify number of scan points and/or axes sizes") | |
| _logger.warning("Fail to identify number of scan points and/or axes sizes") |
There was a problem hiding this comment.
This module use print everywhere. Should I change all of them then to keep consistency?
There was a problem hiding this comment.
Well if it says print("WARNING: I would say that is a clear _logger.warning(
Production code very rarely uses print. Up to you.
| dataset = xDatasetList[i].reshape(-1) | ||
| datasize = self.data.shape[i] | ||
| if i == mcaIndex: | ||
| if i == self.info["McaIndex"]: |
There was a problem hiding this comment.
mcaIndex vs. self.info["McaIndex"] is beyond my understanding like in the previous PR so at leave it at your discretion.
Do you want MWE as a video or as a code? |
|
MWE. I used generated data. First time I've selected per-point motor positioners (both with dimensions =15). Second time I've selected regular motors (dimensions of 3 and 5). Screen.Recording.2026-06-22.140152.mp4 |
Is it useful to have possibility to use scatter plot without positioners? My logic was that it should not happen. |
|
Fix all requested by @woutdenolf |
Both. At least code. Add a screenshot when the problem/feature is static. Add a screen recording when the problem/feature is dynamic. |
woutdenolf
left a comment
There was a problem hiding this comment.
Nice work!
Some tweaking of the messages shown to the user but LGTM.
| self.mainLayout.addWidget(self.stackIndexWidget, 0) | ||
|
|
||
| self._scatterCheckBox = qt.QCheckBox( | ||
| "Scatter plot (X, Y coordinates are set per image point)", self) |
There was a problem hiding this comment.
When "1D is last dimension" this checkbox toggles between
- Grid (False): x(Nx), y(Ny), data(Ny, Nx, Nchan)
- Scatter (True): x(Npoints), y(Npoints), data(Npoints, Nchan)
Perhaps this is better?
"Scatter plot (X, Y coordinate per data point)"
Not sure. Up to you. I just though "image point" was trying to mix two concepts: "image pixel" with "data point".
There was a problem hiding this comment.
After discussion: "Scatter plot (X, Y coordinate per 1D data)" since the toggle boxes above also use "1D data".
| "Most probably the selected motor positions hold one value per scan point." | ||
| "The regular grid can not be defined." | ||
| "Enable 'Scatter plot' or select different axes.") |
There was a problem hiding this comment.
| "Most probably the selected motor positions hold one value per scan point." | |
| "The regular grid can not be defined." | |
| "Enable 'Scatter plot' or select different axes.") | |
| "Most probably the selected motor positions hold one value per scan point. " | |
| "The regular grid can not be defined. " | |
| "Enable 'Scatter plot' or select different axes.") |
Needs spaces.
Perhaps also harmonize with "Scatter plot (X, Y coordinates are set per image point)".
You use the terms "image point" and "scan point" for the same thing. In my suggestion about I called it "data point".
| if scatter and not all(size == axisSize for size in axisSizes): | ||
| self.showMessage( | ||
| "Axes should have same number of positions " | ||
| "(as they hold one value per scan point)") |
There was a problem hiding this comment.
"scan point" -> to be harmonized with the others.
There was a problem hiding this comment.
Perhaps suggest unchecking the scatter checkbox or select other axes? Like you did in another message.
| "dimension besides the channels).") | ||
| return False | ||
|
|
||
| # the axes hold one value per scan point so they must have equal sizes |
There was a problem hiding this comment.
"scan point" -> to be harmonized with the others.

The generalized solution discussed in #1219