ewoksid16a 0.1.0__tar.gz

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.
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ **Copyright (c) 2021-2025 European Synchrotron Radiation Facility**
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: ewoksid16a
3
+ Version: 0.1.0
4
+ Summary: Data processing workflows for ID16A
5
+ Author-email: ESRF <dau-pydev@esrf.fr>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.esrf.fr/workflow/ewoksapps/ewoksid16a/
8
+ Project-URL: Documentation, https://ewoksid16a.readthedocs.io/
9
+ Project-URL: Repository, https://gitlab.esrf.fr/workflow/ewoksapps/ewoksid16a/
10
+ Project-URL: Issues, https://gitlab.esrf.fr/workflow/ewoksapps/ewoksid16a/issues
11
+ Project-URL: Changelog, https://gitlab.esrf.fr/workflow/ewoksapps/ewoksid16a/-/blob/main/CHANGELOG.md
12
+ Keywords: orange3 add-on,ewoks
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: ewoksjob
19
+ Requires-Dist: ewoksfluo
20
+ Requires-Dist: blissdata
21
+ Requires-Dist: ewoksorange
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest>=7; extra == "test"
24
+ Requires-Dist: pyqt6; extra == "test"
25
+ Provides-Extra: dev
26
+ Requires-Dist: ewoksid16a[test]; extra == "dev"
27
+ Requires-Dist: black>=25; extra == "dev"
28
+ Requires-Dist: flake8>=4; extra == "dev"
29
+ Requires-Dist: ruff; extra == "dev"
30
+ Requires-Dist: bandit; extra == "dev"
31
+ Provides-Extra: doc
32
+ Requires-Dist: ewoksid16a[test]; extra == "doc"
33
+ Requires-Dist: sphinx>=4.5; extra == "doc"
34
+ Requires-Dist: sphinx-autodoc-typehints>=1.16; extra == "doc"
35
+ Requires-Dist: pydata-sphinx-theme; extra == "doc"
36
+ Dynamic: license-file
37
+
38
+ # ewoksid16a
39
+
40
+ Data processing workflows for ID16A
41
+
42
+ ## Documentation
43
+
44
+ https://ewoksid16a.readthedocs.io/
@@ -0,0 +1,7 @@
1
+ # ewoksid16a
2
+
3
+ Data processing workflows for ID16A
4
+
5
+ ## Documentation
6
+
7
+ https://ewoksid16a.readthedocs.io/
@@ -0,0 +1,97 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=61",
4
+ ]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [project]
8
+ name = "ewoksid16a"
9
+ version = "0.1.0"
10
+ keywords = [
11
+ "orange3 add-on",
12
+ "ewoks"
13
+ ]
14
+
15
+ authors = [{name = "ESRF", email = "dau-pydev@esrf.fr"}]
16
+ description = "Data processing workflows for ID16A"
17
+ readme = {file = "README.md", content-type = "text/markdown"}
18
+ license = "MIT"
19
+ license-files = ["LICENSE"]
20
+ classifiers = [
21
+ "Intended Audience :: Science/Research",
22
+ "Programming Language :: Python :: 3",
23
+ ]
24
+ requires-python = ">=3.9"
25
+ dependencies = [
26
+ "ewoksjob",
27
+ "ewoksfluo",
28
+ "blissdata",
29
+ "ewoksorange",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://gitlab.esrf.fr/workflow/ewoksapps/ewoksid16a/"
34
+ Documentation = "https://ewoksid16a.readthedocs.io/"
35
+ Repository = "https://gitlab.esrf.fr/workflow/ewoksapps/ewoksid16a/"
36
+ Issues = "https://gitlab.esrf.fr/workflow/ewoksapps/ewoksid16a/issues"
37
+ Changelog = "https://gitlab.esrf.fr/workflow/ewoksapps/ewoksid16a/-/blob/main/CHANGELOG.md"
38
+
39
+ [project.optional-dependencies]
40
+ test = [
41
+ "pytest >=7",
42
+ "pyqt6",
43
+ ]
44
+ dev = [
45
+ "ewoksid16a[test]",
46
+ "black >=25",
47
+ "flake8 >=4",
48
+ "ruff",
49
+ "bandit"
50
+ ]
51
+ doc = [
52
+ "ewoksid16a[test]",
53
+ "sphinx >=4.5",
54
+ "sphinx-autodoc-typehints >=1.16",
55
+ "pydata-sphinx-theme",
56
+ ]
57
+
58
+ [tool.setuptools]
59
+ package-dir = {""= "src"}
60
+
61
+ [tool.setuptools.packages.find]
62
+ where = ["src"]
63
+
64
+ [tool.setuptools.package-data]
65
+ "ewoksid16a.tests.data"= ["*/*"]
66
+ "*" = ["*.ows", "*.png", "*.svg"]
67
+
68
+ [project.entry-points."ewoks.tasks.class"]
69
+ "ewoksid16a.tasks.*" = "ewoksid16a"
70
+
71
+ [project.entry-points."orange3.addon"]
72
+ "ewoksid16a" = "orangecontrib.ewoksid16a"
73
+
74
+ [project.entry-points."orange.widgets"]
75
+ "Examples" = "orangecontrib.ewoksid16a"
76
+ "ExamplesCategories" = "orangecontrib.ewoksid16a.categories"
77
+
78
+ [project.entry-points."orangecanvas.examples"]
79
+ "Examples" = "orangecontrib.ewoksid16a.tutorials"
80
+ "Examples1" = "orangecontrib.ewoksid16a.categories.examples1.tutorials"
81
+ "Examples2" = "orangecontrib.ewoksid16a.categories.examples2.tutorials"
82
+
83
+ [project.entry-points."orange.widgets.tutorials"]
84
+ "Examples" = "orangecontrib.ewoksid16a.tutorials"
85
+ "Examples1" = "orangecontrib.ewoksid16a.categories.examples1.tutorials"
86
+ "Examples2" = "orangecontrib.ewoksid16a.categories.examples2.tutorials"
87
+
88
+ [project.entry-points."orange.canvas.help"]
89
+ "html-index" = "orangecontrib.ewoksid16a:WIDGET_HELP_PATH"
90
+
91
+ [tool.coverage.run]
92
+ omit = [
93
+ "*/tests/*"
94
+ ]
95
+
96
+ [tool.bandit.assert_used]
97
+ skips = ["*/test_*.py"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
File without changes
@@ -0,0 +1,152 @@
1
+ from ewokscore import Task
2
+
3
+ from ewoksfluo.tasks import nexus_utils
4
+ from ewoksfluo.tasks.hdf5_utils import link_bliss_scan, create_hdf5_link
5
+
6
+ # import h5py
7
+ # import hdf5plugin # noqa: F401
8
+ import numpy as np
9
+ from configparser import ConfigParser
10
+
11
+ from silx.io import h5py_utils
12
+
13
+
14
+ class NormalizeCurrent(
15
+ Task,
16
+ input_names=[
17
+ "bliss_scan_uri",
18
+ "output_root_uri",
19
+ "current_counter",
20
+ "reference_uri",
21
+ "dwelltime_counter",
22
+ "ref_current_counter",
23
+ "diode_factor",
24
+ ],
25
+ optional_input_names=["gamma_coef", "min_ref", "max_ref", "exp_default"],
26
+ output_names=["bliss_scan_uri", "output_root_uri"],
27
+ ):
28
+ """Add single-scan XRF results of multiple detectors"""
29
+
30
+ def run(self):
31
+ start_time = nexus_utils.now()
32
+ bliss_scan_uri = self.inputs.bliss_scan_uri
33
+ output_root_uri = self.inputs.output_root_uri
34
+ scanuris = bliss_scan_uri.split("::")
35
+
36
+ filename = scanuris[0]
37
+ scanno = scanuris[1]
38
+
39
+ counter_template = f"/{scanno}/instrument/%s/data"
40
+
41
+ ref_scanuris = self.inputs.reference_uri.split("::")
42
+
43
+ ref_filename = ref_scanuris[0]
44
+ ref_scanno = ref_scanuris[1]
45
+
46
+ ref_counter_template = f"/{ref_scanno}/instrument/%s/data"
47
+
48
+ with h5py_utils.open_item(
49
+ ref_filename, ref_counter_template % self.inputs["ref_current_counter"]
50
+ ) as ds: # type: ignore[reportGeneralTypeIssues]
51
+ # ref_epoch = np.array(fd[ref_counter_template%self.inputs.get("ref_epoch_counter", "epoch")])
52
+ ref_values = np.array(ds)
53
+
54
+ with h5py_utils.open_item(filename, "/") as fd: # type: ignore[reportGeneralTypeIssues]
55
+ # epoch = np.array(fd[ref_counter_template%self.inputs.get("data_epoch_counter", "epoch_trig")])
56
+ values = np.array(fd[counter_template % self.inputs["current_counter"]])
57
+ dwelltime = np.mean(fd[counter_template % self.inputs["dwelltime_counter"]])
58
+
59
+ min_ref = self.get_input_value("min_ref", 1e8)
60
+ max_ref = self.get_input_value("max_ref", 1e15)
61
+
62
+ diode_factor = self.inputs.diode_factor * 1e12 # Convert ph/pA to ph/A
63
+
64
+ ref_msk = np.logical_and(
65
+ ref_values * diode_factor > min_ref, ref_values * diode_factor < max_ref
66
+ ) # When shutter was open and not saturated
67
+
68
+ # ref_epoch = ref_epoch[ref_msk]
69
+ ref_values = ref_values[ref_msk]
70
+
71
+ gamma = np.mean(values) / (np.mean(ref_values) * dwelltime)
72
+ gamma_coef = self.get_input_value("gamma_coef", 2.0)
73
+
74
+ real_gamma_exp = np.log10(gamma / gamma_coef)
75
+ gamma_exp = np.round(real_gamma_exp)
76
+
77
+ gamma = gamma_coef * 10**gamma_exp
78
+
79
+ with nexus_utils.save_in_ewoks_process(
80
+ output_root_uri,
81
+ start_time,
82
+ process_config={"real_gamma_exp": real_gamma_exp, "gamma_exp": gamma_exp},
83
+ default_levels=(scanno, "scaled_it"),
84
+ ) as (process_group, already_existed):
85
+ outentry = process_group.parent
86
+ if not already_existed:
87
+
88
+ link_bliss_scan(outentry, bliss_scan_uri, retry_timeout=0)
89
+
90
+ nxdata = nexus_utils.create_nxdata(
91
+ process_group, "scaled_it", signal="data"
92
+ )
93
+ nxdata.attrs["interpretation"] = "spectrum"
94
+ dset = nxdata.create_dataset("data", data=values / gamma * diode_factor)
95
+
96
+ nxdetector = outentry["instrument"].create_group("scaled_it")
97
+ nxdetector.attrs["NX_class"] = "NXdetector"
98
+ create_hdf5_link(nxdetector, "data", dset)
99
+ create_hdf5_link(outentry["measurement"], "scaled_it", dset)
100
+
101
+ output_root_uri = f"{outentry.file.filename}::{outentry.name}"
102
+
103
+ self.outputs.bliss_scan_uri = bliss_scan_uri
104
+ self.outputs.output_root_uri = output_root_uri
105
+
106
+
107
+ class NormalizationFactorFromConfig(
108
+ Task,
109
+ input_names=["bliss_scan_uri", "xrf_results_uri", "config_file"],
110
+ output_names=[
111
+ "bliss_scan_uri",
112
+ "xrf_results_uri",
113
+ "counter_normalization_template",
114
+ ],
115
+ ):
116
+ """Add single-scan XRF results of multiple detectors"""
117
+
118
+ def get_from_cfg(self, cfg):
119
+ cp = ConfigParser()
120
+ cp.read(cfg)
121
+
122
+ flux = cp.getfloat("concentrations", "flux")
123
+ dwelltime = cp.getfloat("concentrations", "time")
124
+
125
+ matrix = cp.get("attenuators", "Matrix")
126
+ matdat = matrix.replace(" ", "").split(",")
127
+ matrix_composition = matdat[1]
128
+ matrix_density = float(matdat[2])
129
+ matrix_thickness = float(matdat[3])
130
+ print("INFO FROM CONFIG FILE")
131
+ print("Config input flux: {0:g} (ph/s)".format(flux))
132
+ print("Config dwell time: {0} (s)".format(dwelltime))
133
+ print("Matrix composition: {0}".format(matrix_composition))
134
+ print("Matrix density: {0} (g/cm**3)".format(matrix_density))
135
+ print("Matrix thickness: {0} (cm)".format(matrix_thickness))
136
+ areal_dens_ratio = matrix_density * matrix_thickness * 1e7 # ng/mm**2
137
+ return flux, areal_dens_ratio, dwelltime
138
+
139
+ def run(self):
140
+
141
+ self.outputs.bliss_scan_uri = self.inputs.bliss_scan_uri
142
+ self.outputs.xrf_results_uri = self.inputs.xrf_results_uri
143
+
144
+ cfg_flux, areal_dens_ratio, cfg_dwelltime = self.get_from_cfg(
145
+ self.inputs.config_file
146
+ )
147
+
148
+ factor = cfg_flux * cfg_dwelltime * areal_dens_ratio
149
+
150
+ self.outputs.counter_normalization_template = (
151
+ f"{factor:.04e}/<instrument/{{}}/data>"
152
+ )
@@ -0,0 +1,100 @@
1
+ import h5py
2
+ import hdf5plugin # noqa: F401
3
+ from silx.io import h5py_utils
4
+ import numpy as np
5
+ from ewokscore import Task
6
+
7
+
8
+ class SpectralRegrid(
9
+ Task,
10
+ input_names=["bliss_scan_uri", "output_root_uri", "counter_name"],
11
+ optional_input_names=[],
12
+ output_names=["bliss_scan_uri", "output_root_uri"],
13
+ ):
14
+ """Regrid raw spectrums"""
15
+
16
+ def run(self):
17
+
18
+ _uris = self.inputs.bliss_scan_uri.split("::")
19
+ filename_src = _uris[0]
20
+ scan_src = _uris[1]
21
+ cntnam = self.inputs.counter_name
22
+
23
+ fscanuri = f"{scan_src}/instrument/fscan_parameters"
24
+
25
+ with h5py_utils.open_item(filename_src, "/") as fd: # type: ignore[reportGeneralTypeIssues]
26
+ fastmot = fd[f"{fscanuri}/fast_motor"][()].decode()
27
+ mode = fd[f"{fscanuri}/fast_motor_mode"][()].decode()
28
+ fast_n = fd[f"{fscanuri}/fast_npoints"][()]
29
+ slow_n = fd[f"{fscanuri}/slow_npoints"][()]
30
+
31
+ cnt = fd[f"{scan_src}/instrument/{cntnam}/data"]
32
+ cnt_shape = cnt.shape
33
+ cnt_dtype = cnt.dtype
34
+
35
+ A = np.arange(fast_n * slow_n)
36
+ A.shape = (fast_n, slow_n)
37
+
38
+ if fastmot.endswith("z"):
39
+ A = A.swapaxes(0, 1)
40
+
41
+ if mode == "ZIGZAG":
42
+ A[1::2, :] = A[1::2, :][:, ::-1]
43
+
44
+ _uris = self.inputs.output_root_uri.split("::")
45
+ filename = _uris[0]
46
+ scan = _uris[1]
47
+
48
+ with h5py_utils.open_item(filename, "/", mode="a") as fd: # type: ignore[reportGeneralTypeIssues]
49
+ grp = fd
50
+ S = scan.split("/")
51
+
52
+ for i, s in enumerate(S):
53
+ if s == "":
54
+ continue
55
+
56
+ grp = grp.require_group(s)
57
+
58
+ if i == len(S) - 1:
59
+ grp.attrs.update(
60
+ {
61
+ "NX_class": "NXdata",
62
+ "signal": "data",
63
+ "interpretation": "image",
64
+ }
65
+ )
66
+ else:
67
+ grp.attrs.update({"NX_class": "NXcollection", "default": S[i + 1]})
68
+
69
+ layout = h5py.VirtualLayout(
70
+ shape=(
71
+ cnt_shape[1],
72
+ *A.shape,
73
+ ),
74
+ dtype=cnt_dtype,
75
+ )
76
+ layoutpymca = h5py.VirtualLayout(
77
+ shape=(
78
+ *A.shape,
79
+ cnt_shape[1],
80
+ ),
81
+ dtype=cnt_dtype,
82
+ )
83
+ vsource = h5py.VirtualSource(
84
+ filename_src,
85
+ f"{scan_src}/instrument/{cntnam}/data",
86
+ shape=cnt_shape,
87
+ dtype=cnt.dtype,
88
+ )
89
+
90
+ for i in range(cnt_shape[0]):
91
+ # print(np.unravel_index(i, A.shape), layout.shape, vsource[A.flat[i]].shape)
92
+
93
+ layout[(slice(None), *np.unravel_index(i, A.shape))] = vsource[
94
+ A.flat[i]
95
+ ]
96
+ layoutpymca[(*np.unravel_index(i, A.shape), slice(None))] = vsource[
97
+ A.flat[i]
98
+ ]
99
+ grp.create_virtual_dataset("data", layout, fillvalue=-1)
100
+ grp.create_virtual_dataset("pymca", layoutpymca, fillvalue=-1)
@@ -0,0 +1,165 @@
1
+ from PyMca5.PyMcaIO import ConfigDict
2
+
3
+ # import h5py
4
+ # import hdf5plugin # noqa: F401
5
+ from silx.io import h5py_utils
6
+ import numpy as np
7
+ import time
8
+
9
+
10
+ from PyMca5.PyMcaPhysics.xrf.ClassMcaTheory import ClassMcaTheory
11
+ import matplotlib.pyplot as plt
12
+ import os
13
+ from ewokscore import Task
14
+
15
+
16
+ class AdvancedFitSumSingleDetector(
17
+ Task,
18
+ input_names=[
19
+ "bliss_scan_uri",
20
+ "detector_name",
21
+ "config",
22
+ "output_root_uri",
23
+ ],
24
+ optional_input_names=[
25
+ "figure_filename",
26
+ "batchconfig_suffix",
27
+ "instrument_data_template",
28
+ "batch_force",
29
+ "waitForConfigFile",
30
+ "retryPeriod",
31
+ "retryN",
32
+ ],
33
+ output_names=[
34
+ "bliss_scan_uri",
35
+ "detector_name",
36
+ "output_root_uri",
37
+ "batch_config_filename",
38
+ ],
39
+ ):
40
+
41
+ def run(self):
42
+
43
+ input_uri = self.inputs.bliss_scan_uri
44
+ output_uri = self.inputs.output_root_uri
45
+ detector_name = self.inputs.detector_name
46
+ config_file = self.inputs.config
47
+
48
+ dettmpl = self.get_input_value("instrument_data_template", "instrument/{}/data")
49
+ batch_suffix = self.get_input_value("batchconfig_suffix", None)
50
+ batch_force = self.get_input_value(
51
+ "batch_force", {"fit.stripflag": 0, "fit.escapeflag": 0, "fit.fitweight": 0}
52
+ )
53
+ waitForConfigFile = self.get_input_value("waitForConfigFile", True)
54
+ retryPeriod = self.get_input_value("retryPeriod", 3)
55
+ retryN = self.get_input_value("retryN", 1200)
56
+
57
+ figure_filename = self.get_input_value("figure_filename", None)
58
+
59
+ uris = input_uri.split("::")
60
+ filename = uris[0]
61
+ datpath = uris[1] + "/" + dettmpl.format(detector_name)
62
+
63
+ if waitForConfigFile:
64
+ for i in range(retryN):
65
+ if os.path.isfile(config_file):
66
+ break
67
+ time.sleep(retryPeriod)
68
+ else:
69
+ raise RuntimeError(f"Config file {config_file} not found!")
70
+
71
+ with h5py_utils.open_item(filename, datpath) as ds: # type: ignore[reportGeneralTypeIssues]
72
+ data = np.array(ds)
73
+
74
+ batch_savefile = None
75
+
76
+ if batch_suffix is not None:
77
+ batch_savefile = config_file + batch_suffix
78
+
79
+ if not os.path.isfile(batch_suffix):
80
+
81
+ cfg = ConfigDict.ConfigDict(filelist=config_file)
82
+
83
+ for k in batch_force:
84
+ _c = cfg
85
+ kk = k.split(".")
86
+ for _k in kk[:-1]:
87
+ _c = _c[_k]
88
+
89
+ _c[kk[-1]] = batch_force[k]
90
+
91
+ cfg.write(batch_savefile)
92
+
93
+ data = np.sum(data, keepdims=True, axis=0) / data.shape[0]
94
+
95
+ mcafit = ClassMcaTheory(config_file)
96
+ mcafit.setData(x=np.arange(data.shape[1]), y=data)
97
+ mcafit.estimate()
98
+ p, fit = mcafit.startfit(digest=1)
99
+
100
+ egy = fit["energy"]
101
+ bkg = fit["continuum"]
102
+ grps = fit["groups"]
103
+ yfit = fit["yfit"]
104
+ ydata = fit["ydata"]
105
+
106
+ ouris = output_uri.split("::")
107
+ out_filename = ouris[0]
108
+ out_path = ouris[1]
109
+
110
+ with h5py_utils.open_item(out_filename, "/", mode="a") as fd: # type: ignore[reportGeneralTypeIssues]
111
+
112
+ _res = fd
113
+ for g in out_path.split("/"):
114
+ _g = g.strip()
115
+ if len(_g) == 0:
116
+ continue
117
+
118
+ _res = _res.require_group(_g)
119
+ _res.attrs["NX_class"] = "NXcollection"
120
+
121
+ _res.attrs["NX_class"] = "NXdata"
122
+ _res.attrs["signal"] = "fit"
123
+ _res.attrs["auxiliary_signals"] = [
124
+ "background",
125
+ "spectrum",
126
+ ] + grps
127
+ _res.attrs["axes"] = ["energy"]
128
+
129
+ egyds = _res.create_dataset("energy", data=egy)
130
+ egyds.attrs["units"] = "keV"
131
+
132
+ _res.create_dataset("fit", data=yfit)
133
+ _res.create_dataset("spectrum", data=ydata)
134
+ _res.create_dataset("background", data=bkg)
135
+
136
+ for g in grps:
137
+ _res.create_dataset(g, data=fit[f"y{g}"])
138
+
139
+ if figure_filename is not None:
140
+
141
+ figpath = os.path.dirname(figure_filename)
142
+ os.makedirs(figpath, exist_ok=True)
143
+
144
+ fig, ax = plt.subplots(figsize=(8, 5), layout="constrained")
145
+ colors = plt.get_cmap("nipy_spectral")(np.linspace(0.1, 0.9, len(grps)))
146
+
147
+ ax.plot(egy, ydata, "-", color="0.7", label="Data", linewidth=4)
148
+ # ax.plot(egy, yfit, 'r-', label="Fit", linewidth=2)
149
+
150
+ for c, g in zip(colors, grps):
151
+ ax.plot(egy, fit[f"y{g}"] + bkg, "--", label=g, linewidth=1.5, color=c)
152
+
153
+ ax.plot(egy, bkg, "b", label="Background", linewidth=2)
154
+
155
+ fig.legend(loc="outside right upper")
156
+
157
+ ax.set_yscale("log")
158
+ ax.set_xlabel("Energy (keV)")
159
+
160
+ fig.savefig(figure_filename)
161
+
162
+ self.outputs.output_root_uri = output_uri
163
+ self.outputs.detector_name = detector_name
164
+ self.outputs.bliss_scan_uri = input_uri
165
+ self.outputs.batch_config_filename = batch_savefile
@@ -0,0 +1,117 @@
1
+ # import h5py
2
+ # import hdf5plugin # noqa: F401
3
+ from silx.io import h5py_utils
4
+ import os
5
+ from ewokscore import Task
6
+ import numpy as np
7
+ import matplotlib.pyplot as plt
8
+ import matplotlib as mpl
9
+
10
+ from PIL import Image
11
+
12
+
13
+ class TiffExporterFromRegrid(
14
+ Task,
15
+ input_names=[
16
+ "regrid_uri",
17
+ "output_path",
18
+ ],
19
+ optional_input_names=[
20
+ "output_prefix",
21
+ "output_suffix",
22
+ "output_gallery_path",
23
+ ],
24
+ output_names=[],
25
+ ):
26
+
27
+ def run(self):
28
+
29
+ regrid_uri = self.inputs.regrid_uri
30
+ output_path = self.inputs.output_path
31
+ output_prefix = self.get_input_value("output_prefix", "IMG_")
32
+ output_suffix = self.get_input_value("output_suffix", "")
33
+ output_gallery_path = self.get_input_value("output_gallery_path", None)
34
+
35
+ os.makedirs(output_path, exist_ok=True)
36
+ if output_gallery_path is not None:
37
+ os.makedirs(output_gallery_path, exist_ok=True)
38
+
39
+ uris = regrid_uri.split("::")
40
+ input_filename = uris[0]
41
+ grp = uris[1]
42
+
43
+ with h5py_utils.open_item(input_filename, "/") as fd: # type: ignore[reportGeneralTypeIssues]
44
+ _res = fd[grp]
45
+ is_nxdata = "NX_class" in _res.attrs and _res.attrs["NX_class"] == "NXdata"
46
+
47
+ while not is_nxdata:
48
+ _res = _res[_res.attrs["default"]]
49
+ is_nxdata = (
50
+ "NX_class" in _res.attrs and _res.attrs["NX_class"] == "NXdata"
51
+ )
52
+
53
+ grps = []
54
+ if "signal" in _res.attrs:
55
+ grps += [
56
+ _res.attrs["signal"],
57
+ ]
58
+
59
+ if "auxiliary_signals" in _res.attrs:
60
+ grps += list(_res.attrs["auxiliary_signals"])
61
+
62
+ kwargs = {}
63
+ axes = []
64
+ axnames = []
65
+ if "axes" in _res.attrs:
66
+ resolutions = []
67
+ for a in _res.attrs["axes"]:
68
+ axnames += [
69
+ a,
70
+ ]
71
+ d = np.array(_res[a])
72
+ axes += [
73
+ d,
74
+ ]
75
+ resolutions += [
76
+ 1e4 / float(np.mean(np.diff(d))),
77
+ ]
78
+
79
+ kwargs["resolution_unit"] = 3
80
+ kwargs["resolution"] = resolutions
81
+
82
+ cmap = "jet"
83
+
84
+ for g in grps:
85
+ data = np.array(_res[g], dtype=np.float32)
86
+ im = Image.fromarray(data, mode="F")
87
+ gg = g.replace(" ", "_")
88
+ im.save(
89
+ os.path.join(
90
+ output_path, f"{output_prefix}{gg}{output_suffix}.tiff"
91
+ ),
92
+ **kwargs,
93
+ )
94
+
95
+ # ArraySave.save2DArrayListAsMonochromaticTiff([data], os.path.join(output_path, f"{output_prefix}{gg}.tiff"), dtype=np.float32)
96
+ if output_gallery_path is not None:
97
+ norm = mpl.colors.Normalize(0, np.max(data))
98
+ f, ax = plt.subplots()
99
+ ax.pcolor(*axes, data, norm=norm, cmap=cmap)
100
+ ax.invert_yaxis()
101
+
102
+ if len(axnames) == 2:
103
+ ax.set_xlabel(f"{axnames[1]} (um)")
104
+ ax.set_ylabel(f"{axnames[0]} (um)")
105
+
106
+ f.suptitle(g)
107
+ f.colorbar(
108
+ mpl.cm.ScalarMappable(norm=norm, cmap=cmap),
109
+ ax=ax,
110
+ label="Areal mass density ($ng/mm^2$)",
111
+ )
112
+ f.savefig(
113
+ os.path.join(
114
+ output_gallery_path,
115
+ f"{output_prefix}{gg}{output_suffix}.jpg",
116
+ )
117
+ )
@@ -0,0 +1,143 @@
1
+ from typing import Sequence
2
+
3
+ from ewokscore import Task
4
+
5
+ from ewoksfluo.tasks import xrf_results
6
+ from ewoksfluo.io.hdf5 import ReadHdf5File
7
+ import h5py
8
+ import hdf5plugin # noqa: F401
9
+ import numpy as np
10
+
11
+
12
+ class WeightedSumResults(
13
+ Task,
14
+ input_names=[
15
+ "xrf_norm_uris",
16
+ "xrf_fit_uris",
17
+ "bliss_scan_uri",
18
+ "detector_names",
19
+ "output_root_uri",
20
+ ],
21
+ optional_input_names=["detector_normalization_template"],
22
+ output_names=["xrf_results_uri", "bliss_scan_uri", "output_root_uri"],
23
+ ):
24
+ """Add single-scan XRF results of multiple detectors"""
25
+
26
+ def _read_xrf_results(self, uri):
27
+
28
+ res = dict()
29
+
30
+ fit_filename, fit_h5path = uri.split("::")
31
+
32
+ fit_h5path += "/results"
33
+
34
+ with ReadHdf5File(fit_filename) as h5file:
35
+ try:
36
+ xrf_results_group = h5file[fit_h5path]
37
+ except KeyError:
38
+ raise KeyError(
39
+ f"HDF5 path not found: '{fit_h5path}' in file '{fit_filename}'"
40
+ )
41
+ if not isinstance(xrf_results_group, h5py.Group):
42
+ raise TypeError(
43
+ f"Expected HDF5 Group at '{fit_h5path}', but got {type(xrf_results_group)}"
44
+ )
45
+
46
+ if "massfractions" not in xrf_results_group:
47
+ raise KeyError(f"'massfractions' group missing under '{fit_h5path}'")
48
+ param_group = xrf_results_group["massfractions"]
49
+ if not isinstance(param_group, h5py.Group):
50
+ raise TypeError(
51
+ f"Expected HDF5 Group for 'massfractions', but got {type(param_group)}"
52
+ )
53
+
54
+ for dset_name, dset in param_group.items():
55
+ if not xrf_results.is_peak_area(dset):
56
+ continue
57
+
58
+ res[dset_name] = np.array(param_group[dset_name][()])
59
+
60
+ return res
61
+
62
+ def _dict_op(self, fun, a, b):
63
+
64
+ ka = set(a.keys())
65
+ kb = set(b.keys())
66
+
67
+ s = dict()
68
+
69
+ for k in set.intersection(ka, kb):
70
+ s[k] = fun(a[k], b[k])
71
+
72
+ return s
73
+
74
+ def dict_op(self, fun, *op):
75
+
76
+ if len(op) <= 1:
77
+ return op
78
+
79
+ res = self._dict_op(fun, op[0], op[1])
80
+
81
+ for i in range(2, len(op)):
82
+ res = self._dict_op(fun, res, op[i])
83
+
84
+ return res
85
+
86
+ def dict_plus(self, *op):
87
+ return self.dict_op(lambda a, b: a + b, *op)
88
+
89
+ def dict_mult(self, *op):
90
+ return self.dict_op(lambda a, b: a * b, *op)
91
+
92
+ def run(self) -> None:
93
+ params = {**self.get_input_values()}
94
+
95
+ xrf_norm_uris: Sequence[str] = params["xrf_norm_uris"]
96
+ xrf_fit_uris: Sequence[str] = params["xrf_fit_uris"]
97
+ bliss_scan_uri: str = params["bliss_scan_uri"]
98
+ output_root_uri: str = params["output_root_uri"]
99
+
100
+ if len(xrf_norm_uris) < 1:
101
+ raise ValueError("Expected at least 1 detector to sum")
102
+
103
+ if len(xrf_norm_uris) != len(xrf_fit_uris):
104
+ raise ValueError(
105
+ "Expected the same number of elements in _norm_ and _fit_ arrays."
106
+ )
107
+
108
+ summed_fit = None
109
+ summed_prod = None
110
+
111
+ config = {"xrf_norm_uris": xrf_norm_uris, "xrf_fit_uris": xrf_fit_uris}
112
+
113
+ for norm_uri, fit_uri in zip(xrf_norm_uris, xrf_fit_uris):
114
+
115
+ norm_data = self._read_xrf_results(norm_uri)
116
+ fit_data = self._read_xrf_results(fit_uri)
117
+
118
+ if summed_fit is None:
119
+ summed_fit = fit_data
120
+ else:
121
+ summed_fit = self.dict_plus(summed_fit, fit_data)
122
+
123
+ if summed_prod is None:
124
+ summed_prod = self.dict_mult(norm_data, fit_data)
125
+ else:
126
+ summed_prod = self.dict_plus(
127
+ summed_prod, self.dict_mult(norm_data, fit_data)
128
+ )
129
+
130
+ weighted = self._dict_op(lambda a, b: a / b, summed_prod, summed_fit)
131
+
132
+ xrf_results.save_xrf_results(
133
+ output_root_uri,
134
+ "weighted_ngmm2",
135
+ config,
136
+ None,
137
+ None,
138
+ weighted,
139
+ )
140
+
141
+ self.outputs.bliss_scan_uri = bliss_scan_uri
142
+ self.outputs.output_root_uri = output_root_uri
143
+ self.outputs.xrf_results_uri = output_root_uri + "/results"
File without changes
@@ -0,0 +1,3 @@
1
+ def test_dummy():
2
+ """A bare-bones test so pytest actually finds and runs something."""
3
+ assert True
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: ewoksid16a
3
+ Version: 0.1.0
4
+ Summary: Data processing workflows for ID16A
5
+ Author-email: ESRF <dau-pydev@esrf.fr>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.esrf.fr/workflow/ewoksapps/ewoksid16a/
8
+ Project-URL: Documentation, https://ewoksid16a.readthedocs.io/
9
+ Project-URL: Repository, https://gitlab.esrf.fr/workflow/ewoksapps/ewoksid16a/
10
+ Project-URL: Issues, https://gitlab.esrf.fr/workflow/ewoksapps/ewoksid16a/issues
11
+ Project-URL: Changelog, https://gitlab.esrf.fr/workflow/ewoksapps/ewoksid16a/-/blob/main/CHANGELOG.md
12
+ Keywords: orange3 add-on,ewoks
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: ewoksjob
19
+ Requires-Dist: ewoksfluo
20
+ Requires-Dist: blissdata
21
+ Requires-Dist: ewoksorange
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest>=7; extra == "test"
24
+ Requires-Dist: pyqt6; extra == "test"
25
+ Provides-Extra: dev
26
+ Requires-Dist: ewoksid16a[test]; extra == "dev"
27
+ Requires-Dist: black>=25; extra == "dev"
28
+ Requires-Dist: flake8>=4; extra == "dev"
29
+ Requires-Dist: ruff; extra == "dev"
30
+ Requires-Dist: bandit; extra == "dev"
31
+ Provides-Extra: doc
32
+ Requires-Dist: ewoksid16a[test]; extra == "doc"
33
+ Requires-Dist: sphinx>=4.5; extra == "doc"
34
+ Requires-Dist: sphinx-autodoc-typehints>=1.16; extra == "doc"
35
+ Requires-Dist: pydata-sphinx-theme; extra == "doc"
36
+ Dynamic: license-file
37
+
38
+ # ewoksid16a
39
+
40
+ Data processing workflows for ID16A
41
+
42
+ ## Documentation
43
+
44
+ https://ewoksid16a.readthedocs.io/
@@ -0,0 +1,22 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/ewoksid16a/__init__.py
5
+ src/ewoksid16a.egg-info/PKG-INFO
6
+ src/ewoksid16a.egg-info/SOURCES.txt
7
+ src/ewoksid16a.egg-info/dependency_links.txt
8
+ src/ewoksid16a.egg-info/entry_points.txt
9
+ src/ewoksid16a.egg-info/requires.txt
10
+ src/ewoksid16a.egg-info/top_level.txt
11
+ src/ewoksid16a/tasks/__init__.py
12
+ src/ewoksid16a/tasks/fluo/norm_it.py
13
+ src/ewoksid16a/tasks/fluo/spectral_regrid.py
14
+ src/ewoksid16a/tasks/fluo/sumspectrumfit.py
15
+ src/ewoksid16a/tasks/fluo/tiffexporter.py
16
+ src/ewoksid16a/tasks/fluo/weight.py
17
+ src/ewoksid16a/tests/__init__.py
18
+ src/ewoksid16a/tests/test_dummy.py
19
+ src/orangecontrib/ewoksid16a/__init__.py
20
+ src/orangecontrib/ewoksid16a/categories/__init__.py
21
+ src/orangecontrib/ewoksid16a/icons/__init__.py
22
+ src/orangecontrib/ewoksid16a/tutorials/__init__.py
@@ -0,0 +1,22 @@
1
+ [ewoks.tasks.class]
2
+ ewoksid16a.tasks.* = ewoksid16a
3
+
4
+ [orange.canvas.help]
5
+ html-index = orangecontrib.ewoksid16a:WIDGET_HELP_PATH
6
+
7
+ [orange.widgets]
8
+ Examples = orangecontrib.ewoksid16a
9
+ ExamplesCategories = orangecontrib.ewoksid16a.categories
10
+
11
+ [orange.widgets.tutorials]
12
+ Examples = orangecontrib.ewoksid16a.tutorials
13
+ Examples1 = orangecontrib.ewoksid16a.categories.examples1.tutorials
14
+ Examples2 = orangecontrib.ewoksid16a.categories.examples2.tutorials
15
+
16
+ [orange3.addon]
17
+ ewoksid16a = orangecontrib.ewoksid16a
18
+
19
+ [orangecanvas.examples]
20
+ Examples = orangecontrib.ewoksid16a.tutorials
21
+ Examples1 = orangecontrib.ewoksid16a.categories.examples1.tutorials
22
+ Examples2 = orangecontrib.ewoksid16a.categories.examples2.tutorials
@@ -0,0 +1,21 @@
1
+ ewoksjob
2
+ ewoksfluo
3
+ blissdata
4
+ ewoksorange
5
+
6
+ [dev]
7
+ ewoksid16a[test]
8
+ black>=25
9
+ flake8>=4
10
+ ruff
11
+ bandit
12
+
13
+ [doc]
14
+ ewoksid16a[test]
15
+ sphinx>=4.5
16
+ sphinx-autodoc-typehints>=1.16
17
+ pydata-sphinx-theme
18
+
19
+ [test]
20
+ pytest>=7
21
+ pyqt6
@@ -0,0 +1,2 @@
1
+ ewoksid16a
2
+ orangecontrib
@@ -0,0 +1,23 @@
1
+ import sysconfig
2
+
3
+ NAME = "ewoksid16a"
4
+
5
+ DESCRIPTION = "Data processing workflows for ID16A"
6
+
7
+ LONG_DESCRIPTION = "Data processing workflows for ID16A"
8
+
9
+ ICON = "icons/category.svg"
10
+
11
+ BACKGROUND = "light-blue"
12
+
13
+ WIDGET_HELP_PATH = (
14
+ # Development documentation (make htmlhelp in ./doc)
15
+ ("{DEVELOP_ROOT}/doc/_build/htmlhelp/index.html", None),
16
+ # Documentation included in wheel
17
+ (
18
+ "{}/help/ewoksid16a/index.html".format(sysconfig.get_path("data")),
19
+ None,
20
+ ),
21
+ # Online documentation url
22
+ ("https://ewoksid16a.readthedocs.io", ""),
23
+ )
@@ -0,0 +1,26 @@
1
+ import sysconfig
2
+
3
+ WIDGET_HELP_PATH = (
4
+ # Development documentation (make htmlhelp in ./doc)
5
+ ("{DEVELOP_ROOT}/doc/_build/htmlhelp/index.html", None),
6
+ # Documentation included in wheel
7
+ (
8
+ "{}/help/ewoksid16a/index.html".format(sysconfig.get_path("data")),
9
+ None,
10
+ ),
11
+ # Online documentation url
12
+ ("https://ewoksid16a.readthedocs.io", ""),
13
+ )
14
+
15
+
16
+ # Entry point for main Orange categories/widgets discovery
17
+ def widget_discovery(discovery):
18
+ from ewoksorange.pkg_meta import get_distribution
19
+
20
+ dist = get_distribution("ewoksid16a")
21
+ pkgs = [
22
+ "orangecontrib.ewoksid16a.categories.examples1",
23
+ "orangecontrib.ewoksid16a.categories.examples2",
24
+ ]
25
+ for pkg in pkgs:
26
+ discovery.process_category_package(pkg, distribution=dist)