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
5 changes: 4 additions & 1 deletion .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ jobs:
run: uv sync

- name: Lint
# uv run, not uv tool run: the latter resolves ruff independently and
# would ignore the version pinned in the lint dependency-group, so a new
# ruff release could turn CI red with no change to this repo.
run:
uv tool run ruff check --output-format=github src
uv run ruff check --output-format=github src

- name: Run unit tests
run: uv run pytest tests/unit tests/dim_reduce -v
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.12
rev: v0.16.2
hooks:
- id: ruff
args: [ --fix ]
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ dynamic = ["version"]
dependencies = [
"ezmsg>=3.9.0",
"ezmsg-baseproc>=1.7.0",
"ezmsg-sigproc>=2.34.0",
"ezmsg-sigproc>=3.0.0",
"pandas>=2.2",
"river>=0.22.0",
"scikit-learn>=1.6.0",
Expand All @@ -25,7 +25,7 @@ dev = [
"pre-commit>=4.3.0",
]
lint = [
"ruff>=0.12.9",
"ruff==0.16.2",
]
test = [
"ezmsg-simbiophys>=1.8.0",
Expand Down
18 changes: 4 additions & 14 deletions src/ezmsg/learn/collection/sample_adapt_regressor.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,7 @@ class DecodeOutputAdapterProcessor(

def _reset_state(self, message: AxisArray) -> None:
if self.settings.output_labels is not None:
self.state.ch_axis = AxisArray.CoordinateAxis(
data=np.asarray(self.settings.output_labels), dims=["ch"]
)
self.state.ch_axis = AxisArray.CoordinateAxis(data=np.asarray(self.settings.output_labels), dims=["ch"])

def _process(self, message: AxisArray) -> AxisArray | None:
data = np.asarray(message.data, dtype=float)
Expand All @@ -97,9 +95,7 @@ def _process(self, message: AxisArray) -> AxisArray | None:
ch_axis = self.state.ch_axis
else:
data = data.reshape((data.shape[0], -1)) if data.ndim > 1 else data.reshape((1, -1))
ch_axis = AxisArray.CoordinateAxis(
data=np.asarray([f"ch{i}" for i in range(data.shape[-1])]), dims=["ch"]
)
ch_axis = AxisArray.CoordinateAxis(data=np.asarray([f"ch{i}" for i in range(data.shape[-1])]), dims=["ch"])

# The decoder engines carry a ``time`` axis through (kalman keeps the
# input's; the torch path inherits the windower's renamed ``win``->``time``
Expand Down Expand Up @@ -266,9 +262,7 @@ def configure(self) -> None:
# Window requires zero_pad_until="input" when
# window_shift is None (1:1 mode); "none" there only
# warns and is coerced to "input".
zero_pad_until="none"
if self.SETTINGS.decode_window_shift is not None
else "input",
zero_pad_until="none" if self.SETTINGS.decode_window_shift is not None else "input",
)
)
self.FLATTEN.apply_settings(
Expand All @@ -293,11 +287,7 @@ def configure(self) -> None:
)
)
if needs_adapter:
self.ADAPTER.apply_settings(
DecodeOutputAdapterSettings(
output_labels=self.SETTINGS.output_labels
)
)
self.ADAPTER.apply_settings(DecodeOutputAdapterSettings(output_labels=self.SETTINGS.output_labels))

def network(self) -> ez.NetworkDefinition:
network = []
Expand Down
314 changes: 170 additions & 144 deletions src/ezmsg/learn/process/ssr.py

Large diffs are not rendered by default.

32 changes: 16 additions & 16 deletions tests/benchmark/bench_lrr.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@
# ---------------------------------------------------------------------------

N_CH = 512
N_CLUSTERS = 8
CLUSTER_SIZE = N_CH // N_CLUSTERS # 64
N_GROUPS = 8
GROUP_SIZE = N_CH // N_GROUPS # 64
FS = 30_000.0
CHUNK_SIZES = [20, 50, 100, 150, 200, 300]
WARMUP_ITERS = 20
BENCH_ITERS = 200

CLUSTERS = [list(range(i * CLUSTER_SIZE, (i + 1) * CLUSTER_SIZE)) for i in range(N_CLUSTERS)]
GROUPS = [list(range(i * GROUP_SIZE, (i + 1) * GROUP_SIZE)) for i in range(N_GROUPS)]


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -89,14 +89,14 @@ def _bench_loop_sync(fn, sync_fn, n_warmup: int, n_iters: int) -> list[float]:

def bench_process_numpy() -> None:
_print_header("_process (inference) — NumPy")
print(f" {N_CH} channels, {N_CLUSTERS}x{CLUSTER_SIZE} clusters, {WARMUP_ITERS} warmup, {BENCH_ITERS} iters")
print(f" {N_CH} channels, {N_GROUPS}x{GROUP_SIZE} groups, {WARMUP_ITERS} warmup, {BENCH_ITERS} iters")
print()

rng = np.random.default_rng(0)

# Fit via partial_fit so the message hash is primed for send()
fit_data = rng.standard_normal((2000, N_CH))
proc = LRRTransformer(LRRSettings(channel_clusters=CLUSTERS, min_cluster_size=1))
proc = LRRTransformer(LRRSettings(channel_groups=GROUPS))
proc.partial_fit(_make_msg(fit_data))

for chunk in CHUNK_SIZES:
Expand All @@ -113,11 +113,11 @@ def bench_process_numpy() -> None:

def bench_partial_fit_numpy() -> None:
_print_header("partial_fit (training) — NumPy")
print(f" {N_CH} channels, {N_CLUSTERS}x{CLUSTER_SIZE} clusters, {WARMUP_ITERS} warmup, {BENCH_ITERS} iters")
print(f" {N_CH} channels, {N_GROUPS}x{GROUP_SIZE} groups, {WARMUP_ITERS} warmup, {BENCH_ITERS} iters")
print()

rng = np.random.default_rng(1)
proc = LRRTransformer(LRRSettings(channel_clusters=CLUSTERS, min_cluster_size=1))
proc = LRRTransformer(LRRSettings(channel_groups=GROUPS))

for chunk in CHUNK_SIZES:
data = rng.standard_normal((chunk, N_CH))
Expand All @@ -144,15 +144,15 @@ def bench_process_mps() -> None:
return

_print_header("_process (inference) — Torch MPS")
print(f" {N_CH} channels, {N_CLUSTERS}x{CLUSTER_SIZE} clusters, {WARMUP_ITERS} warmup, {BENCH_ITERS} iters")
print(f" {N_CH} channels, {N_GROUPS}x{GROUP_SIZE} groups, {WARMUP_ITERS} warmup, {BENCH_ITERS} iters")
print()

rng = np.random.default_rng(0)
device = torch.device("mps")

# Fit on CPU (numpy), then send MPS data to trigger device conversion
fit_data = rng.standard_normal((2000, N_CH))
proc = LRRTransformer(LRRSettings(channel_clusters=CLUSTERS, min_cluster_size=1))
proc = LRRTransformer(LRRSettings(channel_groups=GROUPS))
proc.partial_fit(_make_msg(fit_data))

def sync():
Expand All @@ -178,12 +178,12 @@ def bench_partial_fit_mps() -> None:
return

_print_header("partial_fit (training) — Torch MPS")
print(f" {N_CH} channels, {N_CLUSTERS}x{CLUSTER_SIZE} clusters, {WARMUP_ITERS} warmup, {BENCH_ITERS} iters")
print(f" {N_CH} channels, {N_GROUPS}x{GROUP_SIZE} groups, {WARMUP_ITERS} warmup, {BENCH_ITERS} iters")
print()

_ = np.random.default_rng(1)
device = torch.device("mps")
proc = LRRTransformer(LRRSettings(channel_clusters=CLUSTERS, min_cluster_size=1))
proc = LRRTransformer(LRRSettings(channel_groups=GROUPS))

def sync():
torch.mps.synchronize()
Expand Down Expand Up @@ -213,14 +213,14 @@ def bench_process_mlx() -> None:
return

_print_header("_process (inference) — MLX")
print(f" {N_CH} channels, {N_CLUSTERS}x{CLUSTER_SIZE} clusters, {WARMUP_ITERS} warmup, {BENCH_ITERS} iters")
print(f" {N_CH} channels, {N_GROUPS}x{GROUP_SIZE} groups, {WARMUP_ITERS} warmup, {BENCH_ITERS} iters")
print()

rng = np.random.default_rng(0)

# Fit on CPU (numpy), then send MLX data
fit_data = rng.standard_normal((2000, N_CH))
proc = LRRTransformer(LRRSettings(channel_clusters=CLUSTERS, min_cluster_size=1))
proc = LRRTransformer(LRRSettings(channel_groups=GROUPS))
proc.partial_fit(_make_msg(fit_data))

def sync():
Expand Down Expand Up @@ -251,15 +251,15 @@ def bench_partial_fit_mlx() -> None:
return

_print_header("partial_fit (training) — MLX")
print(f" {N_CH} channels, {N_CLUSTERS}x{CLUSTER_SIZE} clusters, {WARMUP_ITERS} warmup, {BENCH_ITERS} iters")
print(f" {N_CH} channels, {N_GROUPS}x{GROUP_SIZE} groups, {WARMUP_ITERS} warmup, {BENCH_ITERS} iters")
# MLX linalg.inv doesn't support GPU yet; run inv on CPU stream
print(" NOTE: linalg.inv runs on mx.cpu stream (GPU not supported)")
print()

import mlx.core as mx

_ = np.random.default_rng(1)
proc = LRRTransformer(LRRSettings(channel_clusters=CLUSTERS, min_cluster_size=1))
proc = LRRTransformer(LRRSettings(channel_groups=GROUPS))

# Monkey-patch _solve_weights to use mx.cpu stream for inv
original_solve = proc._solve_weights
Expand Down Expand Up @@ -301,7 +301,7 @@ def run():
# ---------------------------------------------------------------------------

if __name__ == "__main__":
print(f"LRRTransformer benchmark: {N_CH} channels, {N_CLUSTERS} clusters of {CLUSTER_SIZE}, fs={FS / 1e3:.0f} kHz")
print(f"LRRTransformer benchmark: {N_CH} channels, {N_GROUPS} groups of {GROUP_SIZE}, fs={FS / 1e3:.0f} kHz")

bench_process_numpy()
bench_partial_fit_numpy()
Expand Down
3 changes: 1 addition & 2 deletions tests/unit/test_adaptive_linear_regressor.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import numpy as np
import pytest
import sklearn.linear_model
from ezmsg.sigproc.window import WindowTransformer
from ezmsg.sigproc.window import WindowSettings
from ezmsg.baseproc import SampleTriggerMessage
from ezmsg.sigproc.window import WindowSettings, WindowTransformer
from ezmsg.util.messages.axisarray import AxisArray, replace

from ezmsg.learn.process.adaptive_linear_regressor import AdaptiveLinearRegressorTransformer
Expand Down
Loading
Loading