Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def tests(session: nox.Session) -> None:
"pytest-cov",
"pytest-markdown-docs",
"six", # centrosome runtime dep, not declared in its metadata
".",
".[numba]", # exercise the numba backend + correctness harness in CI
)
except Exception:
session.skip(
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ dependencies = [
]

[project.optional-dependencies]
numba = [
"numba>=0.59",
]
test = [
"pytest>=8.4.2",
"pytest-cov",
Expand Down
14 changes: 14 additions & 0 deletions src/cp_measure/_detect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Backend capability detection.

Detected ONCE at import via ``importlib.util.find_spec`` — availability is
checked without importing the package or catching ImportErrors. Dispatch reads
the flag; the resolved backend path is then called directly and unguarded
(a backend that is flagged present but raises is a real bug and must surface,
not be papered over by a try/except fallback).

Other backends (jax, ...) add their own flags here as they are wired.
"""

import importlib.util

HAS_NUMBA: bool = importlib.util.find_spec("numba") is not None
35 changes: 32 additions & 3 deletions src/cp_measure/bulk.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,30 @@
_3D_FEATURES = ("intensity", "sizeshape", "texture", "granularity")


def _numba_registries() -> dict[str, dict[str, Callable]]:
"""Registries for the 'numba' accelerator.

Composes the numba implementations (``intensity``, ``granularity``) with the
numpy implementations of every other feature — a single global "numba"
selection still yields a full, working feature set, accelerated where a
numba backend exists. This is explicit per-function composition, NOT an
error-driven fallback.
"""
from cp_measure.core.numba import (
get_granularity as _numba_granularity,
get_intensity as _numba_intensity,
)

return {
"core": {
**_CORE,
"intensity": _numba_intensity,
"granularity": _numba_granularity,
},
"correlation": _CORRELATION,
}


def _dispatch(name: str) -> dict[str, Callable]:
from cp_measure import _ACCELERATOR

Expand All @@ -55,9 +79,14 @@ def _dispatch(name: str) -> dict[str, Callable]:
f"'jax' accelerator not yet wired for {name} measurements"
)
if _ACCELERATOR == "numba":
raise NotImplementedError(
f"'numba' accelerator not yet wired for {name} measurements"
)
from cp_measure._detect import HAS_NUMBA

if not HAS_NUMBA:
raise RuntimeError(
"accelerator 'numba' selected but numba is not installed; "
"you can install it via `pip install cp_measure[numba]`"
)
return _numba_registries()[name]
if _ACCELERATOR == "fastest":
raise NotImplementedError("'fastest' logic not yet implemented")
raise ValueError(
Expand Down
15 changes: 15 additions & 0 deletions src/cp_measure/core/numba/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Numba-accelerated backend.

Selected explicitly by import (``from cp_measure.core.numba import get_intensity``)
or globally via ``cp_measure.set_accelerator("numba")``. Requires the optional
``numba`` extra; availability is gated by ``cp_measure._detect.HAS_NUMBA``.

This backend accelerates ``intensity`` and ``granularity``; the global "numba"
accelerator composes them with the numpy implementations of every other feature
(see ``cp_measure.bulk``).
"""

from cp_measure.core.numba.measuregranularity import get_granularity
from cp_measure.core.numba.measureobjectintensity import get_intensity

__all__ = ["get_granularity", "get_intensity"]
Loading