Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ if (NOT SKBUILD)
endif()


# search for Python >= 3.9 including the Development.Module component required by nanobind
find_package(Python 3.9 COMPONENTS Interpreter Development.Module REQUIRED)
# search for Python >= 3.10 including the Development.Module component required by nanobind
find_package(Python 3.10 COMPONENTS Interpreter Development.Module REQUIRED)

# set the build type to Release by default
if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
Expand Down
8 changes: 2 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ maintainers = [
]
readme = "README.md"
keywords = ['beamforming', 'delay and sum', 'ultrasound', 'python', 'cuda']
requires-python = ">=3.9,<3.14"
requires-python = ">=3.10,<3.14"
classifiers = [
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand Down Expand Up @@ -146,7 +145,7 @@ build-dir = "build/{wheel_tag}"
wheel.packages = ["src/mach"]

[tool.ruff]
target-version = "py39"
target-version = "py310"
line-length = 120
fix = true

Expand Down Expand Up @@ -196,8 +195,6 @@ ignore = [
"SIM108",
# Allow assert
"S101",
# Union syntax not available in Python 3.9
"UP007",
]

[tool.ruff.lint.per-file-ignores]
Expand Down Expand Up @@ -226,7 +223,6 @@ testpaths = ["tests"]

[tool.cibuildwheel]
build = [
"cp39-manylinux_x86_64",
"cp310-manylinux_x86_64",
"cp311-manylinux_x86_64",
"cp312-manylinux_x86_64",
Expand Down
6 changes: 3 additions & 3 deletions src/mach/_array_api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Array-API utilities."""

from enum import Enum
from typing import Any, Protocol, Union, cast, runtime_checkable
from typing import Any, Protocol, cast, runtime_checkable

from array_api_compat import array_namespace as xpc_array_namespace

Expand Down Expand Up @@ -112,7 +112,7 @@ def __gt__(self, other: Any) -> "Array": ...
def __getitem__(self, key: Any) -> "Array": ...


def array_namespace(*arrays: Any) -> Union[_ArrayNamespace, _ArrayNamespaceWithLinAlg]:
def array_namespace(*arrays: Any) -> _ArrayNamespace | _ArrayNamespaceWithLinAlg:
"""Typed wrapper around array_api_compat.array_namespace.

Returns the array namespace for the given arrays with proper type hints.
Expand All @@ -137,4 +137,4 @@ def array_namespace(*arrays: Any) -> Union[_ArrayNamespace, _ArrayNamespaceWithL
... # Fallback implementation
... norm = xp.sqrt(xp.sum(arr * arr, axis=-1))
"""
return cast(Union[_ArrayNamespace, _ArrayNamespaceWithLinAlg], xpc_array_namespace(*arrays))
return cast(_ArrayNamespace | _ArrayNamespaceWithLinAlg, xpc_array_namespace(*arrays))
5 changes: 2 additions & 3 deletions src/mach/_vis.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Visualization utilities for test-diagnostics."""

from pathlib import Path
from typing import Optional, Union

try:
import matplotlib.pyplot as plt
Expand Down Expand Up @@ -61,7 +60,7 @@ def plot_slice(bm_slice, lats, deps, angle):

def save_debug_figures(
our_result: np.ndarray,
reference_result: Optional[np.ndarray],
reference_result: np.ndarray | None,
grid_shape: tuple[int, ...],
x_axis: np.ndarray,
z_axis: np.ndarray,
Expand All @@ -70,7 +69,7 @@ def save_debug_figures(
our_label: str = "Our Implementation",
reference_label: str = "Reference Implementation",
power_mode: bool = False,
main_cmap: Optional[Union[str, Colormap]] = None,
main_cmap: str | Colormap | None = None,
diff_cmap: str = "magma",
) -> None:
"""Save debug figures comparing beamforming results.
Expand Down
6 changes: 3 additions & 3 deletions src/mach/experimental.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""The specific function arguments / names (API) are experimental and may change."""

from enum import Enum
from typing import Optional, cast
from typing import cast

from jaxtyping import Num, Real

Expand All @@ -25,13 +25,13 @@ def beamform(
rx_coords_m: Real[Array, "n_rx xyz=3"],
scan_coords_m: Real[Array, "n_points xyz=3"],
tx_wave_arrivals_s: Real[Array, "n_transmits n_points"],
out: Optional[Num[Array, "n_points n_frames"]] = None,
out: Num[Array, "n_points n_frames"] | None = None,
*,
rx_start_s: float,
sampling_freq_hz: float,
f_number: float,
sound_speed_m_s: float,
modulation_freq_hz: Optional[float] = None,
modulation_freq_hz: float | None = None,
tukey_alpha: float = 0.5,
) -> Num[Array, "n_points n_frames"]:
"""Wrapper around kernel.beamform that includes coherent compounding.
Expand Down
21 changes: 10 additions & 11 deletions src/mach/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,17 @@

import math
import numbers
from typing import Union

from jaxtyping import Real

from mach._array_api import Array, _ArrayNamespace, array_namespace


def ultrasound_angles_to_cartesian(
azimuth_rad: Union[Real[Array, " *angles"], float, int],
elevation_rad: Union[Real[Array, " *angles"], float, int],
radius_m: Union[Real[Array, " *angles"], float, int] = 1,
) -> Union[Real[Array, "*angles xyz=3"], tuple[float, float, float]]:
azimuth_rad: Real[Array, " *angles"] | float | int,
elevation_rad: Real[Array, " *angles"] | float | int,
radius_m: Real[Array, " *angles"] | float | int = 1,
) -> Real[Array, "*angles xyz=3"] | tuple[float, float, float]:
"""Convert ultrasound angles (azimuth, elevation, radius) to Cartesian coordinates.

The resulting vectors can be used directly with `mach.wavefront.plane()`.
Expand Down Expand Up @@ -87,10 +86,10 @@ def ultrasound_angles_to_cartesian(


def spherical_to_cartesian(
theta_rad: Union[Real[Array, " *angles"], float, int],
phi_rad: Union[Real[Array, " *angles"], float, int],
radius_m: Union[Real[Array, " *angles"], float, int] = 1,
) -> Union[Real[Array, "*angles xyz=3"], tuple[float, float, float]]:
theta_rad: Real[Array, " *angles"] | float | int,
phi_rad: Real[Array, " *angles"] | float | int,
radius_m: Real[Array, " *angles"] | float | int = 1,
) -> Real[Array, "*angles xyz=3"] | tuple[float, float, float]:
"""Convert standard spherical angle convention to a Cartesian vector.

Uses the physics convention as defined in ISO 80000-2:2019.
Expand Down Expand Up @@ -133,8 +132,8 @@ def spherical_to_cartesian(


def _prepare_inputs_and_namespace(
*inputs: Union[Real[Array, "..."], float, int],
) -> tuple[Union[type[math], _ArrayNamespace], tuple]:
*inputs: Real[Array, "..."] | float | int,
) -> tuple[type[math] | _ArrayNamespace, tuple]:
"""Prepare inputs and determine the appropriate namespace (math or array).

For scalar inputs, to avoid requiring a specific array library import,
Expand Down
4 changes: 2 additions & 2 deletions src/mach/io/uff.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
https://github.com/magnusdk/pyuff_ustb
"""

from typing import Any, Optional
from typing import Any

import einops
import numpy as np
Expand Down Expand Up @@ -35,7 +35,7 @@ def extract_wave_directions(sequence: list[Any], xp) -> Array:


def compute_tx_wave_arrivals_s(
directions: Array, scan_coords_m: Array, speed_of_sound: float, origin: Optional[Array] = None, xp=None
directions: Array, scan_coords_m: Array, speed_of_sound: float, origin: Array | None = None, xp=None
) -> Array:
"""
Compute transmit arrival times for plane wave imaging.
Expand Down
37 changes: 19 additions & 18 deletions src/mach/io/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""General utilities for downloading and caching files with integrity verification.

This module provides:
- Efficient file hashing compatible with Python 3.9+ (backport of hashlib.file_digest)
- Efficient file hashing compatible with Python 3.10 (backport of hashlib.file_digest)
- Robust file downloading with progress bars and integrity verification
- Smart caching with automatic re-download on corruption
- Support for multiple hash algorithms (SHA1, SHA256, MD5, etc.)
Expand All @@ -18,8 +18,9 @@
import hashlib
import sys
import warnings
from collections.abc import Callable
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
from typing import TYPE_CHECKING, Any

try:
import requests
Expand All @@ -34,16 +35,16 @@
try:
from tqdm import tqdm
except ImportError:
tqdm: Optional[Any] = None
tqdm: Any | None = None

CACHE_DIR = Path.home() / ".cache" / "mach"


def file_digest(fileobj, digest: Union[str, Callable[[], "hashlib._Hash"]]) -> "hashlib._Hash":
def file_digest(fileobj, digest: str | Callable[[], "hashlib._Hash"]) -> "hashlib._Hash":
"""Return a digest object that has been updated with contents of file object.

This is a backport-compatible implementation of hashlib.file_digest()
that works with Python 3.9+ and follows the same API as Python 3.11+.
that works with Python 3.10 and follows the same API as Python 3.11+.

Args:
fileobj: File-like object opened for reading in binary mode
Expand Down Expand Up @@ -91,7 +92,7 @@ def file_digest(fileobj, digest: Union[str, Callable[[], "hashlib._Hash"]]) -> "
def verify_file_integrity(
file_path: Path,
expected_hash: str,
digest: Union[str, Callable[[], "hashlib._Hash"]],
digest: str | Callable[[], "hashlib._Hash"],
) -> bool:
"""Verify file integrity using specified hash algorithm.

Expand All @@ -114,9 +115,9 @@ def verify_file_integrity(

def _verify_file(
output_path: Path,
expected_size: Optional[int],
expected_hash: Optional[str],
digest: Optional[Union[str, Callable[[], "hashlib._Hash"]]],
expected_size: int | None,
expected_hash: str | None,
digest: str | Callable[[], "hashlib._Hash"] | None,
) -> bool:
"""Verify if existing file meets size and hash requirements.

Expand Down Expand Up @@ -176,14 +177,14 @@ def _download_with_progress(

def download_file(
url: str,
output_path: Union[str, Path],
output_path: str | Path,
timeout: int = 30,
chunk_size: int = 1024 * 1024, # 1MB
*,
overwrite: bool = False,
expected_hash: Optional[str] = None,
digest: Union[None, str, Callable[[], "hashlib._Hash"]] = None,
expected_size: Optional[int] = None,
expected_hash: str | None = None,
digest: None | str | Callable[[], "hashlib._Hash"] = None,
expected_size: int | None = None,
show_progress: bool = (tqdm is not None),
) -> Path:
"""Download a file from a URL with optional progress bar and integrity verification.
Expand Down Expand Up @@ -240,14 +241,14 @@ def download_file(

def cached_download(
url: str,
cache_dir: Union[str, Path] = CACHE_DIR,
filename: Optional[Union[str, Path]] = None,
cache_dir: str | Path = CACHE_DIR,
filename: str | Path | None = None,
timeout: int = 30,
*,
overwrite: bool = False,
expected_size: Optional[int] = None,
expected_hash: Optional[str] = None,
digest: Union[None, str, Callable[[], "hashlib._Hash"]] = None,
expected_size: int | None = None,
expected_hash: str | None = None,
digest: None | str | Callable[[], "hashlib._Hash"] = None,
show_progress: bool = (tqdm is not None),
) -> Path:
"""Download a file and cache it with optional integrity verification.
Expand Down
6 changes: 2 additions & 4 deletions src/mach/kernel.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Python bindings and wrapper for the CUDA kernel."""

from typing import Optional

from array_api_compat import is_writeable_array
from jaxtyping import Num, Real

Expand All @@ -24,13 +22,13 @@ def beamform( # noqa: C901
rx_coords_m: Real[Array, "n_rx xyz=3"],
scan_coords_m: Real[Array, "n_scan xyz=3"],
tx_wave_arrivals_s: Real[Array, " n_scan"],
out: Optional[Num[Array, "n_scan n_frames"]] = None,
out: Num[Array, "n_scan n_frames"] | None = None,
*,
rx_start_s: float,
sampling_freq_hz: float,
f_number: float,
sound_speed_m_s: float,
modulation_freq_hz: Optional[float] = None,
modulation_freq_hz: float | None = None,
tukey_alpha: float = 0.5,
interp_type: InterpolationType = InterpolationType.Linear,
) -> Array:
Expand Down
3 changes: 2 additions & 1 deletion tests/compare/test_pymust.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
pytest tests/test_pymust.py --benchmark-histogram --benchmark-sort=mean
"""

from typing import Any, Callable
from collections.abc import Callable
from typing import Any

import numpy as np
import pytest
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import contextlib
import hashlib
from pathlib import Path
from typing import Optional, cast
from typing import cast

import pytest
from pyuff_ustb import ChannelData, Scan, Uff
Expand Down Expand Up @@ -79,7 +79,7 @@ def test_data_dir():


@pytest.fixture
def output_dir(request) -> Optional[Path]:
def output_dir(request) -> Path | None:
"""Output directory for test results.

Returns None if the --save-output flag is not set.
Expand Down
5 changes: 2 additions & 3 deletions tests/io/test_uff.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Test the UFF data loader."""

from pathlib import Path
from typing import Optional

import numpy as np
import pytest
Expand Down Expand Up @@ -31,7 +30,7 @@ class Scan(OriginalScan):
def test_picmus_phantom_resolution(
picmus_phantom_resolution_channel_data: ChannelData,
picmus_phantom_resolution_scan: Scan,
output_dir: Optional[Path],
output_dir: Path | None,
):
"""Test the Picmus phantom resolution UFF data."""
assert picmus_phantom_resolution_channel_data is not None
Expand Down Expand Up @@ -63,7 +62,7 @@ def test_picmus_phantom_resolution(
def test_picmus_phantom_resolution_single_transmit(
picmus_phantom_resolution_channel_data: ChannelData,
picmus_phantom_resolution_scan: Scan,
output_dir: Optional[Path],
output_dir: Path | None,
):
"""Test the Picmus phantom resolution UFF data with single transmit (backwards compatibility)."""
assert picmus_phantom_resolution_channel_data is not None
Expand Down
3 changes: 1 addition & 2 deletions tests/plot_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import argparse
import json
from pathlib import Path
from typing import Optional

import matplotlib.pyplot as plt
import pandas as pd
Expand Down Expand Up @@ -152,7 +151,7 @@ def plot_benchmark_results(
df: pd.DataFrame,
data: dict,
use_points_per_second: bool = False,
output_path: Optional[str] = None,
output_path: str | None = None,
show_values: bool = True,
) -> None:
"""Create horizontal boxplot of benchmark results."""
Expand Down
Loading