diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c6f304..0dae082 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: types: [python, yaml, markdown] - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.18.1 + rev: v0.18.0 hooks: - id: markdownlint-cli2 args: [] diff --git a/AFMReader/asd.py b/AFMReader/asd.py index a485663..64727f5 100644 --- a/AFMReader/asd.py +++ b/AFMReader/asd.py @@ -2,34 +2,32 @@ import errno import os -from pathlib import Path import sys +from pathlib import Path - +import matplotlib.pyplot as plt import numpy as np import numpy.typing as npt -import matplotlib.pyplot as plt from matplotlib import animation - -from AFMReader.logging import logger from AFMReader.io import ( - read_int32, - read_int16, - read_float, + read_ascii, read_bool, + read_double, + read_float, read_hex_u32, - read_ascii, - read_uint8, - read_null_separated_utf8, read_int8, - read_double, + read_int16, + read_int32, + read_null_separated_utf8, + read_uint8, skip_bytes, ) - +from AFMReader.logging import logger if sys.version_info.minor < 11: from typing import Any, BinaryIO + from typing_extensions import Self else: from typing import Any, BinaryIO, Self diff --git a/AFMReader/general_loader.py b/AFMReader/general_loader.py index fec309f..be27937 100644 --- a/AFMReader/general_loader.py +++ b/AFMReader/general_loader.py @@ -4,7 +4,7 @@ import numpy.typing as npt -from AFMReader import asd, gwy, ibw, jpk, spm, stp, top, topostats +from AFMReader import asd, gwy, h5_jpk, ibw, jpk, spm, stp, top, topostats from AFMReader.logging import logger logger.enable(__package__) @@ -45,7 +45,7 @@ def load(self) -> tuple[npt.NDArray | str, float | None]: # noqa: C901 Returns ------- tuple - The image data (stack if ''.asd'') and the pixel to nanometre scaling ratio. + The image data (stack if ''.asd'' or ''.h5-jpk'') and the pixel to nanometre scaling ratio. Raises ------ @@ -64,6 +64,8 @@ def load(self) -> tuple[npt.NDArray | str, float | None]: # noqa: C901 image, pixel_to_nanometre_scaling_factor = jpk.load_jpk(self.filepath, self.channel) elif self.suffix == ".spm": image, pixel_to_nanometre_scaling_factor = spm.load_spm(self.filepath, self.channel) + elif self.suffix == ".h5-jpk": + image, pixel_to_nanometre_scaling_factor, _ = h5_jpk.load_h5jpk(self.filepath, self.channel) elif self.suffix == ".stp": image, pixel_to_nanometre_scaling_factor = stp.load_stp(self.filepath) elif self.suffix == ".top": diff --git a/AFMReader/gwy.py b/AFMReader/gwy.py index bc98e26..087af4b 100644 --- a/AFMReader/gwy.py +++ b/AFMReader/gwy.py @@ -1,13 +1,13 @@ """For decoding and loading .gwy AFM file format into Python Numpy arrays.""" -from pathlib import Path import re +from pathlib import Path from typing import Any, BinaryIO -from loguru import logger import numpy as np +from loguru import logger -from AFMReader.io import read_uint32, read_null_terminated_string, read_char, read_double +from AFMReader.io import read_char, read_double, read_null_terminated_string, read_uint32 def load_gwy(file_path: Path | str, channel: str) -> tuple[np.ndarray[Any, np.float64], float]: diff --git a/AFMReader/h5_jpk.py b/AFMReader/h5_jpk.py new file mode 100644 index 0000000..13c834b --- /dev/null +++ b/AFMReader/h5_jpk.py @@ -0,0 +1,348 @@ +""" +Module to decode and load .h5-jpk AFM file format into 3D Python NumPy arrays. + +It extracts scan channels, reshapes image frames, applies scaling, and generates +timestamps based on scan metadata. +""" + +from pathlib import Path + +import h5py +import numpy as np + +from AFMReader.logging import logger + +logger.enable(__package__) + + +def _parse_channel_name(channel: str) -> tuple[str, str]: + """ + Validate and split a channel name into its type and trace direction. + + Parameters + ---------- + channel : str + The name of the channel, expected in the form 'name_trace' or 'name_retrace'. + + Returns + ------- + tuple[str, str] + A tuple containing the channel type and trace type. + + Raises + ------ + ValueError + If the format is invalid or the trace type is not 'trace' or 'retrace'. + """ + if "_" not in channel: + raise ValueError(f"Invalid channel format '{channel}'. Expected 'name_trace' or 'name_retrace'.") + + channel_type, trace_type = channel.rsplit("_", 1) + trace_type = trace_type.lower() + + if trace_type not in ("trace", "retrace"): + raise ValueError(f"Invalid trace type '{trace_type}'. Must be 'trace' or 'retrace'.") + + return channel_type, trace_type + + +def _get_channel_info(h5py_file: h5py.File, channel: str): + """ + Retrieve channel-related HDF5 groups and dataset name. + + Parameters + ---------- + h5py_file : h5py.File + The open HDF5 file object. + channel : str + The name of the channel to retrieve. + + Returns + ------- + tuple[h5py.Group, h5py.Group, str] + The channel group, measurement group, and dataset name. + + Raises + ------ + ValueError + If the channel is not found. + """ + _parse_channel_name(channel) # just for validation + + channel_map = _available_channels(h5py_file) + if channel not in channel_map: + raise ValueError(f"'{channel}' not found. Available channels: {list(channel_map)}") + + channel_path = channel_map[channel] + channel_group = h5py_file[channel_path] + measurement_group = h5py_file[channel_path.split("/")[0]] + dataset_name = channel.split("_")[0].capitalize() + + return channel_group, measurement_group, dataset_name + + +def _jpk_pixel_to_nm_scaling_h5(measurement_group: h5py.Group) -> float: + """ + Extract pixel-to-nanometre scaling from an HDF5 JPK measurement group. + + This uses the fast scan axis (u/i) and converts the physical scan size to nanometres + per pixel based on the scan length and pixel count. + + Parameters + ---------- + measurement_group : h5py.Group + HDF5 group corresponding to a Measurement (e.g. '/Measurement_000'). + + Returns + ------- + float + Real-world size of a single pixel in nanometres. + + Raises + ------ + KeyError + If required attributes are missing in the measurement group. + """ + try: + ulength = measurement_group.attrs["position-pattern.grid.ulength"] # physical length in meters + ilength = measurement_group.attrs["position-pattern.grid.ilength"] # number of pixels + + if ilength == 0: + raise ValueError("Pixel count (ilength) is zero; cannot compute scaling.") + + return (ulength / ilength) * 1e9 + + except KeyError as e: + missing = e.args[0] + raise KeyError(f"Missing required attribute '{missing}' in HDF5 measurement group.") from e + + +def _get_z_scaling_h5(channel_group: h5py.Group) -> tuple[float, float]: + """ + Extract the Z scaling multiplier and offset from an HDF5 channel group. + + Parameters + ---------- + channel_group : h5py.Group + The HDF5 group corresponding to a specific channel (e.g. /Measurement_000/Channel_001). + + Returns + ------- + tuple[float, float] + A tuple containing the scaling multiplier and offset. + + Notes + ----- + Defaults to (1.0, 0.0) if attributes are not present. + """ + multiplier = float(channel_group.attrs.get("net-encoder.scaling.multiplier", 1.0)) + offset = float(channel_group.attrs.get("net-encoder.scaling.offset", 0.0)) + + return multiplier, offset + + +def _decode_attr(attr: bytes | str) -> str: + """ + Decode an attribute that may be bytes or a string. + + Parameters + ---------- + attr : bytes or str + The attribute to decode. + + Returns + ------- + str + The decoded string. + """ + if isinstance(attr, bytes): + return attr.decode("utf-8") + return str(attr) + + +def _attr_to_bool(attr: bytes | str | bool | int | float) -> bool: + """ + Convert an attribute to a boolean value. + + Parameters + ---------- + attr : bytes, str, bool, int, or float + The attribute to convert. + + Returns + ------- + bool + The boolean interpretation of the value. + """ + if isinstance(attr, (bytes | str)): + return _decode_attr(attr).strip().lower() == "true" + return bool(attr) + + +def _available_channels(f: h5py.File) -> dict[str, str]: + """ + Discover all available scan channels in the HDF5 file. + + Parameters + ---------- + f : h5py.File + The open HDF5 file. + + Returns + ------- + dict[str, str] + Mapping of channel names (e.g. 'height_trace') to their full HDF5 path. + """ + channel_map = {} + for m_key, m_group in f.items(): + if not m_key.startswith("Measurement_"): + continue + for c_key in m_group.keys(): + if not c_key.startswith("Channel_"): + continue + + c_group = m_group[c_key] + name = c_group.attrs.get("channel.name") + if name is None: + continue + + retrace = _attr_to_bool(c_group.attrs.get("retrace", False)) + tr_rt = "retrace" if retrace else "trace" + full_key = f"{_decode_attr(name).strip().lower()}_{tr_rt}" + full_path = f"{m_key}/{c_key}" + if full_key not in channel_map: + channel_map[full_key] = full_path + return channel_map + + +def _get_line_rate(measurement_group: h5py.Group) -> float: + """ + Extract image line rate from an HDF5 JPK measurement group. + + The line rate is the scan speed in terms of lines per second, + i.e. the speed of imaging in fast scan lines / second. + + Parameters + ---------- + measurement_group : h5py.Group + HDF5 group corresponding to a Measurement (e.g. '/Measurement_000'). + + Returns + ------- + float + The line rate of imaging in lines per second. + + Raises + ------ + KeyError + If required attributes are missing in the measurement group. + """ + try: + return measurement_group.attrs["timing-settings.scanRate"] # scan lines per second + + except KeyError as e: + missing = e.args[0] + raise KeyError(f"Missing required attribute '{missing}' in HDF5 measurement group.") from e + + +def generate_timestamps(num_frames: int, line_rate: float, image_size: int) -> dict: + """ + Generate timestamps for a sequence of frames based on scan line rate and image size. + + Parameters + ---------- + num_frames : int + The total number of frames to generate timestamps for. + line_rate : float + The scan line rate in lines per second (Hz). + image_size : int + The number of horizontal lines per image (i.e., image height in pixels). + + Returns + ------- + dict + A dictionary mapping frame labels (e.g., "frame 0") to timestamps in seconds. + """ + timestamps = np.arange(num_frames) * (image_size / line_rate) + # Compose a dictionary of timestamsps + return {f"frame {i}": timestamp for i, timestamp in enumerate(timestamps)} + + +def load_h5jpk( + file_path: Path | str, channel: str, flip_image: bool = True +) -> tuple[np.ndarray, float, dict[str, float]]: + """ + Load image from JPK Instruments .h5-jpk files. + + Parameters + ---------- + file_path : Path | str + Path to the .h5-jpk file. + channel : str + The channel to extract from the .h5-jpk file. + flip_image : bool, optional + Whether to flip the images vertically. Default is ``True``. + + Returns + ------- + image : np.ndarray + 3D array of shape (frames, height, width) with image data. + pixel_to_nm_scaling : float + Scaling factor converting pixels to nanometers. + timestamps : dict[str, float] + Dictionary mapping frame labels (e.g., "frame 0") to timestamp values in seconds. + + Raises + ------ + FileNotFoundError + If the file is not found. + KeyError + If the channel is not found in the file. + + Examples + -------- + Load height trace channel from the .jpk file. 'height_trace' is the default channel name. + + >>> from AFMReader.jpk import load_h5jpk + >>> frames, pixel_to_nanometre_scaling_factor, timestamps = load_h5jpk(file_path="./my_jpk_file.jpk", + >>> channel="height_trace", + >>> flip_image=True) + """ + logger.info(f"Loading H5-JPK file from : {file_path}") + file_path = Path(file_path) + + # Load HDF5 file + with h5py.File(file_path, "r") as f: + logger.info(f"Opened HDF5 file structure: {list(f.keys())}") + + channel_group, measurement_group, dataset_name = _get_channel_info(f, channel) + + # Load images and scaling factors from channel dataset + images = channel_group[dataset_name][:] + scaling, offset = _get_z_scaling_h5(channel_group) + images = (images * scaling) + offset + + # Select and reshape a flattened frame + image_size = measurement_group.attrs["position-pattern.grid.ilength"] # number of pixels + + # Reshape each column vector (height, width) to get (num_frames, height, width) + num_frames = images.shape[1] + image_stack = np.empty((num_frames, image_size, image_size), dtype=images.dtype) + for i in range(num_frames): + frame = images[:, i].reshape((image_size, image_size)) + + # Flip images + if flip_image: + frame = np.flipud(frame) + image_stack[i] = frame + + # Convert to nm + if dataset_name.lower() in ("height", "error", "measuredheight", "amplitude"): + image_stack = image_stack * 1e9 + + # Generate a dictionary of timestamps + line_rate = _get_line_rate(measurement_group) + timestamps = generate_timestamps(num_frames, line_rate, image_size) + + logger.info(f"[{file_path.stem}] : Extracted {num_frames} frames from channel '{channel}'") + return (image_stack, _jpk_pixel_to_nm_scaling_h5(measurement_group), timestamps) diff --git a/AFMReader/io.py b/AFMReader/io.py index bc03b65..74ada20 100644 --- a/AFMReader/io.py +++ b/AFMReader/io.py @@ -1,9 +1,9 @@ """For reading and writing data from / to files.""" -from pathlib import Path - import struct +from pathlib import Path from typing import BinaryIO + import h5py from loguru import logger from ruamel.yaml import YAML, YAMLError diff --git a/AFMReader/spm.py b/AFMReader/spm.py index 0a165a6..598cfa4 100644 --- a/AFMReader/spm.py +++ b/AFMReader/spm.py @@ -2,8 +2,8 @@ from pathlib import Path -import pySPM import numpy as np +import pySPM from AFMReader.logging import logger diff --git a/AFMReader/stp.py b/AFMReader/stp.py index a3defe5..6ad24dc 100644 --- a/AFMReader/stp.py +++ b/AFMReader/stp.py @@ -1,12 +1,12 @@ """For decoding and loading .stp AFM file format into Python Numpy arrays.""" -from pathlib import Path import re +from pathlib import Path import numpy as np -from AFMReader.logging import logger from AFMReader.io import read_double +from AFMReader.logging import logger logger.enable(__package__) diff --git a/AFMReader/top.py b/AFMReader/top.py index 75d76d0..456be33 100644 --- a/AFMReader/top.py +++ b/AFMReader/top.py @@ -1,12 +1,12 @@ """For decoding and loading .top AFM file format into Python Numpy arrays.""" -from pathlib import Path import re +from pathlib import Path import numpy as np -from AFMReader.logging import logger from AFMReader.io import read_int16 +from AFMReader.logging import logger logger.enable(__package__) diff --git a/AFMReader/topostats.py b/AFMReader/topostats.py index 6f5e20f..8dc9148 100644 --- a/AFMReader/topostats.py +++ b/AFMReader/topostats.py @@ -5,8 +5,8 @@ import h5py -from AFMReader.logging import logger from AFMReader.io import unpack_hdf5 +from AFMReader.logging import logger logger.enable(__package__) diff --git a/README.md b/README.md index 08acdf0..941d1d5 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Supported file formats | `.ibw` | [WaveMetrics](https://www.wavemetrics.com/) | | `.spm` | [Bruker's Format](https://www.bruker.com/) | | `.jpk` | [Bruker](https://www.bruker.com/) | +| `.h5-jpk` | [Bruker](https://www.bruker.com/) | | `.topostats`| [TopoStats](https://github.com/AFM-SPM/TopoStats) | | `.gwy` | [Gwydion]() | | `.stp` | [WSXM AFM software files](http://www.wsxm.eu) | @@ -125,6 +126,20 @@ from AFMReader.jpk import load_jpk image, pixel_to_nanometre_scaling_factor = load_jpk(file_path="./my_jpk_file.jpk", channel="height_trace") ``` +### .h5-jpk + +You can open `.h5-jpk` files using the `load_h5jpk` function. Just pass in the path +to the file and the channel name you want to use. +(If in doubt, use `height_trace` or `measuredHeight_trace`). + +Note: Since `.h5-jpk` stores timeseries AFM data a dictionary of timestamps for each frame is also returned. + +```python +from AFMReader.h5_jpk import load_h5jpk + +frames, pixel_to_nanometre_scaling_factor, timestamp_dict = load_h5jpk(file_path="./my_jpk_file.jpk", channel="height_trace") +``` + ### .stp You can open `.stp` files using the `load_stp` function. Just pass in the path diff --git a/examples/example_01.ipynb b/examples/example_01.ipynb index c6ada70..6675a74 100644 --- a/examples/example_01.ipynb +++ b/examples/example_01.ipynb @@ -188,6 +188,48 @@ "plt.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# H5-JPK Files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import the load_jpk function from AFMReader\n", + "from AFMReader.h5_jpk import load_h5jpk" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load the JPK file as an image and pixel to nm scaling factor\n", + "FILE = \"../tests/resources/sample_0.h5-jpk\"\n", + "frames, pixel_to_nm_scaling, timestamps = load_h5jpk(file_path=FILE, channel=\"height_trace\", flip_image=True)\n", + "logger.info(f\"Loaded {len(frames)} frames from {FILE}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot the image\n", + "import matplotlib.pyplot as plt\n", + "\n", + "plt.imshow(image, cmap=\"afmhot\")\n", + "plt.show()" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/tests/conftest.py b/tests/conftest.py index 0ab7ced..9288b55 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,6 @@ import pySPM import pytest - BASE_DIR = Path.cwd() RESOURCES = BASE_DIR / "tests" / "resources" diff --git a/tests/resources/sample_0.h5-jpk b/tests/resources/sample_0.h5-jpk new file mode 100644 index 0000000..0339f41 Binary files /dev/null and b/tests/resources/sample_0.h5-jpk differ diff --git a/tests/test_asd.py b/tests/test_asd.py index 9713229..3f411df 100644 --- a/tests/test_asd.py +++ b/tests/test_asd.py @@ -1,6 +1,7 @@ """Test the functioning of loading .asd files.""" from pathlib import Path + import pytest from AFMReader import asd diff --git a/tests/test_general_loader.py b/tests/test_general_loader.py index af1120d..b04547d 100644 --- a/tests/test_general_loader.py +++ b/tests/test_general_loader.py @@ -2,10 +2,10 @@ from pathlib import Path -import pytest import numpy as np -from AFMReader import general_loader +import pytest +from AFMReader import general_loader BASE_DIR = Path.cwd() RESOURCES = BASE_DIR / "tests" / "resources" diff --git a/tests/test_gwy.py b/tests/test_gwy.py index 3a0d21a..ac5a8bc 100644 --- a/tests/test_gwy.py +++ b/tests/test_gwy.py @@ -3,10 +3,10 @@ from pathlib import Path from typing import Any -import pytest import numpy as np -from AFMReader import gwy +import pytest +from AFMReader import gwy BASE_DIR = Path.cwd() RESOURCES = BASE_DIR / "tests" / "resources" diff --git a/tests/test_h5jpk.py b/tests/test_h5jpk.py new file mode 100644 index 0000000..52dbefd --- /dev/null +++ b/tests/test_h5jpk.py @@ -0,0 +1,147 @@ +"""Test the loading of .5h-jpk files.""" + +from pathlib import Path + +import numpy as np +import pytest + +from AFMReader import h5_jpk + +BASE_DIR = Path.cwd() +RESOURCES = BASE_DIR / "tests" / "resources" + + +@pytest.mark.parametrize( + ( + "file_name", + "channel", + "flip_image", + "pixel_to_nm_scaling", + "image_shape", + "image_dtype", + "timestamps_dtype", + "image_sum", + ), + [ + pytest.param( + "sample_0.h5-jpk", + "height_trace", + True, + 1.171875, + (4, 128, 128), + float, + dict, + 48525583.047271535, + id="test image 0", + ), + pytest.param( + "sample_0.h5-jpk", + "height_retrace", + True, + 1.171875, + (4, 128, 128), + float, + dict, + 48517762.77380567, + id="test image 0", + ), + pytest.param( + "sample_0.h5-jpk", + "error_trace", + True, + 1.171875, + (4, 128, 128), + float, + dict, + -360.7100517131785, + id="test image 0", + ), + pytest.param( + "sample_0.h5-jpk", + "error_retrace", + True, + 1.171875, + (4, 128, 128), + float, + dict, + 367.81162274907103, + id="test image 0", + ), + pytest.param( + "sample_0.h5-jpk", + "phase_retrace", + True, + 1.171875, + (4, 128, 128), + float, + dict, + 1741828.7412469066, + id="test image 0", + ), + pytest.param( + "sample_0.h5-jpk", + "phase_trace", + True, + 1.171875, + (4, 128, 128), + float, + dict, + 1734511.5577225098, + id="test image 0", + ), + pytest.param( + "sample_0.h5-jpk", + "amplitude_retrace", + True, + 1.171875, + (4, 128, 128), + float, + dict, + 275567.73614739266, + id="test image 0", + ), + pytest.param( + "sample_0.h5-jpk", + "amplitude_trace", + True, + 1.171875, + (4, 128, 128), + float, + dict, + 276296.25732934737, + id="test image 0", + ), + ], +) +def test_load_h5jpk( + file_name: str, + channel: str, + flip_image: bool, + pixel_to_nm_scaling: float, + image_shape: tuple[int, int, int], + image_dtype: type[np.floating], + timestamps_dtype: type, + image_sum: float, +) -> None: + """Test the normal operation of loading a .h5-jpk file.""" + result_image, result_pixel_to_nm_scaling, results_timestamps = h5_jpk.load_h5jpk( + RESOURCES / file_name, channel, flip_image + ) + + assert result_pixel_to_nm_scaling == pixel_to_nm_scaling + assert isinstance(result_image, np.ndarray) + assert result_image.shape == image_shape + assert result_image.dtype == np.dtype(image_dtype) + assert isinstance(results_timestamps, timestamps_dtype) + assert result_image.sum() == image_sum + assert len(results_timestamps) == result_image.shape[0] + assert all( + results_timestamps[f"frame {i}"] < results_timestamps[f"frame {i+1}"] + for i in range(len(results_timestamps) - 1) + ) + + +def test_load_h5jpk_file_not_found() -> None: + """Ensure FileNotFound error is raised.""" + with pytest.raises(FileNotFoundError): + h5_jpk.load_h5jpk("nonexistant_file.h5-jpk", channel="TP") diff --git a/tests/test_ibw.py b/tests/test_ibw.py index e03ecf3..af2a302 100644 --- a/tests/test_ibw.py +++ b/tests/test_ibw.py @@ -1,9 +1,9 @@ """Test the loading of ibw files.""" from pathlib import Path -import pytest import numpy as np +import pytest from AFMReader import ibw diff --git a/tests/test_io.py b/tests/test_io.py index 15403ca..885628f 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -2,11 +2,10 @@ from pathlib import Path -import numpy as np import h5py +import numpy as np - -from AFMReader.io import unpack_hdf5, read_yaml +from AFMReader.io import read_yaml, unpack_hdf5 # mypy: disable-error-code="index" diff --git a/tests/test_jpk.py b/tests/test_jpk.py index 4e0011d..9bd0c16 100644 --- a/tests/test_jpk.py +++ b/tests/test_jpk.py @@ -1,9 +1,9 @@ """Test the loading of jpk files.""" from pathlib import Path -import pytest import numpy as np +import pytest from AFMReader import jpk diff --git a/tests/test_spm.py b/tests/test_spm.py index 0d9cde7..b40331c 100644 --- a/tests/test_spm.py +++ b/tests/test_spm.py @@ -2,7 +2,7 @@ import re from pathlib import Path -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import numpy as np import pySPM diff --git a/tests/test_stp.py b/tests/test_stp.py index 897c365..dc6400f 100644 --- a/tests/test_stp.py +++ b/tests/test_stp.py @@ -1,9 +1,9 @@ """Test the loading of .stp files.""" from pathlib import Path -import pytest import numpy as np +import pytest from AFMReader.stp import load_stp diff --git a/tests/test_top.py b/tests/test_top.py index 6eef2bf..eacfb26 100644 --- a/tests/test_top.py +++ b/tests/test_top.py @@ -1,9 +1,9 @@ """Test the loading of .top files.""" from pathlib import Path -import pytest import numpy as np +import pytest from AFMReader.top import load_top diff --git a/tests/test_topostats.py b/tests/test_topostats.py index f6913c0..9f4b6e4 100644 --- a/tests/test_topostats.py +++ b/tests/test_topostats.py @@ -1,8 +1,8 @@ """Test the loading of topostats (HDF5 format) files.""" from pathlib import Path -import pytest +import pytest from AFMReader import topostats