From afdb50f9835416864adbffeb713c1fab36d44785 Mon Sep 17 00:00:00 2001 From: Neil Shephard Date: Mon, 26 Jan 2026 11:50:10 +0000 Subject: [PATCH] feature(spm): calculate px scaling from size if not available Further to #181 this commit calculates the `pixel_to_nm_scaling` from the `SPM.size` attributes if the `SPM.pxs` function returns a value of `0`. Includes test to check that this is used and calculated correctly. --- AFMReader/spm.py | 12 +++++++++++- tests/test_spm.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/AFMReader/spm.py b/AFMReader/spm.py index f8a24b1..0a803e6 100644 --- a/AFMReader/spm.py +++ b/AFMReader/spm.py @@ -38,8 +38,18 @@ def spm_pixel_to_nm_scaling(filename: str, channel_data: pySPM.SPM.SPM_image) -> px_to_real[0][0] * unit_dict[px_to_real[0][1]], px_to_real[1][0] * unit_dict[px_to_real[1][1]], )[0] + # ns-rse : Perhaps just switch to _always_ using the parameters from channel_data.size to calculate scaling? if px_to_real[0][0] == 0 and px_to_real[1][0] == 0: - raise ValueError(f"[{filename}] : Pixel to nm scaling could not be determined from metadata.") + logger.info( + f"[{filename}] : Pixel to nm scaling not directly available, calculating from 'channel_data.size['real']' " + "and 'channel_data.size['pixels']'." + ) + pixel_to_nm_scaling = ( + (channel_data.size["real"]["x"] / channel_data.size["pixels"]["x"]) + / unit_dict[channel_data.size["real"]["unit"]], + (channel_data.size["real"]["y"] / channel_data.size["pixels"]["y"]) + / unit_dict[channel_data.size["real"]["unit"]], + )[0] logger.info(f"[{filename}] : Pixel to nm scaling : {pixel_to_nm_scaling}") return pixel_to_nm_scaling diff --git a/tests/test_spm.py b/tests/test_spm.py index 9906b53..f15632e 100644 --- a/tests/test_spm.py +++ b/tests/test_spm.py @@ -46,6 +46,38 @@ def test_load_spm( assert result_image.sum() == pytest.approx(image_sum) +@patch("pySPM.SPM.SPM_image") +@pytest.mark.parametrize( + ("filename", "size", "expected_px2nm"), + [ + pytest.param( + "square", + {"pixels": {"x": 1024, "y": 1024}, "real": {"x": 505.859, "y": 505.859, "unit": "nm"}}, + 0.4940029296875, + id="square 0.494", + ), + pytest.param( + "square", + {"pixels": {"x": 2048, "y": 2048}, "real": {"x": 505.859, "y": 505.859, "unit": "nm"}}, + 0.24700146484375, + id="square 0.247", + ), + ], +) +def test_spm_pixel_to_nm_scaling_( + mock_spm: "MagicMock", + filename: str, + size: dict[str, dict[str, int | str]], + expected_px2nm: float, +) -> None: + """Test obtaining scaling directly when ``pixel_to_nm_scale`` attribute is zero.""" + # Mock the pxs attribute to be zero which triggers derivation of sacling from the size attributes + mock_spm.pxs.return_value = [(0, "nm"), (0, "nm")] + mock_spm.size = size + result = spm.spm_pixel_to_nm_scaling(filename, mock_spm) + assert result == expected_px2nm + + @patch("pySPM.SPM.SPM_image.pxs") @pytest.mark.parametrize( ("filename", "unit", "x", "y", "expected_px2nm"),