ewoksxes 0.0.1__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.
- ewoksxes-0.0.1/LICENSE.md +20 -0
- ewoksxes-0.0.1/PKG-INFO +116 -0
- ewoksxes-0.0.1/README.md +61 -0
- ewoksxes-0.0.1/pyproject.toml +93 -0
- ewoksxes-0.0.1/setup.cfg +4 -0
- ewoksxes-0.0.1/src/ewoksxes/__init__.py +0 -0
- ewoksxes-0.0.1/src/ewoksxes/tasks/__init__.py +19 -0
- ewoksxes-0.0.1/src/ewoksxes/tasks/calibrate_energy.py +69 -0
- ewoksxes-0.0.1/src/ewoksxes/tasks/combine_spectra.py +117 -0
- ewoksxes-0.0.1/src/ewoksxes/tasks/compute_flat_and_mask.py +176 -0
- ewoksxes-0.0.1/src/ewoksxes/tasks/compute_roi.py +50 -0
- ewoksxes-0.0.1/src/ewoksxes/tasks/fit_polynomial_2d.py +68 -0
- ewoksxes-0.0.1/src/ewoksxes/tasks/flat_field_correction.py +102 -0
- ewoksxes-0.0.1/src/ewoksxes/tasks/load_raw_data.py +66 -0
- ewoksxes-0.0.1/src/ewoksxes/tasks/save_flat_and_mask.py +71 -0
- ewoksxes-0.0.1/src/ewoksxes/tasks/save_spectrum.py +113 -0
- ewoksxes-0.0.1/src/ewoksxes/tasks/utils.py +33 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/__init__.py +0 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/conftest.py +52 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/data/flat.edf +0 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/data/mask.npy +0 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/data/von_hamos_0000.h5 +0 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/test_calibrate_energy.py +137 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/test_combine_spectra.py +134 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/test_compute_flat_and_mask.py +55 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/test_compute_roi.py +90 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/test_fit_polynomial_2d.py +31 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/test_flat_field_correction.py +84 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/test_flat_field_workflow.py +107 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/test_load_raw_data.py +37 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/test_save_flat_and_mask.py +55 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/test_save_spectrum.py +141 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/test_utils.py +43 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/test_xes_calibration_workflow.py +115 -0
- ewoksxes-0.0.1/src/ewoksxes/tests/test_xes_processing_workflow.py +99 -0
- ewoksxes-0.0.1/src/ewoksxes/workflows/__init__.py +0 -0
- ewoksxes-0.0.1/src/ewoksxes/workflows/xes_calibration.json +55 -0
- ewoksxes-0.0.1/src/ewoksxes/workflows/xes_flat_field.json +63 -0
- ewoksxes-0.0.1/src/ewoksxes/workflows/xes_processing.json +75 -0
- ewoksxes-0.0.1/src/ewoksxes.egg-info/PKG-INFO +116 -0
- ewoksxes-0.0.1/src/ewoksxes.egg-info/SOURCES.txt +43 -0
- ewoksxes-0.0.1/src/ewoksxes.egg-info/dependency_links.txt +1 -0
- ewoksxes-0.0.1/src/ewoksxes.egg-info/entry_points.txt +2 -0
- ewoksxes-0.0.1/src/ewoksxes.egg-info/requires.txt +18 -0
- ewoksxes-0.0.1/src/ewoksxes.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
**Copyright (c) 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.
|
ewoksxes-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ewoksxes
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Data processing workflows for XES
|
|
5
|
+
Author-email: ESRF <dau-pydev@esrf.fr>
|
|
6
|
+
License: # MIT License
|
|
7
|
+
|
|
8
|
+
**Copyright (c) 2025 European Synchrotron Radiation Facility**
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
11
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
12
|
+
the Software without restriction, including without limitation the rights to
|
|
13
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
14
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
15
|
+
subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
22
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
23
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
24
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
25
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Homepage, https://gitlab.esrf.fr/workflow/ewoksapps/ewoksxes/
|
|
28
|
+
Project-URL: Documentation, https://ewoksxes.readthedocs.io/
|
|
29
|
+
Project-URL: Repository, https://gitlab.esrf.fr/workflow/ewoksapps/ewoksxes/
|
|
30
|
+
Project-URL: Issues, https://gitlab.esrf.fr/workflow/ewoksapps/ewoksxes/issues
|
|
31
|
+
Project-URL: Changelog, https://gitlab.esrf.fr/workflow/ewoksapps/ewoksxes/-/blob/main/CHANGELOG.md
|
|
32
|
+
Keywords: xes,ewoks
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Requires-Python: >=3.10
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
License-File: LICENSE.md
|
|
39
|
+
Requires-Dist: ewoks
|
|
40
|
+
Requires-Dist: scipy
|
|
41
|
+
Requires-Dist: h5py
|
|
42
|
+
Requires-Dist: fabio
|
|
43
|
+
Provides-Extra: test
|
|
44
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
45
|
+
Requires-Dist: pyqt6; extra == "test"
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Requires-Dist: ewoksxes[test]; extra == "dev"
|
|
48
|
+
Requires-Dist: ruff; extra == "dev"
|
|
49
|
+
Provides-Extra: doc
|
|
50
|
+
Requires-Dist: ewoksxes[test]; extra == "doc"
|
|
51
|
+
Requires-Dist: sphinx>=4.5; extra == "doc"
|
|
52
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.16; extra == "doc"
|
|
53
|
+
Requires-Dist: pydata-sphinx-theme; extra == "doc"
|
|
54
|
+
Dynamic: license-file
|
|
55
|
+
|
|
56
|
+
# ewoksxes
|
|
57
|
+
|
|
58
|
+
The **ewoksxes** project is a Python library designed to provide workflow tasks for X-ray Emission Spectroscopy Data Processing using Ewoks (Extensible Workflow System).
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
By default, at the ESRF, `ewoksxes` should be installed on Ewoks workers using an Ansible script by the DAU team.
|
|
63
|
+
If you wish to install `ewoksxes` manually, ensure you have Python 3.10+ and `pip` installed. You can install the library directly from PyPI:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
pip install ewoksxes
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Alternatively, to install from source, clone this repository and run:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
git clone https://gitlab.esrf.fr/workflow/ewoksapps/ewoksxes.git
|
|
73
|
+
cd ewoksxes
|
|
74
|
+
pip install -e .
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Quickstart Guide
|
|
78
|
+
|
|
79
|
+
### Running an `ewoksxes` Workflow
|
|
80
|
+
|
|
81
|
+
Most of the time, the workflow will be automatically ran from the Bliss control system.
|
|
82
|
+
However if you wish to execute the workflow by hand, you can use the following:
|
|
83
|
+
|
|
84
|
+
`ewoks execute workflow.json`
|
|
85
|
+
|
|
86
|
+
Some examples of workflow are found in `ewoksxes/workflows`
|
|
87
|
+
|
|
88
|
+
## How-To Guides
|
|
89
|
+
|
|
90
|
+
For detailed instructions on various tasks, please refer to the How-To Guides in the documentation, which cover topics such as:
|
|
91
|
+
|
|
92
|
+
- Configuration of XES workflows
|
|
93
|
+
- Running workflows locally for testing
|
|
94
|
+
- Using the API to run specific tasks (e.g., calibration, combining spectra)
|
|
95
|
+
|
|
96
|
+
## Documentation
|
|
97
|
+
|
|
98
|
+
Comprehensive documentation, including an API reference, tutorials, and conceptual explanations, can be found in the [doc directory](./doc) or online at the [ReadTheDocs page](https://ewoksxes.readthedocs.io).
|
|
99
|
+
|
|
100
|
+
## Contributing
|
|
101
|
+
|
|
102
|
+
Contributions are welcome! To contribute, please:
|
|
103
|
+
|
|
104
|
+
1. Clone the repository and create a new branch for your feature or fix.
|
|
105
|
+
2. Write tests and ensure that the code is well-documented.
|
|
106
|
+
3. Submit a merge request for review.
|
|
107
|
+
|
|
108
|
+
See the [`CONTRIBUTING.md`](./CONTRIBUTING.md) file for more details.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
This project is licensed under the MIT License. See the [`LICENSE.md`](./LICENSE.md) file for details.
|
|
113
|
+
|
|
114
|
+
## Support
|
|
115
|
+
|
|
116
|
+
If you have any questions or issues, please open an issue on the GitLab repository or contact the support team via a [data processing request ticket](https://requests.esrf.fr/plugins/servlet/desk/portal/41).
|
ewoksxes-0.0.1/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# ewoksxes
|
|
2
|
+
|
|
3
|
+
The **ewoksxes** project is a Python library designed to provide workflow tasks for X-ray Emission Spectroscopy Data Processing using Ewoks (Extensible Workflow System).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
By default, at the ESRF, `ewoksxes` should be installed on Ewoks workers using an Ansible script by the DAU team.
|
|
8
|
+
If you wish to install `ewoksxes` manually, ensure you have Python 3.10+ and `pip` installed. You can install the library directly from PyPI:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pip install ewoksxes
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Alternatively, to install from source, clone this repository and run:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
git clone https://gitlab.esrf.fr/workflow/ewoksapps/ewoksxes.git
|
|
18
|
+
cd ewoksxes
|
|
19
|
+
pip install -e .
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quickstart Guide
|
|
23
|
+
|
|
24
|
+
### Running an `ewoksxes` Workflow
|
|
25
|
+
|
|
26
|
+
Most of the time, the workflow will be automatically ran from the Bliss control system.
|
|
27
|
+
However if you wish to execute the workflow by hand, you can use the following:
|
|
28
|
+
|
|
29
|
+
`ewoks execute workflow.json`
|
|
30
|
+
|
|
31
|
+
Some examples of workflow are found in `ewoksxes/workflows`
|
|
32
|
+
|
|
33
|
+
## How-To Guides
|
|
34
|
+
|
|
35
|
+
For detailed instructions on various tasks, please refer to the How-To Guides in the documentation, which cover topics such as:
|
|
36
|
+
|
|
37
|
+
- Configuration of XES workflows
|
|
38
|
+
- Running workflows locally for testing
|
|
39
|
+
- Using the API to run specific tasks (e.g., calibration, combining spectra)
|
|
40
|
+
|
|
41
|
+
## Documentation
|
|
42
|
+
|
|
43
|
+
Comprehensive documentation, including an API reference, tutorials, and conceptual explanations, can be found in the [doc directory](./doc) or online at the [ReadTheDocs page](https://ewoksxes.readthedocs.io).
|
|
44
|
+
|
|
45
|
+
## Contributing
|
|
46
|
+
|
|
47
|
+
Contributions are welcome! To contribute, please:
|
|
48
|
+
|
|
49
|
+
1. Clone the repository and create a new branch for your feature or fix.
|
|
50
|
+
2. Write tests and ensure that the code is well-documented.
|
|
51
|
+
3. Submit a merge request for review.
|
|
52
|
+
|
|
53
|
+
See the [`CONTRIBUTING.md`](./CONTRIBUTING.md) file for more details.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
This project is licensed under the MIT License. See the [`LICENSE.md`](./LICENSE.md) file for details.
|
|
58
|
+
|
|
59
|
+
## Support
|
|
60
|
+
|
|
61
|
+
If you have any questions or issues, please open an issue on the GitLab repository or contact the support team via a [data processing request ticket](https://requests.esrf.fr/plugins/servlet/desk/portal/41).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools>=61",
|
|
4
|
+
]
|
|
5
|
+
build-backend = "setuptools.build_meta"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "ewoksxes"
|
|
9
|
+
version = "0.0.1"
|
|
10
|
+
keywords = [
|
|
11
|
+
"xes",
|
|
12
|
+
"ewoks"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
authors = [{name = "ESRF", email = "dau-pydev@esrf.fr"}]
|
|
16
|
+
description = "Data processing workflows for XES"
|
|
17
|
+
readme = {file = "README.md", content-type = "text/markdown"}
|
|
18
|
+
license = {file = "LICENSE.md"}
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
]
|
|
24
|
+
requires-python = ">=3.10"
|
|
25
|
+
dependencies = [
|
|
26
|
+
"ewoks",
|
|
27
|
+
"scipy",
|
|
28
|
+
"h5py",
|
|
29
|
+
"fabio",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://gitlab.esrf.fr/workflow/ewoksapps/ewoksxes/"
|
|
34
|
+
Documentation = "https://ewoksxes.readthedocs.io/"
|
|
35
|
+
Repository = "https://gitlab.esrf.fr/workflow/ewoksapps/ewoksxes/"
|
|
36
|
+
Issues = "https://gitlab.esrf.fr/workflow/ewoksapps/ewoksxes/issues"
|
|
37
|
+
Changelog = "https://gitlab.esrf.fr/workflow/ewoksapps/ewoksxes/-/blob/main/CHANGELOG.md"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
test = [
|
|
41
|
+
"pytest >=7",
|
|
42
|
+
"pyqt6",
|
|
43
|
+
]
|
|
44
|
+
dev = [
|
|
45
|
+
"ewoksxes[test]",
|
|
46
|
+
"ruff",
|
|
47
|
+
]
|
|
48
|
+
doc = [
|
|
49
|
+
"ewoksxes[test]",
|
|
50
|
+
"sphinx >=4.5",
|
|
51
|
+
"sphinx-autodoc-typehints >=1.16",
|
|
52
|
+
"pydata-sphinx-theme",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.setuptools]
|
|
56
|
+
package-dir = {""= "src"}
|
|
57
|
+
|
|
58
|
+
[tool.setuptools.packages.find]
|
|
59
|
+
where = ["src"]
|
|
60
|
+
|
|
61
|
+
[tool.setuptools.package-data]
|
|
62
|
+
"ewoksxes.tests.data" = ["*.h5", "*.edf", "*.npy"]
|
|
63
|
+
"ewoksxes.workflows" = ["*.json"]
|
|
64
|
+
"*" = ["*.ows", "*.png", "*.svg"]
|
|
65
|
+
|
|
66
|
+
[project.entry-points."ewoks.tasks.class"]
|
|
67
|
+
"ewoksxes.tasks.*" = "ewoksxes"
|
|
68
|
+
|
|
69
|
+
[tool.coverage.run]
|
|
70
|
+
omit = [
|
|
71
|
+
"*/tests/*"
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.bandit.assert_used]
|
|
75
|
+
skips = ["*/test_*.py"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff]
|
|
78
|
+
line-length = 88
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint]
|
|
81
|
+
select = [
|
|
82
|
+
"E", # pycodestyle errors
|
|
83
|
+
"F", # pyflakes
|
|
84
|
+
"I", # isort
|
|
85
|
+
"S", # flake8-bandit
|
|
86
|
+
"W", # pycodestyle warnings
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
[tool.ruff.lint.per-file-ignores]
|
|
90
|
+
"**/test_*.py" = ["S101"] # Allow asserts
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint.isort]
|
|
93
|
+
known-first-party = ["ewoksxes"]
|
ewoksxes-0.0.1/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .calibrate_energy import CalibrateEnergy
|
|
2
|
+
from .combine_spectra import CombineSpectra
|
|
3
|
+
from .compute_flat_and_mask import ComputeFlatAndMask
|
|
4
|
+
from .compute_roi import ComputeROI
|
|
5
|
+
from .fit_polynomial_2d import FitPolynomial2D
|
|
6
|
+
from .flat_field_correction import FlatFieldCorrection
|
|
7
|
+
from .load_raw_data import LoadRawDataAverage
|
|
8
|
+
from .save_flat_and_mask import SaveFlatAndMask
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CalibrateEnergy",
|
|
12
|
+
"CombineSpectra",
|
|
13
|
+
"ComputeFlatAndMask",
|
|
14
|
+
"ComputeROI",
|
|
15
|
+
"FitPolynomial2D",
|
|
16
|
+
"FlatFieldCorrection",
|
|
17
|
+
"LoadRawDataAverage",
|
|
18
|
+
"SaveFlatAndMask",
|
|
19
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ewokscore import Task
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CalibrateEnergy(
|
|
10
|
+
Task,
|
|
11
|
+
input_names=["spectra", "kb_px", "vtc_px", "e_kb", "e_vtc"],
|
|
12
|
+
output_names=["energies", "spectra", "slope", "intercept"],
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
Two-point linear calibration from pixel -> energy for each ROI.
|
|
16
|
+
|
|
17
|
+
Inputs
|
|
18
|
+
------
|
|
19
|
+
spectra : list[np.ndarray]
|
|
20
|
+
One 1D spectrum per ROI.
|
|
21
|
+
kb_px : list[float] | np.ndarray
|
|
22
|
+
Pixel index of the Kβ reference for each ROI.
|
|
23
|
+
vtc_px : list[float] | np.ndarray
|
|
24
|
+
Pixel index of the VTC reference for each ROI.
|
|
25
|
+
e_kb : float
|
|
26
|
+
Known energy of the Kβ line (eV).
|
|
27
|
+
e_vtc : float
|
|
28
|
+
Known energy of the VTC line (eV).
|
|
29
|
+
|
|
30
|
+
Outputs
|
|
31
|
+
-------
|
|
32
|
+
energies : list[np.ndarray]
|
|
33
|
+
Energy axis per ROI (same length as its spectrum).
|
|
34
|
+
spectra : list[np.ndarray]
|
|
35
|
+
Pass-through of the input spectra (pipeline convenience).
|
|
36
|
+
slope : np.ndarray
|
|
37
|
+
ΔE / Δpixel per ROI.
|
|
38
|
+
intercept : np.ndarray
|
|
39
|
+
Intercept per ROI so that E = slope * x + intercept.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def run(self):
|
|
43
|
+
spectra = list(self.inputs.spectra)
|
|
44
|
+
kb = np.asarray(self.inputs.kb_px, dtype=float).ravel()
|
|
45
|
+
vtc = np.asarray(self.inputs.vtc_px, dtype=float).ravel()
|
|
46
|
+
e_kb = float(self.inputs.e_kb)
|
|
47
|
+
e_vtc = float(self.inputs.e_vtc)
|
|
48
|
+
|
|
49
|
+
if len(spectra) != kb.size or kb.size != vtc.size:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"Lengths must match: len(spectra) == len(kb_px) == len(vtc_px)"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
dv = vtc - kb
|
|
55
|
+
if np.any(dv == 0.0):
|
|
56
|
+
raise ValueError("kb_px and vtc_px must differ for every ROI")
|
|
57
|
+
|
|
58
|
+
slope = (e_vtc - e_kb) / dv # shape (n_rois,)
|
|
59
|
+
intercept = e_kb - slope * kb # shape (n_rois,)
|
|
60
|
+
|
|
61
|
+
energies = []
|
|
62
|
+
for s, m, b in zip(spectra, slope, intercept):
|
|
63
|
+
x = np.arange(s.shape[0], dtype=float)
|
|
64
|
+
energies.append(m * x + b)
|
|
65
|
+
|
|
66
|
+
self.outputs.energies = energies
|
|
67
|
+
self.outputs.spectra = spectra
|
|
68
|
+
self.outputs.slope = slope
|
|
69
|
+
self.outputs.intercept = intercept
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ewokscore import Task
|
|
5
|
+
from scipy.interpolate import interp1d
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CombineSpectra(
|
|
11
|
+
Task,
|
|
12
|
+
input_names=["energies", "spectra", "energy_range", "n_points"],
|
|
13
|
+
optional_input_names=["normalize", "norm_range"],
|
|
14
|
+
output_names=["energy", "summed_spectrum"],
|
|
15
|
+
):
|
|
16
|
+
"""
|
|
17
|
+
Optionally normalize spectra, then interpolate each onto a common energy axis
|
|
18
|
+
and sum them.
|
|
19
|
+
|
|
20
|
+
Inputs
|
|
21
|
+
------
|
|
22
|
+
- energies: list of 1D energy arrays (or lists), one per spectrum
|
|
23
|
+
- spectra: list of 1D spectra (same length as energies)
|
|
24
|
+
- energy_range: (emin, emax) -> the range of the FINAL output energy axis
|
|
25
|
+
- n_points: int >= 2 -> number of points on the FINAL output energy axis
|
|
26
|
+
|
|
27
|
+
Optional
|
|
28
|
+
--------
|
|
29
|
+
- normalize: bool (default False). When True, each spectrum is normalized by
|
|
30
|
+
its integral over 'norm_range' before interpolation/summing.
|
|
31
|
+
- norm_range: (emin, emax) normalization window (required when normalize=True)
|
|
32
|
+
|
|
33
|
+
Outputs
|
|
34
|
+
-------
|
|
35
|
+
- energy: np.ndarray of length n_points, spanning [emin, emax]
|
|
36
|
+
- summed_spectrum: np.ndarray of same length, sum of all interpolated spectra
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def run(self):
|
|
40
|
+
raw_energies = self.inputs.energies
|
|
41
|
+
raw_spectra = self.inputs.spectra
|
|
42
|
+
energy_range = self.inputs.energy_range
|
|
43
|
+
n_points = self.inputs.n_points
|
|
44
|
+
|
|
45
|
+
# Validate and USE energy_range as the FINAL axis range
|
|
46
|
+
try:
|
|
47
|
+
emin, emax = energy_range
|
|
48
|
+
except (TypeError, ValueError):
|
|
49
|
+
raise ValueError(f"energy_range must be (emin, emax); got {energy_range}")
|
|
50
|
+
if emin >= emax:
|
|
51
|
+
raise ValueError(f"Invalid energy_range: {energy_range}")
|
|
52
|
+
if not isinstance(n_points, int) or n_points < 2:
|
|
53
|
+
raise ValueError(f"n_points must be integer >= 2, got {n_points}")
|
|
54
|
+
|
|
55
|
+
# Convert to arrays
|
|
56
|
+
energies_list = [np.asarray(e, dtype=float) for e in raw_energies]
|
|
57
|
+
spectra = [np.asarray(s, dtype=float) for s in raw_spectra]
|
|
58
|
+
|
|
59
|
+
if len(energies_list) != len(spectra):
|
|
60
|
+
raise ValueError("energies_list and spectra must have the same length")
|
|
61
|
+
|
|
62
|
+
# Optional normalization
|
|
63
|
+
if bool(self.get_input_value("normalize", False)):
|
|
64
|
+
norm_range = self.get_input_value("norm_range", None)
|
|
65
|
+
if norm_range is None or len(norm_range) != 2:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
"When normalize=True, norm_range=(emin, emax) must be "
|
|
68
|
+
f"provided; got {norm_range}"
|
|
69
|
+
)
|
|
70
|
+
nmin, nmax = float(norm_range[0]), float(norm_range[1])
|
|
71
|
+
|
|
72
|
+
for idx, (energies, spectrum) in enumerate(zip(energies_list, spectra)):
|
|
73
|
+
mask = (energies >= nmin) & (energies <= nmax)
|
|
74
|
+
if not np.any(mask):
|
|
75
|
+
logger.warning(
|
|
76
|
+
f"Spectrum {idx}: no points in norm_range {nmin}-{nmax}; "
|
|
77
|
+
"leaving spectrum unchanged"
|
|
78
|
+
)
|
|
79
|
+
continue
|
|
80
|
+
norm_factor = np.trapezoid(spectrum[mask], energies[mask])
|
|
81
|
+
if norm_factor == 0 or not np.isfinite(norm_factor):
|
|
82
|
+
logger.warning(
|
|
83
|
+
f"Spectrum {idx}: invalid normalization factor "
|
|
84
|
+
f"{norm_factor}; leaving spectrum unchanged"
|
|
85
|
+
)
|
|
86
|
+
continue
|
|
87
|
+
spectra[idx] = spectrum / norm_factor
|
|
88
|
+
logger.info("CombineSpectra: normalization completed.")
|
|
89
|
+
|
|
90
|
+
# Build FINAL energy axis USING energy_range
|
|
91
|
+
energy = np.linspace(emin, emax, n_points, dtype=float)
|
|
92
|
+
summed = np.zeros_like(energy)
|
|
93
|
+
|
|
94
|
+
# Interpolate & sum
|
|
95
|
+
for idx, (energies, spectrum) in enumerate(zip(energies_list, spectra)):
|
|
96
|
+
if energies.shape[0] != spectrum.shape[0]:
|
|
97
|
+
common = min(energies.shape[0], spectrum.shape[0])
|
|
98
|
+
logger.warning(
|
|
99
|
+
f"Spectrum {idx} length {spectrum.shape[0]} and energies "
|
|
100
|
+
f"{energies.shape[0]} mismatch; trimming to first {common} "
|
|
101
|
+
"points for interpolation."
|
|
102
|
+
)
|
|
103
|
+
energies_to_use = energies[:common]
|
|
104
|
+
spectrum_to_use = spectrum[:common]
|
|
105
|
+
else:
|
|
106
|
+
energies_to_use = energies
|
|
107
|
+
spectrum_to_use = spectrum
|
|
108
|
+
|
|
109
|
+
interp = interp1d(
|
|
110
|
+
energies_to_use, spectrum_to_use, bounds_error=False, fill_value=0.0
|
|
111
|
+
)
|
|
112
|
+
summed += interp(energy)
|
|
113
|
+
logger.info(f"CombineSpectra: added spectrum {idx} to sum.")
|
|
114
|
+
|
|
115
|
+
self.outputs.energy = energy
|
|
116
|
+
self.outputs.summed_spectrum = summed
|
|
117
|
+
logger.info("CombineSpectra completed.")
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ewokscore import Task
|
|
5
|
+
|
|
6
|
+
from .utils import poly2d_eval
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _expand_columns(mask: np.ndarray, expand: int) -> np.ndarray:
|
|
12
|
+
"""Binary dilate a column mask by ±expand columns (no extra deps)."""
|
|
13
|
+
if expand <= 0:
|
|
14
|
+
return mask
|
|
15
|
+
out = mask.copy()
|
|
16
|
+
for k in range(1, expand + 1):
|
|
17
|
+
out[:, k:] |= mask[:, :-k]
|
|
18
|
+
out[:, :-k] |= mask[:, k:]
|
|
19
|
+
return out
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _expand_rows(mask: np.ndarray, expand: int) -> np.ndarray:
|
|
23
|
+
"""Binary dilate a row mask by ±expand rows (no extra deps)."""
|
|
24
|
+
if expand <= 0:
|
|
25
|
+
return mask
|
|
26
|
+
out = mask.copy()
|
|
27
|
+
for k in range(1, expand + 1):
|
|
28
|
+
out[k:, :] |= mask[:-k, :]
|
|
29
|
+
out[:-k, :] |= mask[k:, :]
|
|
30
|
+
return out
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ComputeFlatAndMask(
|
|
34
|
+
Task,
|
|
35
|
+
input_names=["image", "coeffs"],
|
|
36
|
+
optional_input_names=[
|
|
37
|
+
# base validity thresholds
|
|
38
|
+
"min_intensity", # discard pixels with raw <= this (default: 0.0)
|
|
39
|
+
"min_fitted", # discard where fitted surface <= this (default: 1e-6)
|
|
40
|
+
# gap detection via residual ratio
|
|
41
|
+
"ratio_threshold", # ratio=image/fitted below this => bad (default: 0.2)
|
|
42
|
+
# fraction of bad pixels in a column to flag gap (default: 0.8)
|
|
43
|
+
"column_fraction_threshold",
|
|
44
|
+
"row_fraction_threshold", # same for rows (default: 0.95)
|
|
45
|
+
"detect_columns", # default True
|
|
46
|
+
"detect_rows", # default False (usually gaps are vertical)
|
|
47
|
+
"gap_expand", # dilate gap bands by this many pixels (default: 1)
|
|
48
|
+
# flat output
|
|
49
|
+
"invert", # output flat = fitted/image if True (default True)
|
|
50
|
+
"clip_flat", # optional clip for flat magnitudes, e.g. (0.25, 4.0)
|
|
51
|
+
],
|
|
52
|
+
output_names=["flat", "mask"],
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Compute flat-field and detect gap bands *automatically* from the 2D fit residuals.
|
|
56
|
+
|
|
57
|
+
Pipeline:
|
|
58
|
+
1) Evaluate fitted surface S = poly2d(x,y; coeffs).
|
|
59
|
+
2) Build a base invalid mask:
|
|
60
|
+
- image <= min_intensity OR ~finite OR S <= min_fitted
|
|
61
|
+
3) Residual ratio R = image / S on valid base pixels; elsewhere treat as 0.
|
|
62
|
+
4) Candidate bad pixels = base_invalid OR (R <= ratio_threshold).
|
|
63
|
+
5) Gap detection (bands):
|
|
64
|
+
- per-column bad fraction; mark columns >= column_fraction_threshold as gaps.
|
|
65
|
+
- optional per-row bad fraction for horizontal bands.
|
|
66
|
+
- optional dilation by 'gap_expand' px.
|
|
67
|
+
6) Final valid mask = NOT(gap_bands) AND NOT(base_invalid)
|
|
68
|
+
7) Flat:
|
|
69
|
+
- if invert: flat = S / image on valid pixels (else flat = image / S)
|
|
70
|
+
- fill invalid pixels with 1.0 (so correction leaves them unchanged;
|
|
71
|
+
mask will zero them later).
|
|
72
|
+
- optional clipping.
|
|
73
|
+
|
|
74
|
+
Outputs:
|
|
75
|
+
- flat (float32, finite)
|
|
76
|
+
- mask (float32, 1 valid / 0 invalid). Gaps are 0 in the mask.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def run(self):
|
|
80
|
+
img = np.asarray(self.inputs.image, dtype=np.float64)
|
|
81
|
+
coeffs = np.asarray(self.inputs.coeffs, dtype=np.float64)
|
|
82
|
+
|
|
83
|
+
# --- Parameters & defaults
|
|
84
|
+
min_intensity = float(self.get_input_value("min_intensity", 0.0))
|
|
85
|
+
min_fitted = float(self.get_input_value("min_fitted", 1e-6))
|
|
86
|
+
|
|
87
|
+
ratio_thr = float(self.get_input_value("ratio_threshold", 0.2))
|
|
88
|
+
col_fr_thr = float(self.get_input_value("column_fraction_threshold", 0.8))
|
|
89
|
+
row_fr_thr = float(self.get_input_value("row_fraction_threshold", 0.95))
|
|
90
|
+
detect_columns = bool(self.get_input_value("detect_columns", True))
|
|
91
|
+
detect_rows = bool(self.get_input_value("detect_rows", False))
|
|
92
|
+
gap_expand = int(self.get_input_value("gap_expand", 1))
|
|
93
|
+
|
|
94
|
+
invert = bool(self.get_input_value("invert", True))
|
|
95
|
+
clip_flat = self.get_input_value("clip_flat", None) # e.g., (0.25, 4.0) or None
|
|
96
|
+
|
|
97
|
+
H, W = img.shape
|
|
98
|
+
y = np.arange(H, dtype=np.float64)
|
|
99
|
+
x = np.arange(W, dtype=np.float64)
|
|
100
|
+
X, Y = np.meshgrid(x, y)
|
|
101
|
+
|
|
102
|
+
# --- Evaluate fitted surface
|
|
103
|
+
fitted = poly2d_eval((X, Y), coeffs)
|
|
104
|
+
|
|
105
|
+
# --- Base invalid mask
|
|
106
|
+
base_invalid = (
|
|
107
|
+
(img <= min_intensity)
|
|
108
|
+
| ~np.isfinite(img)
|
|
109
|
+
| (fitted <= min_fitted)
|
|
110
|
+
| ~np.isfinite(fitted)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# --- Residual ratio
|
|
114
|
+
ratio = np.zeros_like(img, dtype=np.float64)
|
|
115
|
+
good = ~base_invalid
|
|
116
|
+
ratio[good] = img[good] / np.maximum(fitted[good], min_fitted)
|
|
117
|
+
candidate_bad = base_invalid | (ratio <= ratio_thr)
|
|
118
|
+
|
|
119
|
+
# --- Gap detection (bands)
|
|
120
|
+
gap_bands = np.zeros_like(candidate_bad, dtype=bool)
|
|
121
|
+
|
|
122
|
+
if detect_columns:
|
|
123
|
+
col_bad_frac = candidate_bad.mean(axis=0) # per-column fraction bad
|
|
124
|
+
gap_cols = col_bad_frac >= col_fr_thr
|
|
125
|
+
if gap_cols.any():
|
|
126
|
+
gap_bands[:, gap_cols] = True
|
|
127
|
+
logger.info(
|
|
128
|
+
"Detected %d gap columns (thr=%.2f)",
|
|
129
|
+
int(gap_cols.sum()),
|
|
130
|
+
col_fr_thr,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if detect_rows:
|
|
134
|
+
row_bad_frac = candidate_bad.mean(axis=1) # per-row fraction bad
|
|
135
|
+
gap_rows = row_bad_frac >= row_fr_thr
|
|
136
|
+
if gap_rows.any():
|
|
137
|
+
gap_bands[gap_rows, :] = True
|
|
138
|
+
logger.info(
|
|
139
|
+
"Detected %d gap rows (thr=%.2f)",
|
|
140
|
+
int(gap_rows.sum()),
|
|
141
|
+
row_fr_thr,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# --- Expand/dilate gap bands to cover edges
|
|
145
|
+
if detect_columns and gap_expand > 0:
|
|
146
|
+
gap_bands = _expand_columns(gap_bands, gap_expand)
|
|
147
|
+
if detect_rows and gap_expand > 0:
|
|
148
|
+
gap_bands = _expand_rows(gap_bands, gap_expand)
|
|
149
|
+
|
|
150
|
+
# --- Final valid mask
|
|
151
|
+
valid = (~gap_bands) & (~base_invalid)
|
|
152
|
+
|
|
153
|
+
# --- Flat computation
|
|
154
|
+
# invert=True => flat = fitted / image (typical: multiply raw by flat)
|
|
155
|
+
# invert=False => flat = image / fitted
|
|
156
|
+
eps = 1e-12
|
|
157
|
+
flat = np.ones_like(img, dtype=np.float64)
|
|
158
|
+
if invert:
|
|
159
|
+
flat[valid] = fitted[valid] / np.maximum(img[valid], eps)
|
|
160
|
+
else:
|
|
161
|
+
flat[valid] = img[valid] / np.maximum(fitted[valid], eps)
|
|
162
|
+
|
|
163
|
+
if clip_flat is not None:
|
|
164
|
+
lo, hi = clip_flat
|
|
165
|
+
flat = np.clip(flat, lo, hi)
|
|
166
|
+
|
|
167
|
+
# Fill invalids with 1.0 (so downstream (image*flat*mask) → masked zeros)
|
|
168
|
+
flat[~valid] = 1.0
|
|
169
|
+
|
|
170
|
+
self.outputs.flat = flat.astype(np.float32)
|
|
171
|
+
self.outputs.mask = valid.astype(np.float32)
|
|
172
|
+
logger.info(
|
|
173
|
+
"ComputeFlatAndMask: valid=%.2f%%, gaps=%.2f%%",
|
|
174
|
+
100.0 * valid.mean(),
|
|
175
|
+
100.0 * gap_bands.mean(),
|
|
176
|
+
)
|