Skip to content

Commit

Permalink
Merge 72a9856 into 3537050
Browse files Browse the repository at this point in the history
  • Loading branch information
DaniBodor authored Mar 11, 2024
2 parents 3537050 + 72a9856 commit f30183c
Show file tree
Hide file tree
Showing 17 changed files with 121 additions and 53 deletions.
73 changes: 63 additions & 10 deletions eitprocessing/binreader/reader.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import io
import struct
from dataclasses import dataclass
from typing import Any, TypeVar
from typing import Any, Literal, TypeVar

import numpy as np
from numpy.typing import NDArray
Expand All @@ -12,14 +12,45 @@

@dataclass
class Reader:
"""Helper class for reading binary files from disk.
Args:
file_handle: a buffered reader handle, e.g. the result of the `open()` function.
endian: the endianness of the binary data. Either 'little' or 'big', or None.
"""

file_handle: io.BufferedReader
endian: str | None = None
endian: Literal["little", "big"] | None = None

def read_single(self, type_code: str, cast: type[T]) -> T:
"""Read and return a single unit of the given type code.
The type of data to be read should be provided as a single typ code. See
https://docs.python.org/3.10/library/struct.html#byte-order-size-and-alignment for a list of available type
codes.
A unit returns a single value, and can be one or more bytes of data. E.g. requesting a signed 32-bit integer
('q') will result in reading 8 bytes of data.
`cast` should be a type, e.g. `int` or `float` used to cast the value to the proper type.
Args:
type_code: singular type code.
cast: the associated type.
"""
data = self._read_full_type_code(type_code)
return cast(data[0])

def read_list(self, type_code: str, cast: type[T], length: int) -> list[T]:
"""Read multiple values of the same type and return as list.
See `read_single()`.
Args:
type_code: singular type code.
cast: the associated type.
length: number of values to be read.
"""
full_type_code = f"{length}{type_code}"
data = self._read_full_type_code(full_type_code)
return [cast(d) for d in data]
Expand All @@ -30,16 +61,31 @@ def read_array(
cast: type[N],
length: int,
) -> NDArray[N]:
"""Read multiple values of the same type and return as NumPy array.
See `read_list()`.
"""
full_type_code = f"{length}{type_code}"
data = self._read_full_type_code(full_type_code)
return np.array(data, dtype=cast)

def read_string(self, length=1):
def read_string(self, length: int = 1) -> str:
"""Read and return a string with a given length.
Reads `length` characters of type code 's' and returns as a string. When length is not provided, a single
character is returned.
Args:
length: number of characters.
"""
full_type_code = f"{length}s"
data = self._read_full_type_code(full_type_code)
return data[0].decode().rstrip()

def _read_full_type_code(self, full_type_code) -> tuple[Any, ...]:
string = read_string

def _read_full_type_code(self, full_type_code: str) -> tuple[Any, ...]:
"""Read the data associated with the type code."""
if self.endian:
if self.endian not in ["little", "big"]:
msg = f"Endian type '{self.endian}' not recognized. Allowed values are 'little' and 'big'."
Expand All @@ -53,34 +99,41 @@ def _read_full_type_code(self, full_type_code) -> tuple[Any, ...]:
return struct.unpack(full_type_code, packed_data)

def float32(self) -> float:
"""Read and return a single signed 32-bit floating point value."""
return self.read_single(type_code="f", cast=float)

def float64(self) -> float:
"""Read and return a single signed 64-bit floating point value."""
return self.read_single(type_code="d", cast=float)

def npfloat32(self, length=1) -> NDArray[np.float32]:
def npfloat32(self, length: int = 1) -> NDArray[np.float32]:
"""Read and return an array of signed 32-bit floating point values."""
return self.read_array(type_code="f", cast=np.float32, length=length)

def npfloat64(self, length=1) -> NDArray[np.float64]:
def npfloat64(self, length: int = 1) -> NDArray[np.float64]:
"""Read and return an array of signed 64-bit floating point values."""
return self.read_array(type_code="d", cast=np.float64, length=length)

def int32(self) -> int:
"""Read and return a single signed 32-bit integer value."""
return self.read_single(type_code="i", cast=int)

def npint32(self, length=1) -> NDArray[np.int32]:
def npint32(self, length: int = 1) -> NDArray[np.int32]:
"""Read and return an array of signed 32-bit integer values."""
return self.read_array(type_code="i", cast=np.int32, length=length)

def string(self, length=1) -> str:
return self.read_string(length=length)

def uint8(self) -> int:
"""Read and return a single unsigned 8-bit integer value."""
return self.read_single(type_code="B", cast=int)

def uint16(self) -> int:
"""Read and return a single unsigned 16-bit integer value."""
return self.read_single(type_code="H", cast=int)

def uint32(self) -> int:
"""Read and return a single unsigned 32-bit integer value."""
return self.read_single(type_code="I", cast=int)

def uint64(self) -> int:
"""Read and return a single unsigned 64-bit integer value."""
return self.read_single(type_code="Q", cast=int)
3 changes: 2 additions & 1 deletion eitprocessing/eit_data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ def _from_path(
first_frame: int | None = None,
max_frames: int | None = None,
return_non_eit_data: bool = False,
) -> DataCollection | tuple[DataCollection, DataCollection, DataCollection]: ...
) -> DataCollection | tuple[DataCollection, DataCollection, DataCollection]:
...

@staticmethod
def _ensure_path_list(path: str | Path | list[str | Path]) -> list[Path]:
Expand Down
2 changes: 1 addition & 1 deletion eitprocessing/eit_data/draeger.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def _read_frame( # noqa: PLR0913
event_text = reader.string(length=30)
timing_error = reader.int32()

frame_medibus_data = reader.npfloat32(length=52) # noqa;
frame_medibus_data = reader.npfloat32(length=52)

if index < 0:
# do not keep any loaded data, just return the event marker
Expand Down
2 changes: 2 additions & 0 deletions eitprocessing/eit_data/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

@dataclass
class Event:
"""Single time point event registered during an EIT measurement."""

index: int
time: float
marker: int
Expand Down
8 changes: 5 additions & 3 deletions eitprocessing/eit_data/phases.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@

@dataclass
class PhaseIndicator:
"""Parent class for phase indications."""

index: int
time: float


@dataclass
class MinValue(PhaseIndicator):
pass
"""Automatically registered local minimum of an EIT measurement."""


@dataclass
class MaxValue(PhaseIndicator):
pass
"""Automatically registered local maximum of an EIT measurement."""


@dataclass
class QRSMark(PhaseIndicator):
pass
"""Automatically registered QRS mark an EIT measurement from a Timpel device."""
14 changes: 10 additions & 4 deletions eitprocessing/filters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
from abc import ABC, abstractmethod
from typing import NoReturn

import numpy.typing as npt


class TimeDomainFilter(ABC):
"""Parent class for time domain filters."""

class TimeDomainFilter:
available_in_gui = True

def apply_filter(self, input_data) -> NoReturn:
msg = "Implement in subclass"
raise NotImplementedError(msg)
@abstractmethod
def apply_filter(self, input_data: npt.ArrayLike) -> NoReturn:
"""Apply the filter to the input data."""
...
19 changes: 8 additions & 11 deletions eitprocessing/filters/butterworth_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class ButterworthFilter(TimeDomainFilter):
sample_frequency: float
ignore_max_order: InitVar[bool] = False

def __post_init__(self, ignore_max_order):
def __post_init__(self, ignore_max_order: bool):
self._check_init(ignore_max_order)
self._set_filter_type_class()

Expand All @@ -73,9 +73,8 @@ def _set_filter_type_class(self) -> None:
cls = FILTER_TYPES[self.filter_type]
self.__class__ = cls

def _check_init(self, ignore_max_order) -> None:
"""
Check the arguments of __init__ and raise exceptions when they don't meet requirements.
def _check_init(self, ignore_max_order: bool) -> None: # noqa:C901
"""Check the arguments of __init__ and raise exceptions when they don't meet requirements.
Raises:
ValueError: if the `filter_type` is unknown.
Expand All @@ -92,24 +91,22 @@ def _check_init(self, ignore_max_order) -> None:
"""
if self.filter_type in ("lowpass", "highpass"):
if not isinstance(self.cutoff_frequency, int | float):
msg = "Invalid `cutoff_frequency`. " f"Must be an integer or float, not {type(self.cutoff_frequency)}."
msg = f"Invalid `cutoff_frequency`. Must be an integer or float, not {type(self.cutoff_frequency)}."
raise TypeError(msg)

elif self.filter_type in ("bandpass", "bandstop"):
if isinstance(self.cutoff_frequency, list):
self.cutoff_frequency = tuple(self.cutoff_frequency)
elif not isinstance(self.cutoff_frequency, tuple):
msg = "Invalid `cutoff_frequency`. " f"Must be a tuple, not {type(self.cutoff_frequency)}."
msg = f"Invalid `cutoff_frequency`. Must be a tuple, not {type(self.cutoff_frequency)}."
raise TypeError(msg)

if len(self.cutoff_frequency) != 2:
msg = f"Invalid `cutoff_frequency` ({self.cutoff_frequency}). " "Must have length 2."
if len(self.cutoff_frequency) != 2: # noqa: PLR2004
msg = f"Invalid `cutoff_frequency` ({self.cutoff_frequency}). Must have length 2."
raise ValueError(msg)

if not all(isinstance(value, int | float) for value in self.cutoff_frequency):
msg = (
f"Invalid `cutoff_frequency` ({self.cutoff_frequency}). " "Must be a tuple containing two numbers."
)
msg = f"Invalid `cutoff_frequency` ({self.cutoff_frequency}). Must be a tuple containing two numbers."
raise TypeError(msg)
else:
msg = f"Invalid `filter_type` ({self.filter_type}). Must be one of {', '.join(FILTER_TYPES.keys())}."
Expand Down
2 changes: 0 additions & 2 deletions eitprocessing/helper.py

This file was deleted.

2 changes: 1 addition & 1 deletion eitprocessing/mixins/slicing.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class SelectByTime(SelectByIndex):

time: NDArray

def select_by_time(
def select_by_time( # noqa: D417
self,
start_time: float | None = None,
end_time: float | None = None,
Expand Down
Empty file.
3 changes: 3 additions & 0 deletions eitprocessing/plotting/animate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

from eitprocessing.eit_data.eit_data_variant import EITDataVariant

# TODO: remove line below to activate linting
# ruff: noqa


def animate_EITDataVariant(
eit_data_variant: EITDataVariant,
Expand Down
3 changes: 3 additions & 0 deletions eitprocessing/plotting/plot.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from matplotlib import pyplot as plt

# TODO: remove line below to activate linting
# ruff: noqa


def plot_waveforms(self, waveforms=None) -> None:
if waveforms is None:
Expand Down
2 changes: 1 addition & 1 deletion eitprocessing/sparse_data/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
class SparseData:
...
"""SparseData."""
35 changes: 16 additions & 19 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ dependencies = [
"scipy >= 1.10.1",
"tqdm >= 4.65.0",
"strenum >= 0.4.10",
"ruff >= 0.1.13",
"ruff >= 0.3",
]

[project.optional-dependencies]
Expand Down Expand Up @@ -94,6 +94,8 @@ extras = dev

[tool.ruff]
line-length = 120

[tool.ruff.lint]
select = ["ALL"]
ignore = [
# Unwanted (potentially)
Expand Down Expand Up @@ -131,20 +133,9 @@ ignore = [
"D413",
]

# Allow autofix for all enabled rules.
# Set autofixing.
fixable = ["ALL"]
unfixable = ["F401"] # unused imports (should not disappear while editing)

[tool.ruff.lint.per-file-ignores]
"*.ipynb" = ["ERA001"] # Commented out code
"tests/*" = [
"S101", # Use of `assert` detected
"ANN201", # Missing return type
"D103", # Missing function docstring
]
"docs/*" = ["ALL"]

[tool.ruff.lint]
extend-safe-fixes = [
"D415", # First line should end with a period, question mark, or exclamation point
"D300", # Use triple double quotes `"""`
Expand All @@ -156,11 +147,17 @@ extend-safe-fixes = [
"B006", # Mutable default argument
"TID252", # Absolute imports over relative imports
]
pylint.max-args = 7

[tool.ruff.isort]
known-first-party = ["eitprocessing"]
# Override default settings for specific rules
pylint.max-args = 7
flake8-tidy-imports.ban-relative-imports = "all" # Disallow all relative imports.
isort.known-first-party = ["eitprocessing"]

[tool.ruff.lint.flake8-tidy-imports]
# Disallow all relative imports.
ban-relative-imports = "all"
[tool.ruff.lint.per-file-ignores]
"*.ipynb" = ["ERA001"] # Commented out code
"tests/*" = [
"S101", # Use of `assert` detected
"ANN201", # Missing return type
"D103", # Missing function docstring
]
"docs/*" = ["ALL"]
2 changes: 2 additions & 0 deletions tests/mixins/test_eq.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# OLD FILE. TESTS NOT YET FUNCTIONAL
# TODO: remove line below to activate linting
# ruff: noqa

import bisect
import os
Expand Down
2 changes: 2 additions & 0 deletions tests/mixins/test_slicing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import pytest

from eitprocessing.eit_data.draeger import DraegerEITData
# TODO: remove line below to activate linting
# ruff: noqa

environment = os.environ.get(
"EIT_PROCESSING_TEST_DATA",
Expand Down
2 changes: 2 additions & 0 deletions tests/test_butterworth_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
)

SpecifiedFilter: TypeAlias = type[LowPassFilter | HighPassFilter | BandPassFilter | BandStopFilter]
# TODO: remove line below to activate linting
# ruff: noqa


@pytest.fixture()
Expand Down

0 comments on commit f30183c

Please sign in to comment.