active-units 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- active_units/__init__.py +34 -0
- active_units/activity.py +228 -0
- active_units/cli.py +148 -0
- active_units/dashboard.py +1030 -0
- active_units/gui_launcher.py +644 -0
- active_units/utils.py +188 -0
- active_units-0.1.0.dist-info/METADATA +230 -0
- active_units-0.1.0.dist-info/RECORD +12 -0
- active_units-0.1.0.dist-info/WHEEL +5 -0
- active_units-0.1.0.dist-info/entry_points.txt +3 -0
- active_units-0.1.0.dist-info/licenses/LICENSE +674 -0
- active_units-0.1.0.dist-info/top_level.txt +1 -0
active_units/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Active Units: Interactive unit filtering by activity response.
|
|
3
|
+
|
|
4
|
+
Minimal, flat-structure package for computing and filtering neural units
|
|
5
|
+
based on baseline/response firing rates.
|
|
6
|
+
|
|
7
|
+
Three inputs:
|
|
8
|
+
1. sorted_spikes_dir: Path to spike sorting output folder
|
|
9
|
+
2. trials_csv: Path to trials.csv (columns: experiment, trial, sample)
|
|
10
|
+
3. config: Path to config.json or dict with activity_filter parameters
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
>>> from active_units import compute_activity
|
|
14
|
+
>>> summary = compute_activity(
|
|
15
|
+
... sorted_spikes_dir="/path/to/sorted_spikes",
|
|
16
|
+
... trials_csv="/path/to/trials.csv",
|
|
17
|
+
... config="/path/to/config.json",
|
|
18
|
+
... output_dir="/path/to/output"
|
|
19
|
+
... )
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
__author__ = "Lab Team"
|
|
24
|
+
|
|
25
|
+
from .activity import compute_activity
|
|
26
|
+
from .dashboard import make_app
|
|
27
|
+
from .utils import load_config, load_trials
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"compute_activity",
|
|
31
|
+
"make_app",
|
|
32
|
+
"load_config",
|
|
33
|
+
"load_trials",
|
|
34
|
+
]
|
active_units/activity.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Compute unit activity metrics from kilosort output."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Union, Dict, Any, Tuple
|
|
7
|
+
from scipy import stats
|
|
8
|
+
|
|
9
|
+
from .utils import load_config, load_trials, validate_sorted_spikes_dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def compute_activity(
|
|
13
|
+
sorted_spikes_dir: str,
|
|
14
|
+
trials_csv: str,
|
|
15
|
+
config: Union[str, Dict[str, Any]],
|
|
16
|
+
output_dir: str = None,
|
|
17
|
+
verbose: bool = False,
|
|
18
|
+
) -> pd.DataFrame:
|
|
19
|
+
"""
|
|
20
|
+
Compute baseline/response activity for all units.
|
|
21
|
+
|
|
22
|
+
Loads spike data from spike sorting output, samples random trials per experiment,
|
|
23
|
+
calculates baseline and response firing rates, performs paired t-tests.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
sorted_spikes_dir: Path to spike sorting output folder with spike_times.npy,
|
|
27
|
+
spike_clusters.npy, cluster_group.tsv
|
|
28
|
+
(works with kilosort4, kilosort3, IBL pipeline, etc.)
|
|
29
|
+
trials_csv: Path to trials.csv with columns: experiment, trial, sample
|
|
30
|
+
config: Path to config.json or dict with activity_filter parameters
|
|
31
|
+
Required keys: baseline_window_start/end, response_window_start/end,
|
|
32
|
+
n_random_trials, sample_rate
|
|
33
|
+
output_dir: Directory to save activity_summary.csv (default: current dir)
|
|
34
|
+
verbose: Print diagnostic info
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
pd.DataFrame: Summary statistics with columns:
|
|
38
|
+
experiment, unit, bsl_mean, bsl_std, signal_mean,
|
|
39
|
+
signal_std, difference, p_value
|
|
40
|
+
|
|
41
|
+
Saves:
|
|
42
|
+
- activity_summary.csv: Activity summary per unit per experiment
|
|
43
|
+
- clusters_included.txt: List of cluster IDs that were included
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> summary = compute_activity(
|
|
47
|
+
... sorted_spikes_dir="/path/to/sorted_spikes",
|
|
48
|
+
... trials_csv="/path/to/trials.csv",
|
|
49
|
+
... config="/path/to/config.json",
|
|
50
|
+
... output_dir="/tmp/results",
|
|
51
|
+
... verbose=True
|
|
52
|
+
... )
|
|
53
|
+
>>> print(summary[['experiment', 'unit', 'bsl_mean', 'signal_mean']])
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# Load configuration
|
|
57
|
+
if verbose:
|
|
58
|
+
print("Loading configuration...")
|
|
59
|
+
config_dict = load_config(config)
|
|
60
|
+
|
|
61
|
+
baseline_start_s = config_dict["baseline_window_start"]
|
|
62
|
+
baseline_end_s = config_dict["baseline_window_end"]
|
|
63
|
+
response_start_s = config_dict["response_window_start"]
|
|
64
|
+
response_end_s = config_dict["response_window_end"]
|
|
65
|
+
n_random = config_dict["n_random_trials"]
|
|
66
|
+
sample_rate = config_dict["sample_rate"]
|
|
67
|
+
|
|
68
|
+
# Load trials
|
|
69
|
+
if verbose:
|
|
70
|
+
print("Loading trials...")
|
|
71
|
+
trials_df = load_trials(trials_csv)
|
|
72
|
+
|
|
73
|
+
# Validate sorted spikes directory
|
|
74
|
+
if verbose:
|
|
75
|
+
print("Validating spike sorting output directory...")
|
|
76
|
+
ss_files = validate_sorted_spikes_dir(sorted_spikes_dir)
|
|
77
|
+
|
|
78
|
+
# Load spike data
|
|
79
|
+
if verbose:
|
|
80
|
+
print("Loading spike data...")
|
|
81
|
+
spike_times = np.load(ss_files["spike_times"]).flatten()
|
|
82
|
+
spike_clusters = np.load(ss_files["spike_clusters"]).flatten()
|
|
83
|
+
|
|
84
|
+
# Load or create cluster_group
|
|
85
|
+
if ss_files["cluster_group"] and Path(ss_files["cluster_group"]).exists():
|
|
86
|
+
cluster_group = pd.read_csv(ss_files["cluster_group"], sep="\t")
|
|
87
|
+
valid_mask = cluster_group["group"].isin(["good", "mua"])
|
|
88
|
+
valid_units = cluster_group[valid_mask]["cluster_id"].values
|
|
89
|
+
else:
|
|
90
|
+
# If no cluster_group, use all unique cluster IDs
|
|
91
|
+
valid_units = np.unique(spike_clusters)
|
|
92
|
+
if verbose:
|
|
93
|
+
print(" (cluster_group.tsv not found, using all units)")
|
|
94
|
+
|
|
95
|
+
if verbose:
|
|
96
|
+
print(f" {len(valid_units)} valid units")
|
|
97
|
+
print(f" {len(spike_times)} total spikes")
|
|
98
|
+
|
|
99
|
+
# Convert time windows from seconds to samples
|
|
100
|
+
baseline_start_samples = int(baseline_start_s * sample_rate)
|
|
101
|
+
baseline_end_samples = int(baseline_end_s * sample_rate)
|
|
102
|
+
response_start_samples = int(response_start_s * sample_rate)
|
|
103
|
+
response_end_samples = int(response_end_s * sample_rate)
|
|
104
|
+
|
|
105
|
+
baseline_duration = (baseline_end_samples - baseline_start_samples) / sample_rate
|
|
106
|
+
response_duration = (response_end_samples - response_start_samples) / sample_rate
|
|
107
|
+
|
|
108
|
+
if verbose:
|
|
109
|
+
print(f" Baseline window: {baseline_start_s:.2f} to {baseline_end_s:.2f}s ({baseline_duration:.2f}s)")
|
|
110
|
+
print(f" Response window: {response_start_s:.2f} to {response_end_s:.2f}s ({response_duration:.2f}s)")
|
|
111
|
+
|
|
112
|
+
# Results
|
|
113
|
+
summary_data = []
|
|
114
|
+
|
|
115
|
+
# Process each experiment
|
|
116
|
+
experiments = sorted(trials_df["experiment"].unique())
|
|
117
|
+
|
|
118
|
+
if verbose:
|
|
119
|
+
print(f"\nProcessing {len(experiments)} experiments...")
|
|
120
|
+
|
|
121
|
+
for exp_id in experiments:
|
|
122
|
+
exp_trials = trials_df[trials_df["experiment"] == exp_id]
|
|
123
|
+
|
|
124
|
+
if verbose:
|
|
125
|
+
print(f" Experiment {exp_id}: {len(exp_trials)} trials")
|
|
126
|
+
|
|
127
|
+
# Sample random trials
|
|
128
|
+
if len(exp_trials) > n_random:
|
|
129
|
+
sampled_trials = exp_trials.sample(n=n_random, random_state=int(exp_id))
|
|
130
|
+
else:
|
|
131
|
+
sampled_trials = exp_trials
|
|
132
|
+
|
|
133
|
+
if verbose:
|
|
134
|
+
print(f" Sampling {len(sampled_trials)} trials for activity estimation")
|
|
135
|
+
|
|
136
|
+
# For each valid unit
|
|
137
|
+
for unit_id in valid_units:
|
|
138
|
+
unit_mask = spike_clusters == unit_id
|
|
139
|
+
unit_spikes = spike_times[unit_mask]
|
|
140
|
+
|
|
141
|
+
baseline_rates = []
|
|
142
|
+
response_rates = []
|
|
143
|
+
|
|
144
|
+
# Process each trial
|
|
145
|
+
for _, trial_row in sampled_trials.iterrows():
|
|
146
|
+
trial_sample = int(trial_row["sample"])
|
|
147
|
+
|
|
148
|
+
# Baseline period
|
|
149
|
+
baseline_start_sample = trial_sample + baseline_start_samples
|
|
150
|
+
baseline_end_sample = trial_sample + baseline_end_samples
|
|
151
|
+
baseline_spikes = unit_spikes[
|
|
152
|
+
(unit_spikes >= baseline_start_sample) &
|
|
153
|
+
(unit_spikes < baseline_end_sample)
|
|
154
|
+
]
|
|
155
|
+
baseline_rate = len(baseline_spikes) / baseline_duration if baseline_duration > 0 else 0
|
|
156
|
+
baseline_rates.append(baseline_rate)
|
|
157
|
+
|
|
158
|
+
# Response period
|
|
159
|
+
response_start_sample = trial_sample + response_start_samples
|
|
160
|
+
response_end_sample = trial_sample + response_end_samples
|
|
161
|
+
response_spikes = unit_spikes[
|
|
162
|
+
(unit_spikes >= response_start_sample) &
|
|
163
|
+
(unit_spikes < response_end_sample)
|
|
164
|
+
]
|
|
165
|
+
response_rate = len(response_spikes) / response_duration if response_duration > 0 else 0
|
|
166
|
+
response_rates.append(response_rate)
|
|
167
|
+
|
|
168
|
+
# Calculate statistics
|
|
169
|
+
baseline_rates = np.array(baseline_rates)
|
|
170
|
+
response_rates = np.array(response_rates)
|
|
171
|
+
|
|
172
|
+
bsl_mean = np.mean(baseline_rates)
|
|
173
|
+
bsl_std = np.std(baseline_rates)
|
|
174
|
+
sig_mean = np.mean(response_rates)
|
|
175
|
+
sig_std = np.std(response_rates)
|
|
176
|
+
difference = sig_mean - bsl_mean
|
|
177
|
+
|
|
178
|
+
# Paired t-test (baseline vs response for same trials)
|
|
179
|
+
if len(baseline_rates) > 1:
|
|
180
|
+
t_stat, p_value = stats.ttest_rel(response_rates, baseline_rates)
|
|
181
|
+
else:
|
|
182
|
+
p_value = np.nan
|
|
183
|
+
|
|
184
|
+
summary_data.append({
|
|
185
|
+
"experiment": exp_id,
|
|
186
|
+
"unit": unit_id,
|
|
187
|
+
"bsl_mean": bsl_mean,
|
|
188
|
+
"bsl_std": bsl_std,
|
|
189
|
+
"signal_mean": sig_mean,
|
|
190
|
+
"signal_std": sig_std,
|
|
191
|
+
"difference": difference,
|
|
192
|
+
"p_value": p_value,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
if verbose:
|
|
196
|
+
print(f" ✓ Processed {len(valid_units)} units")
|
|
197
|
+
|
|
198
|
+
# Create summary dataframe
|
|
199
|
+
summary_df = pd.DataFrame(summary_data)
|
|
200
|
+
|
|
201
|
+
# Round for display
|
|
202
|
+
for col in ["bsl_mean", "bsl_std", "signal_mean", "signal_std", "difference"]:
|
|
203
|
+
summary_df[col] = summary_df[col].round(2)
|
|
204
|
+
summary_df["p_value"] = summary_df["p_value"].round(4)
|
|
205
|
+
|
|
206
|
+
# Save results
|
|
207
|
+
if output_dir is None:
|
|
208
|
+
output_dir = "."
|
|
209
|
+
|
|
210
|
+
output_path = Path(output_dir)
|
|
211
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
212
|
+
|
|
213
|
+
summary_file = output_path / "activity_summary.csv"
|
|
214
|
+
summary_df.to_csv(summary_file, index=False)
|
|
215
|
+
|
|
216
|
+
clusters_file = output_path / "clusters_included.txt"
|
|
217
|
+
with open(clusters_file, "w") as f:
|
|
218
|
+
for unit_id in sorted(valid_units):
|
|
219
|
+
f.write(f"{unit_id}\n")
|
|
220
|
+
|
|
221
|
+
if verbose:
|
|
222
|
+
print(f"\n✓ Summary saved to: {summary_file}")
|
|
223
|
+
print(f" Total rows: {len(summary_df)}")
|
|
224
|
+
print(f" Unique units: {summary_df['unit'].nunique()}")
|
|
225
|
+
print(f" Unique experiments: {summary_df['experiment'].nunique()}")
|
|
226
|
+
print(f" Clusters included: {len(valid_units)}")
|
|
227
|
+
|
|
228
|
+
return summary_df
|
active_units/cli.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Command-line interface for active-units."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .activity import compute_activity
|
|
8
|
+
from .dashboard import make_app
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def cli():
|
|
13
|
+
"""Active Units — compute and filter neural units interactively."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@cli.command()
|
|
18
|
+
@click.argument('sorted_spikes_dir', type=click.Path(exists=True))
|
|
19
|
+
@click.argument('trials_csv', type=click.Path(exists=True))
|
|
20
|
+
@click.argument('config', type=click.Path(exists=True))
|
|
21
|
+
@click.option('--output-dir', type=click.Path(), default=None,
|
|
22
|
+
help='Output directory (default: current directory)')
|
|
23
|
+
@click.option('--verbose', is_flag=True, help='Print diagnostic info')
|
|
24
|
+
def compute(sorted_spikes_dir, trials_csv, config, output_dir, verbose):
|
|
25
|
+
"""
|
|
26
|
+
Compute baseline/response activity for all units.
|
|
27
|
+
|
|
28
|
+
SORTED_SPIKES_DIR: Path to spike sorting output folder
|
|
29
|
+
(works with kilosort4, kilosort3, IBL pipeline, etc.)
|
|
30
|
+
TRIALS_CSV: Path to trials.csv (columns: experiment, trial, sample)
|
|
31
|
+
CONFIG: Path to config.json (activity_filter parameters)
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
active-units compute /path/to/sorted_spikes trials.csv config.json --output-dir ./results
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
summary = compute_activity(
|
|
38
|
+
sorted_spikes_dir=sorted_spikes_dir,
|
|
39
|
+
trials_csv=trials_csv,
|
|
40
|
+
config=config,
|
|
41
|
+
output_dir=output_dir,
|
|
42
|
+
verbose=verbose
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if verbose:
|
|
46
|
+
print("\nActivity Summary Statistics:")
|
|
47
|
+
print(summary[['experiment', 'unit', 'bsl_mean', 'signal_mean', 'difference', 'p_value']].head(10))
|
|
48
|
+
|
|
49
|
+
except Exception as e:
|
|
50
|
+
click.secho(f"Error: {e}", fg='red')
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@cli.command()
|
|
55
|
+
@click.argument('activity_csv', type=click.Path(exists=True))
|
|
56
|
+
@click.argument('trials_csv', type=click.Path(exists=True))
|
|
57
|
+
@click.argument('sorted_spikes_dir', type=click.Path(exists=True))
|
|
58
|
+
@click.argument('config', type=click.Path(exists=True))
|
|
59
|
+
@click.option('--port', type=int, default=5006, help='Bokeh server port (default: 5006)')
|
|
60
|
+
@click.option('--show', is_flag=True, help='Open in browser')
|
|
61
|
+
def dashboard(activity_csv, trials_csv, sorted_spikes_dir, config, port, show):
|
|
62
|
+
"""
|
|
63
|
+
Launch interactive filtering dashboard.
|
|
64
|
+
|
|
65
|
+
ACTIVITY_CSV: Path to activity_summary.csv (output from compute command)
|
|
66
|
+
TRIALS_CSV: Path to trials.csv
|
|
67
|
+
SORTED_SPIKES_DIR: Path to spike sorting output folder
|
|
68
|
+
CONFIG: Path to config.json
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
active-units dashboard results/activity_summary.csv trials.csv /path/to/sorted_spikes config.json --port 5006 --show
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
from bokeh.server.server import Server
|
|
75
|
+
from bokeh.plotting import curdoc
|
|
76
|
+
|
|
77
|
+
def bkapp(doc):
|
|
78
|
+
layout = make_app(
|
|
79
|
+
activity_csv=activity_csv,
|
|
80
|
+
trials_csv=trials_csv,
|
|
81
|
+
sorted_spikes_dir=sorted_spikes_dir,
|
|
82
|
+
config=config,
|
|
83
|
+
)
|
|
84
|
+
doc.add_root(layout)
|
|
85
|
+
doc.title = "Unit Activity Filter"
|
|
86
|
+
|
|
87
|
+
server = Server(
|
|
88
|
+
{'/': bkapp},
|
|
89
|
+
port=port,
|
|
90
|
+
show=show,
|
|
91
|
+
allow_websocket_origin=['localhost:' + str(port)]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
click.secho(f"✓ Dashboard running at http://localhost:{port}", fg='green')
|
|
95
|
+
click.echo("Press Ctrl+C to stop")
|
|
96
|
+
|
|
97
|
+
server.start()
|
|
98
|
+
server.io_loop.start()
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
click.secho(f"Error: {e}", fg='red')
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@cli.command()
|
|
106
|
+
@click.argument('sorted_spikes_dir', type=click.Path(exists=True))
|
|
107
|
+
@click.argument('trials_csv', type=click.Path(exists=True))
|
|
108
|
+
def status(sorted_spikes_dir, trials_csv):
|
|
109
|
+
"""
|
|
110
|
+
Check setup and data integrity.
|
|
111
|
+
|
|
112
|
+
SORTED_SPIKES_DIR: Path to spike sorting output folder
|
|
113
|
+
TRIALS_CSV: Path to trials.csv
|
|
114
|
+
|
|
115
|
+
Validates that all required files exist and have expected structure.
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
from .utils import validate_sorted_spikes_dir, load_trials
|
|
119
|
+
|
|
120
|
+
click.echo("Checking spike sorting output directory...")
|
|
121
|
+
ss_files = validate_sorted_spikes_dir(sorted_spikes_dir)
|
|
122
|
+
click.secho(" ✓ Spike sorting files found", fg='green')
|
|
123
|
+
|
|
124
|
+
click.echo("Loading spike data...")
|
|
125
|
+
import numpy as np
|
|
126
|
+
spike_times = np.load(ss_files["spike_times"]).flatten()
|
|
127
|
+
spike_clusters = np.load(ss_files["spike_clusters"]).flatten()
|
|
128
|
+
click.secho(f" ✓ {len(spike_times)} spikes from {len(np.unique(spike_clusters))} clusters", fg='green')
|
|
129
|
+
|
|
130
|
+
click.echo("Loading trials...")
|
|
131
|
+
trials = load_trials(trials_csv)
|
|
132
|
+
click.secho(f" ✓ {len(trials)} trials across {trials['experiment'].nunique()} experiments", fg='green')
|
|
133
|
+
|
|
134
|
+
click.echo("\n" + "=" * 50)
|
|
135
|
+
click.secho("Setup looks good!", fg='green')
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
click.secho(f"Error: {e}", fg='red')
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main():
|
|
143
|
+
"""Entry point for CLI."""
|
|
144
|
+
cli()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == '__main__':
|
|
148
|
+
main()
|