From 206ad5b41fd17ddd8540cdb77f8911cfb23fd250 Mon Sep 17 00:00:00 2001
From: payno <henri.payno@gmail.com>
Date: Fri, 13 Dec 2024 14:58:27 +0100
Subject: [PATCH] NXtomoeditor: add handling of source / sample distance

Rename some 'distance' to smaple- detector distance
---
 src/tomwer/gui/edit/nxtomoeditor.py           | 82 ++++++++++++++-----
 src/tomwer/gui/edit/tests/test_nx_editor.py   | 35 ++++++--
 .../test/test_nx_tomo_metadata_viewer.py      |  4 +-
 src/tomwer/tasks/edit/nxtomoeditor.py         | 15 ++++
 .../widgets/edit/tests/test_nxtomo_editor.py  |  5 +-
 5 files changed, 110 insertions(+), 31 deletions(-)

diff --git a/src/tomwer/gui/edit/nxtomoeditor.py b/src/tomwer/gui/edit/nxtomoeditor.py
index dc1a338009..d5e4fce156 100644
--- a/src/tomwer/gui/edit/nxtomoeditor.py
+++ b/src/tomwer/gui/edit/nxtomoeditor.py
@@ -114,8 +114,26 @@ class NXtomoEditor(qt.QWidget):
         self._energyLockerLB.setMaximumSize(30, 30)
         self._lockerPBs.append(self._energyLockerLB)
         self._tree.setItemWidget(self._energyQTWI, 2, self._energyLockerLB)
+        # 1.1 source
+        self._sourceQTWI = qt.QTreeWidgetItem(self._instrumentQTWI)
+        self._sourceQTWI.setText(0, "source")
+        self._sourceSampleDistanceQTWI = qt.QTreeWidgetItem(self._sourceQTWI)
+        self._sourceSampleDistanceQTWI.setText(0, "distance")
+
+        self._sourceSampleDistanceMetricEntry = MetricEntry("", parent=self)
+        self._sourceSampleDistanceMetricEntry.layout().setContentsMargins(2, 2, 2, 2)
+        self._tree.setItemWidget(
+            self._sourceSampleDistanceQTWI, 1, self._sourceSampleDistanceMetricEntry
+        )
+        self._editableWidgets.append(self._sourceSampleDistanceMetricEntry)
+        self._sourceSampleDistanceLB = PadlockButton(self)
+        self._sourceSampleDistanceLB.setMaximumSize(30, 30)
+        self._lockerPBs.append(self._sourceSampleDistanceLB)
+        self._tree.setItemWidget(
+            self._sourceSampleDistanceQTWI, 2, self._sourceSampleDistanceLB
+        )
 
-        # 1.1 detector
+        # 1.2 detector
         self._detectorQTWI = qt.QTreeWidgetItem(self._instrumentQTWI)
         self._detectorQTWI.setText(0, "detector")
         ## pixel size
@@ -141,19 +159,21 @@ class NXtomoEditor(qt.QWidget):
         self._lockerPBs.append(self._yPixelSizeLB)
         self._tree.setItemWidget(self._yPixelSizeQTWI, 2, self._yPixelSizeLB)
 
-        ## distance
+        ## sample - detector distance
         self._sampleDetectorDistanceQTWI = qt.QTreeWidgetItem(self._detectorQTWI)
         self._sampleDetectorDistanceQTWI.setText(0, "distance")
-        self._distanceMetricEntry = MetricEntry("", parent=self)
-        self._distanceMetricEntry.layout().setContentsMargins(2, 2, 2, 2)
+        self._sampleDetectorDistanceMetricEntry = MetricEntry("", parent=self)
+        self._sampleDetectorDistanceMetricEntry.layout().setContentsMargins(2, 2, 2, 2)
+        self._tree.setItemWidget(
+            self._sampleDetectorDistanceQTWI, 1, self._sampleDetectorDistanceMetricEntry
+        )
+        self._editableWidgets.append(self._sampleDetectorDistanceMetricEntry)
+        self._sampleDetectorDistanceLB = PadlockButton(self)
+        self._sampleDetectorDistanceLB.setMaximumSize(30, 30)
+        self._lockerPBs.append(self._sampleDetectorDistanceLB)
         self._tree.setItemWidget(
-            self._sampleDetectorDistanceQTWI, 1, self._distanceMetricEntry
+            self._sampleDetectorDistanceQTWI, 2, self._sampleDetectorDistanceLB
         )
-        self._editableWidgets.append(self._distanceMetricEntry)
-        self._distanceLB = PadlockButton(self)
-        self._distanceLB.setMaximumSize(30, 30)
-        self._lockerPBs.append(self._distanceLB)
-        self._tree.setItemWidget(self._sampleDetectorDistanceQTWI, 2, self._distanceLB)
 
         ## field of view
         self._fieldOfViewQTWI = qt.QTreeWidgetItem(self._detectorQTWI)
@@ -210,6 +230,7 @@ class NXtomoEditor(qt.QWidget):
         self._sampleQTWI.setExpanded(True)
         self._beamQTWI.setExpanded(True)
         self._detectorQTWI.setExpanded(True)
+        self._sourceQTWI.setExpanded(True)
         self.hideLockers(hide_lockers)
 
         # connect signal / slot
@@ -219,8 +240,14 @@ class NXtomoEditor(qt.QWidget):
         self._xPixelSizeLB.toggled.connect(self._editingFinished)
         self._yPixelSizeMetricEntry.editingFinished.connect(self._editingFinished)
         self._yPixelSizeLB.toggled.connect(self._editingFinished)
-        self._distanceMetricEntry.editingFinished.connect(self._editingFinished)
-        self._distanceLB.toggled.connect(self._editingFinished)
+        self._sampleDetectorDistanceMetricEntry.editingFinished.connect(
+            self._editingFinished
+        )
+        self._sampleDetectorDistanceLB.toggled.connect(self._editingFinished)
+        self._sourceSampleDistanceMetricEntry.editingFinished.connect(
+            self._editingFinished
+        )
+        self._sourceSampleDistanceLB.toggled.connect(self._editingFinished)
         self._fieldOfViewCB.currentIndexChanged.connect(self._editingFinished)
         self._fieldOfViewLB.toggled.connect(self._editingFinished)
         self._xFlippedCB.toggled.connect(self._editingFinished)
@@ -244,7 +271,8 @@ class NXtomoEditor(qt.QWidget):
                 "pixel size": self._updatePixelSize,
                 "frame flips": self._updateFlipped,
                 "field of view": self._updateFieldOfView,
-                "sample-detector distance": self._updateDistance,
+                "sample-detector distance": self._updateSampleDetectorDistance,
+                "source-sample distance": self._updateSourceSampleDistance,
             }.items():
                 try:
                     fct(scan=scan)
@@ -325,10 +353,16 @@ class NXtomoEditor(qt.QWidget):
         if (not self._yFlippedLB.isLocked()) and flip_ud is not None:
             self._yFlippedCB.setChecked(flip_ud)
 
-    def _updateDistance(self, scan: NXtomoScan) -> None:
-        if not self._distanceLB.isLocked():
+    def _updateSampleDetectorDistance(self, scan: NXtomoScan) -> None:
+        if not self._sampleDetectorDistanceLB.isLocked():
             # if in ''auto mode: we want to overwrite the NXtomo existing value by the one of the GUI
-            self._distanceMetricEntry.setValue(scan.sample_detector_distance)
+            self._sampleDetectorDistanceMetricEntry.setValue(
+                scan.sample_detector_distance
+            )
+
+    def _updateSourceSampleDistance(self, scan: NXtomoScan) -> None:
+        if not self._sourceSampleDistanceLB.isLocked():
+            self._sourceSampleDistanceMetricEntry.setValue(scan.source_sample_distance)
 
     def _updateEnergy(self, scan: NXtomoScan) -> None:
         assert isinstance(scan, NXtomoScan)
@@ -400,8 +434,12 @@ class NXtomoEditor(qt.QWidget):
                 self._yPixelSizeLB.isLocked(),
             ),
             NXtomoEditorKeys.SAMPLE_DETECTOR_DISTANCE: (
-                self._distanceMetricEntry.getValue(),
-                self._distanceLB.isLocked(),
+                self._sampleDetectorDistanceMetricEntry.getValue(),
+                self._sampleDetectorDistanceLB.isLocked(),
+            ),
+            NXtomoEditorKeys.SOURCE_SAMPLE_DISTANCE: (
+                self._sourceSampleDistanceMetricEntry.getValue(),
+                self._sourceSampleDistanceLB.isLocked(),
             ),
             NXtomoEditorKeys.FIELD_OF_VIEW: (
                 self._fieldOfViewCB.currentText(),
@@ -441,9 +479,15 @@ class NXtomoEditor(qt.QWidget):
         detector_sample_distance = config.get("instrument.detector.distance", None)
         if detector_sample_distance is not None:
             detector_sample_distance, distance_locked = detector_sample_distance
-            self._distanceMetricEntry.setValue(detector_sample_distance)
+            self._sampleDetectorDistanceMetricEntry.setValue(detector_sample_distance)
             self._sampleDetectorDistanceLB.setLock(distance_locked)
 
+        source_sample_distance = config.get("instrument.source.distance", None)
+        if source_sample_distance is not None:
+            source_sample_distance, distance_locked = source_sample_distance
+            self._sourceSampleDistanceMetricEntry.setValue(source_sample_distance)
+            self._sourceSampleDistanceLB.setLock(distance_locked)
+
         field_of_view = config.get("instrument.detector.field_of_view", None)
         if field_of_view is not None:
             field_of_view, field_of_view_locked = field_of_view
diff --git a/src/tomwer/gui/edit/tests/test_nx_editor.py b/src/tomwer/gui/edit/tests/test_nx_editor.py
index ade4095c4c..71688de70c 100644
--- a/src/tomwer/gui/edit/tests/test_nx_editor.py
+++ b/src/tomwer/gui/edit/tests/test_nx_editor.py
@@ -26,7 +26,8 @@ from tomwer.tests.conftest import qtapp  # noqa F401
 @pytest.mark.parametrize("x_pixel_size", (None, 0.12))
 @pytest.mark.parametrize("y_pixel_size", (None, 0.0065))
 @pytest.mark.parametrize("field_of_view", FOV.values())
-@pytest.mark.parametrize("distance", (None, 1.2))
+@pytest.mark.parametrize("sample_detector_distance", (None, 1.2))
+@pytest.mark.parametrize("source_sample_distance", (None, 30.2))
 @pytest.mark.parametrize("energy", (None, 23.5))
 @pytest.mark.parametrize("x_flipped", (True, False))
 @pytest.mark.parametrize("y_flipped", (True, False))
@@ -38,7 +39,8 @@ def test_nx_editor(
     x_pixel_size,
     y_pixel_size,
     field_of_view,
-    distance,
+    sample_detector_distance,
+    source_sample_distance,
     energy,
     x_flipped,
     y_flipped,
@@ -50,12 +52,13 @@ def test_nx_editor(
     nx_tomo.instrument.detector.x_pixel_size = x_pixel_size
     nx_tomo.instrument.detector.y_pixel_size = y_pixel_size
     nx_tomo.instrument.detector.field_of_view = field_of_view
-    nx_tomo.instrument.detector.distance = distance
+    nx_tomo.instrument.detector.distance = sample_detector_distance
     nx_tomo.energy = energy
     nx_tomo.sample.x_translation = x_translation
     nx_tomo.sample.z_translation = z_translation
     nx_tomo.instrument.detector.image_key_control = [ImageKey.PROJECTION.value] * 12
     nx_tomo.instrument.detector.data = numpy.empty(shape=(12, 10, 10))
+    nx_tomo.instrument.source.distance = source_sample_distance
     nx_tomo.sample.rotation_angle = numpy.linspace(0, 20, num=12)
 
     nx_tomo.instrument.detector.transformations.add_transformation(
@@ -89,8 +92,14 @@ def test_nx_editor(
     assert check_metric(y_pixel_size, widget._yPixelSizeMetricEntry.getValue())
     assert widget._yPixelSizeMetricEntry._qcbUnit.currentText() == "m"
 
-    assert check_metric(distance, widget._distanceMetricEntry.getValue())
-    assert widget._distanceMetricEntry._qcbUnit.currentText() == "m"
+    assert check_metric(
+        sample_detector_distance, widget._sampleDetectorDistanceMetricEntry.getValue()
+    )
+    assert widget._sampleDetectorDistanceMetricEntry._qcbUnit.currentText() == "m"
+    assert check_metric(
+        source_sample_distance, widget._sourceSampleDistanceMetricEntry.getValue()
+    )
+    assert widget._sourceSampleDistanceMetricEntry._qcbUnit.currentText() == "m"
 
     assert field_of_view == widget._fieldOfViewCB.currentText()
     assert x_flipped == widget._xFlippedCB.isChecked()
@@ -120,7 +129,8 @@ def test_nx_editor(
     widget._energyEntry.setText("23.789")
     widget._xPixelSizeMetricEntry.setUnit("nm")
     widget._yPixelSizeMetricEntry.setValue(2.1e-7)
-    widget._distanceMetricEntry.setValue("unknown")
+    widget._sampleDetectorDistanceMetricEntry.setValue("unknown")
+    widget._sourceSampleDistanceMetricEntry.setValue("unknown")
     widget._fieldOfViewCB.setCurrentText(FOV.HALF.value)
     widget._xFlippedCB.setChecked(not x_flipped)
     widget._xTranslationQLE.setValue(1.8)
@@ -204,6 +214,7 @@ def test_nx_editor_lock(
     nx_tomo_1.instrument.detector.image_key_control = [ImageKey.PROJECTION.value] * 12
     nx_tomo_1.instrument.detector.data = numpy.empty(shape=(12, 10, 10))
     nx_tomo_1.sample.rotation_angle = numpy.linspace(0, 20, num=12)
+    nx_tomo_1.instrument.source.distance = 1.1
 
     file_path = os.path.join(tmp_path, "nxtomo.nx")
     entry = "entry0000"
@@ -229,6 +240,7 @@ def test_nx_editor_lock(
     nx_tomo_2.instrument.detector.image_key_control = [ImageKey.PROJECTION.value] * 12
     nx_tomo_2.instrument.detector.data = numpy.empty(shape=(12, 10, 10))
     nx_tomo_2.sample.rotation_angle = numpy.linspace(0, 20, num=12)
+    nx_tomo_2.instrument.source.distance = 6.02
 
     file_path = os.path.join(tmp_path, "nxtomo.nx")
     entry = "entry0001"
@@ -251,7 +263,8 @@ def test_nx_editor_lock(
     assert widget._energyEntry.getValue() == 5.9
     assert widget._xPixelSizeMetricEntry.getValue() == 0.023
     assert widget._yPixelSizeMetricEntry.getValue() == 0.025
-    assert widget._distanceMetricEntry.getValue() == 2.4
+    assert widget._sampleDetectorDistanceMetricEntry.getValue() == 2.4
+    assert widget._sourceSampleDistanceMetricEntry.getValue() == 1.1
     assert widget._fieldOfViewCB.currentText() == "Full"
     assert not widget._xFlippedCB.isChecked()
     assert widget._yFlippedCB.isChecked()
@@ -286,6 +299,10 @@ def test_nx_editor_lock(
         overwrite_nx_tomo.instrument.detector.distance.value
         == nx_tomo_1.instrument.detector.distance.value
     )
+    assert (
+        overwrite_nx_tomo.instrument.source.distance.value
+        == nx_tomo_1.instrument.source.distance.value
+    )
     assert (
         overwrite_nx_tomo.instrument.detector.x_flipped
         == nx_tomo_1.instrument.detector.x_flipped
@@ -306,6 +323,7 @@ def test_nx_editor_lock(
         "instrument.detector.y_flipped": (True, True),
         "sample.x_translation": (None,),
         "sample.z_translation": (None,),
+        "instrument.source.distance": (1.1, True),
     }
 
     for lockerButton in widget._lockerPBs:
@@ -321,6 +339,7 @@ def test_nx_editor_lock(
         "instrument.detector.y_flipped": (True, False),
         "sample.x_translation": (None,),
         "sample.z_translation": (None,),
+        "instrument.source.distance": (1.1, False),
     }
 
 
@@ -361,7 +380,7 @@ def test_nxtomo_editor_with_missing_paths(
 
     widget.setScan(scan=scan)
 
-    widget._distanceMetricEntry.setValue(0.05)
+    widget._sampleDetectorDistanceMetricEntry.setValue(0.05)
     widget._energyEntry.setValue(50)
     widget._xPixelSizeMetricEntry.setValue(0.02)
     widget._yPixelSizeMetricEntry.setValue(0.03)
diff --git a/src/tomwer/gui/visualization/test/test_nx_tomo_metadata_viewer.py b/src/tomwer/gui/visualization/test/test_nx_tomo_metadata_viewer.py
index c03a4ec35f..998ef0e68d 100644
--- a/src/tomwer/gui/visualization/test/test_nx_tomo_metadata_viewer.py
+++ b/src/tomwer/gui/visualization/test/test_nx_tomo_metadata_viewer.py
@@ -54,8 +54,8 @@ def test_nx_editor(
     assert check_metric(2.5e-6, widget._yPixelSizeMetricEntry.getValue())
     assert widget._yPixelSizeMetricEntry._qcbUnit.currentText() == "m"
 
-    assert check_metric(59, widget._distanceMetricEntry.getValue())
-    assert widget._distanceMetricEntry._qcbUnit.currentText() == "m"
+    assert check_metric(59, widget._sampleDetectorDistanceMetricEntry.getValue())
+    assert widget._sampleDetectorDistanceMetricEntry._qcbUnit.currentText() == "m"
 
     assert "Half" == widget._fieldOfViewCB.currentText()
     assert widget._xFlippedCB.isChecked()
diff --git a/src/tomwer/tasks/edit/nxtomoeditor.py b/src/tomwer/tasks/edit/nxtomoeditor.py
index bd4c30aaae..b99489fe98 100644
--- a/src/tomwer/tasks/edit/nxtomoeditor.py
+++ b/src/tomwer/tasks/edit/nxtomoeditor.py
@@ -36,6 +36,7 @@ class NXtomoEditorKeys:
     X_PIXEL_SIZE = "instrument.detector.x_pixel_size"
     Y_PIXEL_SIZE = "instrument.detector.y_pixel_size"
     SAMPLE_DETECTOR_DISTANCE = "instrument.detector.distance"
+    SOURCE_SAMPLE_DISTANCE = "instrument.source.distance"
     FIELD_OF_VIEW = "instrument.detector.field_of_view"
     X_FLIPPED = "instrument.detector.x_flipped"
     Y_FLIPPED = "instrument.detector.y_flipped"
@@ -272,6 +273,20 @@ class NXtomoEditorTask(
                 name="sample detector distance",
                 n_value=1,
             ),
+            # source / sample distance
+            NXtomoEditorKeys.SOURCE_SAMPLE_DISTANCE: _EditorFieldInfo(
+                nexus_path="/".join(
+                    [
+                        nexus_paths.INSTRUMENT_PATH,
+                        nexus_paths.nx_instrument_paths.SOURCE,
+                        nexus_paths.nx_source_paths.DISTANCE,
+                    ]
+                ),
+                expected_type=float,
+                units="m",
+                name="source sample distance",
+                n_value=1,
+            ),
             # overwrite FOV
             NXtomoEditorKeys.FIELD_OF_VIEW: _EditorFieldInfo(
                 nexus_path=nexus_paths.FOV_PATH,
diff --git a/src/tomwer/tests/orangecontrib/tomwer/widgets/edit/tests/test_nxtomo_editor.py b/src/tomwer/tests/orangecontrib/tomwer/widgets/edit/tests/test_nxtomo_editor.py
index cb79ae76eb..547083afa5 100644
--- a/src/tomwer/tests/orangecontrib/tomwer/widgets/edit/tests/test_nxtomo_editor.py
+++ b/src/tomwer/tests/orangecontrib/tomwer/widgets/edit/tests/test_nxtomo_editor.py
@@ -29,6 +29,7 @@ def getDefaultConfig() -> dict:
         NXtomoEditorKeys.Y_FLIPPED: (False, False),
         NXtomoEditorKeys.X_TRANSLATION: (0.0,),
         NXtomoEditorKeys.Z_TRANSLATION: (0.0,),
+        NXtomoEditorKeys.SOURCE_SAMPLE_DISTANCE: (1.3, True),
     }
 
 
@@ -50,10 +51,10 @@ def test_NXtomoEditorOW(
     signal_listener = SignalListener()
     window.sigScanReady.connect(signal_listener)
     # set up the widget to define and lock distance, energy and x pixel size
-    distance_widget = window.widget.mainWidget._distanceMetricEntry
+    distance_widget = window.widget.mainWidget._sampleDetectorDistanceMetricEntry
     distance_widget.setValue(0.6)
     distance_widget.setUnit("mm")
-    distance_locker = window.widget.mainWidget._distanceLB
+    distance_locker = window.widget.mainWidget._sampleDetectorDistanceLB
     distance_locker.setLock(True)
     energy_widget = window.widget.mainWidget._energyEntry
     energy_widget.setValue(88.058)
-- 
GitLab