Skip to content

Commit c2e2126

Browse files
authored
v0.5.23 PR
v0.5.23
2 parents 6a465b7 + 7d08047 commit c2e2126

45 files changed

Lines changed: 2507 additions & 1145 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ line-length = 88
7575

7676
[tool.ruff.lint]
7777
extend-select = ["E", "F", "I", "N", "B", "SIM", "NPY", "RUF", "PTH", "PD", "FURB"]
78-
ignore = ["F722", "N812", "SIM102", "SIM103", "SIM105", "SIM108", "E501", "N806", "N803", "N815", "RUF005", "SIM300", "C408", "TRY401"]
78+
ignore = ["F722", "N812", "SIM102", "SIM103", "SIM105", "SIM108", "SIM114", "E501", "N806", "N803", "N815", "RUF005", "SIM300", "C408", "TRY401"]
7979

8080
[tool.ruff.lint.per-file-ignores]
8181
"**/__init__.py" = ["F401", "F403"]
@@ -96,5 +96,4 @@ replace-imports-with-any = ["hdbscan"]
9696

9797
[tool.ty.rules]
9898
"unsupported-base" = "ignore"
99-
"invalid-key" = "ignore"
10099
"possibly-unresolved-reference" = "error"

src/dartsort/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,12 @@
7676
get_logger,
7777
set_log_level,
7878
)
79-
from .util.motion import MotionInfo, get_motion_info, try_load_motion_info
79+
from .util.motion import (
80+
MotionInfo,
81+
detect_for_motion,
82+
get_motion_info,
83+
try_load_motion_info,
84+
)
8085
from .util.noise_util import EmbeddedNoise
8186
from .util.preprocess_util import preprocess
8287
from .util.py_util import databag, ensure_path

src/dartsort/clustering/agglomerate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1156,7 +1156,7 @@ def _calc_coentropy(
11561156
u = cands[i]
11571157
q = resps[i]
11581158
log_q = np.log(q)
1159-
np.nan_to_num(log_q, copy=False, neginf=0.0)
1159+
np.nan_to_num(log_q, copy=False, neginf=0.0, posinf=np.inf)
11601160
dh = q * log_q
11611161

11621162
ui0 = u[0]

src/dartsort/clustering/cluster_util.py

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
from typing import cast
22

33
import h5py
4+
import numba
45
import numpy as np
56
from scipy.cluster.hierarchy import fcluster, linkage
67
from scipy.spatial import KDTree
78

8-
from ..util import data_util, waveform_util
9+
from ..util import waveform_util
910
from ..util.data_util import (
1011
DARTsortSorting,
1112
apply_label_remapping_in_place,
13+
count_not_sorted,
1214
mean_by_label_1d,
1315
pos_int_unique_and_counts,
16+
yield_masked_chunks,
1417
)
1518
from ..util.logging_util import get_logger
1619
from ..util.motion import MotionInfo
20+
from ..util.py_util import databag
1721

1822
logger = get_logger(__name__)
1923

@@ -509,7 +513,7 @@ def get_main_channel_pcs(
509513
with h5py.File(sorting.parent_h5_path, "r", locking=False) as h5:
510514
feats_dset = h5[dataset_name]
511515
channel_index = cast(h5py.Dataset, h5["channel_index"])[:]
512-
for ixs, feats in data_util.yield_masked_chunks(
516+
for ixs, feats in yield_masked_chunks(
513517
mask, feats_dset, show_progress=show_progress, desc_prefix="Main channel"
514518
):
515519
feats = feats[:, :rank]
@@ -576,3 +580,102 @@ def decrumb(
576580
if flatten:
577581
sorting = sorting.flatten(in_place=in_place)
578582
return sorting
583+
584+
585+
@databag
586+
class ViolationCounts:
587+
unit_ids: np.ndarray
588+
spike_counts: np.ndarray
589+
"""Same shape as unit_ids (flat)"""
590+
viol_counts: np.ndarray
591+
"""Indexed by pair of ids (not flat)"""
592+
593+
594+
def violation_matrix(
595+
st: DARTsortSorting, *, censor_ms: float = 0.25, viol_ms: float = 1.0
596+
) -> ViolationCounts:
597+
"""Count ACG and CCG violations within viol_ms
598+
599+
Times within censor_ms of each other are ignored in the violation
600+
count. The censorship is right-exclusive, so that if censor_ms is 0,
601+
exact duplicates are counted; if censor_ms corresponds to 10 samples,
602+
9-sample viols are excluded and 10-sample viols are counted.
603+
"""
604+
assert st.labels is not None
605+
censor_samples = int(censor_ms * (st.sampling_frequency / 1000.0))
606+
viol_samples = int(viol_ms * (st.sampling_frequency / 1000.0))
607+
608+
unit_ids, spike_counts, _ = pos_int_unique_and_counts(st.labels)
609+
nu = (unit_ids.max() + 1).item() if unit_ids.size else 0
610+
if not nu or (viol_samples < max(0, censor_samples)):
611+
# nothing can be counted, but keep the matrix shape consistent
612+
return ViolationCounts(
613+
unit_ids=unit_ids,
614+
spike_counts=spike_counts,
615+
viol_counts=np.zeros((nu, nu), dtype=np.int64),
616+
)
617+
618+
labels = st.labels
619+
times = st.times_samples
620+
if count_not_sorted(times) > 0:
621+
tsort = np.argsort(times, kind="stable")
622+
labels = labels[tsort]
623+
times = times[tsort]
624+
625+
# count in chunks, per thread buffer; counts are ti<=tj
626+
n = times.size
627+
nchunks = max(1, numba.get_num_threads())
628+
nchunks = min(nchunks, max(n, 1))
629+
starts = (np.arange(nchunks + 1) * n) // nchunks
630+
viol_counts = np.zeros((nchunks, nu, nu), dtype=np.int64)
631+
632+
_violation_count_matrix(
633+
times, labels, censor_samples, viol_samples, starts, viol_counts
634+
)
635+
636+
viol_counts = viol_counts.sum(axis=0)
637+
viol_diag = np.diagonal(viol_counts).copy()
638+
viol_counts += viol_counts.T
639+
np.fill_diagonal(viol_counts, viol_diag)
640+
641+
return ViolationCounts(
642+
unit_ids=unit_ids,
643+
spike_counts=spike_counts,
644+
viol_counts=viol_counts,
645+
)
646+
647+
648+
@numba.njit(nogil=True, parallel=True)
649+
def _violation_count_matrix(
650+
times: np.ndarray,
651+
labels: np.ndarray,
652+
censor_samples: int,
653+
viol_samples: int,
654+
starts: np.ndarray,
655+
counts: np.ndarray,
656+
):
657+
n = times.shape[0]
658+
659+
# parallelize over chunks
660+
for c in numba.prange(starts.shape[0] - 1): # ty: ignore[not-iterable]
661+
out = counts[c]
662+
663+
for i in range(starts[c], starts[c + 1]):
664+
li = labels[i]
665+
if li < 0:
666+
continue
667+
668+
ti = times[i]
669+
first = ti + censor_samples
670+
last = ti + viol_samples
671+
672+
# make sure to read js past the chunk end!
673+
for j in range(i + 1, n):
674+
if times[j] < first:
675+
continue
676+
if times[j] > last: # be inclusive here i suppose
677+
break
678+
lj = labels[j]
679+
if lj < 0:
680+
continue
681+
out[li, lj] += 1

src/dartsort/clustering/density.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def get_smoothed_density_ratio(
188188
else:
189189
dens_ = dens[0]
190190
dens_ /= dens[1]
191-
np.nan_to_num(dens_, out=dens_) # type: ignore
191+
np.nan_to_num(dens_, out=dens_, posinf=np.inf, neginf=-np.inf) # type: ignore
192192
return dens_
193193

194194

@@ -407,7 +407,7 @@ def kdt_density(
407407
desc=f"KDTdens[{n_jobs}]",
408408
):
409409
density[i0:i1] = dens
410-
np.nan_to_num(density, copy=False)
410+
np.nan_to_num(density, posinf=np.inf, neginf=-np.inf, copy=False)
411411
return density
412412

413413

src/dartsort/clustering/merge.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -358,21 +358,47 @@ def get_deconv_resid_decrease_iter(
358358
)
359359

360360

361-
def combine_templates(template_data_a, template_data_b):
361+
def combine_templates(template_data_a: TemplateData, template_data_b: TemplateData):
362362
rgeom = template_data_a.registered_geom
363363
if rgeom is not None:
364+
assert template_data_b.registered_geom is not None
364365
if not np.array_equal(rgeom, template_data_b.registered_geom):
365366
raise ValueError(
366367
f"Template data had different registered geoms: "
367368
f"{template_data_a.registered_geom=} {template_data_b.registered_geom=}"
368369
)
369370

371+
ta = template_data_a.templates
372+
tb = template_data_b.templates
373+
assert ta.shape[2] == tb.shape[2]
374+
if ta.shape[1] > tb.shape[1]:
375+
i0 = (
376+
template_data_a.trough_offset_samples
377+
- template_data_b.trough_offset_samples
378+
)
379+
assert i0 >= 0
380+
i1 = i0 + template_data_b.spike_length_samples
381+
ta = ta[:, i0:i1]
382+
trough_offset_samples = template_data_b.trough_offset_samples
383+
sampling_frequency = template_data_b.sampling_frequency
384+
elif tb.shape[1] > ta.shape[1]:
385+
i0 = (
386+
template_data_b.trough_offset_samples
387+
- template_data_a.trough_offset_samples
388+
)
389+
assert i0 >= 0
390+
i1 = i0 + template_data_a.spike_length_samples
391+
tb = tb[:, i0:i1]
392+
trough_offset_samples = template_data_a.trough_offset_samples
393+
sampling_frequency = template_data_a.sampling_frequency
394+
else:
395+
trough_offset_samples = template_data_a.trough_offset_samples
396+
sampling_frequency = template_data_a.sampling_frequency
397+
370398
ids_a = template_data_a.unit_ids
371399
ids_b = template_data_b.unit_ids + ids_a.max() + 1
372400
unit_ids = np.concatenate((ids_a, ids_b))
373-
templates = np.concatenate(
374-
(template_data_a.templates, template_data_b.templates), axis=0
375-
)
401+
templates = np.concatenate((ta, tb), axis=0)
376402
spike_counts = np.concatenate(
377403
(template_data_a.spike_counts, template_data_b.spike_counts)
378404
)
@@ -389,8 +415,8 @@ def combine_templates(template_data_a, template_data_b):
389415
spike_counts=spike_counts,
390416
registered_geom=rgeom,
391417
spike_counts_by_channel=spike_counts_by_channel,
392-
trough_offset_samples=template_data_a.trough_offset_samples,
393-
sampling_frequency=template_data_a.sampling_frequency,
418+
trough_offset_samples=trough_offset_samples,
419+
sampling_frequency=sampling_frequency,
394420
)
395421

396422
cross_mask = np.logical_and(

src/dartsort/clustering/mixture.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5855,31 +5855,40 @@ def _noise_factors(*, noise, obs_ix, miss_near_ix, cache_prefix):
58555855
jmnixv = jmnix[jmnixvix]
58565856
ncoi = joixv.numel()
58575857
ncmi = jmnixv.numel()
5858+
assert ncoi > 0
58585859

5860+
# -- observed-only stuff
5861+
# calculate observed-only stuff
58595862
jCoo = noise.marginal_covariance(
58605863
channels=joixv, cache_prefix=cache_prefix, cache_key=j
58615864
)
5862-
jCom = noise.offdiag_covariance(
5863-
channels_left=joixv, channels_right=jmnixv, device=dev
5864-
)
5865-
jCom = jCom.to_dense().to(device=dev)
5866-
58675865
jL = jCoo.cholesky(upper=False) # C = LL'
58685866
jLinv = jL.inverse().to_dense()
58695867
jCooinv = jLinv.T @ jLinv # Cinv = Linv' Linv
58705868
jlogdet = 2.0 * jL.to_dense().diagonal(dim1=-2, dim2=-1).log().sum()
5871-
jCooinvCom = jCooinv @ jCom
58725869

5870+
# set observed-only stuff
58735871
logdet[j] = jlogdet
58745872
# fancy inds to front! love that.
5875-
jCooinv = jCooinv.view(rank, ncoi, rank, ncoi)
5876-
Cooinv[j, :, joixvix[:, None], :, joixvix[None]] = jCooinv.permute(1, 3, 0, 2)
5873+
jCooinv_out = jCooinv.view(rank, ncoi, rank, ncoi).permute(1, 3, 0, 2)
5874+
Cooinv[j, :, joixvix[:, None], :, joixvix[None]] = jCooinv_out
5875+
jLinv_out = jLinv.view(rank, ncoi, rank, ncoi).permute(1, 3, 0, 2)
5876+
Linv[j, :, joixvix[:, None], :, joixvix[None]] = jLinv_out
5877+
5878+
# -- observed-missing stuff
5879+
if ncmi == 0:
5880+
# i am a lonely island
5881+
# importantly, that means my Com is 0. so everything is 0 below.
5882+
continue
5883+
jCom = noise.offdiag_covariance(
5884+
channels_left=joixv, channels_right=jmnixv, device=dev
5885+
)
5886+
jCom = jCom.to_dense().to(device=dev)
5887+
jCooinvCom = jCooinv @ jCom
58775888
jCooinvCom = jCooinvCom.view(rank, ncoi, rank, ncmi)
58785889
CooinvCom[j, :, joixvix[:, None], :, jmnixvix[None]] = jCooinvCom.permute(
58795890
1, 3, 0, 2
58805891
)
5881-
jLinv = jLinv.view(rank, ncoi, rank, ncoi)
5882-
Linv[j, :, joixvix[:, None], :, joixvix[None]] = jLinv.permute(1, 3, 0, 2)
58835892

58845893
obsdim = rank * nc_obs
58855894
missdim = rank * nc_miss_near
@@ -6739,7 +6748,7 @@ def _update_lut_mean_batch(
67396748

67406749
# constplogdet. add in the signal-rank-0-only terms.
67416750
lut_params.constplogdet[i0:i1] = neighb_cov.nobs[nn].mul_(LOG_2PI) # type: ignore # ty: ignore[x]
6742-
lut_params.constplogdet[i0:i1] += neighb_cov.b.logdet[nn]
6751+
lut_params.constplogdet[i0:i1] += neighb_cov.b.logdet[nn] # ty: ignore[not-subscriptable]
67436752
if pnoid:
67446753
assert lut_params.constplogdet[i0:i1].isfinite().all() # type: ignore # ty: ignore[x]
67456754

@@ -6799,7 +6808,7 @@ def _update_lut_ppca_batch(
67996808
assert lut_params.Tpad is not None
68006809
lut_params.Tpad[i0:i1, :, :-1] = T # type: ignore # ty: ignore[x]
68016810
cap_logdet = L.diagonal(dim1=-2, dim2=-1).log().sum(dim=1).mul_(2.0)
6802-
lut_params.constplogdet[i0:i1] += cap_logdet
6811+
lut_params.constplogdet[i0:i1] += cap_logdet # ty: ignore[not-subscriptable]
68036812
if pnoid:
68046813
assert lut_params.b.constplogdet[i0:i1].isfinite().all()
68056814

src/dartsort/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ class DARTsortUserConfig:
205205
"""Upsampling of templates during matching to allow for temporal aliasing of waveforms."""
206206

207207
# -- final merge step
208+
#TODO name this more prominently, clarify flags for dedup ms, decouple dedup and agg
208209
agg_kind: Literal["none", "template_distance", "qda"] = "qda"
209210
"""Final distance or GMM-based merge type."""
210211

0 commit comments

Comments
 (0)