Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5b2cb68
ci(dependabot): bump the actions group with 2 updates
dependabot[bot] Apr 1, 2026
9780d28
ci(pre-commit.ci): autoupdate
pre-commit-ci[bot] Jun 1, 2026
aa047fd
Migrate to numpy 2.x, pydantic 2.13, and target Python 3.12-3.14
tlambert03 Jun 22, 2026
8cf007a
refactor: remove unused TypeVar and clean up imports
tlambert03 Jun 22, 2026
1068873
unskip mpl
tlambert03 Jun 22, 2026
9b2ceb9
feat: update pillow dependency constraints for Python 3.14+
tlambert03 Jun 22, 2026
84633da
fix(ci): py3.14 ResourceWarnings + transitive cp314 wheel floors
tlambert03 Jun 22, 2026
e327bcd
fix(docs): pin griffe-fieldz==0.1.2 (0.5.0 needs newer griffe than pi…
tlambert03 Jun 22, 2026
bab1ab6
fix(ci): ignore postgrest parameter-deprecation warnings
tlambert03 Jun 22, 2026
b0d3230
test(ci): use COVERAGE_CORE=sysmon (py3.12+)
tlambert03 Jun 22, 2026
e7547f6
test(ci): TEMP diagnostic - import each C-extension to find Py_Finali…
tlambert03 Jun 22, 2026
569adb3
test(ci): TEMP diagnostic - exercise backends to find Windows teardow…
tlambert03 Jun 22, 2026
213fa52
fix(deps): pin tensorstore versions for Windows compatibility with Py…
tlambert03 Jun 22, 2026
96efeb4
test(ci): remove TEMP Windows teardown diagnostic step
tlambert03 Jun 22, 2026
cfe6c14
Apply suggestion from @tlambert03
tlambert03 Jun 23, 2026
c6f99aa
Merge branches 'pr-136' and 'pr-137' into numpy2-py312-modernization
tlambert03 Jun 24, 2026
9c7c23e
style: apply ruff 0.15 UP042 (str+Enum -> StrEnum)
tlambert03 Jun 24, 2026
90e418b
Merge branch 'numpy2-py312-modernization' of https://github.com/tlamb…
tlambert03 Jun 24, 2026
2c65537
chore: update dependencies in pre-commit and pyproject configurations
tlambert03 Jun 24, 2026
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
13 changes: 10 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,15 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.12", "3.13", "3.14"]
min: [""]
exclude:
# tensorstore has no clean cp314 Windows wheel: the first one (0.1.79)
# and everything >=0.1.77 crash at interpreter shutdown on Windows
# (google/tensorstore#275), and there's no earlier cp314 wheel to pin
# to. Drop this leg until tensorstore ships a fix.
- os: windows-latest
python-version: "3.14"
include:
- os: ubuntu-latest
python-version: "3.12"
Expand All @@ -51,7 +58,7 @@ jobs:

- run: uv run coverage run -p -m pytest --color=yes -v

- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: covreport-${{ matrix.os }}-py${{ matrix.python-version }}-${{ matrix.min }}
path: ./.coverage*
Expand Down Expand Up @@ -82,7 +89,7 @@ jobs:
contents: write
steps:
- name: Download built artifact to dist/
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: Packages
path: dist
Expand Down
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,29 @@ ci:

repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1
rev: v0.25
hooks:
- id: validate-pyproject

- repo: https://github.com/adhtruong/mirrors-typos
rev: v1.41.0
rev: v1.47.2
hooks:
- id: typos
args: [--force-exclude] # omitting --write-changes

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.10
rev: v0.15.19
hooks:
- id: ruff-check
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
rev: v2.1.0
hooks:
- id: mypy
files: "^src/"
additional_dependencies:
- numpy<2.0
- numpy
- pydantic
- pydantic-settings
- xarray
Expand Down
65 changes: 46 additions & 19 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,40 @@ name = "microsim"
dynamic = ["version"]
description = "Light microscopy simulation in python"
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.12"
license = { text = "BSD-3-Clause" }
authors = [{ name = "Talley Lambert", email = "talley.lambert@gmail.com" }]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = [
"annotated-types >=0.7.0",
"numpy >=1.26,<2.0", # something still breaks with 2+
# min pins per-python reflect the first release shipping a wheel for that
# interpreter (newer pythons need newer compiled deps).
"numpy >=2.3.2; python_version >= '3.14'",
"numpy >=2.1.0; python_version >= '3.13'",
"numpy >=2.0",
"pint >=0.23",
"platformdirs >=4.3.6",
"pydantic >=2.12; python_version >= '3.14'",
"pydantic >=2.8",
"fpbase",
"pydantic-settings >2.5.0",
"scipy >=1.16.1; python_version >= '3.14'",
"scipy >=1.15.2",
"tensorstore >=0.1.70,!=0.1.72",
# tensorstore >=0.1.77 crashes at interpreter shutdown on Windows when the
# S3/HTTP transport has been used (google/tensorstore#275). Pin below it on
# Windows where a clean wheel exists (no cp314 wheel before 0.1.79, so
# py3.14 can't be pinned and is unaffected by this cap).
"tensorstore <0.1.77; sys_platform == 'win32' and python_version < '3.14'",
"tensorstore >=0.1.79; python_version >= '3.14'",
"tensorstore >=0.1.76",
"tqdm >=4.67.1",
"xarray >=2024.1.0",
"zarr >=2.17,<3.0",
Expand All @@ -50,7 +62,13 @@ repository = "https://github.com/tlambert03/microsim"

# # https://peps.python.org/pep-0621/#dependencies-optional-dependencies
[project.optional-dependencies]
view = ["ndv[pyqt,vispy]", "matplotlib>=3.10.1", "pyopengl-accelerate"]
view = [
"ndv[pyqt,vispy]",
"matplotlib>=3.10.5; python_version >= '3.14'",
"matplotlib>=3.10.1",
# optional OpenGL speedup; no cp314 wheel and fails to build from source
"pyopengl-accelerate>=3.1.9; python_version < '3.14'",
]
io = ["tifffile>=2025.2.18", "imageio[ffmpeg]>=2.22"]
cosem = [
"dask>=2024.1.0",
Expand All @@ -61,26 +79,22 @@ cosem = [
"xarray-datatree>=0.0.14",
]
torch = ["torch>=2.6.0", "torchaudio>=2.7.0"]
jax = [
"jax[cpu]>=0.5.2",
"dm-tree ==0.1.8; sys_platform == 'darwin'",
"dm-tree >=0.1.7",
]
jax = ["jax[cpu]>=0.5.2"]
all = ["microsim[view,io,cosem]", "rich>=13"]

[dependency-groups]
test = ["pytest>=8.3.5", "pytest-cov>=6.0.0"]
dev = [
{ include-group = "test" },
"microsim[all,jax,torch]",
"boto3-stubs[s3]",
"ipython",
"mypy",
"pdbpp; sys_platform == 'darwin'",
"pre-commit-uv",
"rich",
"ruff",
"types-tqdm",
"prek>=0.4.5",
"rich>=15.0.0",
"ruff>=0.15.18",
"types-tqdm>=4.68.0.20260608",
"mypy>=2.1.0",
"ipython>=9.14.1",
"boto3-stubs[s3]>=1.43.34",
]
docs = [
"mkdocs >=1.5.3",
Expand All @@ -90,15 +104,20 @@ docs = [
"mkdocstrings-python ==1.9.2",
"mkdocs-jupyter==0.24.8",
"griffe==0.45.3",
"griffe-fieldz",
"griffe-fieldz==0.1.2", # 0.5.0 needs a newer griffe than the pin above
]

[tool.uv]
# pillow (transitive via matplotlib/imageio) only ships cp314 wheels from 11.3.0
constraint-dependencies = ["pillow>=11.3.0; python_version >= '3.14'"]

[tool.uv.sources]
microsim = { workspace = true }


[tool.ruff]
line-length = 88
target-version = "py312"
src = ["src"]
fix = true
unsafe-fixes = true
Expand Down Expand Up @@ -167,6 +186,14 @@ filterwarnings = [
"ignore:unclosed transport:ResourceWarning", # only on py3.10... not sure why
"ignore:Clipping negative intensity values:UserWarning", # when importing FPbase spectra
"ignore:__array__ implementation doesn't accept a copy keyword",
# xarray triggers this numpy>=2.4 deprecation at import time (xarray/core/dtypes.py)
"ignore:The 'generic' unit for NumPy timedelta is deprecated:DeprecationWarning",
# py3.14's GC surfaces unclosed sockets/responses from third-party network
# clients (supabase/s3fs/urllib) as fatal unraisable ResourceWarnings.
"ignore::ResourceWarning",
"ignore::pytest.PytestUnraisableExceptionWarning",
# supabase>=2.31 passes now-deprecated params (timeout/verify/proxy) to postgrest
"ignore:The '.*' parameter is deprecated:DeprecationWarning",
]

# https://coverage.readthedocs.io/en/6.4/config.html
Expand Down
5 changes: 4 additions & 1 deletion src/microsim/_data_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
from collections.abc import Mapping, Sequence
from typing import TYPE_CHECKING, Any, Protocol, cast, runtime_checkable

import numpy as np
import xarray
from pydantic import BaseModel

if TYPE_CHECKING:
from collections.abc import Hashable, Iterable, Mapping, Sequence
from pathlib import Path

import numpy as np
import numpy.typing as npt

from microsim.schema.backend import NumpyAPI
Expand Down Expand Up @@ -82,6 +82,9 @@ def to_cache(da: xrDataArray, path: Path, dtype: npt.DTypeLike | None = None) ->
da.attrs = _serializable_attrs(da.attrs)
if hasattr(da.data, "get"):
da.data = da.data.get()
# coerce to a contiguous numpy array: numcodecs calls np.array(..., copy=False),
# which raises on numpy>=2 for non-contiguous or non-numpy (e.g. jax) buffers.
da.data = np.ascontiguousarray(da.data)
da.to_zarr(path, mode="w")


Expand Down
16 changes: 9 additions & 7 deletions src/microsim/cosem/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import os
import urllib.response
from contextlib import suppress
from urllib.error import URLError
from urllib.error import HTTPError, URLError

try:
import boto3
Expand All @@ -26,7 +26,7 @@
from functools import cache
from pathlib import Path
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, TypeVar, get_args
from typing import TYPE_CHECKING, Any, get_args

import tqdm
from pydantic import BaseModel
Expand Down Expand Up @@ -107,7 +107,12 @@ def _guess_cosem_url_key() -> tuple[str, str]:
url = js_text.split("SUPABASE_URL:")[1].split(",")[0].strip("\"'")
key = js_text.split("SUPABASE_KEY:")[1].split(",")[0].strip("\"'")
return url, key
except URLError:
except URLError as e:
# HTTPError (raised by urlopen on e.g. a 404) is a file-like
# object holding an open socket; close it so it isn't reclaimed
# later as a ResourceWarning (fatal under filterwarnings=error).
if isinstance(e, HTTPError):
e.close()
continue
except Exception as e:
raise ValueError(f"Failed to fetch Supabase URL and key: {e}") from e
Expand Down Expand Up @@ -203,10 +208,7 @@ def _collect_fields(model: type[BaseModel]) -> Iterator[str | tuple]:
yield field


T = TypeVar("T", bound=BaseModel)


def fetch_all(type_: type[T]) -> tuple[T, ...]:
def fetch_all[T: BaseModel](type_: type[T]) -> tuple[T, ...]:
table_name = type_.__name__.lower().replace("cosem", "")
query = model_query(type_)
try:
Expand Down
6 changes: 4 additions & 2 deletions src/microsim/fft/convolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@


@no_type_check
def _apply_conv_mode_no_copy(ret, s1, s2, mode, axes):
# same as scipy.signal._signaltools._apply_conv_mode but without the .copy() call
def _apply_conv_mode_no_copy(ret, s1, s2, mode, axes, xp=None):
# same as scipy.signal._signaltools._apply_conv_mode but without the copy.
# `xp` is accepted (and ignored) for compatibility with scipy >=1.15, which
# added an array-namespace argument and copies via `xp_copy(..., xp=xp)`.
if mode == "full":
return ret
elif mode == "same":
Expand Down
2 changes: 1 addition & 1 deletion src/microsim/illum/_sim.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def axial_sim_plane(
wvl: float = 0.488,
) -> np.ndarray:
"""Return single axial SIM illumination plane."""
return structillum_2d( # type: ignore [no-any-return]
return structillum_2d(
nz=nz + 1,
nx=nx,
dz=dz,
Expand Down
2 changes: 1 addition & 1 deletion src/microsim/schema/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

def validate_np_dtype(x: Any) -> np.dtype:
try:
return np.dtype(x)
return np.dtype(x) # type: ignore [no-any-return]
except Exception as e:
raise ValueError(f"Cannot cast {x!r} to a numpy dtype: {e}") from e

Expand Down
10 changes: 5 additions & 5 deletions src/microsim/schema/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@
if TYPE_CHECKING:
from collections.abc import Sequence
from types import ModuleType
from typing import SupportsIndex, TypeAlias
from typing import SupportsIndex

import jax

from microsim._data_array import ArrayProtocol

_Shape: TypeAlias = tuple[int, ...]
type _Shape = tuple[int, ...]

# Anything that can be coerced to a shape tuple
_ShapeLike: TypeAlias = SupportsIndex | Sequence[SupportsIndex]
type _ShapeLike = SupportsIndex | Sequence[SupportsIndex]

DeviceName = Literal["cpu", "gpu", "auto"]
BackendName = Literal["numpy", "torch", "jax", "cupy", "auto"]
Expand Down Expand Up @@ -93,14 +93,14 @@ def asarray(
return self.xp.asarray(x, dtype=dtype)

def zeros(
self, shape: int | Sequence[int], dtype: npt.DTypeLike = None
self, shape: int | Sequence[int], dtype: npt.DTypeLike | None = None
) -> npt.NDArray:
if dtype is None:
dtype = self.float_dtype
return self.xp.zeros(shape, dtype=dtype)

def ones(
self, shape: int | Sequence[int], dtype: npt.DTypeLike = None
self, shape: int | Sequence[int], dtype: npt.DTypeLike | None = None
) -> npt.NDArray:
if dtype is None:
dtype = self.float_dtype
Expand Down
4 changes: 2 additions & 2 deletions src/microsim/schema/dimensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- here intensity values correspond to gray values (0-255) in the image.
"""

from enum import Enum
from enum import StrEnum
from typing import NamedTuple


Expand All @@ -31,7 +31,7 @@ def categorical(self) -> bool:
return self.category_type is not None


class Axis(str, Enum):
class Axis(StrEnum):
"""Named axes used in microsim."""

X = "x"
Expand Down
5 changes: 4 additions & 1 deletion src/microsim/schema/modality/_simple_psf.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,10 @@ def bin_spectrum(
# a bin length of at least min_bin_length and at most max_bin_length
if isinstance(bins, int):
num_bins = bins
w_min, w_max = masked.w.min(), masked.w.max()
# cast to float: passing 0-d xarray DataArrays to np.linspace triggers
# __array_wrap__ on numpy >=2, which rewraps the result into a Variable
# with mismatched dims.
w_min, w_max = float(masked.w.min()), float(masked.w.max())
w_range = w_max - w_min
bin_length = w_range / num_bins
if max_bin_length is not None:
Expand Down
3 changes: 3 additions & 0 deletions src/microsim/schema/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,9 @@ def _write(self, result: xr.DataArray) -> None:
return
if hasattr(result.data, "get"):
result = result.copy(data=result.data.get(), deep=False)
# coerce backend arrays (e.g. jax) to a contiguous numpy array; numcodecs
# calls np.array(..., copy=False), which raises on numpy>=2 otherwise.
result = result.copy(data=np.ascontiguousarray(result.data), deep=False)
with suppress(Exception):
try:
sim_data = self.model_dump_json()
Expand Down
Loading
Loading