|
1 | 1 | from typing import cast |
2 | 2 |
|
3 | 3 | import h5py |
| 4 | +import numba |
4 | 5 | import numpy as np |
5 | 6 | from scipy.cluster.hierarchy import fcluster, linkage |
6 | 7 | from scipy.spatial import KDTree |
7 | 8 |
|
8 | | -from ..util import data_util, waveform_util |
| 9 | +from ..util import waveform_util |
9 | 10 | from ..util.data_util import ( |
10 | 11 | DARTsortSorting, |
11 | 12 | apply_label_remapping_in_place, |
| 13 | + count_not_sorted, |
12 | 14 | mean_by_label_1d, |
13 | 15 | pos_int_unique_and_counts, |
| 16 | + yield_masked_chunks, |
14 | 17 | ) |
15 | 18 | from ..util.logging_util import get_logger |
16 | 19 | from ..util.motion import MotionInfo |
| 20 | +from ..util.py_util import databag |
17 | 21 |
|
18 | 22 | logger = get_logger(__name__) |
19 | 23 |
|
@@ -509,7 +513,7 @@ def get_main_channel_pcs( |
509 | 513 | with h5py.File(sorting.parent_h5_path, "r", locking=False) as h5: |
510 | 514 | feats_dset = h5[dataset_name] |
511 | 515 | 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( |
513 | 517 | mask, feats_dset, show_progress=show_progress, desc_prefix="Main channel" |
514 | 518 | ): |
515 | 519 | feats = feats[:, :rank] |
@@ -576,3 +580,102 @@ def decrumb( |
576 | 580 | if flatten: |
577 | 581 | sorting = sorting.flatten(in_place=in_place) |
578 | 582 | 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 |
0 commit comments