From 81e8d6264948c27e0cdc43417eb92c9a8f1f37c0 Mon Sep 17 00:00:00 2001 From: Chris Meyer <34664+cmeyer@users.noreply.github.com> Date: Tue, 23 Dec 2025 10:23:04 -0800 Subject: [PATCH 1/3] Use register_processing_descriptions for ThicknessMap. --- .../nion_eels_analysis/ThicknessMap.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/nionswift_plugin/nion_eels_analysis/ThicknessMap.py b/nionswift_plugin/nion_eels_analysis/ThicknessMap.py index 0e495e7..69cae25 100644 --- a/nionswift_plugin/nion_eels_analysis/ThicknessMap.py +++ b/nionswift_plugin/nion_eels_analysis/ThicknessMap.py @@ -6,6 +6,7 @@ # local libraries from nion.data import DataAndMetadata from nion.swift import Facade +from nion.swift.model import DocumentModel from nion.swift.model import Symbolic @@ -51,7 +52,6 @@ def map_thickness_xdata(src_xdata: DataAndMetadata.DataAndMetadata) -> typing.Op class EELSThicknessMapping: - label = _("Thickness Map") def __init__(self, computation: Facade.Computation, **kwargs: typing.Any) -> None: self.computation = computation @@ -81,3 +81,16 @@ def map_thickness(api: Facade.API_1, window: Facade.DocumentWindow) -> None: ComputationCallable = typing.Callable[[Symbolic._APIComputation], Symbolic.ComputationHandlerLike] Symbolic.register_computation_type("eels.thickness_mapping", typing.cast(ComputationCallable, EELSThicknessMapping)) + + +DocumentModel.DocumentModel.register_processing_descriptions({ + "eels.thickness_mapping": { + "title": _("Thickness Map"), + "sources": [ + {"name": "spectrum_image_data_item", "label": _("Spectrum Image"), "data_type": "xdata"} + ], + "outputs": [ + {"name": "map", "label": _("Thickness Map")} + ] + } +}) From 256ec31ba52da8e05be66f9efeb82d382b2fbf02 Mon Sep 17 00:00:00 2001 From: Chris Meyer <34664+cmeyer@users.noreply.github.com> Date: Tue, 23 Dec 2025 10:24:29 -0800 Subject: [PATCH 2/3] Introduce single pixel align ZLP. --- .../nion_eels_analysis/AlignZLP.py | 84 ++++++++++++++++++- .../nion_eels_analysis/__init__.py | 2 + .../nion_eels_analysis/test/AlignZLP_test.py | 42 +++++++++- 3 files changed, 122 insertions(+), 6 deletions(-) diff --git a/nionswift_plugin/nion_eels_analysis/AlignZLP.py b/nionswift_plugin/nion_eels_analysis/AlignZLP.py index 72e8ac8..3f1d4b3 100755 --- a/nionswift_plugin/nion_eels_analysis/AlignZLP.py +++ b/nionswift_plugin/nion_eels_analysis/AlignZLP.py @@ -1,15 +1,21 @@ # imports -import logging +import contextlib import copy -import numpy +import gettext +import logging +import math import typing -import contextlib + +# third party libraries +import numpy import scipy.ndimage # local libraries from nion.data import DataAndMetadata from nion.eels_analysis import ZLP_Analysis from nion.swift import Facade +from nion.swift.model import DocumentModel +from nion.swift.model import Symbolic from nion.typeshed import API_1_0 as API from nion.ui import Declarative from nion.ui import Dialog @@ -17,6 +23,10 @@ from nion.utils import Converter from nion.utils import Event + +_ = gettext.gettext + + DataArrayType = numpy.typing.NDArray[typing.Any] @@ -337,3 +347,71 @@ def wc(w: Window.Window) -> None: def calibrate_spectrum(api: Facade.API_1, window: Facade.DocumentWindow) -> None: _calibrate_spectrum(api, window) + + +class AlignZLPComputation(Symbolic.ComputationHandlerLike): + + def __init__(self, computation: Facade.Computation, **kwargs: typing.Any) -> None: + self.computation = computation + self.__aligned_eels_data: typing.Optional[DataAndMetadata.DataAndMetadata] = None + + def execute(self, **kwargs: typing.Any) -> None: + eels_data_item = typing.cast(Facade.DataSource, kwargs["eels_data_item"]) + target_index = typing.cast(int, kwargs.get("target_index", 50)) + eels_data_xdata = eels_data_item.xdata + assert eels_data_xdata + mx_pos = ZLP_Analysis.estimate_zlp_amplitude_position_width_com(eels_data_xdata.data)[1] or 0.0 + # fallback to simple max + if not math.isfinite(mx_pos): + mx_pos = float(numpy.argmax(eels_data_xdata.data)) + # determine the offset and apply it + interpolation_order = 1 + offset = mx_pos - target_index + aligned_eels_data = scipy.ndimage.shift(eels_data_xdata.data, -offset, order=interpolation_order) + + eels_data_calibration = eels_data_xdata.dimensional_calibrations[-1] + eels_data_calibration.offset = -(target_index + 0.5) * eels_data_calibration.scale + + offset_calibration = copy.copy(eels_data_calibration) + offset_calibration.offset = 0 + + self.__aligned_eels_data = DataAndMetadata.new_data_and_metadata(aligned_eels_data, eels_data_xdata.intensity_calibration, (eels_data_calibration, )) + + def commit(self) -> None: + assert self.__aligned_eels_data + self.computation.set_referenced_xdata("aligned_eels_data", self.__aligned_eels_data) + + +def apply_align_zlp(api: Facade.API_1, window: Facade.DocumentWindow) -> None: + target_display = window.target_display + target_data_item_ = target_display._display_item.data_items[0] if target_display and len(target_display._display_item.data_items) > 0 else None + if target_data_item_ and target_display: + eels_data_item = Facade.DataItem(target_data_item_) + if eels_data_item: + assert eels_data_item.display_xdata + if not eels_data_item.display_xdata.is_data_1d: + logging.error("Failed: Data is not a sequence or collection of 1D spectra.") + return + aligned_eels_data_item = api.library.create_data_item_from_data(numpy.zeros_like(eels_data_item.display_xdata.data)) + api.library.create_computation("eels.align_zlp.a0", inputs={"eels_data_item": eels_data_item, "target_index": 50}, outputs={"aligned_eels_data": aligned_eels_data_item}) + window.display_data_item(aligned_eels_data_item) + + +ComputationCallable = typing.Callable[[Symbolic._APIComputation], Symbolic.ComputationHandlerLike] +Symbolic.register_computation_type("eels.align_zlp.a0", typing.cast(ComputationCallable, AlignZLPComputation)) + + +DocumentModel.DocumentModel.register_processing_descriptions({ + "eels.align_zlp.a0": { + "title": _("Align ZLP"), + "sources": [ + {"name": "eels_data_item", "label": _("EELS Data"), "data_type": "xdata", "requirements": [{"type": "datum_rank", "values": (1,)}]} + ], + "parameters": [ + {"name": "target_index", "label": _("Target Index"), "type": "integral", "value": 50, "value_default": 50, "value_min": 0} + ], + "outputs": [ + {"name": "aligned_eels_data", "label": _("Aligned EELS Data")} + ] + } +}) diff --git a/nionswift_plugin/nion_eels_analysis/__init__.py b/nionswift_plugin/nion_eels_analysis/__init__.py index 7cddb39..928d0b1 100755 --- a/nionswift_plugin/nion_eels_analysis/__init__.py +++ b/nionswift_plugin/nion_eels_analysis/__init__.py @@ -71,6 +71,8 @@ def __build_menus(self, document_window: DocumentController.DocumentController) eels_menu.add_menu_item(_("Align ZLP (com method)"), functools.partial(AlignZLP.align_zlp_com, api, window)) eels_menu.add_menu_item(_("Align ZLP (peak fit method)"), functools.partial(AlignZLP.align_zlp_fit, api, window)) eels_menu.add_separator() + eels_menu.add_menu_item(_("Align ZLP"), functools.partial(AlignZLP.apply_align_zlp, api, window)) + eels_menu.add_separator() eels_menu.add_menu_item(_("Show Live Thickness Measurement"), functools.partial(LiveThickness.attach_measure_thickness, api, window)) eels_menu.add_menu_item(_("Show Live ZLP Measurement"), functools.partial(LiveZLP.attach_measure_zlp, api, window)) eels_menu.add_separator() diff --git a/nionswift_plugin/nion_eels_analysis/test/AlignZLP_test.py b/nionswift_plugin/nion_eels_analysis/test/AlignZLP_test.py index fd4b712..0b985d6 100755 --- a/nionswift_plugin/nion_eels_analysis/test/AlignZLP_test.py +++ b/nionswift_plugin/nion_eels_analysis/test/AlignZLP_test.py @@ -1,14 +1,14 @@ -import numpy import typing import unittest +import numpy +import scipy + from nion.data import Calibration from nion.data import DataAndMetadata -from nion.swift import Application from nion.swift import Facade from nion.swift.model import DataItem from nion.swift.test import TestContext -from nion.ui import TestUI from .. import AlignZLP @@ -16,6 +16,22 @@ Facade.initialize() +def create_memory_profile_context() -> TestContext.MemoryProfileContext: + return TestContext.MemoryProfileContext() + + +def generate_peak_data(*, range_ev: float = 100.0, length: int = 1000, add_noise: bool = False, is_biased: bool = False) -> DataAndMetadata.DataAndMetadata: + x_axis = numpy.arange(-range_ev / 10, range_ev, range_ev / length) + if is_biased: + x_axis[length // 10:] = numpy.arange(0, range_ev / 2.5, range_ev / 2.5 / length) + data = 1e6 * scipy.stats.norm.pdf(x_axis, 0, 1) + if add_noise: + data += numpy.abs(numpy.random.normal(0, 5, data.shape)) + intensity_calibration = Calibration.Calibration(units="counts") + dimensional_calibrations = [Calibration.Calibration(scale=range_ev / length, offset=-range_ev / 10, units="eV")] + return DataAndMetadata.new_data_and_metadata(data, intensity_calibration=intensity_calibration, dimensional_calibrations=dimensional_calibrations) + + class TestBackgroundSubtraction(unittest.TestCase): def setUp(self) -> None: @@ -67,3 +83,23 @@ def test_calibrate_spectrum_for_single_spectrum(self) -> None: self.assertEqual(0, len(document_model.data_items)) self.assertEqual(0, len(document_model.display_items)) self.assertEqual(0, len(document_model.data_structures)) + + def test_align_zlp_computation(self) -> None: + with create_memory_profile_context() as test_context: + document_controller = test_context.create_document_controller_with_application() + document_model = document_controller.document_model + peak_xdata = generate_peak_data() + eels_data_item = DataItem.new_data_item(peak_xdata) + document_model.append_data_item(eels_data_item) + display_panel = document_controller.selected_display_panel + display_item = document_model.get_display_item_for_data_item(eels_data_item) + display_panel.set_display_panel_display_item(display_item) + api = Facade.get_api("~1.0", "~1.0") + AlignZLP.apply_align_zlp(api, Facade.DocumentWindow(document_controller)) + document_model.recompute_all() + document_controller.periodic() + self.assertFalse(any(computation.error_text for computation in document_model.computations)) + self.assertEqual(2, len(document_model.data_items)) + self.assertIn("(Align ZLP)", document_model.data_items[1].title) + self.assertAlmostEqual(50.0, numpy.argmax(document_model.data_items[1].xdata)) + self.assertAlmostEqual(0.0, document_model.data_items[1].dimensional_calibrations[-1].convert_to_calibrated_value(50.5)) From 627050f5db0b032ffc4a19d30b6231f510aa3668 Mon Sep 17 00:00:00 2001 From: Chris Meyer <34664+cmeyer@users.noreply.github.com> Date: Sun, 28 Dec 2025 10:31:35 -0800 Subject: [PATCH 3/3] WIP: Align ZLP processor. --- .../nion_eels_analysis/AlignZLP.py | 148 ++++++++++++------ .../nion_eels_analysis/__init__.py | 2 +- 2 files changed, 101 insertions(+), 49 deletions(-) diff --git a/nionswift_plugin/nion_eels_analysis/AlignZLP.py b/nionswift_plugin/nion_eels_analysis/AlignZLP.py index 3f1d4b3..2c73e7a 100755 --- a/nionswift_plugin/nion_eels_analysis/AlignZLP.py +++ b/nionswift_plugin/nion_eels_analysis/AlignZLP.py @@ -14,7 +14,6 @@ from nion.data import DataAndMetadata from nion.eels_analysis import ZLP_Analysis from nion.swift import Facade -from nion.swift.model import DocumentModel from nion.swift.model import Symbolic from nion.typeshed import API_1_0 as API from nion.ui import Declarative @@ -22,6 +21,7 @@ from nion.ui import Window from nion.utils import Converter from nion.utils import Event +from nion.utils import Registry _ = gettext.gettext @@ -349,17 +349,32 @@ def calibrate_spectrum(api: Facade.API_1, window: Facade.DocumentWindow) -> None _calibrate_spectrum(api, window) -class AlignZLPComputation(Symbolic.ComputationHandlerLike): +from nion.swift.model import Processing - def __init__(self, computation: Facade.Computation, **kwargs: typing.Any) -> None: - self.computation = computation - self.__aligned_eels_data: typing.Optional[DataAndMetadata.DataAndMetadata] = None - def execute(self, **kwargs: typing.Any) -> None: - eels_data_item = typing.cast(Facade.DataSource, kwargs["eels_data_item"]) +class ProcessingAlignZLP(Processing.ProcessingBase): + def __init__(self, **kwargs: typing.Any) -> None: + super().__init__() + self.processing_id = "eels.align_zlp.a0" + self.title = _("Align ZLP") + self.sections = {"eels"} + self.sources = [ + {"name": "src", "label": _("EELS Data"), "data_type": "xdata", "requirements": [{"type": "datum_rank", "values": (1,)}]} + ] + self.parameters = [ + {"name": "target_index", "label": _("Target Index"), "type": "integral", "value": 50, "value_default": 50, "value_min": 0} + ] + self.outputs = [ + {"name": "target", "label": _("Aligned EELS Data")}, + {"name": "offset", "label": _("Applied Offset")} + ] + self.is_mappable = True + + def process(self, data_sources: typing.Mapping[str, DataAndMetadata.DataAndMetadata], **kwargs: typing.Any) -> typing.Mapping[str, Processing._ProcessingResult]: + eels_data_xdata = data_sources.get("src", None) target_index = typing.cast(int, kwargs.get("target_index", 50)) - eels_data_xdata = eels_data_item.xdata assert eels_data_xdata + mx_pos = ZLP_Analysis.estimate_zlp_amplitude_position_width_com(eels_data_xdata.data)[1] or 0.0 # fallback to simple max if not math.isfinite(mx_pos): @@ -375,43 +390,80 @@ def execute(self, **kwargs: typing.Any) -> None: offset_calibration = copy.copy(eels_data_calibration) offset_calibration.offset = 0 - self.__aligned_eels_data = DataAndMetadata.new_data_and_metadata(aligned_eels_data, eels_data_xdata.intensity_calibration, (eels_data_calibration, )) - - def commit(self) -> None: - assert self.__aligned_eels_data - self.computation.set_referenced_xdata("aligned_eels_data", self.__aligned_eels_data) - - -def apply_align_zlp(api: Facade.API_1, window: Facade.DocumentWindow) -> None: - target_display = window.target_display - target_data_item_ = target_display._display_item.data_items[0] if target_display and len(target_display._display_item.data_items) > 0 else None - if target_data_item_ and target_display: - eels_data_item = Facade.DataItem(target_data_item_) - if eels_data_item: - assert eels_data_item.display_xdata - if not eels_data_item.display_xdata.is_data_1d: - logging.error("Failed: Data is not a sequence or collection of 1D spectra.") - return - aligned_eels_data_item = api.library.create_data_item_from_data(numpy.zeros_like(eels_data_item.display_xdata.data)) - api.library.create_computation("eels.align_zlp.a0", inputs={"eels_data_item": eels_data_item, "target_index": 50}, outputs={"aligned_eels_data": aligned_eels_data_item}) - window.display_data_item(aligned_eels_data_item) - - -ComputationCallable = typing.Callable[[Symbolic._APIComputation], Symbolic.ComputationHandlerLike] -Symbolic.register_computation_type("eels.align_zlp.a0", typing.cast(ComputationCallable, AlignZLPComputation)) - - -DocumentModel.DocumentModel.register_processing_descriptions({ - "eels.align_zlp.a0": { - "title": _("Align ZLP"), - "sources": [ - {"name": "eels_data_item", "label": _("EELS Data"), "data_type": "xdata", "requirements": [{"type": "datum_rank", "values": (1,)}]} - ], - "parameters": [ - {"name": "target_index", "label": _("Target Index"), "type": "integral", "value": 50, "value_default": 50, "value_min": 0} - ], - "outputs": [ - {"name": "aligned_eels_data", "label": _("Aligned EELS Data")} - ] - } -}) + aligned_eels_xdata = DataAndMetadata.new_data_and_metadata(aligned_eels_data, eels_data_xdata.intensity_calibration, (eels_data_calibration, )) + + return { + "target": aligned_eels_xdata, + "offset": DataAndMetadata.ScalarAndMetadata(lambda: offset, offset_calibration) + } + + +Registry.register_component(ProcessingAlignZLP(), {"processing-component"}) + + +# class AlignZLPComputation(Symbolic.ComputationHandlerLike): +# +# def __init__(self, computation: Facade.Computation, **kwargs: typing.Any) -> None: +# self.computation = computation +# self.__aligned_eels_data: typing.Optional[DataAndMetadata.DataAndMetadata] = None +# +# def execute(self, **kwargs: typing.Any) -> None: +# eels_data_item = typing.cast(Facade.DataSource, kwargs["eels_data_item"]) +# target_index = typing.cast(int, kwargs.get("target_index", 50)) +# eels_data_xdata = eels_data_item.xdata +# assert eels_data_xdata +# mx_pos = ZLP_Analysis.estimate_zlp_amplitude_position_width_com(eels_data_xdata.data)[1] or 0.0 +# # fallback to simple max +# if not math.isfinite(mx_pos): +# mx_pos = float(numpy.argmax(eels_data_xdata.data)) +# # determine the offset and apply it +# interpolation_order = 1 +# offset = mx_pos - target_index +# aligned_eels_data = scipy.ndimage.shift(eels_data_xdata.data, -offset, order=interpolation_order) +# +# eels_data_calibration = eels_data_xdata.dimensional_calibrations[-1] +# eels_data_calibration.offset = -(target_index + 0.5) * eels_data_calibration.scale +# +# offset_calibration = copy.copy(eels_data_calibration) +# offset_calibration.offset = 0 +# +# self.__aligned_eels_data = DataAndMetadata.new_data_and_metadata(aligned_eels_data, eels_data_xdata.intensity_calibration, (eels_data_calibration, )) +# +# def commit(self) -> None: +# assert self.__aligned_eels_data +# self.computation.set_referenced_xdata("aligned_eels_data", self.__aligned_eels_data) + + +# def apply_align_zlp(api: Facade.API_1, window: Facade.DocumentWindow) -> None: +# target_display = window.target_display +# target_data_item_ = target_display._display_item.data_items[0] if target_display and len(target_display._display_item.data_items) > 0 else None +# if target_data_item_ and target_display: +# eels_data_item = Facade.DataItem(target_data_item_) +# if eels_data_item: +# assert eels_data_item.display_xdata +# if not eels_data_item.display_xdata.is_data_1d: +# logging.error("Failed: Data is not a sequence or collection of 1D spectra.") +# return +# aligned_eels_data_item = api.library.create_data_item_from_data(numpy.zeros_like(eels_data_item.display_xdata.data)) +# api.library.create_computation("eels.align_zlp.a0", inputs={"src": eels_data_item, "target_index": 50}, outputs={"target": aligned_eels_data_item}) +# window.display_data_item(aligned_eels_data_item) + + +# ComputationCallable = typing.Callable[[Symbolic._APIComputation], Symbolic.ComputationHandlerLike] +# Symbolic.register_computation_type("eels.align_zlp.a0", typing.cast(ComputationCallable, AlignZLPComputation)) + + +# DocumentModel.DocumentModel.register_processing_descriptions({ +# "eels.align_zlp.a0": { +# "title": _("Align ZLP"), +# "sources": [ +# {"name": "eels_data_item", "label": _("EELS Data"), "data_type": "xdata", "requirements": [{"type": "datum_rank", "values": (1,)}]} +# ], +# "parameters": [ +# {"name": "target_index", "label": _("Target Index"), "type": "integral", "value": 50, "value_default": 50, "value_min": 0} +# ], +# "outputs": [ +# {"name": "aligned_eels_data", "label": _("Aligned EELS Data")} +# ] +# } +# }) diff --git a/nionswift_plugin/nion_eels_analysis/__init__.py b/nionswift_plugin/nion_eels_analysis/__init__.py index 928d0b1..a6c8962 100755 --- a/nionswift_plugin/nion_eels_analysis/__init__.py +++ b/nionswift_plugin/nion_eels_analysis/__init__.py @@ -71,7 +71,7 @@ def __build_menus(self, document_window: DocumentController.DocumentController) eels_menu.add_menu_item(_("Align ZLP (com method)"), functools.partial(AlignZLP.align_zlp_com, api, window)) eels_menu.add_menu_item(_("Align ZLP (peak fit method)"), functools.partial(AlignZLP.align_zlp_fit, api, window)) eels_menu.add_separator() - eels_menu.add_menu_item(_("Align ZLP"), functools.partial(AlignZLP.apply_align_zlp, api, window)) + eels_menu.add_menu_item(_("Align ZLP"), lambda: document_window.perform_action("processing.eels.align_zlp.a0")) eels_menu.add_separator() eels_menu.add_menu_item(_("Show Live Thickness Measurement"), functools.partial(LiveThickness.attach_measure_thickness, api, window)) eels_menu.add_menu_item(_("Show Live ZLP Measurement"), functools.partial(LiveZLP.attach_measure_zlp, api, window))