From 689de88614d6da4bf73170c7dccc5bde8b0d38e2 Mon Sep 17 00:00:00 2001 From: SylviaWhittle Date: Tue, 14 May 2024 16:41:55 +0100 Subject: [PATCH 01/13] Add hdf5 unpacker --- AFMReader/io.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/AFMReader/io.py b/AFMReader/io.py index 4189626..6c8ee2e 100644 --- a/AFMReader/io.py +++ b/AFMReader/io.py @@ -2,6 +2,7 @@ import struct from typing import BinaryIO +import h5py def read_uint8(open_file: BinaryIO) -> int: @@ -215,3 +216,39 @@ def skip_bytes(open_file: BinaryIO, length_bytes: int = 1) -> bytes: The bytes that were skipped. """ return open_file.read(length_bytes) + + +def unpack_hdf5(open_hdf5_file: h5py.File, group_path: str) -> dict: + """ + Read a dictionary from an open hdf5 file. + + Parameters + ---------- + open_hdf5_file : h5py.File + An open hdf5 file object. + group_path : str + Path to the group in the hdf5 to start reading the data from. + + Returns + ------- + dict + Dictionary containing the data from the hdf5 file. + + Examples + -------- + >>> with h5py.File("path/to/file.h5", "r") as f: + >>> data = unpack_hdf5(open_hdf5_file=f, group_path="/") + """ + data = {} + for key, item in open_hdf5_file[group_path].items(): + if isinstance(item, h5py.Group): + # Incur recursion for nested groups + data[key] = unpack_hdf5(open_hdf5_file, f"{group_path}/{key}") + # Decode byte strings to utf-8. The data type "O" is a byte string. + elif isinstance(item, h5py.Dataset) and item.dtype == "O": + # Byte string + data[key] = item[()].decode("utf-8") + else: + # Another type of dataset + data[key] = item[()] + return data From 08f54578fc133cee337257ffd579084b45117d74 Mon Sep 17 00:00:00 2001 From: SylviaWhittle Date: Tue, 14 May 2024 16:42:23 +0100 Subject: [PATCH 02/13] Add h5py as a dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index bd44678..583a8fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "tifffile", "pySPM", "loguru", + "h5py", ] [project.optional-dependencies] From 7339f540b76c49f57d133b824b2869029f8f8774 Mon Sep 17 00:00:00 2001 From: SylviaWhittle Date: Tue, 14 May 2024 16:45:09 +0100 Subject: [PATCH 03/13] Add tests for hdf5 unpacking --- tests/test_io.py | 187 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 tests/test_io.py diff --git a/tests/test_io.py b/tests/test_io.py new file mode 100644 index 0000000..f4545fd --- /dev/null +++ b/tests/test_io.py @@ -0,0 +1,187 @@ +"""Test the reading and writing of data from / to files.""" + +from pathlib import Path + +import numpy as np +import h5py + + +from AFMReader.io import unpack_hdf5 + + +def test_unpack_hdf5_all_together_group_path_default(tmp_path: Path) -> None: + """Test loading a nested dictionary with arrays from HDF5 format with group path as default.""" + to_save = { + "a": 1, + "b": np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), + "c": "test", + "d": {"e": 1, "f": np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), "g": "test"}, + } + + group_path = "/" + + # Manually save the dictionary to HDF5 format + with h5py.File(tmp_path / "hdf5_file_nested_with_arrays_group_path_standard.hdf5", "w") as f: + # Write the datasets and groups to the file without using the dict_to_hdf5 function + f.create_dataset("a", data=to_save["a"]) + f.create_dataset("b", data=to_save["b"]) + f.create_dataset("c", data=to_save["c"]) + d = f.create_group("d") + d.create_dataset("e", data=to_save["d"]["e"]) + d.create_dataset("f", data=to_save["d"]["f"]) + d.create_dataset("g", data=to_save["d"]["g"]) + + # Load it back in and check if the dictionary is the same + with h5py.File(tmp_path / "hdf5_file_nested_with_arrays_group_path_standard.hdf5", "r") as f: + result = unpack_hdf5(open_hdf5_file=f, group_path=group_path) + + np.testing.assert_equal(result, to_save) + + +def test_unpack_hdf5_all_together_group_path_non_standard(tmp_path: Path) -> None: + """Test loading a nested dictionary with arrays from HDF5 format with a non-standard group path.""" + to_save = { + "a": 1, + "b": np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), + "c": "test", + "d": {"e": 1, "f": np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), "g": "test"}, + } + + group_path = "/d/" + + expected = { + "e": 1, + "f": np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), + "g": "test", + } + + # Manually save the dictionary to HDF5 format + with h5py.File(tmp_path / "hdf5_file_all_together_group_path_nonstandard.hdf5", "w") as f: + # Write the datasets and groups to the file without using the dict_to_hdf5 function + f.create_dataset("a", data=to_save["a"]) + f.create_dataset("b", data=to_save["b"]) + f.create_dataset("c", data=to_save["c"]) + d = f.create_group("d") + d.create_dataset("e", data=to_save["d"]["e"]) + d.create_dataset("f", data=to_save["d"]["f"]) + d.create_dataset("g", data=to_save["d"]["g"]) + + # Load it back in and check if the dictionary is the same + with h5py.File(tmp_path / "hdf5_file_all_together_group_path_nonstandard.hdf5", "r") as f: + result = unpack_hdf5(open_hdf5_file=f, group_path=group_path) + + np.testing.assert_equal(result, expected) + + +def test_unpack_hdf5_int(tmp_path: Path) -> None: + """Test loading a dictionary with an integer from HDF5 format.""" + to_save = {"a": 1, "b": 2} + + group_path = "/" + + # Manually save the dictionary to HDF5 format + with h5py.File(tmp_path / "hdf5_file_int.hdf5", "w") as f: + # Write the datasets and groups to the file without using the dict_to_hdf5 function + f.create_dataset("a", data=to_save["a"]) + f.create_dataset("b", data=to_save["b"]) + + # Load it back in and check if the dictionary is the same + with h5py.File(tmp_path / "hdf5_file_int.hdf5", "r") as f: + result = unpack_hdf5(open_hdf5_file=f, group_path=group_path) + + np.testing.assert_equal(result, to_save) + + +def test_unpack_hdf5_float(tmp_path: Path) -> None: + """Test loading a dictionary with a float from HDF5 format.""" + to_save = {"a": 0.01, "b": 0.02} + + group_path = "/" + + # Manually save the dictionary to HDF5 format + with h5py.File(tmp_path / "hdf5_file_float.hdf5", "w") as f: + # Write the datasets and groups to the file without using the dict_to_hdf5 function + f.create_dataset("a", data=to_save["a"]) + f.create_dataset("b", data=to_save["b"]) + + # Load it back in and check if the dictionary is the same + with h5py.File(tmp_path / "hdf5_file_float.hdf5", "r") as f: + result = unpack_hdf5(open_hdf5_file=f, group_path=group_path) + + np.testing.assert_equal(result, to_save) + + +def test_unpack_hdf5_str(tmp_path: Path) -> None: + """Test loading a dictionary with a string from HDF5 format.""" + to_save = {"a": "test", "b": "test2"} + + group_path = "/" + + # Manually save the dictionary to HDF5 format + with h5py.File(tmp_path / "hdf5_file_str.hdf5", "w") as f: + # Write the datasets and groups to the file without using the dict_to_hdf5 function + f.create_dataset("a", data=to_save["a"]) + f.create_dataset("b", data=to_save["b"]) + + # Load it back in and check if the dictionary is the same + with h5py.File(tmp_path / "hdf5_file_str.hdf5", "r") as f: + result = unpack_hdf5(open_hdf5_file=f, group_path=group_path) + + np.testing.assert_equal(result, to_save) + + +def test_unpack_hdf5_dict_nested_dict(tmp_path: Path) -> None: + """Test loading a nested dictionary from HDF5 format.""" + to_save = { + "a": 1, + "b": 2, + "c": {"d": 3, "e": 4}, + } + + group_path = "/" + + # Manually save the dictionary to HDF5 format + with h5py.File(tmp_path / "hdf5_file_nested_dict.hdf5", "w") as f: + # Write the datasets and groups to the file without using the dict_to_hdf5 function + f.create_dataset("a", data=to_save["a"]) + f.create_dataset("b", data=to_save["b"]) + c = f.create_group("c") + c.create_dataset("d", data=to_save["c"]["d"]) + c.create_dataset("e", data=to_save["c"]["e"]) + + # Load it back in and check if the dictionary is the same + with h5py.File(tmp_path / "hdf5_file_nested_dict.hdf5", "r") as f: + result = unpack_hdf5(open_hdf5_file=f, group_path=group_path) + + np.testing.assert_equal(result, to_save) + + +def test_unpack_hdf5_nested_dict_group_path(tmp_path: Path) -> None: + """Test loading a nested dictionary from HDF5 format with a non-standard group path.""" + to_save = { + "a": 1, + "b": 2, + "c": {"d": 3, "e": 4}, + } + + group_path = "/c/" + + expected = { + "d": 3, + "e": 4, + } + + # Manually save the dictionary to HDF5 format + with h5py.File(tmp_path / "hdf5_file_nested_dict_group_path.hdf5", "w") as f: + # Write the datasets and groups to the file without using the dict_to_hdf5 function + f.create_dataset("a", data=to_save["a"]) + f.create_dataset("b", data=to_save["b"]) + c = f.create_group("c") + c.create_dataset("d", data=to_save["c"]["d"]) + c.create_dataset("e", data=to_save["c"]["e"]) + + # Load it back in and check if the dictionary is the same + with h5py.File(tmp_path / "hdf5_file_nested_dict_group_path.hdf5", "r") as f: + result = unpack_hdf5(open_hdf5_file=f, group_path=group_path) + + np.testing.assert_equal(result, expected) From e6f69f29f9a2c136f0908722c9c51cb05cb9f92a Mon Sep 17 00:00:00 2001 From: SylviaWhittle Date: Tue, 14 May 2024 16:46:03 +0100 Subject: [PATCH 04/13] Add topostats loader --- AFMReader/topostats.py | 55 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 AFMReader/topostats.py diff --git a/AFMReader/topostats.py b/AFMReader/topostats.py new file mode 100644 index 0000000..cc63f1a --- /dev/null +++ b/AFMReader/topostats.py @@ -0,0 +1,55 @@ +"""For decoding and loading .topostats (HDF5 format) AFM file format into Python Nympy arrays.""" + +from __future__ import annotations +from pathlib import Path + +import h5py + +from AFMReader.logging import logger +from AFMReader.io import unpack_hdf5 + +logger.enable(__package__) + + +def load_topostats(file_path: Path | str) -> tuple: + """ + Extract image and pixel to nm scaling from the .topostats (HDF5 format) file. + + Parameters + ---------- + file_path : Path or str + Path to the .topostats file. + + Returns + ------- + tuple(np.ndarray, float) + A tuple containing the image, its pixel to nm scaling factor and the data dictionary + containing all the extra image data and metadata in dictionary format. + + Raises + ------ + OSError + If the file is not found. + + Examples + -------- + >>> image, pixel_to_nm_scaling = load_topostats("path/to/topostats_file.topostats") + """ + logger.info(f"Loading image from : {file_path}") + file_path = Path(file_path) + filename = file_path.stem + try: + with h5py.File(file_path, "r") as f: + data = unpack_hdf5(open_hdf5_file=f, group_path="/") + + file_version = data["topostats_file_version"] + logger.info(f"TopoStats file version: {file_version}") + image = data["image"] + pixel_to_nm_scaling = data["pixel_to_nm_scaling"] + + except OSError as e: + if "Unable to open file" in str(e): + logger.error(f"[{filename}] File not found : {file_path}") + raise e + + return (image, pixel_to_nm_scaling, data) From bdad4cc3cc606450b861e1ae87fc5c9dc5ff512b Mon Sep 17 00:00:00 2001 From: SylviaWhittle Date: Tue, 14 May 2024 16:46:32 +0100 Subject: [PATCH 05/13] Add test for loading topostats files --- tests/test_topostats.py | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/test_topostats.py diff --git a/tests/test_topostats.py b/tests/test_topostats.py new file mode 100644 index 0000000..54c0c44 --- /dev/null +++ b/tests/test_topostats.py @@ -0,0 +1,49 @@ +"""Test the loading of topostats (HDF5 format) files.""" + +from pathlib import Path +import pytest + +import numpy as np + +from AFMReader.topostats import load_topostats + +BASE_DIR = Path.cwd() +RESOURCES = BASE_DIR / "tests" / "resources" + + +@pytest.mark.parametrize( + ("file_name", "topostats_file_version", "image_shape", "pixel_to_nm_scaling", "data_keys", "image_sum"), + [ + pytest.param( + "sample_0.topostats", + 0.1, + (64, 64), + 1.97601171875, + {"topostats_file_version", "image", "pixel_to_nm_scaling"}, + 112069.51332503435, + id="version", + ), + ], +) +def test_load_topostats( + file_name: str, + topostats_file_version: float, + image_shape: tuple[int, int], + pixel_to_nm_scaling: float, + data_keys: set[str], + image_sum: float, +) -> None: + """Test the normal operation of loading a .topostats (HDF5 format) file.""" + result_image = np.ndarray + result_pixel_to_nm_scaling = float + result_data = dict + + file_path = RESOURCES / file_name + result_image, result_pixel_to_nm_scaling, result_data = load_topostats(file_path) + + assert result_pixel_to_nm_scaling == pixel_to_nm_scaling + assert isinstance(result_image, np.ndarray) + assert result_image.shape == image_shape + assert set(result_data.keys()) == data_keys + assert result_data["topostats_file_version"] == topostats_file_version + assert result_image.sum() == image_sum From 25041a2e0edfe75479d4fe25c3f03ee8342e0021 Mon Sep 17 00:00:00 2001 From: SylviaWhittle Date: Tue, 14 May 2024 16:47:00 +0100 Subject: [PATCH 06/13] Add test file for topostats loading test --- tests/resources/sample_0.topostats | Bin 0 -> 34832 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/resources/sample_0.topostats diff --git a/tests/resources/sample_0.topostats b/tests/resources/sample_0.topostats new file mode 100644 index 0000000000000000000000000000000000000000..c714e7aa1e6fd68614451604699048a66bfe7e5f GIT binary patch literal 34832 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 literal 0 HcmV?d00001 From 3131efc7bb326b3ce3cfd38858b7758ae1537ec7 Mon Sep 17 00:00:00 2001 From: SylviaWhittle Date: Tue, 14 May 2024 16:48:44 +0100 Subject: [PATCH 07/13] Update README with .topostats documentation --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 6e41153..3e1f449 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Supported file formats | `.ibw` | [WaveMetrics](https://www.wavemetrics.com/) | | `.spm` | [Bruker's Format](https://www.bruker.com/) | | `.jpk` | [Bruker](https://www.bruker.com/) | +| `.topostats`| [TopoStats](https://github.com/AFM-SPM/TopoStats) | Support for the following additional formats is planned. Some of these are already supported in TopoStats and are awaiting refactoring to move their functionality into AFMReader these are denoted in bold below. @@ -41,6 +42,16 @@ awaiting refactoring to move their functionality into AFMReader these are denote If you wish to process AFM images supported by `AFMReader` it is recommend you use [TopoStats](https://github.com/AFM-SPM/TopoStats) to do so, however the library can be used on its own. +### .topostats + +You can open `.topostats` files using the `load_topostats` function. Just pass in the path to the file. + +```python +from AFMReader.topostats import load_topostats + +image, pixel_to_nanometre_scaling_factor, metadata = load_topostats(file_path="./my_topostats_file.topostats") +``` + ### .spm You can open `.spm` files using the `load_spm` function. Just pass in the path to the file and the From 0fdb9cc18d93df27becf09188d384dbf43d332ff Mon Sep 17 00:00:00 2001 From: Max Gamill Date: Wed, 29 May 2024 10:48:06 +0100 Subject: [PATCH 08/13] Replaced 2nd .ibw in notebook with .topostats --- examples/example_01.ipynb | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/example_01.ipynb b/examples/example_01.ipynb index c2762d0..51837a9 100644 --- a/examples/example_01.ipynb +++ b/examples/example_01.ipynb @@ -9,7 +9,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": {}, "outputs": [], "source": [ @@ -187,33 +187,33 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# IBW Files" + "# TopoStats Files" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# Import the load_ibw function from AFMReader\n", - "from AFMReader.ibw import load_ibw" + "from AFMReader.topostats import load_topostats" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ - "# Load the IBW file as an image and pixel to nm scaling factor\n", - "FILE = \"../tests/resources/sample_0.ibw\"\n", - "image, pixel_to_nm_scaling = load_ibw(file_path=FILE, channel=\"HeightTracee\")" + "# Load the TopoStats file as an image, pixel to nm scaling factor, and metadata\n", + "FILE = \"../tests/resources/sample_0.topostats\"\n", + "image, pixel_to_nm_scaling, metadata = load_topostats(file_path=FILE)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -241,7 +241,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.11.9" } }, "nbformat": 4, From b8496753c03c2dd8fa0a6d4a8d5f2a0c786d996f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 09:58:36 +0000 Subject: [PATCH 09/13] [pre-commit.ci] Fixing issues with pre-commit --- examples/example_01.ipynb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/example_01.ipynb b/examples/example_01.ipynb index 51837a9..3f39bad 100644 --- a/examples/example_01.ipynb +++ b/examples/example_01.ipynb @@ -9,7 +9,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -192,7 +192,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -202,7 +202,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -213,7 +213,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ From 848fe9dcd1644c52fd3691505155db0ab458b588 Mon Sep 17 00:00:00 2001 From: SylviaWhittle Date: Thu, 6 Jun 2024 22:41:34 +0100 Subject: [PATCH 10/13] Improve docs for unpack hdf5 --- AFMReader/io.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/AFMReader/io.py b/AFMReader/io.py index 6c8ee2e..852f684 100644 --- a/AFMReader/io.py +++ b/AFMReader/io.py @@ -218,7 +218,7 @@ def skip_bytes(open_file: BinaryIO, length_bytes: int = 1) -> bytes: return open_file.read(length_bytes) -def unpack_hdf5(open_hdf5_file: h5py.File, group_path: str) -> dict: +def unpack_hdf5(open_hdf5_file: h5py.File, group_path: str = "/") -> dict: """ Read a dictionary from an open hdf5 file. @@ -227,7 +227,7 @@ def unpack_hdf5(open_hdf5_file: h5py.File, group_path: str) -> dict: open_hdf5_file : h5py.File An open hdf5 file object. group_path : str - Path to the group in the hdf5 to start reading the data from. + Path to the group in the hdf5 file to start reading the data from. Returns ------- @@ -236,8 +236,12 @@ def unpack_hdf5(open_hdf5_file: h5py.File, group_path: str) -> dict: Examples -------- + Read the data from the root group of the hdf5 file. >>> with h5py.File("path/to/file.h5", "r") as f: >>> data = unpack_hdf5(open_hdf5_file=f, group_path="/") + Read data from a particular dataset in the hdf5 file. + >>> with h5py.File("path/to/file.h5", "r") as f: + >>> data = unpack_hdf5(open_hdf5_file=f, group_path="/dataset_name") """ data = {} for key, item in open_hdf5_file[group_path].items(): From 157ab92b322aa679d628c3003aeac91041905877 Mon Sep 17 00:00:00 2001 From: SylviaWhittle Date: Thu, 6 Jun 2024 22:42:28 +0100 Subject: [PATCH 11/13] Improve naming of topostats test file to reflect version 0.1 --- .../{sample_0.topostats => sample_0_1.topostats} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/resources/{sample_0.topostats => sample_0_1.topostats} (100%) diff --git a/tests/resources/sample_0.topostats b/tests/resources/sample_0_1.topostats similarity index 100% rename from tests/resources/sample_0.topostats rename to tests/resources/sample_0_1.topostats From 11ae33794dd7a98966bc38c645f34b9f8f5e3abf Mon Sep 17 00:00:00 2001 From: SylviaWhittle Date: Thu, 6 Jun 2024 22:43:40 +0100 Subject: [PATCH 12/13] Add filename to topostats file version logging line --- AFMReader/topostats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AFMReader/topostats.py b/AFMReader/topostats.py index cc63f1a..d92b270 100644 --- a/AFMReader/topostats.py +++ b/AFMReader/topostats.py @@ -43,7 +43,7 @@ def load_topostats(file_path: Path | str) -> tuple: data = unpack_hdf5(open_hdf5_file=f, group_path="/") file_version = data["topostats_file_version"] - logger.info(f"TopoStats file version: {file_version}") + logger.info(f"[{filename}] TopoStats file version : {file_version}") image = data["image"] pixel_to_nm_scaling = data["pixel_to_nm_scaling"] From 0145511f195e00780c482909ae15a336bb04f4a4 Mon Sep 17 00:00:00 2001 From: SylviaWhittle Date: Fri, 7 Jun 2024 09:23:33 +0100 Subject: [PATCH 13/13] Fix loading the renamed test file --- examples/example_01.ipynb | 2 +- tests/test_topostats.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/example_01.ipynb b/examples/example_01.ipynb index 3f39bad..91a1582 100644 --- a/examples/example_01.ipynb +++ b/examples/example_01.ipynb @@ -207,7 +207,7 @@ "outputs": [], "source": [ "# Load the TopoStats file as an image, pixel to nm scaling factor, and metadata\n", - "FILE = \"../tests/resources/sample_0.topostats\"\n", + "FILE = \"../tests/resources/sample_0_1.topostats\"\n", "image, pixel_to_nm_scaling, metadata = load_topostats(file_path=FILE)" ] }, diff --git a/tests/test_topostats.py b/tests/test_topostats.py index 54c0c44..e6de6b3 100644 --- a/tests/test_topostats.py +++ b/tests/test_topostats.py @@ -15,7 +15,7 @@ ("file_name", "topostats_file_version", "image_shape", "pixel_to_nm_scaling", "data_keys", "image_sum"), [ pytest.param( - "sample_0.topostats", + "sample_0_1.topostats", 0.1, (64, 64), 1.97601171875,