Reproducing Yu et al. (2016) from dandiset 000005¶
Paper: Yu J, Gutnisky DA, Hires SA, Svoboda K (2016). Layer 4 fast-spiking interneurons filter thalamocortical signals during active somatosensation. Nature Neuroscience 19:1647-1657, doi:10.1038/nn.4412.
Dandiset: DANDI:000005, version
0.220126.1853. The paper cites no dandiset version (it predates the archive). 0.220126.1853 is the
only published version, so it is pinned; the draft (last modified 2026-02-12) lists the same 148
asset paths and sizes (printed below). The version is the same as that of the earlier attempt,
000005-yu-2016-01.
Claims: claims/000005/10.1038-nn.4412.yaml, written by a reader from the version of record with its
Online Methods and the supplement (Supplementary Figures 1 to 14, Supplementary Tables 1 and 2), and
checked by a second reader on 2026-10-20. It replaces the claims file of the earlier attempt, which
was written from the abstract. The ids of eight earlier claims are kept; their criteria are new.
The paper's data statement points to CRCNS ssc-7; no analysis code was found.
This is a repeat attempt. The earlier attempt's findings are checked again in the data check.
Outside input: Supplementary Table 1 of the paper (the mouse line of each mouse), read at run time from the supplement PDF on the publisher's site. It is used to name the mouse lines that the files leave blank and to compare the cells per line with Supplementary Table 2.
| Claim | Tested |
|---|---|
behavior-touches |
yes |
vpm-touch |
yes |
vpm-whisking |
yes |
vpm-whisking-phase |
yes |
exc-vm-touch-whisking |
yes |
exc-threshold-touch-vs-whisking |
yes |
exc-effective-reversal |
yes |
exc-inhibition-timing |
blocked: data missing |
exc-inhibition-whisking |
blocked: data missing |
fs-touch-response |
yes |
fs-vm-touch-whisking |
yes |
fs-whisking-rate |
yes |
fs-whisking-phase |
yes |
fs-touch-and-whisking |
yes |
exc-mainly-touch |
yes |
vpm-photoinhibition |
blocked: data missing |
exc-thalamus-photoinhibition |
yes |
thalamus-drives-fs |
run; blocked, too few cells |
rabies-vpm-input |
blocked: data missing |
ion-cut-exc-no-touch, ion-cut-exc-whisking, ion-cut-exc-phase, ion-cut-fs-no-touch, sensory-input-drives-fs, ion-cut-fs-phase, ion-cut-behavior |
blocked: metadata insufficient |
pv-enphr-suppression |
yes |
fs-suppress-movement-spiking |
yes |
fs-enhance-touch-selectivity |
yes |
exc-touch-spikes-enphr |
yes |
auc-weak-strong |
yes |
auc-groups |
yes |
behavior-false-alarms |
yes |
Readings. The claims file names readings for details the paper leaves open (O1 filter direction, O2 samples of the rank-sum test, O3 threshold rule, O4 resting Vm, O5 light windows, O6 fraction of control, O7 laser powers, O8 derivative, O9 time of a crossing bin, O10 whisking-onset windows, O11 touches bounding whisking epochs). Each is run where it applies.
Answers. A reader answered this attempt's seven questions on 2026-10-20 (questions/000005.yaml),
and the claims file was updated: dVm/dt is taken at the recording's sampling rate, not in 1 ms bins
(O8); the PSTH latency is run with the bin's left edge and its centre (O9); fs-touch-response runs
four FS readings (driven at p < 0.001 or p < 0.01, each with and without the waveform boundary);
behavior-false-alarms runs the chi-squared test with and without Yates's correction; the paired
tests of thalamus-drives-fs are two-sided; behavior-touches runs every trial and trials without
light; exc-vm-touch-whisking runs the whisking-onset and touch readings O10 and O11. The sections
of the claims these bear on say what each answer changed. Cell groups hold the cells
of every mouse ("all"), since the files do not mark the ION-cut mice; a variant without the mice
flagged as lacking touch input ("no_flagged") is shown and does not decide an outcome.
Revision after the first verification round. The tag test now counts every 5 ms pulse of the 5 and 10 Hz trains (the earlier rule kept only pulses with no other pulse within 100 ms); the touch readings O11 are run for every claim on whisking epochs, and O10 and O11 for every claim on whisking onsets; trials of a whole-cell cell held near 0 mV are left out at rest; and the data check adds spike detection quality, the tagging coverage of every VGAT-ChR2 cell, the injected current of every file, the pole times and the ION-cut candidates. New outputs show the cause of the near-zero Vm latencies and the stability of the AUC claims over 20 seeds.
import re
import time
import warnings
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy import stats
import yu2016
pd.set_option("display.width", 220)
pd.set_option("display.max_columns", 40)
pd.set_option("display.max_rows", 300)
warnings.filterwarnings("ignore", category=RuntimeWarning)
started = time.time()
recs = yu2016.load_all()
vpm = [r for r in recs if r["kind"] == "VPM"]
l4 = [r for r in recs if r["kind"] == "L4"]
by_path = {r["path"]: r for r in l4}
print(f"loaded {len(recs)} files from dandiset {yu2016.DANDISET_ID} version {yu2016.DANDISET_VERSION} "
f"in {time.time() - started:.0f} s")
loaded 148 files from dandiset 000005 version 0.220126.1853 in 355 s
Data check¶
No random numbers are drawn in the data check. Each check of the metadata against the values is stated under its output.
vc = yu2016.versions_check()
print("Versions (identifier, last modified):", vc["versions"])
print("Assets in the pinned version:", vc["pinned_assets"], "; in the draft:", vc["draft_assets"],
"; same paths and sizes:", vc["draft_same_paths_and_sizes"])
files = pd.DataFrame([{"kind": r["kind"], "subject": r["subject"], "genotype": r["genotype"] or "(none)",
"path": r["path"], "trials": len(r["trials"])} for r in recs])
print("Files by kind:", files.kind.value_counts().to_dict(), "; subjects:", files.subject.nunique(),
"(VPM", files[files.kind == "VPM"].subject.nunique(), ", L4", files[files.kind == "L4"].subject.nunique(),
", in both", len(set(files[files.kind == "VPM"].subject) & set(files[files.kind == "L4"].subject)), ")")
print("Trials: VPM", int(files[files.kind == "VPM"].trials.sum()), ", L4", int(files[files.kind == "L4"].trials.sum()))
lines = yu2016.supplement_mouse_lines()
line_of = dict(zip(lines.subject, lines.line))
files["line"] = files.subject.map(line_of)
print(f"\nSupplementary Table 1 lists {len(lines)} mice; {files.subject.drop_duplicates().isin(lines.subject).sum()} "
f"of the {files.subject.nunique()} mice of the dandiset are in it.")
print("Files by genotype in the files (rows) against the mouse line in Supplementary Table 1 (columns):")
print(pd.crosstab([files.kind, files.genotype], files.line.fillna("(not listed)")).to_string())
print("Mice without a genotype in their files, or given as Ai32:",
files[files.genotype.isin(["(none)", "Ai32"])].drop_duplicates("subject").groupby(["genotype", "line"]).subject.apply(list).to_dict())
meta = pd.DataFrame([{"path": r["path"], "subject": r["subject"], "general_fields": ",".join(r["general_fields"]),
"subject_fields": ",".join(r["subject_fields"]),
"region": r["ic_location"].split(";")[0].replace("brain_region:", "").strip(),
"layer": r["ic_location"].split("cortical_layer:")[1].split(";")[0].strip(),
"dv_mm": float(r["ic_location"].split("coordinate_dv:")[1].split(";")[0])} for r in l4])
print("\nFields under general/ in the L4 files:", meta.general_fields.value_counts().to_dict())
print("Fields under general/subject:", meta.subject_fields.value_counts().to_dict())
print("Electrode location of the L4 files: region", meta.region.value_counts().to_dict(), "; cortical layer",
meta.layer.value_counts().to_dict(), f"; depth (coordinate_dv) {meta.dv_mm.min():.2f} to {meta.dv_mm.max():.2f} mm")
print("VPM electrode groups:", pd.Series([loc.split(";")[1].strip() for r in vpm for loc in r["location"]]).value_counts().to_dict())
terms = {"ION": r"\bION\b", "infraorbital": r"(?i)infraorbital", "nerve": r"(?i)\bnerves?\b", "transect": r"(?i)transect",
"lesion": r"(?i)lesion", "cut": r"(?i)\bcut\b", "surgery": r"(?i)surg", "sham": r"(?i)\bsham\b",
"control": r"(?i)\bcontrol\b", "rabies": r"(?i)rabies", "histology": r"(?i)histolog", "eNpHR": r"(?i)NpHR|halorhodopsin"}
print("L4 files whose metadata text (every string under general/ and the session description) contains each term:",
{k: sum(bool(re.search(v, r["metadata_text"])) for r in l4) for k, v in terms.items()})
print("Units in the VPM files:", sum(len(r["units"]) for r in vpm), "in", len(vpm), "files; unit descriptions start with",
pd.Series([u["desc"].split(".")[0] for r in vpm for u in r["units"]]).value_counts().to_dict(),
"; VPM files with a photostimulation series:", sum(r["has_light"] for r in vpm))
print("L4 files with an electrode outside barrel cortex:", int((meta.region != "barrel cortex").sum()))
Versions (identifier, last modified): [('draft', '2026-02-12 14:38:18'), ('0.220126.1853', '2022-01-26 18:53:27')]
Assets in the pinned version: 148 ; in the draft: 148 ; same paths and sizes: True
Files by kind: {'L4': 130, 'VPM': 18} ; subjects: 55 (VPM 6 , L4 49 , in both 0 )
Trials: VPM 7656 , L4 11211
Supplementary Table 1 lists 55 mice; 55 of the 55 mice of the dandiset are in it.
Files by genotype in the files (rows) against the mouse line in Supplementary Table 1 (columns):
line C57BL/6 PV-ChR2 PV-IRES-Cre PV-IRES-Cre x Ai32 SOM-IRES-Cre x Ai32 VGAT-ChR2-EYFP
kind genotype
L4 (none) 0 1 0 1 0 1
Ai32 0 0 0 0 5 0
C57BL6 8 0 0 0 0 0
PV-Cre 0 0 48 0 0 0
VGAT-ChR2-EYFP 0 0 0 0 0 66
VPM (none) 0 0 0 0 0 18
Mice without a genotype in their files, or given as Ai32: {('(none)', 'PV-ChR2'): ['anm250509'], ('(none)', 'PV-IRES-Cre x Ai32'): ['anm223517'], ('(none)', 'VGAT-ChR2-EYFP'): ['anm184389', 'anm186997', 'anm199549', 'anm199551', 'anm199552', 'anm203464', 'anm260074'], ('Ai32', 'SOM-IRES-Cre x Ai32'): ['anm308247', 'anm324650', 'anm337495', 'anm337496', 'anm342684']}
Fields under general/ in the L4 files: {'devices,experiment_description,experimenter,institution,intracellular_ephys,keywords,optogenetics,related_publications,subject': 121, 'devices,experiment_description,experimenter,institution,intracellular_ephys,keywords,related_publications,subject': 9}
Fields under general/subject: {'genotype,sex,species,subject_id': 130}
Electrode location of the L4 files: region {'barrel cortex': 130} ; cortical layer {'4': 130} ; depth (coordinate_dv) 0.42 to 0.62 mm
VPM electrode groups: {'brain_subregion: VPM': 18}
L4 files whose metadata text (every string under general/ and the session description) contains each term: {'ION': 0, 'infraorbital': 0, 'nerve': 0, 'transect': 0, 'lesion': 0, 'cut': 0, 'surgery': 0, 'sham': 0, 'control': 0, 'rabies': 0, 'histology': 0, 'eNpHR': 0}
Units in the VPM files: 29 in 18 files; unit descriptions start with {'thalamic': 29} ; VPM files with a photostimulation series: 0
L4 files with an electrode outside barrel cortex: 0
0.220126.1853 is the only published version, and the draft holds the same asset paths and sizes.
The 18 VPM files come from 6 mice and the 130 L4 files from 49 others; all 55 mice are in
Supplementary Table 1. Where a file gives a genotype it agrees with the table's mouse line, but 9 mice
have none (the 6 VPM mice and 3 L4 mice) and the 5 SOM-IRES-Cre x Ai32 mice are given as Ai32
(earlier finding 3, still present). The files hold no surgery, lesion, opsin or notes field, and no
text names ION, infraorbital, nerve, transection, cut, sham or eNpHR (earlier findings 2 and 5, still
present). Every L4 file gives barrel cortex, layer 4, at 0.42 to 0.62 mm (earlier finding 4, still
present). The VPM files hold no photostimulation series, and no L4 file is outside barrel cortex, so
the thalamic multiunit recordings of Fig. 6b,c are not in the dandiset.
mode = pd.DataFrame([{"path": r["path"], "subject": r["subject"], "line": line_of[r["subject"]], "label": r["label_mode"],
"from_trace": r["mode"], "vm_median": r["vm_median"], "lowfreq_sd": r["lowfreq_sd_mv"], "vm_hz": r["vm_hz"],
"trial_s": r["vm_trial_s"], "spikes": len(r["spikes"]),
"cur_conv": r.get("current_conversion", np.nan), "cur_unit": r.get("current_unit", ""),
"cur_test": r.get("current_end_of_trial_median", np.nan), "cur_min": r.get("current_trial_min", np.nan),
"trials_near_0mV": int(np.sum(r["trial_vm_median"] > -20)) if r["mode"] == "whole-cell" else 0,
"cur_trial_median": r.get("current_trial_median", np.nan),
"trials_current_above_0.5": int(np.nansum(r.get("trial_current_median", np.zeros(1)) > 0.5)),
"trial_vm_median_min": float(np.nanmin(r["trial_vm_median"])), "trial_vm_median_max": float(np.nanmax(r["trial_vm_median"]))}
for r in l4])
print("Recording mode in the session description (rows) against the trace (columns; whole-cell when the median trace "
"is below -20 mV):")
print(pd.crosstab([mode.line, mode.label], mode.from_trace).to_string())
print("\nMedian of the trace and s.d. of its 1 to 100 Hz band (stored values; unit 'volts', conversion 0.001, so mV):")
print(mode.groupby("from_trace")[["vm_median", "lowfreq_sd"]].describe().round(2).T.to_string())
print("Trace sampling rate (Hz) and trial length (s), files:", mode.groupby(["vm_hz", "trial_s"]).size().to_dict())
print("First file at 20 kHz:", min(p.split("_ses-")[1][:8] for p in mode[mode.vm_hz == 20000].path),
"; last at 10 kHz:", max(p.split("_ses-")[1][:8] for p in mode[mode.vm_hz == 10000].path))
ts = pd.DataFrame([{"outside": r["vm_outside_trials"], "first_ms": r["vm_block_first_rel"] * 1000} for r in l4])
print("Trace samples outside every trial:", int(ts.outside.sum()), "; largest offset of a trial's first sample from the "
"trial start (ms):", round(ts.first_ms.max(), 2))
print("\nInjected current (CurrentClampStimulus): unit", mode.cur_unit.unique().tolist(), ", conversion",
mode.cur_conv.unique().tolist(), "; stored value of the end-of-trial test pulse in whole-cell files: median",
round(mode[mode.from_trace == "whole-cell"].cur_min.median(), 3), "(files at about -0.1:",
int(np.isclose(mode[mode.from_trace == "whole-cell"].cur_min, -0.1, atol=0.01).sum()), "of",
int((mode.from_trace == "whole-cell").sum()), "); the Methods give a -0.1 nA test pulse")
print("\nWhole-cell files with trials whose median Vm is above -20 mV (a cell held near 0 mV by current):")
print(mode[mode.trials_near_0mV > 0][["path", "line", "trials_near_0mV", "vm_median"]].to_string(index=False))
print("Held trials (median Vm above -20 mV) are left out of every whole-cell measure at rest:", int(sum(r["held_trials"].sum() for r in l4)),
"trials in", sum(bool(r["held_trials"].any()) for r in l4), "file")
# Fig. 3 recordings: depolarizing current of about 1 nA (stored in nA, see above) in any trial of any file
print("\nMedian injected current per trial (stored values, nA), over files, by mode from the trace:")
print(mode.groupby("from_trace").cur_trial_median.describe().round(3).to_string())
print("Files with any trial whose median injected current is above 0.5 nA:",
mode[mode["trials_current_above_0.5"] > 0][["path", "from_trace", "line", "trials_current_above_0.5", "cur_trial_median", "vm_median", "lowfreq_sd"]]
.assign(path=lambda d: d.path.str.split("/").str[1].str[:40]).round(3).to_dict("records"))
cur_ = mode[mode["trials_current_above_0.5"] > 0]
print("These files:", len(cur_), "; modes", cur_.from_trace.value_counts().to_dict(), "; lines", cur_.line.value_counts().to_dict(), "; 1 to 100 Hz s.d. at most",
round(cur_.lowfreq_sd.max(), 2), "mV (whole-cell files: median", round(mode[mode.from_trace == "whole-cell"].lowfreq_sd.median(), 2), "mV)")
ca_ = mode[mode.from_trace == "cell-attached"]
print("Cell-attached files (by the trace): trial-median Vm from", round(ca_.trial_vm_median_min.min(), 2), "to", round(ca_.trial_vm_median_max.max(), 2),
"mV; files with any trial-median Vm below -20 mV:", int((ca_.trial_vm_median_min < -20).sum()))
print("Cell-attached files (by the trace) with a 1 to 100 Hz s.d. above 0.5 mV:",
mode[(mode.from_trace == "cell-attached") & (mode.lowfreq_sd > 0.5)][["path", "vm_median", "lowfreq_sd"]].round(2).to_dict("records"))
Recording mode in the session description (rows) against the trace (columns; whole-cell when the median trace is below -20 mV):
from_trace cell-attached whole-cell
line label
C57BL/6 whole-cell 0 8
PV-ChR2 whole-cell 0 1
PV-IRES-Cre juxtacellular 16 1
whole-cell 23 8
PV-IRES-Cre x Ai32 whole-cell 0 1
SOM-IRES-Cre x Ai32 juxtacellular 4 0
whole-cell 0 1
VGAT-ChR2-EYFP juxtacellular 30 0
whole-cell 0 37
Median of the trace and s.d. of its 1 to 100 Hz band (stored values; unit 'volts', conversion 0.001, so mV):
from_trace cell-attached whole-cell
vm_median count 73.00 57.00
mean 1.76 -60.82
std 5.19 9.09
min -16.89 -76.81
25% 0.09 -67.37
50% 2.26 -62.51
75% 4.44 -56.02
max 12.97 -39.21
lowfreq_sd count 73.00 57.00
mean 0.19 2.43
std 0.18 1.27
min 0.08 0.14
25% 0.12 1.58
50% 0.15 1.95
75% 0.18 3.21
max 1.30 5.85
Trace sampling rate (Hz) and trial length (s), files: {(10000.0, 5.2): 29, (10000.0, 5.4999): 1, (10000.0, 5.5): 62, (20000.0, 5.9): 38}
First file at 20 kHz: 20150827 ; last at 10 kHz: 20150320
Trace samples outside every trial: 0 ; largest offset of a trial's first sample from the trial start (ms): 4.76
Injected current (CurrentClampStimulus): unit ['amperes'] , conversion [1e-06] ; stored value of the end-of-trial test pulse in whole-cell files: median 0.0 (files at about -0.1: 14 of 57 ); the Methods give a -0.1 nA test pulse
Whole-cell files with trials whose median Vm is above -20 mV (a cell held near 0 mV by current):
path line trials_near_0mV vm_median
sub-anm250509/sub-anm250509_ses-20140817_behavior+icephys+ogen.nwb PV-ChR2 13 -39.208553
Held trials (median Vm above -20 mV) are left out of every whole-cell measure at rest: 13 trials in 1 file
Median injected current per trial (stored values, nA), over files, by mode from the trace:
count mean std min 25% 50% 75% max
from_trace
cell-attached 73.0 0.442 0.807 -0.0 0.0 0.0 0.0 1.897
whole-cell 57.0 0.004 0.010 -0.0 0.0 0.0 0.0 0.043
Files with any trial whose median injected current is above 0.5 nA: [{'path': 'sub-anm260070_ses-20150217_obj-1p0sabo_b', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 110, 'cur_trial_median': 1.897, 'vm_median': -2.274, 'lowfreq_sd': 0.168}, {'path': 'sub-anm260070_ses-20150217_obj-2vta88_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 126, 'cur_trial_median': 1.896, 'vm_median': 2.783, 'lowfreq_sd': 0.101}, {'path': 'sub-anm260070_ses-20150218_obj-vgzohl_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 154, 'cur_trial_median': 1.897, 'vm_median': 2.883, 'lowfreq_sd': 0.145}, {'path': 'sub-anm260070_ses-20150219_obj-1dodaf3_b', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 122, 'cur_trial_median': 1.897, 'vm_median': 6.47, 'lowfreq_sd': 0.331}, {'path': 'sub-anm260070_ses-20150219_obj-trbrrk_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 96, 'cur_trial_median': 1.896, 'vm_median': 4.445, 'lowfreq_sd': 0.149}, {'path': 'sub-anm260070_ses-20150223_obj-t5wgzh_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 92, 'cur_trial_median': 1.896, 'vm_median': 4.578, 'lowfreq_sd': 0.134}, {'path': 'sub-anm260070_ses-20150223_obj-vy6mbx_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 83, 'cur_trial_median': 1.897, 'vm_median': 3.636, 'lowfreq_sd': 0.147}, {'path': 'sub-anm260077_ses-20150301_obj-1ox41vu_b', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 50, 'cur_trial_median': 1.897, 'vm_median': 0.79, 'lowfreq_sd': 0.18}, {'path': 'sub-anm260077_ses-20150301_obj-fmhxwt_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 38, 'cur_trial_median': 1.896, 'vm_median': 1.533, 'lowfreq_sd': 0.182}, {'path': 'sub-anm260077_ses-20150304_behavior+icep', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 42, 'cur_trial_median': 1.897, 'vm_median': 6.996, 'lowfreq_sd': 0.137}, {'path': 'sub-anm273347_ses-20150224_obj-1h07z6p_b', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 45, 'cur_trial_median': 1.896, 'vm_median': 6.515, 'lowfreq_sd': 0.226}, {'path': 'sub-anm273347_ses-20150224_obj-i6vwao_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 50, 'cur_trial_median': 1.897, 'vm_median': 5.431, 'lowfreq_sd': 0.124}, {'path': 'sub-anm273348_ses-20150226_obj-1cv3c91_b', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 78, 'cur_trial_median': 1.897, 'vm_median': 2.491, 'lowfreq_sd': 0.111}, {'path': 'sub-anm273348_ses-20150226_obj-1m5xcbf_b', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 81, 'cur_trial_median': 1.896, 'vm_median': 2.799, 'lowfreq_sd': 0.168}, {'path': 'sub-anm273348_ses-20150226_obj-efumcd_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 95, 'cur_trial_median': 1.897, 'vm_median': 4.86, 'lowfreq_sd': 0.153}, {'path': 'sub-anm273348_ses-20150227_behavior+icep', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 58, 'cur_trial_median': 1.897, 'vm_median': 3.309, 'lowfreq_sd': 0.148}, {'path': 'sub-anm281589_ses-20150319_behavior+icep', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 168, 'cur_trial_median': 1.896, 'vm_median': 5.36, 'lowfreq_sd': 0.141}]
These files: 17 ; modes {'cell-attached': 17} ; lines {'VGAT-ChR2-EYFP': 17} ; 1 to 100 Hz s.d. at most 0.33 mV (whole-cell files: median 1.95 mV)
Cell-attached files (by the trace): trial-median Vm from -21.85 to 20.72 mV; files with any trial-median Vm below -20 mV: 1
Cell-attached files (by the trace) with a 1 to 100 Hz s.d. above 0.5 mV: [{'path': 'sub-anm244025/sub-anm244025_ses-20141026_obj-1npw0ls_behavior+icephys+ogen.nwb', 'vm_median': -8.17, 'lowfreq_sd': 1.09}, {'path': 'sub-anm266945/sub-anm266945_ses-20141220_obj-7o69ev_behavior+icephys+ogen.nwb', 'vm_median': -16.89, 'lowfreq_sd': 1.3}]
The recording mode in the session description disagrees with the trace for 24 files, all from PV-IRES-Cre mice: 23 described as whole-cell sit near 0 mV with a 1 to 100 Hz s.d. of 0.08 to 0.3 mV and small extracellular spikes, as cell-attached recordings do, and one described as juxtacellular sits near -65 mV (earlier finding 6, still present). The mode read from the trace is used below; the stored trace is in mV (conversion 0.001 to volts), which the whole-cell resting potentials confirm. The trace is sampled at 10 kHz up to March 2015 and at 20 kHz from late August 2015, which only the timestamps state (earlier finding 8, still present); timestamps start at the trial start and no sample lies outside a trial.
New: the injected current (CurrentClampStimulus) is stored with unit amperes and conversion 1e-6,
so a stored -0.1 would be -0.1 uA. The end-of-trial test pulse, which the Methods give as -0.1 nA,
is stored as about -0.1 in 14 of the 57 whole-cell files, so the stored values are in nA and the
conversion is wrong by a factor of 1,000. Only one whole-cell file has trials held near 0 mV (13
trials of the PV-ChR2 cell); these trials are now left out of every measure at rest. 17 cell-attached
files of VGAT-ChR2 mice carry a stored current of about 1.9 in every trial, but their traces sit near
0 mV with a 1 to 100 Hz s.d. of at most 0.33 mV (whole-cell files: median 1.95 mV), so they are not
whole-cell cells held near 0 mV; no trial of any other file has a median current above 0.5. The
trial-median trace of the cell-attached files spans -21.85 to 20.72 mV, and only one file has a trial
below -20 mV. So the recordings of L4 excitatory neurons depolarized to reveal inhibition (Fig. 3, 6
neurons) are not in the dandiset. Two cell-attached files have a larger slow component (1.1 and 1.3
mV), still far below the whole-cell values; both are from PV-IRES-Cre mice.
wh = []
for r in recs:
b = r["beh"]
row = {"kind": r["kind"], "path": r["path"], "principal": b["principal"] is not None, "whiskers": len(b["whisker_names"])}
if b["principal"] is not None:
row.update(dt_ms=b["dt_median"] * 1000, angle_min=b["angle_range"][0], angle_max=b["angle_range"][1],
angle_nan=b["angle_nan"], touches=len(b["touch_on"]), licks=len(b["licks"]),
samples_per_trial=float(np.median(np.bincount(b["trial"][b["trial"] >= 0])[np.bincount(b["trial"][b["trial"] >= 0]) > 0])))
wh.append(row)
wh = pd.DataFrame(wh)
p = wh[wh.principal]
print("Files with a whisker named principal:", len(p), "of", len(wh), "; without:", [x.split("/")[1] for x in wh[~wh.principal].path])
print("Whisker sampling interval (ms):", sorted(p.dt_ms.round(3).unique().tolist()), "; samples per trial, files by value:",
p.samples_per_trial.value_counts().sort_index().to_dict())
print("Principal whisker angle (deg): min", round(p.angle_min.min(), 1), ", max", round(p.angle_max.max(), 1),
"; median fraction of NaN samples", round(p.angle_nan.median(), 4))
print("Touch onsets of the principal whisker per file: median", int(p.touches.median()), ", range", int(p.touches.min()),
"to", int(p.touches.max()), "; lick onsets per file: median", int(p.licks.median()))
ep = pd.DataFrame([{"kind": r["kind"], **{f"{f}:{k}": r["beh"][f][k].sum() / 1000 for f in yu2016.FILTERS for k in ("whisk", "nonwhisk")},
**{f"{f}:onsets": len(r["beh"][f]["onsets"]) for f in yu2016.FILTERS}} for r in recs if r["beh"]["principal"]])
print("\nWhisking epochs and nonwhisking periods (s per file) and whisking onsets per file, medians, by filter reading:")
print(ep.groupby("kind").median().round(1).T.to_string())
rows = []
for r in recs:
b = r["beh"]
if b["principal"] is None:
continue
on_t = b["t"][b["touch_on"]]
tr = r["trials"]
for s, e, ft in zip(tr.start, tr.stop, tr.first_touch_time):
x = on_t[(on_t >= s) & (on_t < e)]
if len(x) and np.isfinite(ft):
rows.append({"kind": r["kind"], "table_rel": ft - s, "series_rel": x[0] - s})
ft = pd.DataFrame(rows)
for kind, g in ft.groupby("kind"):
print(f"\n{kind}: {len(g)} trials with a touch. first_touch_time minus trial start, median {g.table_rel.median():.4f} s; "
f"first touch onset of the series, median {g.series_rel.median():.3f} s; table equals series within 2 ms: "
f"{np.mean(np.abs(g.table_rel - g.series_rel) < 0.002):.3f}; table x 1000 within 2 ms of the series: "
f"{np.mean(np.abs(g.table_rel * 1000 - g.series_rel) < 0.002):.3f}")
# pole times in the trial table, relative to the trial start (AUC reading a uses them; first_touch_time is scaled wrongly)
pt = pd.DataFrame([{"kind": r["kind"], "pole_in": x0 - s, "pole_out": x1 - s} for r in recs
for s, x0, x1 in zip(r["trials"].start, r["trials"].pole_in_time, r["trials"].pole_out_time)])
print("\npole_in_time and pole_out_time minus trial start (s), by kind:")
print(pt.groupby("kind").describe(percentiles=[0.05, 0.5, 0.95]).round(3).T.to_string())
print("L4 trials with pole_in after the trial start and pole_out within the 5 s video, 0.5 s or more apart:",
round(float(((pt.kind == "L4") & (pt.pole_in >= 0) & (pt.pole_out <= 5) & (pt.pole_out - pt.pole_in >= 0.5)).sum() / (pt.kind == "L4").sum()), 3))
# median first touch of the touch series against pole in, L4
ftp = []
for r in l4:
b = r["beh"]
if b["principal"] is None:
continue
on_t = b["t"][b["touch_on"]]
for s, e, x0 in zip(r["trials"].start, r["trials"].stop, r["trials"].pole_in_time):
x = on_t[(on_t >= s) & (on_t < e)]
if len(x):
ftp.append(x[0] - x0)
print("L4 trials with a touch: first touch onset minus pole_in_time (s), median", round(float(np.median(ftp)), 3), ", 5th percentile",
round(float(np.percentile(ftp, 5)), 3), ", fraction negative", round(float(np.mean(np.array(ftp) < 0)), 4))
# VPM: spikes per touch (VPM definition) of each unit for the touches of each tracked whisker
rows = []
for r in vpm:
b = r["beh"]
allm = np.ones(len(r["trials"]), bool)
for u in r["units"]:
c = yu2016.counts_1k(u["spikes"], b)
res = {}
for name, w in b["whisker_touches"].items():
bb = yu2016.with_whisker(b, name)
s = yu2016.spikes_per_touch(c, b["trial"], yu2016.touches(bb, allm, 0.025), "vpm")
res[name] = s["spt"] if s["n_touches"] >= 20 else np.nan
pw = b["principal"].split("_")[-1].upper()
best = max(res, key=lambda k: -np.inf if np.isnan(res[k]) else res[k])
rows.append({"path": r["path"], "unit": u["unit"], "file_pw": pw, "best": best, "spt_pw": res[pw], "spt_best": res[best],
"whiskers": len(res)})
pwu = pd.DataFrame(rows)
multi = pwu[pwu.whiskers > 1]
print(f"\nVPM units in files with more than one tracked whisker: {len(multi)}; units whose largest response (spikes per touch, "
f"at least 20 touches) is to a whisker other than the file's principal one: {int((multi.best != multi.file_pw).sum())}")
print(multi[multi.best != multi.file_pw].assign(path=lambda d: d.path.str.split("/").str[1].str[:30]).round(2).to_string(index=False))
Files with a whisker named principal: 140 of 148 ; without: ['sub-anm230103_ses-20140131_behavior+icephys+ogen.nwb', 'sub-anm266945_ses-20141222_obj-ianjb9_behavior+icephys+ogen.nwb', 'sub-anm281589_ses-20150320_behavior+icephys+ogen.nwb', 'sub-anm300172_ses-20150908_obj-n5zk5z_behavior+icephys+ogen.nwb', 'sub-anm318530_ses-20151128_behavior+icephys+ogen.nwb', 'sub-anm318532_ses-20151125_obj-1kqnryo_behavior+icephys+ogen.nwb', 'sub-anm324650_ses-20160422_behavior+icephys+ogen.nwb', 'sub-anm337496_ses-20160524_behavior+icephys+ogen.nwb']
Whisker sampling interval (ms): [1.0] ; samples per trial, files by value: {5000.0: 140}
Principal whisker angle (deg): min -98.4 , max 94.4 ; median fraction of NaN samples 0.0
Touch onsets of the principal whisker per file: median 354 , range 0 to 4265 ; lick onsets per file: median 842
Whisking epochs and nonwhisking periods (s per file) and whisking onsets per file, medians, by filter reading:
kind L4 VPM
filtfilt:whisk 77.4 402.3
filtfilt:nonwhisk 132.1 1164.3
lfilter:whisk 79.3 425.5
lfilter:nonwhisk 124.9 1117.0
filtfilt:onsets 69.0 410.0
lfilter:onsets 69.5 402.5
L4: 5627 trials with a touch. first_touch_time minus trial start, median 0.0014 s; first touch onset of the series, median 1.373 s; table equals series within 2 ms: 0.000; table x 1000 within 2 ms of the series: 0.956
VPM: 3154 trials with a touch. first_touch_time minus trial start, median 1.3240 s; first touch onset of the series, median 1.324 s; table equals series within 2 ms: 1.000; table x 1000 within 2 ms of the series: 0.000
pole_in_time and pole_out_time minus trial start (s), by kind:
kind L4 VPM
pole_in count 11211.000 7656.000
mean 0.840 1.012
std 0.043 0.198
min 0.832 0.000
5% 0.832 0.428
50% 0.832 1.060
95% 0.833 1.077
max 1.084 1.296
pole_out count 11211.000 7656.000
mean 3.485 3.026
std 1.783 0.642
min 2.433 0.000
5% 2.803 2.171
50% 2.835 3.376
95% 6.524 3.558
max 44.315 3.752
L4 trials with pole_in after the trial start and pole_out within the 5 s video, 0.5 s or more apart: 0.899
L4 trials with a touch: first touch onset minus pole_in_time (s), median 0.528 , 5th percentile 0.265 , fraction negative 0.0
VPM units in files with more than one tracked whisker: 20; units whose largest response (spikes per touch, at least 20 touches) is to a whisker other than the file's principal one: 11
path unit file_pw best spt_pw spt_best whiskers
sub-anm184389_ses-20130207_beh 0 B2 B1 0.12 0.24 3
sub-anm184389_ses-20130207_beh 1 B2 BETA 0.63 0.96 3
sub-anm184389_ses-20130211_beh 1 BETA B1 -0.01 1.02 3
sub-anm184389_ses-20130212_beh 0 B2 B1 0.08 0.31 3
sub-anm184389_ses-20130212_beh 1 B2 B1 0.81 0.91 3
sub-anm184389_ses-20130213_beh 0 B1 B2 NaN 0.17 3
sub-anm184389_ses-20130213_beh 1 B1 B2 NaN 0.13 3
sub-anm184389_ses-20130213_beh 2 B1 BETA NaN 1.07 3
sub-anm184389_ses-20130213_beh 3 B1 B2 NaN 0.03 3
sub-anm184389_ses-20130213_beh 4 B1 B2 NaN 0.03 3
sub-anm203464_ses-20130702_beh 0 C2 C3 NaN 1.24 3
The whisker variables are at 1 kHz, 5,000 samples (5 s) per trial, in degrees with a plausible
range. 8 files name no principal whisker (earlier finding 9, still present); they are left out of
every measure that needs touch or whisking. In the L4 files first_touch_time matches the touch
series only when multiplied by 1000 (0.956 of trials), so the latency was divided by 1000 once too
often; in the VPM files it is correct (earlier finding 7, still present). The analysis uses the touch
series. The pole times are not scaled like first_touch_time: in the L4 files pole_in_time lies
0.832 s after the trial start in the median trial (0.832 to 1.084 s), pole_out_time 2.835 s in the
median, and the first touch follows pole in by 0.528 s in the median and never precedes it, so the
no-touch windows of AUC reading (a), drawn between them, lie where the pole is present (0.899 of L4
trials have pole in and out 0.5 s or more apart within the 5 s video). In the VPM files each file names one principal whisker, though the session description says
each unit has its own: 11 of the 20 units in files with more than one tracked whisker respond most to
another whisker, some strongly (earlier finding 9, now shown per unit).
light = []
for r in l4:
tr = r["trials"]
for site, L in r["light"].items():
w = yu2016.light_windows(r["light"], site, tr)
modes = tr.photo_stim_mode.to_numpy()[w.trial.to_numpy(int)] if len(w) else []
dur = np.diff(L["pulses"], axis=1).ravel() if len(L["pulses"]) else np.zeros(0)
light.append({"line": line_of[r["subject"]], "site_region": L["region"], "lambda_nm": L["lambda_nm"],
"power_max": L["power_max"], "lit_trials": len(w), "short_pulses": int((dur < 0.0075).sum()),
"long_epochs": int((dur > 0.1).sum()), "isolated_pulses": len(yu2016.isolated_pulses(r["light"], site)),
"tag_pulses": len(yu2016.tag_pulses(r["light"], site)),
"pulses_per_lit_trial": w.n_pulses.median() if len(w) else 0, "modes": ",".join(sorted(set(modes)))})
light = pd.DataFrame(light)
print("Photostimulation sites by mouse line, labelled region and wavelength:")
print(light.groupby(["line", "site_region", "lambda_nm"]).agg(
files=("lit_trials", "size"), with_light=("lit_trials", lambda x: int((x > 0).sum())), lit_trials=("lit_trials", "sum"),
short_pulses=("short_pulses", "sum"), isolated=("isolated_pulses", "sum"), tag=("tag_pulses", "sum"), long_epochs=("long_epochs", "sum"),
pulses_per_trial=("pulses_per_lit_trial", "median"), power_max=("power_max", "max")).to_string())
print("\nphoto_stim_mode values of the lit trials, by site region:")
print(light[light.lit_trials > 0].groupby("site_region").modes.value_counts().to_string())
desc = pd.DataFrame([r["trials"].attrs.get("descriptions", {}) for r in recs])
print("Descriptions of trial-table columns (distinct values over files):",
{c: sorted(desc[c].dropna().unique().tolist()) for c in ["type", "response", "photo_stim_mode", "photo_stim_power", "stim_present"] if c in desc})
print("Trial table photo_stim_power is NaN in", sum(int(r["trials"].photo_stim_power.isna().all()) for r in l4), "of", len(l4), "L4 files")
# the pulse rate of the trains at the site labelled VPM
rates = []
for r in l4:
for site, L in r["light"].items():
if L["region"] == "VPM" and len(L["pulses"]) > 10:
d = np.diff(L["pulses"][:, 0])
rates.append(1 / np.median(d[d < 0.05]) if np.any(d < 0.05) else np.nan)
print("Pulse rate within trains at the site labelled VPM (Hz), files:", pd.Series(np.round(rates)).value_counts().to_dict())
Photostimulation sites by mouse line, labelled region and wavelength:
files with_light lit_trials short_pulses isolated tag long_epochs pulses_per_trial power_max
line site_region lambda_nm
PV-ChR2 VPM 594.0 1 1 58 6660 0 41 17 150.0 15.0
PV-IRES-Cre vS1 594.0 47 47 1490 0 0 0 1832 1.0 3.0
PV-IRES-Cre x Ai32 vS1 594.0 1 0 0 0 0 0 0 0.0 0.0
SOM-IRES-Cre x Ai32 vM1 473.0 3 3 54 5400 0 54 0 100.0 15.0
vS1 594.0 5 3 16 100 100 100 0 5.0 15.0
VGAT-ChR2-EYFP VPM 594.0 34 32 760 165357 32 785 1 250.0 15.0
VPM and vS1 473.0 24 21 119 11383 689 760 5 12.0 15.0
vM1 473.0 25 23 360 40149 25 406 0 100.0 15.0
vS1 594.0 33 29 431 15160 1449 1707 0 10.0 15.0
photo_stim_mode values of the lit trials, by site region:
site_region modes
VPM shift 21
5ms_pulse 10
5ms_pulse,constant 1
constant_shift,shift 1
VPM and vS1 5ms_pulse 19
5ms_pulse,constant,constant_shift 1
5ms_pulse,constant 1
vM1 5ms_pulse 18
shift 7
5ms_grid,5ms_pulse 1
vS1 constant 47
shift 17
5ms_pulse 14
shift,shiftgrid 1
Descriptions of trial-table columns (distinct values over files): {'type': [''], 'response': [''], 'photo_stim_mode': [''], 'photo_stim_power': ['(mW) stimulation power'], 'stim_present': ['is this a stim or no-stim trial']}
Trial table photo_stim_power is NaN in 130 of 130 L4 files
Pulse rate within trains at the site labelled VPM (Hz), files: {100.0: 21, 50.0: 12}
photo_stim_mode and type have empty descriptions, photo_stim_power is NaN in every L4 file,
and the mode labels do not map to one kind of light (earlier finding 5, still present). The light
epochs are read from the photostimulation series. In VGAT-ChR2 mice the site labelled VPM is given as
594 nm, and pulses at the site labelled vS1 (594 nm) drive VGAT-ChR2 cells at short latency (below);
ChR2 is driven by blue light, so these wavelengths look wrong (earlier finding 10, still present).
New: the trains at the site labelled VPM run at 100 Hz in 21 files and at 50 Hz in 12, while the
Methods give 100 Hz. Trial types are spelled both Nogo and NoGo (new); they are read as one label.
# Alignment of spikes and behaviour: each cell's touch response (spikes per touch, VPM or Fig. 5 definition) and
# phase-coupling r2 with its spikes in their own trial, and moved to the next trial at the same time after its start.
rows = []
for r in recs:
b = r["beh"]
if b["principal"] is None:
continue
allm = np.ones(len(r["trials"]), bool) if r["kind"] == "VPM" else ~yu2016.lit_trials(r["light"], r["trials"])
cells = [(u["unit"], u["spikes"]) for u in r["units"]] if r["kind"] == "VPM" else [(0, r["spikes"])]
for unit, sp in cells:
if len(sp) < 50:
continue
out = {"kind": r["kind"], "path": r["path"], "unit": unit}
for tag, s in (("true", sp), ("shift", yu2016.shift_spikes(sp, r["trials"]))):
c = yu2016.counts_1k(s, b)
c = np.where(allm[np.clip(b["trial"], 0, None)], c, 0)
out[f"spt_{tag}"] = yu2016.spikes_per_touch(c, b["trial"], yu2016.touches(b, allm), "vpm" if r["kind"] == "VPM" else "cortex5")["spt"]
vals, _ = yu2016.phase_binned(b, "filtfilt", count=c)
out[f"r2_{tag}"] = yu2016.phase_fit(vals)["r2"]
rows.append(out)
align = pd.DataFrame(rows)
for kind, g in align.groupby("kind"):
g = g.dropna()
print(f"{kind}: {len(g)} units or cells with at least 50 spikes. Spikes per touch, median own trial {g.spt_true.median():.3f}, "
f"next trial {g.spt_shift.median():.3f}, larger in own trial for {(g.spt_true > g.spt_shift).sum()} "
f"(signed-rank p = {stats.wilcoxon(g.spt_true, g.spt_shift).pvalue:.2g}); phase r2, median own {g.r2_true.median():.2f}, "
f"next {g.r2_shift.median():.2f} (p = {stats.wilcoxon(g.r2_true, g.r2_shift).pvalue:.2g}); r2 > 0.25 own "
f"{(g.r2_true > 0.25).mean():.2f}, next {(g.r2_shift > 0.25).mean():.2f}")
bad = align[(align.spt_true <= align.spt_shift) & (align.r2_true <= align.r2_shift)]
print("Units or cells where neither measure is larger in their own trial:", len(bad), "of", len(align))
# Duplicates: identical traces, identical spike trains, or identical trial tables across files
hashes = pd.Series([r["vm_hash"] for r in l4])
print("\nL4 files sharing a trace hash with another file:", int(hashes.duplicated(keep=False).sum()))
sk = {}
for r in vpm:
for u in r["units"]:
sk.setdefault((len(u["spikes"]), round(float(u["spikes"][:5].sum()), 4)), []).append((r["path"], u["unit"]))
print("VPM units with identical spike count and first spike times:", [v for v in sk.values() if len(v) > 1])
tt = pd.Series([(r["subject"], len(r["trials"]), round(float(r["trials"].start.iloc[-1]), 3)) for r in recs])
print("Files sharing subject, trial count and last trial start with another file:", int(tt.duplicated(keep=False).sum()))
L4: 90 units or cells with at least 50 spikes. Spikes per touch, median own trial 0.346, next trial 0.004, larger in own trial for 73 (signed-rank p = 7e-11); phase r2, median own 0.37, next 0.19 (p = 5.6e-06); r2 > 0.25 own 0.63, next 0.41 VPM: 29 units or cells with at least 50 spikes. Spikes per touch, median own trial 0.268, next trial 0.000, larger in own trial for 24 (signed-rank p = 1.8e-05); phase r2, median own 0.58, next 0.30 (p = 0.0015); r2 > 0.25 own 0.86, next 0.55 Units or cells where neither measure is larger in their own trial: 9 of 121 L4 files sharing a trace hash with another file: 0 VPM units with identical spike count and first spike times: [] Files sharing subject, trial count and last trial start with another file: 0
Spikes and behaviour share a clock in the data as a whole: for VPM units and L4 cells, the touch response and the phase coupling are larger with spikes in their own trial than moved to the next trial (signed-rank p printed above). This cannot be shown for a cell that responds to neither; 9 of 121 such cells are consistent with the clock, not shown to share it. No two files share a trace, a VPM spike train or a trial table, so no recording is deposited twice. Chronic VPM units recorded on several days may be the same neurons; the files give no identifiers that link them, and each unit is counted once, as in the paper's 29.
# Cell classes, by the rules of the claims file's header (see the helper's docstring for the tests):
# FS: in VGAT-ChR2-EYFP mice, driven by 5 ms light pulses at the cortical site within 5 ms of onset (isolated pulses,
# the pulses of 5 and 10 Hz trains, one-sided binomial test against the same 5 ms 50 ms earlier; the paper gives no test, so the claims file names
# p < 0.001 and p < 0.01 as readings) and inside the Supp. Fig. 6 waveform boundary (cell-attached cells only;
# with and without the boundary are readings of fs-touch-response); in PV-IRES-Cre mice, PV cells inactivated by the eNpHR light (rate inside
# the light lower than in the same span of trials without light, Mann-Whitney over trials, p < 0.05); in lines
# without a tag, whole-cell cells with spikes no wider than 0.35 ms at half height (the largest FS width the Methods
# give). Every other L4 cell is excitatory (E).
def light_effect(r):
"""Spike rate inside the light (median span of the lit trials) against the same span of trials without light."""
out = None
ctl = ~yu2016.lit_trials(r["light"], r["trials"])
for site, L in r["light"].items():
w = yu2016.light_windows(r["light"], site, r["trials"])
if len(w) == 0 or not (np.diff(L["pulses"], axis=1) > 0.1).any():
continue
lit = np.zeros(len(r["trials"]), bool)
lit[w.trial.to_numpy(int)] = True
win = (float(np.median(w.rel_start)), float(np.median(w.rel_stop)))
sp, st = np.sort(r["spikes"]), r["trials"].start.to_numpy()
per = lambda m: (np.searchsorted(sp, st[m] + win[1]) - np.searchsorted(sp, st[m] + win[0])) / (win[1] - win[0])
a, b = per(lit), per(ctl)
p = stats.mannwhitneyu(a, b, alternative="less").pvalue if len(a) and len(b) and (a.sum() + b.sum()) > 0 else np.nan
out = {"light_site": L["region"], "light_win": win, "lit_trials": int(lit.sum()), "ctl_trials": int(ctl.sum()),
"rate_ctl": b.mean(), "rate_light": a.mean(), "light_p_lower": p}
return out
rows = []
for r in l4:
row = {"path": r["path"], "subject": r["subject"], "line": line_of[r["subject"]], "mode": r["mode"], "spikes": len(r["spikes"]),
"fwhm_ms": r["fwhm_ms"], "ca_duration_ms": r["ca_duration_ms"], "ca_ratio": r["ca_ratio"], "pw": r["beh"]["principal"] is not None}
best = None
for site, L in r["light"].items():
if "vS1" not in L["region"]:
continue
lr = yu2016.light_pulse_response(r["light"], r["spikes"], site)
if lr["n_pulses"] >= yu2016.TAG_MIN_PULSES and (best is None or lr["p_evoked"] - lr["p_base"] > best["p_evoked"] - best["p_base"]):
on_ = yu2016.tag_pulses(r["light"], site)
ipi = np.diff(L["pulses"][:, 0])
best = {**lr, "tag_site": L["region"], "ipi_ms": float(np.median(ipi[ipi < 1]) * 1000) if np.any(ipi < 1) else np.nan,
**{f"{k}_{rule}": yu2016.light_pulse_response(r["light"], r["spikes"], site, rule).get(k, np.nan)
for rule in ("first", "isolated") for k in ("n_pulses", "p_value")}}
if best:
row.update(tag_site=best["tag_site"], pulses=best["n_pulses"], p_evoked=best["p_evoked"], p_base=best["p_base"],
tag_p=best["p_value"], tag_latency_ms=best["median_latency_ms"], ipi_ms=best["ipi_ms"],
pulses_first=best["n_pulses_first"], tag_p_first=best["p_value_first"],
pulses_isolated=best["n_pulses_isolated"], tag_p_isolated=best["p_value_isolated"])
le = light_effect(r) if row["line"] == "PV-IRES-Cre" else None
if le:
row.update(le)
rows.append(row)
cells = pd.DataFrame(rows)
cells["tag_tested"] = cells.get("pulses", pd.Series(np.nan, index=cells.index)).notna()
cells["driven"] = cells.tag_p < yu2016.TAG_MAX_P
cells["driven_01"] = cells.tag_p < 0.01
cells["in_boundary"] = (cells.ca_duration_ms < yu2016.FS_MAX_DURATION_MS) & (cells.ca_ratio < yu2016.FS_MAX_RATIO)
cells["pv_inactivated"] = (cells.line == "PV-IRES-Cre") & (cells.light_p_lower < 0.05) & (cells.rate_light < cells.rate_ctl)
untagged_lines = ~cells.line.isin(["VGAT-ChR2-EYFP", "PV-IRES-Cre"])
cells["thin_wc"] = untagged_lines & (cells["mode"] == "whole-cell") & (cells.fwhm_ms <= 0.35)
def classify(driven_col="driven", boundary=True):
driven = (cells.line == "VGAT-ChR2-EYFP") & cells[driven_col]
fs = driven & ((cells["mode"] == "whole-cell") | cells.in_boundary) if boundary else driven
cls = np.where(fs | cells.pv_inactivated | cells.thin_wc, "FS", "E")
# driven (GABAergic) cells outside the boundary are neither FS nor excitatory (Supp. Fig. 6)
return np.where(driven & ~fs, "other GABAergic", cls)
cells["cls"] = classify() # the claims file's rule: driven (p < 0.001) and inside the boundary
cells["cls_01"] = classify("driven_01") # reading: driven at p < 0.01
cells["cls_tag"] = classify(boundary=False) # driven at p < 0.001, any waveform (reading of fs-touch-response only)
cells["cls_01_tag"] = classify("driven_01", boundary=False) # driven at p < 0.01, any waveform (same)
print("L4 cells by mouse line, mode and class (driven at p < 0.001 and, cell-attached, inside the waveform boundary):")
print(pd.crosstab([cells.line, cells["mode"]], cells.cls).to_string())
print("Cells whose class changes with the tag at p < 0.01:", cells[cells.cls != cells.cls_01].path.str.split("/").str[1].str[:40].tolist())
print("Cells that are FS without the waveform boundary (driven, outside it):",
cells[cells.cls != cells.cls_tag][["path", "ca_duration_ms", "ca_ratio"]].assign(path=lambda x: x.path.str.split("/").str[1].str[:36]).round(2).values.tolist())
vg = cells[(cells.line == "VGAT-ChR2-EYFP")]
print(f"\nVGAT-ChR2 cells: {len(vg)}; tested with at least 10 tag pulses at a cortical site {int(vg.tag_tested.sum())}; driven (p < 0.001) "
f"{int(vg.driven.sum())}, (p < 0.01) {int(vg.driven_01.sum())}")
d = vg[vg.driven]
print("Driven cells: evoked spike probability median", round(d.p_evoked.median(), 2), "(baseline", round(d.p_base.median(), 2),
"); first-spike latency median", round(d.tag_latency_ms.median(), 2), "ms, range", round(d.tag_latency_ms.min(), 2), "to",
round(d.tag_latency_ms.max(), 2))
print("VGAT-ChR2 cells without at least 10 tag pulses at a cortical site:", vg[~vg.tag_tested][["path", "mode", "spikes", "fwhm_ms"]]
.assign(path=lambda x: x.path.str.split("/").str[1].str[:40]).round(2).to_dict("records"))
print("\nTagging coverage, every VGAT-ChR2 cell with a cortical site: tag pulses (5 and 10 Hz trains, the rule used), median interval "
"between pulses (ms), and the tag p under the rule used, the first pulse of each train, and isolated pulses (the earlier rule):")
print(vg[vg.tag_tested][["path", "mode", "spikes", "pulses", "ipi_ms", "tag_p", "pulses_first", "tag_p_first", "pulses_isolated", "tag_p_isolated",
"tag_latency_ms"]].assign(path=lambda x: x.path.str.split("/").str[1].str[:34]).round(4).to_string(index=False))
print("Driven at p < 0.001 under each pulse rule: trains (used)", int((vg.tag_p < 0.001).sum()), "; first pulse of each train",
int((vg.tag_p_first < 0.001).sum()), "; isolated pulses", int((vg.tag_p_isolated < 0.001).sum()))
print("Cells driven under the rule used and not under the first-pulse rule:",
vg[(vg.tag_p < 0.001) & ~(vg.tag_p_first < 0.001)].path.str.split("/").str[1].str[:34].tolist())
ca = vg[vg["mode"] == "cell-attached"]
print("\nCell-attached VGAT-ChR2 cells, Supp. Fig. 6 measures (peak-to-trough duration ms, peak/trough ratio), by tag:")
print(ca.groupby("driven")[["ca_duration_ms", "ca_ratio"]].describe().round(2).T.to_string())
print("Driven cell-attached cells inside the boundary (duration < 0.27 ms and ratio < 2.8):", int((ca.driven & ca.in_boundary).sum()),
"of", int(ca.driven.sum()), "; undriven inside it:", int((~ca.driven & ca.in_boundary).sum()), "of", int((~ca.driven).sum()))
pv = cells[cells.line == "PV-IRES-Cre"].dropna(subset=["rate_ctl"])
print(f"\nPV-IRES-Cre cells with the eNpHR light: {len(pv)} (light span {sorted(set(tuple(np.round(w, 2)) for w in pv.light_win))[:3]} ...); "
f"rate lower with light (p < 0.05): {int(pv.pv_inactivated.sum())}; ratio light/control below 0.5 among them: "
f"{int((pv.pv_inactivated & (pv.rate_light < 0.5 * pv.rate_ctl)).sum())}")
print(pv[pv.pv_inactivated | (pv.rate_light < 0.8 * pv.rate_ctl)][["path", "mode", "spikes", "rate_ctl", "rate_light", "light_p_lower", "ca_duration_ms", "ca_ratio"]]
.assign(path=lambda x: x.path.str.split("/").str[1].str[:40]).round(3).to_string(index=False))
print("\nThin-spiking whole-cell cells of lines without a tag:", cells[cells.thin_wc][["path", "line", "fwhm_ms", "spikes"]]
.assign(path=lambda x: x.path.str.split("/").str[1].str[:40]).round(2).to_dict("records"))
print("Whole-cell spike width at half height (ms) of the lines without a tag:",
cells[untagged_lines & (cells["mode"] == "whole-cell")][["line", "fwhm_ms"]].round(2).values.tolist())
# Against Supplementary Table 2 (cells per line, mode and type; control and ION-cut cells together)
paper = pd.DataFrame([("VGAT-ChR2-EYFP", "whole-cell", 32, 5), ("VGAT-ChR2-EYFP", "cell-attached", 6, 24),
("PV-IRES-Cre", "whole-cell", 5, 0), ("PV-IRES-Cre", "cell-attached", 38, 4),
("PV-IRES-Cre x Ai32", "whole-cell", 1, 0), ("C57BL/6", "whole-cell", 7, 1), ("PV-ChR2", "whole-cell", 0, 1),
("SOM-IRES-Cre x Ai32", "whole-cell", 1, 0), ("SOM-IRES-Cre x Ai32", "cell-attached", 4, 0)],
columns=["line", "mode", "paper_E", "paper_FS"])
ours = cells.groupby(["line", "mode"]).cls.value_counts().unstack(fill_value=0).reindex(columns=["E", "FS", "other GABAergic"], fill_value=0)
cmp_ = paper.merge(ours.reset_index().rename(columns={"E": "files_E", "FS": "files_FS", "other GABAergic": "files_other"}),
on=["line", "mode"], how="outer").fillna(0)
print("\nCells per line, mode and type: Supplementary Table 2 (E includes 9 whole-cell ION-cut cells, FS 10 cell-attached ION-cut cells) "
"against the files:")
print(cmp_.to_string(index=False))
print("Totals: paper E", int(cmp_.paper_E.sum()), "FS", int(cmp_.paper_FS.sum()), "; files E", int(cmp_.files_E.sum()), "FS",
int(cmp_.files_FS.sum()), "other GABAergic", int(cmp_.files_other.sum()))
L4 cells by mouse line, mode and class (driven at p < 0.001 and, cell-attached, inside the waveform boundary):
cls E FS other GABAergic
line mode
C57BL/6 whole-cell 7 1 0
PV-ChR2 whole-cell 1 0 0
PV-IRES-Cre cell-attached 36 3 0
whole-cell 9 0 0
PV-IRES-Cre x Ai32 whole-cell 1 0 0
SOM-IRES-Cre x Ai32 cell-attached 4 0 0
whole-cell 1 0 0
VGAT-ChR2-EYFP cell-attached 10 12 8
whole-cell 33 4 0
Cells whose class changes with the tag at p < 0.01: ['sub-anm273347_ses-20150224_obj-i6vwao_be', 'sub-anm273348_ses-20150227_behavior+icep', 'sub-anm318531_ses-20151230_obj-1jbfh1x_b']
Cells that are FS without the waveform boundary (driven, outside it): [['sub-anm260070_ses-20150217_obj-2vta8', 0.7, 13.99], ['sub-anm273348_ses-20150226_obj-1cv3c', 0.3, 2.88], ['sub-anm273348_ses-20150226_obj-1m5xc', 0.7, 12.2], ['sub-anm273348_ses-20150226_obj-efumc', 0.9, 12.31], ['sub-anm275231_ses-20150303_obj-10ju1', 0.3, 2.31], ['sub-anm275231_ses-20150303_obj-g3yot', 0.8, 19.23], ['sub-anm275231_ses-20150304_behavior+', 0.3, 2.26], ['sub-anm281589_ses-20150319_behavior+', 0.6, 2.56]]
VGAT-ChR2 cells: 67; tested with at least 10 tag pulses at a cortical site 41; driven (p < 0.001) 24, (p < 0.01) 27
Driven cells: evoked spike probability median 1.0 (baseline 0.05 ); first-spike latency median 2.3 ms, range 1.6 to 4.55
VGAT-ChR2 cells without at least 10 tag pulses at a cortical site: [{'path': 'sub-anm190964_ses-20130504_behavior+icep', 'mode': 'whole-cell', 'spikes': 35, 'fwhm_ms': 0.66}, {'path': 'sub-anm190964_ses-20130507_behavior+icep', 'mode': 'whole-cell', 'spikes': 1, 'fwhm_ms': nan}, {'path': 'sub-anm230103_ses-20140130_behavior+icep', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm230103_ses-20140131_behavior+icep', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm231235_ses-20140129_obj-1swt3sc_b', 'mode': 'whole-cell', 'spikes': 1, 'fwhm_ms': nan}, {'path': 'sub-anm231235_ses-20140129_obj-z4ez73_be', 'mode': 'whole-cell', 'spikes': 3, 'fwhm_ms': nan}, {'path': 'sub-anm231235_ses-20140131_behavior+icep', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm234411_ses-20140216_behavior+icep', 'mode': 'whole-cell', 'spikes': 198, 'fwhm_ms': 0.89}, {'path': 'sub-anm234412_ses-20140204_behavior+icep', 'mode': 'whole-cell', 'spikes': 1, 'fwhm_ms': nan}, {'path': 'sub-anm234412_ses-20140205_behavior+icep', 'mode': 'whole-cell', 'spikes': 2, 'fwhm_ms': nan}, {'path': 'sub-anm253096_ses-20140817_behavior+icep', 'mode': 'whole-cell', 'spikes': 1, 'fwhm_ms': nan}, {'path': 'sub-anm253096_ses-20140818_behavior+icep', 'mode': 'whole-cell', 'spikes': 3, 'fwhm_ms': nan}, {'path': 'sub-anm260070_ses-20150219_obj-1dodaf3_b', 'mode': 'cell-attached', 'spikes': 16350, 'fwhm_ms': nan}, {'path': 'sub-anm260073_ses-20141008_behavior+icep', 'mode': 'whole-cell', 'spikes': 2, 'fwhm_ms': nan}, {'path': 'sub-anm260073_ses-20141009_behavior+icep', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm260074_ses-20141016_behavior+icep', 'mode': 'whole-cell', 'spikes': 11087, 'fwhm_ms': 0.37}, {'path': 'sub-anm260076_ses-20141015_behavior+icep', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm260077_ses-20150301_obj-fmhxwt_be', 'mode': 'cell-attached', 'spikes': 926, 'fwhm_ms': nan}, {'path': 'sub-anm273347_ses-20150224_obj-1h07z6p_b', 'mode': 'cell-attached', 'spikes': 774, 'fwhm_ms': nan}, {'path': 'sub-anm281592_ses-20150308_obj-pqe4n1_be', 'mode': 'whole-cell', 'spikes': 434, 'fwhm_ms': 0.84}, {'path': 'sub-anm309008_ses-20150924_behavior+icep', 'mode': 'whole-cell', 'spikes': 88, 'fwhm_ms': 0.83}, {'path': 'sub-anm318530_ses-20151127_obj-1qoeopw_b', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm318530_ses-20151127_obj-1sp1rkm_b', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm318530_ses-20151127_obj-h9a8vz_be', 'mode': 'whole-cell', 'spikes': 39, 'fwhm_ms': 1.57}, {'path': 'sub-anm318532_ses-20151125_obj-1nndczg_b', 'mode': 'whole-cell', 'spikes': 5, 'fwhm_ms': 0.35}, {'path': 'sub-anm318532_ses-20151125_obj-ombqp3_be', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}]
Tagging coverage, every VGAT-ChR2 cell with a cortical site: tag pulses (5 and 10 Hz trains, the rule used), median interval between pulses (ms), and the tag p under the rule used, the first pulse of each train, and isolated pulses (the earlier rule):
path mode spikes pulses ipi_ms tag_p pulses_first tag_p_first pulses_isolated tag_p_isolated tag_latency_ms
sub-anm190963_ses-20130408_behavio whole-cell 1157 26.0 10.00 1.0000 26.0 1.0000 0.0 NaN NaN
sub-anm220466_ses-20140402_behavio whole-cell 0 12.0 20.00 1.0000 12.0 1.0000 0.0 NaN NaN
sub-anm220466_ses-20140403_behavio whole-cell 4221 196.0 20.00 0.0000 25.0 0.0000 190.0 0.0000 3.600
sub-anm220470_ses-20140409_obj-1hu whole-cell 156 27.0 20.00 1.0000 9.0 1.0000 20.0 1.0000 NaN
sub-anm220470_ses-20140409_obj-bb0 whole-cell 2040 213.0 20.00 0.0000 24.0 0.0039 210.0 0.0000 4.550
sub-anm230107_ses-20130922_behavio whole-cell 2 11.0 20.00 1.0000 11.0 1.0000 0.0 NaN NaN
sub-anm260070_ses-20150217_obj-1p0 cell-attached 10110 38.0 10.00 0.0000 38.0 0.0000 35.0 0.0000 1.600
sub-anm260070_ses-20150217_obj-2vt cell-attached 9966 53.0 10.00 0.0000 9.0 0.0078 48.0 0.0000 2.100
sub-anm260070_ses-20150218_obj-h5o cell-attached 1054 36.0 200.00 1.0000 3.0 1.0000 36.0 1.0000 NaN
sub-anm260070_ses-20150218_obj-vgz cell-attached 7678 74.0 20.00 0.0000 8.0 0.0039 72.0 0.0000 2.600
sub-anm260070_ses-20150219_obj-trb cell-attached 6575 72.0 200.00 0.0000 6.0 0.0312 72.0 0.0000 2.500
sub-anm260070_ses-20150222_behavio cell-attached 1397 24.0 200.00 0.2500 2.0 0.5000 24.0 0.2500 4.750
sub-anm260070_ses-20150223_obj-t5w cell-attached 5774 25.0 10.00 0.0002 3.0 0.1250 24.0 0.0005 2.350
sub-anm260070_ses-20150223_obj-vy6 cell-attached 10209 25.0 10.00 0.0002 3.0 0.2500 24.0 0.0005 2.400
sub-anm260077_ses-20150301_obj-1ox cell-attached 1554 36.0 200.00 0.0312 3.0 0.5000 36.0 0.0312 2.900
sub-anm260077_ses-20150304_behavio cell-attached 7435 39.0 10.00 0.3633 4.0 0.7500 18.0 0.7500 2.500
sub-anm273341_ses-20150307_behavio cell-attached 2036 147.0 100.00 0.0000 9.0 0.0020 76.0 0.0000 2.300
sub-anm273341_ses-20150308_behavio cell-attached 7328 210.0 20.00 0.0003 65.0 0.0007 181.0 0.0007 2.500
sub-anm273347_ses-20150224_obj-i6v cell-attached 1026 36.0 200.00 0.0017 3.0 0.2500 36.0 0.0017 2.250
sub-anm273348_ses-20150226_obj-1cv cell-attached 3565 24.0 200.00 0.0000 2.0 0.2500 24.0 0.0000 2.100
sub-anm273348_ses-20150226_obj-1m5 cell-attached 4765 49.0 10.00 0.0000 5.0 0.0312 48.0 0.0000 2.500
sub-anm273348_ses-20150226_obj-efu cell-attached 8884 37.0 10.00 0.0000 4.0 0.0625 36.0 0.0000 1.900
sub-anm273348_ses-20150227_behavio cell-attached 5312 12.0 200.00 0.0020 1.0 0.5000 12.0 0.0020 1.600
sub-anm275231_ses-20150303_obj-10j cell-attached 2579 63.0 20.00 0.0000 8.0 0.0039 60.0 0.0000 1.900
sub-anm275231_ses-20150303_obj-g3y cell-attached 4281 13.0 20.00 0.0001 2.0 0.2500 12.0 0.0002 1.900
sub-anm275231_ses-20150304_behavio cell-attached 18665 113.0 20.00 0.0000 8.0 0.0078 41.0 0.0000 2.100
sub-anm281589_ses-20150319_behavio cell-attached 16794 37.0 20.00 0.0000 15.0 0.0000 24.0 0.0000 2.500
sub-anm281589_ses-20150320_behavio cell-attached 9055 182.0 20.00 0.0000 17.0 0.0000 180.0 0.0000 2.200
sub-anm281592_ses-20150308_obj-10s cell-attached 4148 122.0 20.00 0.0000 12.0 0.0005 120.0 0.0000 2.300
sub-anm281592_ses-20150309_behavio cell-attached 286 49.0 10.00 0.0000 5.0 0.0312 48.0 0.0000 2.300
sub-anm305599_ses-20150915_behavio whole-cell 5806 58.0 NaN 0.0000 58.0 0.0000 58.0 0.0000 2.175
sub-anm307766_ses-20150909_behavio whole-cell 6411 108.0 NaN 0.0000 108.0 0.0000 108.0 0.0000 2.150
sub-anm307766_ses-20150910_behavio whole-cell 281 37.0 NaN 0.7500 37.0 0.7500 37.0 0.7500 0.300
sub-anm309008_ses-20150925_behavio whole-cell 0 68.0 200.00 1.0000 32.0 1.0000 68.0 1.0000 NaN
sub-anm318530_ses-20151127_obj-1fc whole-cell 6 10.0 200.00 1.0000 1.0 1.0000 10.0 1.0000 NaN
sub-anm318530_ses-20151128_behavio cell-attached 10401 20.0 200.00 0.0001 2.0 0.2500 20.0 0.0001 2.500
sub-anm318531_ses-20151230_obj-1jb cell-attached 7047 10.0 207.25 0.0020 1.0 0.5000 10.0 0.0020 1.650
sub-anm318531_ses-20151230_obj-meu whole-cell 2 10.0 207.25 1.0000 1.0 1.0000 10.0 1.0000 NaN
sub-anm318532_ses-20151125_obj-1jh whole-cell 177 92.0 20.00 1.0000 11.0 1.0000 90.0 1.0000 12.350
sub-anm318532_ses-20151125_obj-1kq cell-attached 166 10.0 200.00 0.0010 1.0 0.5000 10.0 0.0010 2.900
sub-anm318532_ses-20151125_obj-1ny whole-cell 0 10.0 200.00 1.0000 1.0 1.0000 10.0 1.0000 NaN
Driven at p < 0.001 under each pulse rule: trains (used) 24 ; first pulse of each train 8 ; isolated pulses 24
Cells driven under the rule used and not under the first-pulse rule: ['sub-anm220470_ses-20140409_obj-bb0', 'sub-anm260070_ses-20150217_obj-2vt', 'sub-anm260070_ses-20150218_obj-vgz', 'sub-anm260070_ses-20150219_obj-trb', 'sub-anm260070_ses-20150223_obj-t5w', 'sub-anm260070_ses-20150223_obj-vy6', 'sub-anm273341_ses-20150307_behavio', 'sub-anm273348_ses-20150226_obj-1cv', 'sub-anm273348_ses-20150226_obj-1m5', 'sub-anm273348_ses-20150226_obj-efu', 'sub-anm275231_ses-20150303_obj-10j', 'sub-anm275231_ses-20150303_obj-g3y', 'sub-anm275231_ses-20150304_behavio', 'sub-anm281592_ses-20150309_behavio', 'sub-anm318530_ses-20151128_behavio', 'sub-anm318532_ses-20151125_obj-1kq']
Cell-attached VGAT-ChR2 cells, Supp. Fig. 6 measures (peak-to-trough duration ms, peak/trough ratio), by tag:
driven False True
ca_duration_ms count 10.00 20.00
mean 0.55 0.35
std 0.24 0.24
min 0.20 0.15
25% 0.32 0.20
50% 0.60 0.20
75% 0.70 0.38
max 0.90 0.90
ca_ratio count 10.00 20.00
mean 12.07 4.34
std 11.02 5.37
min 1.30 1.18
25% 2.62 1.36
50% 9.85 2.28
75% 16.88 2.66
max 30.56 19.23
Driven cell-attached cells inside the boundary (duration < 0.27 ms and ratio < 2.8): 12 of 20 ; undriven inside it: 1 of 10
PV-IRES-Cre cells with the eNpHR light: 47 (light span [(np.float64(0.73), np.float64(2.73)), (np.float64(0.82), np.float64(2.32)), (np.float64(0.82), np.float64(2.82))] ...); rate lower with light (p < 0.05): 3; ratio light/control below 0.5 among them: 3
path mode spikes rate_ctl rate_light light_p_lower ca_duration_ms ca_ratio
sub-anm244024_ses-20141112_obj-oqgb91_be cell-attached 865 5.808 4.434 0.079 0.6 9.993
sub-anm244024_ses-20141112_obj-vigb9a_be cell-attached 5123 17.561 7.539 0.000 0.2 1.429
sub-anm266945_ses-20141220_obj-7o69ev_be cell-attached 10811 41.400 3.965 0.000 0.7 9.114
sub-anm266945_ses-20141221_obj-1vgsemf_b whole-cell 84 2.485 1.067 0.203 NaN NaN
sub-anm300169_ses-20150926_obj-rp0tw4_be cell-attached 8966 28.767 1.458 0.000 0.3 3.767
Thin-spiking whole-cell cells of lines without a tag: [{'path': 'sub-anm236461_ses-20140212_behavior+icep', 'line': 'C57BL/6', 'fwhm_ms': 0.25, 'spikes': 3186}]
Whole-cell spike width at half height (ms) of the lines without a tag: [['C57BL/6', nan], ['C57BL/6', 0.66], ['C57BL/6', 0.74], ['PV-IRES-Cre x Ai32', 1.03], ['C57BL/6', 0.73], ['C57BL/6', nan], ['C57BL/6', 0.25], ['C57BL/6', nan], ['C57BL/6', 0.95], ['PV-ChR2', 0.44], ['SOM-IRES-Cre x Ai32', nan]]
Cells per line, mode and type: Supplementary Table 2 (E includes 9 whole-cell ION-cut cells, FS 10 cell-attached ION-cut cells) against the files:
line mode paper_E paper_FS files_E files_FS files_other
C57BL/6 whole-cell 7 1 7 1 0
PV-ChR2 whole-cell 0 1 1 0 0
PV-IRES-Cre cell-attached 38 4 36 3 0
PV-IRES-Cre whole-cell 5 0 9 0 0
PV-IRES-Cre x Ai32 whole-cell 1 0 1 0 0
SOM-IRES-Cre x Ai32 cell-attached 4 0 4 0 0
SOM-IRES-Cre x Ai32 whole-cell 1 0 1 0 0
VGAT-ChR2-EYFP cell-attached 6 24 10 12 8
VGAT-ChR2-EYFP whole-cell 32 5 33 4 0
Totals: paper E 94 FS 35 ; files E 102 FS 20 other GABAergic 8
No file names the cell type of its L4 neuron (earlier finding 1, still present), so the classes are built from the data by the claims file's rules. The tag test now counts every pulse of the 5 and 10 Hz trains. In VGAT-ChR2 mice, 41 cells have at least 10 tag pulses at a cortical site and 24 of them are driven within 5 ms (latency 1.6 to 4.6 ms), the same 24 as under the earlier rule of isolated pulses; the first pulse of each train alone drives 8, since many cells had few trains. Supplementary Table 2 counts 29 FS cells there (19 control and 10 ION-cut). The other 26 VGAT-ChR2 cells have fewer than 10 short pulses at any cortical site (the tagging table lists every cell tested; the list above names the others, among them the cells with 16,350 and 11,087 spikes), so no pulse rule can test them and they count as excitatory. The waveform boundary of Supplementary Fig. 6, as read in the claims file, keeps 12 of the 20 driven cell-attached cells; the other 8 are GABAergic but outside it and are left out of both classes. Their measures (duration 0.3 to 0.9 ms, ratio 2.26 to 19.23) follow the figure's inset (peak, the trough after it and the time between them), but 4 of these driven cells have ratios of 12.2 to 19.2, beyond the figure's axis (8), where the figure has no cell, so the measure here does not place every cell as the figure does. The tag at p < 0.01 adds 3 cells, all outside the boundary, so it changes no FS group. In PV-IRES-Cre mice 3 cells are inhibited by the light (the paper has 4 PV cells); in the lines without a tag one whole-cell cell has spikes 0.25 ms wide. The PV-ChR2 cell, FS in Supplementary Table 2, has spikes 0.44 ms wide here and counts as excitatory. The result: 20 FS cells against the table's 35 (25 control and 10 ION-cut), and 102 excitatory cells against 94.
# Spike detection (standard choices, helper docstring) checked against what a spike train allows: the fraction of interspike
# intervals under 1.5 ms (refractory violations), and the size of the detected spikes (whole-cell, median prominence in mV,
# detection needs 20; cell-attached, median peak in robust s.d. of the high-passed trace, detection needs 6). Rates by mode and
# class are set against the paper's.
sq = []
for r in l4:
sp = np.sort(r["spikes"])
isi = np.diff(sp)
c = cells.set_index("path").loc[r["path"]]
sq.append({"path": r["path"], "subject": r["subject"], "line": c.line, "mode": r["mode"], "cls": c.cls, "spikes": len(sp),
"isi_viol": float(np.mean(isi < 0.0015)) if len(isi) else np.nan, "spike_size": r["spike_size"],
"rate_all": len(sp) / (len(r["trials"]) * yu2016.ANALYSIS_END_S)})
spikeq = pd.DataFrame(sq)
print("L4 cells, by mode and class: fraction of interspike intervals under 1.5 ms, spike size, and spikes/s over the first 5 s of every trial")
print(spikeq.groupby(["mode", "cls"]).agg(cells=("path", "size"), isi_viol_median=("isi_viol", "median"), isi_viol_max=("isi_viol", "max"),
above_1pct=("isi_viol", lambda x: int((x > 0.01).sum())), size_median=("spike_size", "median"),
size_min=("spike_size", "min"), rate_median=("rate_all", "median"), rate_mean=("rate_all", "mean")).round(4).to_string())
print("Cells with more than 1% of intervals under 1.5 ms:", spikeq[spikeq.isi_viol > 0.01][["path", "mode", "cls", "spikes", "isi_viol", "spike_size", "rate_all"]]
.assign(path=lambda d: d.path.str.split("/").str[1].str[:34]).round(3).to_dict("records"))
vq = [float(np.mean(np.diff(np.sort(u["spikes"])) < 0.0015)) for r in vpm for u in r["units"] if len(u["spikes"]) > 1]
print("VPM units (sorted in the files): fraction under 1.5 ms, median", round(float(np.median(vq)), 4), ", max", round(float(np.max(vq)), 4))
e_ = spikeq[spikeq.cls == "E"]
print("E cells, spikes/s over all time, by mode: whole-cell mean", round(e_[e_["mode"] == "whole-cell"].rate_all.mean(), 3), ", cell-attached mean",
round(e_[e_["mode"] == "cell-attached"].rate_all.mean(), 3), "; the paper's E rates are 0.4 (nonwhisking) and 0.6 (whisking) spikes/s over 85 cells")
def dead_time(sp, dead=0.002):
"""Spike times with every spike less than `dead` s after the last kept spike removed."""
keep, last = [], -np.inf
for t in np.sort(sp):
if t - last >= dead:
keep.append(t)
last = t
return np.array(keep)
ca_e = [r for r in l4 if r["mode"] == "cell-attached" and cells.set_index("path").cls[r["path"]] == "E"]
r0 = np.array([len(r["spikes"]) / (len(r["trials"]) * yu2016.ANALYSIS_END_S) for r in ca_e])
r1 = np.array([len(dead_time(r["spikes"])) / (len(r["trials"]) * yu2016.ANALYSIS_END_S) for r in ca_e])
print(f"Cell-attached E cells: with a 2 ms dead time imposed on the detected spikes the mean rate goes from {r0.mean():.2f} to {r1.mean():.2f} "
f"spikes/s (median {np.median(r0):.2f} to {np.median(r1):.2f}), so double detection removes {1 - r1.sum() / r0.sum():.3f} of the spikes")
for thr in (6, 8, 10):
big = e_[(e_["mode"] == "cell-attached")]
print(f"Cell-attached E cells whose median spike is at least {thr} robust s.d.: {int((big.spike_size >= thr).sum())} of {len(big)}")
L4 cells, by mode and class: fraction of interspike intervals under 1.5 ms, spike size, and spikes/s over the first 5 s of every trial
cells isi_viol_median isi_viol_max above_1pct size_median size_min rate_median rate_mean
mode cls
cell-attached E 50 0.0518 0.2378 40 20.9195 7.6509 4.2550 6.2016
FS 15 0.0146 0.0914 9 46.3597 21.1511 13.4184 14.3382
other GABAergic 8 0.0443 0.3122 6 40.7092 12.1231 13.6473 15.0751
whole-cell E 52 0.0000 0.0001 0 42.7639 20.2737 0.0326 0.8798
FS 5 0.0000 0.0019 0 30.0857 22.9691 10.9590 10.5278
Cells with more than 1% of intervals under 1.5 ms: [{'path': 'sub-anm244024_ses-20141111_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 662, 'isi_viol': 0.098, 'spike_size': 25.454, 'rate_all': 1.351}, {'path': 'sub-anm244024_ses-20141112_obj-b6y', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 3113, 'isi_viol': 0.05, 'spike_size': 44.203, 'rate_all': 2.707}, {'path': 'sub-anm244024_ses-20141112_obj-ccd', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1301, 'isi_viol': 0.076, 'spike_size': 8.776, 'rate_all': 4.909}, {'path': 'sub-anm244024_ses-20141112_obj-oqg', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 865, 'isi_viol': 0.093, 'spike_size': 15.41, 'rate_all': 3.089}, {'path': 'sub-anm244024_ses-20141112_obj-vig', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 5123, 'isi_viol': 0.036, 'spike_size': 63.168, 'rate_all': 15.293}, {'path': 'sub-anm244024_ses-20141130_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 413, 'isi_viol': 0.022, 'spike_size': 41.588, 'rate_all': 1.53}, {'path': 'sub-anm244024_ses-20141201_obj-11k', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1536, 'isi_viol': 0.085, 'spike_size': 10.669, 'rate_all': 5.585}, {'path': 'sub-anm244024_ses-20141201_obj-wv1', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 2068, 'isi_viol': 0.053, 'spike_size': 13.839, 'rate_all': 9.4}, {'path': 'sub-anm244024_ses-20141203_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1078, 'isi_viol': 0.135, 'spike_size': 18.534, 'rate_all': 1.978}, {'path': 'sub-anm244025_ses-20141024_obj-1x7', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1030, 'isi_viol': 0.037, 'spike_size': 41.636, 'rate_all': 5.282}, {'path': 'sub-anm244025_ses-20141026_obj-13g', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1662, 'isi_viol': 0.063, 'spike_size': 29.284, 'rate_all': 3.227}, {'path': 'sub-anm244025_ses-20141026_obj-1np', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 516, 'isi_viol': 0.035, 'spike_size': 28.218, 'rate_all': 0.759}, {'path': 'sub-anm244028_ses-20141021_obj-12p', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1450, 'isi_viol': 0.057, 'spike_size': 44.657, 'rate_all': 4.143}, {'path': 'sub-anm244028_ses-20141021_obj-mgz', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1733, 'isi_viol': 0.091, 'spike_size': 20.075, 'rate_all': 4.03}, {'path': 'sub-anm244028_ses-20141021_obj-xh7', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 453, 'isi_viol': 0.06, 'spike_size': 39.912, 'rate_all': 1.888}, {'path': 'sub-anm244028_ses-20141022_obj-1fh', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1445, 'isi_viol': 0.06, 'spike_size': 10.241, 'rate_all': 3.568}, {'path': 'sub-anm260070_ses-20150217_obj-1p0', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 10110, 'isi_viol': 0.018, 'spike_size': 69.608, 'rate_all': 18.054}, {'path': 'sub-anm260070_ses-20150217_obj-2vt', 'mode': 'cell-attached', 'cls': 'other GABAergic', 'spikes': 9966, 'isi_viol': 0.047, 'spike_size': 35.512, 'rate_all': 15.572}, {'path': 'sub-anm260070_ses-20150218_obj-h5o', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1054, 'isi_viol': 0.234, 'spike_size': 23.847, 'rate_all': 4.302}, {'path': 'sub-anm260070_ses-20150222_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1397, 'isi_viol': 0.027, 'spike_size': 12.268, 'rate_all': 3.287}, {'path': 'sub-anm260070_ses-20150223_obj-t5w', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 5774, 'isi_viol': 0.015, 'spike_size': 49.136, 'rate_all': 12.285}, {'path': 'sub-anm260070_ses-20150223_obj-vy6', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 10209, 'isi_viol': 0.015, 'spike_size': 55.861, 'rate_all': 24.307}, {'path': 'sub-anm260077_ses-20150301_obj-1ox', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1554, 'isi_viol': 0.055, 'spike_size': 26.485, 'rate_all': 6.094}, {'path': 'sub-anm260077_ses-20150301_obj-fmh', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 926, 'isi_viol': 0.037, 'spike_size': 18.818, 'rate_all': 4.749}, {'path': 'sub-anm266945_ses-20141220_obj-1g7', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 532, 'isi_viol': 0.147, 'spike_size': 23.143, 'rate_all': 0.838}, {'path': 'sub-anm266945_ses-20141221_obj-164', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 2105, 'isi_viol': 0.071, 'spike_size': 33.41, 'rate_all': 4.626}, {'path': 'sub-anm266945_ses-20141221_obj-1du', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 6001, 'isi_viol': 0.238, 'spike_size': 12.144, 'rate_all': 11.652}, {'path': 'sub-anm266945_ses-20141222_obj-3ih', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1171, 'isi_viol': 0.072, 'spike_size': 17.589, 'rate_all': 3.903}, {'path': 'sub-anm266945_ses-20141222_obj-ian', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 4575, 'isi_viol': 0.112, 'spike_size': 18.346, 'rate_all': 6.06}, {'path': 'sub-anm273341_ses-20150307_behavio', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 2036, 'isi_viol': 0.011, 'spike_size': 21.151, 'rate_all': 8.664}, {'path': 'sub-anm273341_ses-20150308_behavio', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 7328, 'isi_viol': 0.016, 'spike_size': 46.36, 'rate_all': 11.725}, {'path': 'sub-anm273347_ses-20150224_obj-1h0', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 774, 'isi_viol': 0.021, 'spike_size': 10.127, 'rate_all': 3.365}, {'path': 'sub-anm273348_ses-20150226_obj-1m5', 'mode': 'cell-attached', 'cls': 'other GABAergic', 'spikes': 4765, 'isi_viol': 0.046, 'spike_size': 49.352, 'rate_all': 11.622}, {'path': 'sub-anm273348_ses-20150226_obj-efu', 'mode': 'cell-attached', 'cls': 'other GABAergic', 'spikes': 8884, 'isi_viol': 0.012, 'spike_size': 42.801, 'rate_all': 18.318}, {'path': 'sub-anm273348_ses-20150227_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 5312, 'isi_viol': 0.023, 'spike_size': 30.963, 'rate_all': 18.007}, {'path': 'sub-anm275231_ses-20150303_obj-10j', 'mode': 'cell-attached', 'cls': 'other GABAergic', 'spikes': 2579, 'isi_viol': 0.115, 'spike_size': 78.181, 'rate_all': 11.723}, {'path': 'sub-anm275231_ses-20150304_behavio', 'mode': 'cell-attached', 'cls': 'other GABAergic', 'spikes': 18665, 'isi_viol': 0.043, 'spike_size': 82.01, 'rate_all': 27.449}, {'path': 'sub-anm281589_ses-20150319_behavio', 'mode': 'cell-attached', 'cls': 'other GABAergic', 'spikes': 16794, 'isi_viol': 0.312, 'spike_size': 12.123, 'rate_all': 19.758}, {'path': 'sub-anm281589_ses-20150320_behavio', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 9055, 'isi_viol': 0.057, 'spike_size': 30.22, 'rate_all': 17.085}, {'path': 'sub-anm300169_ses-20150926_obj-rp0', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 8966, 'isi_viol': 0.045, 'spike_size': 47.947, 'rate_all': 20.851}, {'path': 'sub-anm300170_ses-20150917_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 5628, 'isi_viol': 0.102, 'spike_size': 19.031, 'rate_all': 8.658}, {'path': 'sub-anm300170_ses-20150918_obj-1c4', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 2365, 'isi_viol': 0.06, 'spike_size': 21.271, 'rate_all': 5.5}, {'path': 'sub-anm300170_ses-20150918_obj-1dj', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 951, 'isi_viol': 0.148, 'spike_size': 20.569, 'rate_all': 5.944}, {'path': 'sub-anm300171_ses-20150827_obj-dcd', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 2794, 'isi_viol': 0.027, 'spike_size': 7.83, 'rate_all': 8.87}, {'path': 'sub-anm300171_ses-20150827_obj-qlj', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1904, 'isi_viol': 0.036, 'spike_size': 11.916, 'rate_all': 5.77}, {'path': 'sub-anm300171_ses-20150828_obj-1yy', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 928, 'isi_viol': 0.028, 'spike_size': 9.016, 'rate_all': 3.437}, {'path': 'sub-anm300172_ses-20150904_obj-66q', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1258, 'isi_viol': 0.033, 'spike_size': 10.618, 'rate_all': 3.268}, {'path': 'sub-anm300172_ses-20150904_obj-i50', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1002, 'isi_viol': 0.025, 'spike_size': 18.998, 'rate_all': 4.175}, {'path': 'sub-anm300172_ses-20150908_obj-6sz', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 4868, 'isi_viol': 0.118, 'spike_size': 36.661, 'rate_all': 13.337}, {'path': 'sub-anm300172_ses-20150908_obj-n5z', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 975, 'isi_viol': 0.098, 'spike_size': 17.905, 'rate_all': 3.75}, {'path': 'sub-anm318530_ses-20151128_behavio', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 10401, 'isi_viol': 0.091, 'spike_size': 69.479, 'rate_all': 17.192}, {'path': 'sub-anm318531_ses-20151230_obj-1jb', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 7047, 'isi_viol': 0.051, 'spike_size': 52.299, 'rate_all': 14.236}, {'path': 'sub-anm324650_ses-20160422_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1291, 'isi_viol': 0.06, 'spike_size': 33.662, 'rate_all': 2.152}, {'path': 'sub-anm337495_ses-20160601_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1343, 'isi_viol': 0.073, 'spike_size': 14.378, 'rate_all': 3.4}, {'path': 'sub-anm342684_ses-20160609_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 2589, 'isi_viol': 0.017, 'spike_size': 7.651, 'rate_all': 5.284}]
VPM units (sorted in the files): fraction under 1.5 ms, median 0.0097 , max 0.0983
E cells, spikes/s over all time, by mode: whole-cell mean 0.88 , cell-attached mean 6.202 ; the paper's E rates are 0.4 (nonwhisking) and 0.6 (whisking) spikes/s over 85 cells
Cell-attached E cells: with a 2 ms dead time imposed on the detected spikes the mean rate goes from 6.20 to 5.70 spikes/s (median 4.25 to 4.08), so double detection removes 0.081 of the spikes
Cell-attached E cells whose median spike is at least 6 robust s.d.: 50 of 50
Cell-attached E cells whose median spike is at least 8 robust s.d.: 48 of 50
Cell-attached E cells whose median spike is at least 10 robust s.d.: 45 of 50
Spike detection. Whole-cell spikes are clean: no whole-cell E cell has more than 0.01% of its interspike intervals under 1.5 ms, and the median spike is 42.8 mV high. In cell-attached cells the median spike is 20.9 robust s.d. (E) and 46.4 (FS) above the noise, far above the 6 s.d. threshold (45 of 50 cell-attached E cells at 10 s.d. or more), but the median E cell has 5.2% of its intervals under 1.5 ms (40 of 50 above 1%), so some spikes are detected twice or a neighbouring unit is picked up. Imposing a 2 ms dead time removes 8.1% of the cell-attached E spikes and lowers their mean rate only from 6.20 to 5.70 spikes/s. Over all time, cell-attached E cells fire 6.20 spikes/s on average and whole-cell E cells 0.88, against the paper's 0.4 and 0.6 spikes/s for its 85 E cells. Double detection does not explain the high cell-attached rates, which drive the rate values of exc-mainly-touch, fs-suppress-movement-spiking and auc-groups. Whether these cells fire this fast or the recordings hold more than one unit cannot be told from the files. The sorted VPM units have a median of 0.97% of intervals under 1.5 ms.
Measures for every unit and cell¶
Each VPM unit and L4 cell with a named principal whisker, over its trials without light, by the definitions of the helper module.
# Every measure of every VPM unit and L4 cell over its trials without light (claims, Measures; helper docstring).
def ctl_mask(r):
"""Trials without light, and (whole-cell) not held near 0 mV by current."""
if r["kind"] == "VPM":
return np.ones(len(r["trials"]), bool)
return ~yu2016.lit_trials(r["light"], r["trials"]) & ~r["held_trials"]
rows, vrows = [], []
for r in recs:
b = r["beh"]
if b["principal"] is None:
continue
m = ctl_mask(r)
if r["kind"] == "VPM":
for u in r["units"]:
rows.append({"path": r["path"], "subject": r["subject"], "unit": u["unit"], "cls": "VPM", "mode": "probe",
**yu2016.spike_measures(u["spikes"], b, m, "vpm", np.random.default_rng(u["unit"]))})
continue
c = cells.set_index("path").loc[r["path"]]
rows.append({"path": r["path"], "subject": r["subject"], "unit": 0, "cls": c.cls, "mode": r["mode"],
**yu2016.spike_measures(r["spikes"], b, m, "cortex5", np.random.default_rng(0), fs_fit=True)})
if r["mode"] == "whole-cell":
for filt in yu2016.FILTERS:
for reading in ("a", "b"):
v = yu2016.vm_measures(r, m, filt, reading)
vrows.append({"path": r["path"], "subject": r["subject"], "filt": filt, "O3": reading, "touch_avg_n": r["touch_avg_n"],
**{k: x for k, x in v.items() if np.ndim(x) == 0}, "touch_pre": v["touch_pre"], "touch_dvm": v["touch_dvm"],
"whisk_pre": v["whisk_pre"], "whisk_dvm": v["whisk_dvm"]})
meas = pd.DataFrame(rows)
vmm = pd.DataFrame(vrows)
for col in ["cls_01", "cls_tag", "cls_01_tag"]:
meas[col] = meas.path.map(cells.set_index("path")[col]).fillna("VPM")
print("Units and cells measured (named principal whisker):", meas.cls.value_counts().to_dict(), "; whole-cell cells with Vm measures:",
vmm.path.nunique())
print("Dropped for lack of a named principal whisker:", cells[~cells.pw].cls.value_counts().to_dict())
# Whisking-epoch measures under the touch readings O11 (claims header; answer of 2026-10-20): every tracked whisker (a) or the
# principal whisker (b) bounds the epochs, from 10 ms before a touch onset to 100 ms after its offset (i) or onset (ii). a-i is
# the main reading above.
orows = []
for r in recs:
b = r["beh"]
if b["principal"] is None:
continue
m = ctl_mask(r)
units = [(u["unit"], u["spikes"]) for u in r["units"]] if r["kind"] == "VPM" else [(0, r["spikes"])]
for o11 in yu2016.O11:
bo = yu2016.beh_o11(b, o11)
for unit, sp in units:
orows.append({"path": r["path"], "unit": unit, "O11": o11,
**yu2016.spike_measures(sp, bo, m, None, None, fs_fit=r["kind"] != "VPM", touch=False)})
meas_o11 = pd.DataFrame(orows)
RATE_COLS = [c for c in meas_o11.columns if ":" in c]
NUM_COLS = [c for c in RATE_COLS if meas_o11[c].dtype.kind in "fi"]
chk = meas.set_index(["path", "unit"])[NUM_COLS].sub(meas_o11[meas_o11.O11 == "a-i"].set_index(["path", "unit"])[NUM_COLS]).abs()
print("O11 a-i against the main epochs, largest difference of any numeric epoch column:", float(chk.max().max()))
Units and cells measured (named principal whisker): {'E': 97, 'VPM': 29, 'FS': 17, 'other GABAergic': 8} ; whole-cell cells with Vm measures: 56
Dropped for lack of a named principal whisker: {'E': 5, 'FS': 3}
O11 a-i against the main epochs, largest difference of any numeric epoch column: 0.0
# Mice without touch input. ION transection removes the input from the whiskers (Fig. 7); the files do not mark it.
# A cell has a touch input when its spikes per touch (Fig. 5 definition) has a 95% bootstrap interval above zero, or,
# whole-cell, its average touch PSP is at least 1 mV. Mice where no cell has one are flagged.
vm0 = vmm[(vmm.filt == "filtfilt") & (vmm.O3 == "a")].set_index("path")
cm = meas[meas.cls != "VPM"].copy()
cm["line"] = cm.subject.map(line_of)
cm["psp_mv"] = cm.path.map(vm0.amp_mv)
cm["spike_input"] = cm.spt_lo > 0
cm["vm_input"] = cm.psp_mv >= 1.0
perf = {r["path"]: r["trials"].response.isin(["Hit", "CR"]).mean() for r in l4}
cm["perf"] = cm.path.map(perf)
mice = cm.groupby("subject").agg(line=("line", "first"), cells=("path", "size"), whole_cell=("mode", lambda x: int((x == "whole-cell").sum())),
fs=("cls", lambda x: int((x == "FS").sum())), spike_input=("spike_input", "sum"),
vm_input=("vm_input", "sum"), max_psp_mv=("psp_mv", "max"), performance=("perf", "mean"))
mice["no_touch_input"] = (mice.spike_input + mice.vm_input) == 0
# ION-cut mice are VGAT-ChR2-EYFP mice (Supplementary Table 2) and performed at chance (Results); the variant leaves out the
# VGAT-ChR2 mice without touch input or with performance below 0.5. Both rules were chosen after seeing these values.
vgm = mice.line == "VGAT-ChR2-EYFP"
flagged = sorted(mice[vgm & (mice.no_touch_input | (mice.performance < 0.5))].index)
print("Mice without a cell with touch input, and VGAT-ChR2 mice with performance below 0.56:")
print(mice[mice.no_touch_input | (vgm & (mice.performance < 0.56))].round(2).to_string())
fc = cells[cells.subject.isin(flagged)]
print(f"\nFlagged mice (VGAT-ChR2, no touch input or performance below 0.5): {flagged}; their cells by mode and class:", pd.crosstab(fc["mode"], fc.cls).to_dict())
print("Supplementary Table 2: 9 ION-cut VGAT-ChR2 mice with 9 whole-cell E and 10 cell-attached FS cells.")
# ION-cut candidates: Supplementary Table 2 suggests the ION-cut mice had no fibre in VPM. VGAT-ChR2 mice by whether any of their
# files has a light site labelled VPM, against the flag.
vsite = pd.Series({r["subject"]: any(L["region"] == "VPM" for L in r["light"].values()) for r in l4})
vgat_m = mice[vgm].assign(vpm_site=lambda d: d.index.map(vsite), flagged=lambda d: d.index.isin(flagged))
print("VGAT-ChR2 mice by a light site labelled VPM (rows) and the flag (columns):")
print(pd.crosstab(vgat_m.vpm_site, vgat_m.flagged).to_string())
print("VGAT-ChR2 mice without a VPM site:", vgat_m[~vgat_m.vpm_site][["cells", "whole_cell", "spike_input", "vm_input", "max_psp_mv", "performance", "flagged"]]
.round(2).to_string())
print("Performance (proportion correct per recording), median: flagged mice", round(cm[cm.subject.isin(flagged)].perf.median(), 2),
"; other mice", round(cm[~cm.subject.isin(flagged)].perf.median(), 2))
Mice without a cell with touch input, and VGAT-ChR2 mice with performance below 0.56:
line cells whole_cell fs spike_input vm_input max_psp_mv performance no_touch_input
subject
anm223517 PV-IRES-Cre x Ai32 1 1 0 0 0 NaN 0.78 True
anm260074 VGAT-ChR2-EYFP 1 1 0 1 1 5.34 0.51 False
anm273341 VGAT-ChR2-EYFP 2 0 2 0 0 NaN 0.47 True
anm275231 VGAT-ChR2-EYFP 3 0 0 1 0 NaN 0.46 False
anm281589 VGAT-ChR2-EYFP 1 0 0 1 0 NaN 0.55 False
anm318530 VGAT-ChR2-EYFP 4 4 0 0 0 0.75 0.49 True
anm318531 VGAT-ChR2-EYFP 2 1 0 1 0 0.04 0.53 False
anm318532 VGAT-ChR2-EYFP 4 4 0 0 0 0.30 0.55 True
Flagged mice (VGAT-ChR2, no touch input or performance below 0.5): ['anm273341', 'anm275231', 'anm318530', 'anm318532']; their cells by mode and class: {'E': {'cell-attached': 0, 'whole-cell': 8}, 'FS': {'cell-attached': 4, 'whole-cell': 0}, 'other GABAergic': {'cell-attached': 3, 'whole-cell': 0}}
Supplementary Table 2: 9 ION-cut VGAT-ChR2 mice with 9 whole-cell E and 10 cell-attached FS cells.
VGAT-ChR2 mice by a light site labelled VPM (rows) and the flag (columns):
flagged False True
vpm_site
False 11 4
True 12 0
VGAT-ChR2 mice without a VPM site: cells whole_cell spike_input vm_input max_psp_mv performance flagged
subject
anm190963 1 1 1 1 6.74 0.76 False
anm190964 2 2 0 2 6.07 0.79 False
anm220466 2 2 0 2 3.85 0.82 False
anm220470 2 2 1 2 10.84 0.68 False
anm230107 1 1 0 1 3.99 0.76 False
anm273341 2 0 0 0 NaN 0.47 True
anm275231 3 0 1 0 NaN 0.46 True
anm281589 1 0 1 0 NaN 0.55 False
anm281592 3 1 1 0 0.13 0.63 False
anm305599 1 1 1 1 4.90 0.70 False
anm307766 2 2 1 1 8.38 0.58 False
anm309008 2 2 1 2 8.13 0.59 False
anm318530 4 4 0 0 0.75 0.49 True
anm318531 2 1 1 0 0.04 0.53 False
anm318532 4 4 0 0 0.30 0.55 True
Performance (proportion correct per recording), median: flagged mice 0.48 ; other mice 0.69
The files do not mark the 9 ION-cut mice. In 3 VGAT-ChR2 mice no cell has a touch input (the 8 whole-cell excitatory cells of anm318530 and anm318532 have average touch PSPs of 0.75 mV or less), and in anm275231 and anm273341 the mice performed below 0.5 correct. These 4 mice are consistent with the ION-cut mice of Fig. 7 and are left out in the variant "no_flagged". The rule was chosen after seeing the values, and it selects by touch responses, so it favours claims about touch; it never decides an outcome. It finds 8 of the table's 9 whole-cell ION-cut cells and 7 of its 10 cell-attached ones (3 of which are outside the waveform boundary here). Supplementary Table 2 suggests the ION-cut mice had no fibre in VPM: of the 27 VGAT-ChR2 mice with L4 cells, 12 have a light site labelled VPM and none of them is flagged, and the 4 flagged mice are among the 15 without one. The other 11 mice without a VPM site include 8 whose cells have touch PSPs of 3.85 to 10.84 mV, so they are not ION-cut. The remaining 3 (anm281589, anm281592 and anm318531) have no cell with a touch PSP of 1 mV or more (anm281589 has no whole-cell cell), but each has a cell whose spikes per touch has a 95% interval above zero, so by this attempt's rule they have touch input and are weak candidates at most. That makes at most 7 candidates for the paper's 9 ION-cut mice, so either some ION-cut mice had a VPM fibre or the hint does not hold, and the files cannot confirm which mice were cut.
# The findings of the earlier attempt (000005-yu-2016-01, version 0.220126.1853), each checked again on this version.
pc_ = pd.crosstab([mode.line, mode.label], mode.from_trace)
mislabel = int(((mode.label == "whole-cell") & (mode.from_trace == "cell-attached")).sum() + ((mode.label == "juxtacellular") & (mode.from_trace == "whole-cell")).sum())
status = [
("1 No L4 file names the cell type of its neuron", "still present",
f"fields under general/subject: {meta.subject_fields.unique().tolist()}; no cell-type field or text in any L4 file"),
("2 No file marks the ION-transected mice", "still present",
f"files whose metadata text names ION, infraorbital, nerve, transection, lesion, cut, surgery or sham: "
f"{sum(any(re.search(terms[k], r['metadata_text']) for k in ['ION', 'infraorbital', 'nerve', 'transect', 'lesion', 'cut', 'surgery', 'sham']) for r in l4)}"),
("3 Genotype missing for the 6 VPM mice and 3 L4 mice; SOM-IRES-Cre x Ai32 given as Ai32", "still present",
f"mice without a genotype: {files[files.genotype == '(none)'].subject.nunique()}; given as Ai32: {files[files.genotype == 'Ai32'].subject.nunique()}"),
("4 Every L4 file gives cortical layer 4", "still present", f"layers given: {meta.layer.unique().tolist()}, depth {meta.dv_mm.min():.2f} to {meta.dv_mm.max():.2f} mm"),
("5 Optogenetic conditions not described (photo_stim_mode, power NaN, no opsin)", "still present",
f"photo_stim_power NaN in {sum(int(r['trials'].photo_stim_power.isna().all()) for r in l4)} of {len(l4)} L4 files; files naming eNpHR: "
f"{sum(bool(re.search(terms['eNpHR'], r['metadata_text'])) for r in l4)}"),
("6 Session description gives the wrong recording mode for 24 of 130 L4 files", "still present",
f"files whose described mode disagrees with the trace: {mislabel}"),
("7 L4 first_touch_time is the trial start plus the latency divided by 1000", "still present",
f"L4 trials where the table value x 1000 matches the touch series: {np.mean(np.abs(ft[ft.kind == 'L4'].table_rel * 1000 - ft[ft.kind == 'L4'].series_rel) < 0.002):.3f}"),
("8 L4 trace at 10 or 20 kHz, stated only by the timestamps", "still present", f"files by rate: {mode.vm_hz.value_counts().to_dict()}"),
("9 8 files name no principal whisker; VPM files give one per file though units have their own", "still present",
f"files without one: {int((~wh.principal).sum())}; VPM units responding most to another whisker: {int((multi.best != multi.file_pw).sum())} of {len(multi)}"),
("10 VPM-site light given as 594 nm in VGAT-ChR2 mice; vS1 594 nm pulses drive VGAT-ChR2 cells", "still present",
f"VGAT-ChR2 cells driven by pulses at sites labelled vS1 594 nm: {int(((cells.tag_site == 'vS1') & cells.driven).sum())}"),
("11 No analysis code linked by the paper or the dandiset", "still present", "the dandiset links only the paper (related_publications); the claims file found no code"),
]
print(pd.DataFrame(status, columns=["earlier finding", "status", "evidence here"]).to_string(index=False))
earlier finding status evidence here
1 No L4 file names the cell type of its neuron still present fields under general/subject: ['genotype,sex,species,subject_id']; no cell-type field or text in any L4 file
2 No file marks the ION-transected mice still present files whose metadata text names ION, infraorbital, nerve, transection, lesion, cut, surgery or sham: 0
3 Genotype missing for the 6 VPM mice and 3 L4 mice; SOM-IRES-Cre x Ai32 given as Ai32 still present mice without a genotype: 9; given as Ai32: 5
4 Every L4 file gives cortical layer 4 still present layers given: ['4'], depth 0.42 to 0.62 mm
5 Optogenetic conditions not described (photo_stim_mode, power NaN, no opsin) still present photo_stim_power NaN in 130 of 130 L4 files; files naming eNpHR: 0
6 Session description gives the wrong recording mode for 24 of 130 L4 files still present files whose described mode disagrees with the trace: 24
7 L4 first_touch_time is the trial start plus the latency divided by 1000 still present L4 trials where the table value x 1000 matches the touch series: 0.956
8 L4 trace at 10 or 20 kHz, stated only by the timestamps still present files by rate: {10000.0: 92, 20000.0: 38}
9 8 files name no principal whisker; VPM files give one per file though units have their own still present files without one: 8; VPM units responding most to another whisker: 11 of 20
10 VPM-site light given as 594 nm in VGAT-ChR2 mice; vS1 594 nm pulses drive VGAT-ChR2 cells still present VGAT-ChR2 cells driven by pulses at sites labelled vS1 594 nm: 13
11 No analysis code linked by the paper or the dandiset still present the dandiset links only the paper (related_publications); the claims file found no code
Every finding of the earlier attempt is still present on this version, which is the same version as before: the dandiset has not changed since 2022 (draft and published assets are identical). The new findings of this attempt are the current conversion, the missing recordings (Fig. 3 depolarized cells, Fig. 6b,c thalamic multiunits, Supplementary Fig. 10 histology), the 50 Hz trains, and the two spellings of the no-lick trial type.
# Groups used below. The files do not mark ION-cut mice, so every group holds the cells of all mice ("all"); the
# variant leaves out the VGAT-ChR2 mice flagged as lacking touch input or performing below 0.5 ("no_flagged").
FS_READINGS = {"tag p<0.001, boundary": "cls", "tag p<0.01, boundary": "cls_01"}
FS_VARIANT = {"tag p<0.001, any waveform (variant)": "cls_tag"}
# fs-touch-response names four readings of the FS group (answer of 2026-10-20)
FS_READINGS_TOUCH = {"(a)(i) p<0.001, boundary": "cls", "(a)(ii) p<0.001, no boundary": "cls_tag",
"(b)(i) p<0.01, boundary": "cls_01", "(b)(ii) p<0.01, no boundary": "cls_01_tag"}
def group(cls, mode=None, sel="all", col="cls"):
g = meas[meas[col] == cls] if cls != "VPM" else meas[meas.cls == "VPM"]
if mode:
g = g[g["mode"] == mode]
if sel == "no_flagged":
g = g[~g.subject.isin(flagged)]
return g
def o11_group(g, o11):
"""`g` with its whisking-epoch columns taken under touch reading O11 `o11`."""
x = meas_o11[meas_o11.O11 == o11].set_index(["path", "unit"])[RATE_COLS]
return g.drop(columns=RATE_COLS).join(x, on=["path", "unit"])
def rate_summary(g):
"""Per O11 reading, filter (O1) and rank-sum samples (O2): increases, decreases, rates, the signed-rank test of whisking
against nonwhisking rate, the cells with r2 > 0.25, and the cells that multiplex (spikes per touch above zero and a
significant whisking change)."""
rows = []
for o11 in yu2016.O11:
go = o11_group(g, o11)
for f in yu2016.FILTERS:
for o2 in ("O2a", "O2b"):
rows.append({"O11": o11, "filter": f, "samples": o2, "n": len(go), "increase": int((go[f"{f}:{o2}"] == 1).sum()),
"decrease": int((go[f"{f}:{o2}"] == -1).sum()), "nonwhisk_mean": go[f"{f}:rate_nonwhisk"].mean(),
"nonwhisk_median": go[f"{f}:rate_nonwhisk"].median(), "whisk_mean": go[f"{f}:rate_whisk"].mean(),
"signed_rank_p": wilcox(go[f"{f}:rate_nonwhisk"], go[f"{f}:rate_whisk"]),
"whisk_higher": int((go[f"{f}:rate_whisk"] > go[f"{f}:rate_nonwhisk"]).sum()),
"r2_above_0.25": int((go[f"{f}:r2"] > 0.25).sum()), "multiplex": int(((go.spt_lo > 0) & (go[f"{f}:{o2}"] != 0)).sum())})
return pd.DataFrame(rows)
def ranges(t, cols):
"""Range of each column over the rows of a reading table."""
return {c: (float(f"{t[c].min():.3g}"), float(f"{t[c].max():.3g}")) for c in cols}
def fmt_p(t):
"""The p-value columns of a table as text with two significant digits."""
return t.assign(**{c: t[c].map(lambda x: "%.2g" % x) for c in t.columns if c.startswith("p_") or c.endswith("_p")})
def wilcox(x, y, alternative="two-sided"):
x, y = np.asarray(x, float), np.asarray(y, float)
ok = np.isfinite(x) & np.isfinite(y)
if ok.sum() < 2 or np.all(x[ok] == y[ok]):
return np.nan
return float(stats.wilcoxon(x[ok], y[ok], alternative=alternative).pvalue)
def within(value, expected, tol):
return bool(np.isfinite(value) and abs(value - expected) <= tol)
behavior-touches¶
Mice touched the pole often when it was within reach (lick trials, 5.7 +- 5.3 touches per trial over 4,147 trials) and rarely when it was distal (0.9 +- 3.4 over 4,173 trials); performance was 0.72 +- 0.11 in cortical and 0.71 +- 0.07 in VPM recordings.
def touch_counts(r, whiskers="pw", ctl_only=True):
b, tr = r["beh"], r["trials"]
if b["principal"] is None:
return None
m = ctl_mask(r) if ctl_only else np.ones(len(tr), bool)
if whiskers == "pw":
on_t = b["t"][b["touch_on"]]
else:
on_t = np.sort(np.concatenate([b["t"][w["on"]] for w in b["whisker_touches"].values()]))
k = yu2016.per_trial_index(on_t, tr)
n = np.bincount(k[k >= 0], minlength=len(tr))
return pd.DataFrame({"type": tr.type.str.capitalize().to_numpy()[m], "touches": n[m], "correct": tr.response.isin(["Hit", "CR"]).to_numpy()[m]})
# reading (a): the 111 control cortical cells cannot be identified (no ION-cut marks), so every cortical cell counts, as in every
# other group; the cells of the flagged mice left out are a variant that does not decide
ctl_cortex = meas[meas.cls != "VPM"].path.unique()
nf_cortex = meas[(meas.cls != "VPM") & ~meas.subject.isin(flagged)].path.unique()
res = []
for rec_set, paths in (("a: control cortical recordings", set(ctl_cortex)), ("b: all sessions", {r["path"] for r in recs})):
for wk in ("pw", "any"):
for ctl_only in (True, False):
t = [touch_counts(r, wk, ctl_only) for r in recs if r["path"] in paths]
t = pd.concat([x for x in t if x is not None])
go, nogo = t[t.type == "Go"].touches, t[t.type == "Nogo"].touches
res.append({"recordings": rec_set, "whiskers": wk, "light": "y: without light" if ctl_only else "x: every trial",
"go_trials": len(go), "go_mean": go.mean(), "go_sd": go.std(), "nogo_trials": len(nogo), "nogo_mean": nogo.mean(),
"nogo_sd": nogo.std()})
res = pd.DataFrame(res)
print("Trial types in the files (as spelled):", pd.Series([x for r in recs for x in r["trials"].type]).value_counts().to_dict(),
"; responses:", pd.Series([x for r in recs for x in r["trials"].response]).value_counts().to_dict())
print("Touches per trial (paper: Go 5.7 +- 5.3 over 4,147 trials, tolerance 0.57; Nogo 0.9 +- 3.4 over 4,173, tolerance 0.09);")
print("readings: recordings (a), (b); whiskers (i) principal 'pw', (ii) any tracked 'any'; light (x) every trial, (y) trials without light:")
print(res.round(2).to_string(index=False))
# performance per recording averaged over cells (cortex) and over units (VPM)
perf_rows = []
for ctl_only in (True, False):
pc = [(touch_counts(by_path[p], "pw", ctl_only).correct.mean()) for p in ctl_cortex]
pv_ = [touch_counts(r, "pw", ctl_only).correct.mean() for r in vpm for u in r["units"]]
perf_rows.append({"light": "y: without light" if ctl_only else "x: every trial", "cortex_cells": len(pc), "cortex_mean": np.mean(pc), "cortex_sd": np.std(pc, ddof=1),
"vpm_units": len(pv_), "vpm_mean": np.mean(pv_), "vpm_sd": np.std(pv_, ddof=1)})
perf_df = pd.DataFrame(perf_rows)
print("\nPerformance (proportion correct per recording), averaged over cortical cells "
"and over VPM units (reading a: every cortical cell; paper: 0.72 +- 0.11 over 111 cells, 0.71 +- 0.07 over 29 units; tolerances 0.072 and 0.071):")
print(perf_df.round(3).to_string(index=False))
for ctl_only in (True, False):
nfc = [touch_counts(by_path[p], "pw", ctl_only).correct.mean() for p in nf_cortex]
print(f"Variant, flagged mice left out ({'y' if ctl_only else 'x'}):", len(nfc), "cells, mean", round(np.mean(nfc), 3))
ok_touch = res[(res.go_mean.sub(5.7).abs() <= 0.57) & (res.nogo_mean.sub(0.9).abs() <= 0.09)]
print("Readings with both touch counts within tolerance:", ok_touch[["recordings", "whiskers", "light"]].values.tolist())
print("Readings with the lick-trial count within tolerance:", res[res.go_mean.sub(5.7).abs() <= 0.57][["recordings", "whiskers", "light"]].values.tolist())
print("Readings with the no-lick count within tolerance:", res[res.nogo_mean.sub(0.9).abs() <= 0.09][["recordings", "whiskers", "light"]].values.tolist())
Trial types in the files (as spelled): {'Go': 10214, 'Nogo': 5579, 'NoGo': 3074} ; responses: {'Hit': 6000, 'CR': 5868, 'Miss': 4214, 'FA': 2785}
Touches per trial (paper: Go 5.7 +- 5.3 over 4,147 trials, tolerance 0.57; Nogo 0.9 +- 3.4 over 4,173, tolerance 0.09);
readings: recordings (a), (b); whiskers (i) principal 'pw', (ii) any tracked 'any'; light (x) every trial, (y) trials without light:
recordings whiskers light go_trials go_mean go_sd nogo_trials nogo_mean nogo_sd
a: control cortical recordings pw y: without light 3637 7.49 6.82 3639 1.17 3.02
a: control cortical recordings pw x: every trial 5212 7.53 6.79 5149 1.14 2.99
a: control cortical recordings any y: without light 3637 10.05 7.74 3639 1.66 4.12
a: control cortical recordings any x: every trial 5212 10.00 7.82 5149 1.61 4.06
b: all sessions pw y: without light 8219 5.74 7.39 6713 0.80 2.48
b: all sessions pw x: every trial 9794 6.04 7.32 8223 0.85 2.57
b: all sessions any y: without light 8219 8.14 8.82 6713 1.23 3.58
b: all sessions any x: every trial 9794 8.42 8.72 8223 1.28 3.64
Performance (proportion correct per recording), averaged over cortical cells and over VPM units (reading a: every cortical cell; paper: 0.72 +- 0.11 over 111 cells, 0.71 +- 0.07 over 29 units; tolerances 0.072 and 0.071):
light cortex_cells cortex_mean cortex_sd vpm_units vpm_mean vpm_sd
y: without light 122 0.694 0.139 29 0.585 0.101
x: every trial 122 0.681 0.120 29 0.585 0.101
Variant, flagged mice left out (y): 109 cells, mean 0.718 Variant, flagged mice left out (x): 109 cells, mean 0.703 Readings with both touch counts within tolerance: [['b: all sessions', 'pw', 'x: every trial']] Readings with the lick-trial count within tolerance: [['b: all sessions', 'pw', 'y: without light'], ['b: all sessions', 'pw', 'x: every trial']] Readings with the no-lick count within tolerance: [['b: all sessions', 'pw', 'x: every trial']]
Result: partially reproduced. Under light reading (x), every trial, with all sessions (b) and the principal whisker (i), mice touched the pole 6.04 times per lick trial (9,794 trials) and 0.85 times per no-lick trial (8,223 trials), both within tolerance of 5.7 and 0.9; no other reading puts both counts within tolerance. Under light reading (y), trials without light only, the same recordings give 5.74 and 0.80, so the no-lick count is outside tolerance and that reading does not match. The trial counts are more than twice the paper's 4,147 and 4,173, so the matching reading is not the paper's set of trials, and no reading gives the paper's trial counts (the nearest, 5,212 and 5,149, come from reading (a) with every trial). Performance during the cortical recordings, now over all 122 cortical cells as in every other group, is 0.681 under (x) and 0.694 under (y), within tolerance of 0.72 (0.703 and 0.718 without the flagged mice, a variant), but during the VPM recordings, which have no light trials, it is 0.585 over 29 units, outside 0.71 +- 0.071.
vpm-touch¶
VPM neurons respond to active touch with a sharp, short-latency increase in spike rate (0.6 +- 0.5 spikes per touch, median 0.4; latency 3.2 +- 1.0 ms, median 2.5 ms).
v = group("VPM")
print(f"VPM units: {len(v)} (touches with ITI >= 25 ms per unit: median {int(v.n_touches.median())}, range {int(v.n_touches.min())} to {int(v.n_touches.max())})")
print(f"Spikes per touch (VPM definition): mean {v.spt.mean():.3f} (expected 0.6 +- 0.093), median {v.spt.median():.3f} "
f"(expected 0.4 +- 0.04), s.d. {v.spt.std():.3f}, range {v.spt.min():.2f} to {v.spt.max():.2f}; interval above 0 in {(v.spt_lo > 0).sum()}")
lat = v.dropna(subset=["lat_centre"])
print(f"Latency, units passing the exclusion: {len(lat)} (left out: {v.lat_excluded[v.lat_centre.isna()].value_counts().to_dict()})")
for name in ("lat_edge", "lat_centre"):
print(f" {'O9 a, left edge' if name == 'lat_edge' else 'O9 b, centre'} ({name}): mean {lat[name].mean():.2f} ms (expected 3.2 +- 0.32), median {lat[name].median():.2f} ms (expected 2.5 +- 0.25), "
f"s.d. {lat[name].std():.2f}, range {lat[name].min():.1f} to {lat[name].max():.1f}")
print("Latencies above 10 ms:", lat[lat.lat_centre > 10][["path", "unit", "lat_centre", "spt"]].assign(path=lambda d: d.path.str[-40:-20]).to_dict("records"))
print("\nBy mouse:")
print(v.groupby("subject").agg(units=("spt", "size"), spt_mean=("spt", "mean"), lat_median=("lat_centre", "median")).round(2).to_string())
bw = pwu.set_index(["path", "unit"])
vv = v.set_index(["path", "unit"]).join(bw[["best", "spt_best"]])
print(f"\nVariant (not the criterion; biased upward): each unit's most responsive tracked whisker, spikes per touch mean "
f"{vv.spt_best.mean():.3f}, median {vv.spt_best.median():.3f}")
fig, ax = plt.subplots(1, 2, figsize=(8, 2.8))
ax[0].hist(v.spt, bins=np.arange(-0.2, 1.6, 0.1), color="0.5"); ax[0].set_xlabel("spikes per touch"); ax[0].set_ylabel("VPM units")
ax[1].hist(lat.lat_centre, bins=np.arange(0, 41, 1), color="0.5"); ax[1].set_xlabel("latency (ms, bin centre)")
plt.tight_layout(); plt.show()
print("Axes hold every value: spikes per touch", round(v.spt.min(), 2), "to", round(v.spt.max(), 2), "; latency", lat.lat_centre.min(), "to", lat.lat_centre.max())
VPM units: 29 (touches with ITI >= 25 ms per unit: median 640, range 3 to 1415)
Spikes per touch (VPM definition): mean 0.403 (expected 0.6 +- 0.093), median 0.268 (expected 0.4 +- 0.04), s.d. 0.392, range -0.01 to 1.25; interval above 0 in 23
Latency, units passing the exclusion: 25 (left out: {'max rate below 10 spikes/s': 3, 'no crossing': 1})
O9 a, left edge (lat_edge): mean 5.64 ms (expected 3.2 +- 0.32), median 2.00 ms (expected 2.5 +- 0.25), s.d. 13.86, range 1.0 to 71.0
O9 b, centre (lat_centre): mean 6.14 ms (expected 3.2 +- 0.32), median 2.50 ms (expected 2.5 +- 0.25), s.d. 13.86, range 1.5 to 71.5
Latencies above 10 ms: [{'path': '184389_ses-20130213_', 'unit': 1, 'lat_centre': 71.5, 'spt': 0.0}, {'path': '203464_ses-20130702_', 'unit': 0, 'lat_centre': 14.5, 'spt': 1.1111111111111112}]
By mouse:
units spt_mean lat_median
subject
anm184389 12 0.25 4.0
anm186997 2 0.65 3.0
anm199549 5 0.64 1.5
anm199551 1 0.14 3.5
anm199552 7 0.24 2.5
anm203464 2 1.18 8.5
Variant (not the criterion; biased upward): each unit's most responsive tracked whisker, spikes per touch mean 0.485, median 0.285
Axes hold every value: spikes per touch -0.01 to 1.25 ; latency 1.5 to 71.5
Result: not reproduced. Over the 29 VPM units, spikes per touch average 0.403 (median 0.268), outside the tolerances of 0.6 and 0.4. The latency median is 2.5 ms with the bin centre (O9 b) and 2.0 ms with its left edge (O9 a) against 2.5 ms, but the mean is 6.14 or 5.64 ms against 3.2, pulled up by one unit at 71.5 ms; 25 units pass the latency exclusion. The files name one principal whisker per file, and 11 of 20 units in files with several whiskers respond most to another one; taking each unit's most responsive whisker, a choice biased upward that the criterion does not make, raises the mean to 0.485, still outside the tolerance.
vpm-whisking¶
VPM neurons fire at 5.1 +- 5.5 spikes/s without whisker movement or touch; most (25 of 29) increase their rate during whisking without touch, and the mean rate rises from 5.1 to 13.5 spikes/s. Three of 29 do not increase with whisking but still respond to touch.
rows = []
for f in yu2016.FILTERS:
for o2 in ("O2a", "O2b"):
rows.append({"filter": f, "samples": o2, "increase": int((v[f"{f}:{o2}"] == 1).sum()), "decrease": int((v[f"{f}:{o2}"] == -1).sum()),
"nonwhisk_mean": v[f"{f}:rate_nonwhisk"].mean(), "nonwhisk_median": v[f"{f}:rate_nonwhisk"].median(),
"nonwhisk_sd": v[f"{f}:rate_nonwhisk"].std(), "whisk_mean": v[f"{f}:rate_whisk"].mean(),
"whisk_sd": v[f"{f}:rate_whisk"].std(), "signed_rank_p": "%.2g" % wilcox(v[f"{f}:rate_nonwhisk"], v[f"{f}:rate_whisk"]),
"whisk_higher": int((v[f"{f}:rate_whisk"] > v[f"{f}:rate_nonwhisk"]).sum())})
vw = pd.DataFrame(rows)
print("VPM units:", len(v), "; expected: increases 25 +- 2.5 of 29, nonwhisking mean 5.1 +- 1.02, median 3.3 +- 0.33, whisking mean 13.5 +- 2.36 spikes/s")
print(vw.round(4).to_string(index=False))
print("Seconds of nonwhisking and whisking per unit (filtfilt): median", round(v["filtfilt:time_nonwhisk"].median(), 1), "and",
round(v["filtfilt:time_whisk"].median(), 1))
vs = rate_summary(v)
print("\nUnder every O11 reading (touches bounding the epochs), filter and sample reading:")
print(fmt_p(vs.drop(columns=["r2_above_0.25", "multiplex"])).round(4).to_string(index=False))
print("Ranges over the 16 readings:", ranges(vs, ["increase", "nonwhisk_mean", "nonwhisk_median", "whisk_mean", "signed_rank_p"]))
print("Units without a significant increase (filtfilt, O2a): spikes per touch",
v[v["filtfilt:O2a"] != 1][["spt", "spt_lo"]].round(2).values.tolist())
VPM units: 29 ; expected: increases 25 +- 2.5 of 29, nonwhisking mean 5.1 +- 1.02, median 3.3 +- 0.33, whisking mean 13.5 +- 2.36 spikes/s
filter samples increase decrease nonwhisk_mean nonwhisk_median nonwhisk_sd whisk_mean whisk_sd signed_rank_p whisk_higher
filtfilt O2a 24 1 3.7609 1.7079 4.0008 11.5391 10.9661 5.2e-08 28
filtfilt O2b 25 1 3.7609 1.7079 4.0008 11.5391 10.9661 5.2e-08 28
lfilter O2a 21 1 3.9250 1.7213 4.0878 11.0064 10.6188 7.1e-08 28
lfilter O2b 24 1 3.9250 1.7213 4.0878 11.0064 10.6188 7.1e-08 28
Seconds of nonwhisking and whisking per unit (filtfilt): median 1004.6 and 488.9
Under every O11 reading (touches bounding the epochs), filter and sample reading:
O11 filter samples n increase decrease nonwhisk_mean nonwhisk_median whisk_mean signed_rank_p whisk_higher
a-i filtfilt O2a 29 24 1 3.7609 1.7079 11.5391 5.2e-08 28
a-i filtfilt O2b 29 25 1 3.7609 1.7079 11.5391 5.2e-08 28
a-i lfilter O2a 29 21 1 3.9250 1.7213 11.0064 7.1e-08 28
a-i lfilter O2b 29 24 1 3.9250 1.7213 11.0064 7.1e-08 28
a-ii filtfilt O2a 29 24 1 3.7352 1.8357 11.6062 5.2e-08 28
a-ii filtfilt O2b 29 25 1 3.7352 1.8357 11.6062 5.2e-08 28
a-ii lfilter O2a 29 21 1 3.9029 1.8559 11.0658 7.1e-08 28
a-ii lfilter O2b 29 24 1 3.9029 1.8559 11.0658 7.1e-08 28
b-i filtfilt O2a 29 24 1 3.7972 1.7079 12.0716 5.2e-08 28
b-i filtfilt O2b 29 25 1 3.7972 1.7079 12.0716 5.2e-08 28
b-i lfilter O2a 29 22 1 3.9643 1.7213 11.5216 7.1e-08 28
b-i lfilter O2b 29 24 1 3.9643 1.7213 11.5216 7.1e-08 28
b-ii filtfilt O2a 29 24 1 3.7735 1.8357 12.0908 5.2e-08 28
b-ii filtfilt O2b 29 25 1 3.7735 1.8357 12.0908 5.2e-08 28
b-ii lfilter O2a 29 22 1 3.9410 1.8559 11.5426 7.1e-08 28
b-ii lfilter O2b 29 24 1 3.9410 1.8559 11.5426 7.1e-08 28
Ranges over the 16 readings: {'increase': (21.0, 25.0), 'nonwhisk_mean': (3.74, 3.96), 'nonwhisk_median': (1.71, 1.86), 'whisk_mean': (11.0, 12.1), 'signed_rank_p': (5.22e-08, 7.08e-08)}
Units without a significant increase (filtfilt, O2a): spikes per touch [[0.0, 0.0], [0.0, 0.0], [0.79, 0.69], [0.08, 0.06], [1.25, 1.14]]
Result: partially reproduced. The whisking rate is above the nonwhisking rate over the 29 units under every reading of O1, O2 and O11 (signed-rank p = 5.2e-08 to 7.1e-08 for the main epochs; higher in 28). Over the 16 readings 21 to 25 units increase significantly, so the count is outside 25 +- 2.5 under some; the mean whisking rate is 11.01 to 12.09 spikes/s, within 13.5 +- 2.36 under some readings only, and the nonwhisking rate is below the paper's under every reading (mean 3.74 to 3.96 against 5.1 +- 1.02; median 1.71 to 1.86 against 3.3 +- 0.33).
vpm-whisking-phase¶
The spike rate of VPM neurons is modulated by the phase of whisker movement.
for f in yu2016.FILTERS:
print(f"{f}: VPM units with r2 > 0.25: {int((v[f'{f}:r2'] > 0.25).sum())} of {len(v)}; r2 median {v[f'{f}:r2'].median():.2f}; "
f"coupling strength of coupled units median {v[v[f'{f}:r2'] > 0.25][f'{f}:strength'].median():.2f} spikes/s "
f"(range {v[v[f'{f}:r2'] > 0.25][f'{f}:strength'].min():.2f} to {v[v[f'{f}:r2'] > 0.25][f'{f}:strength'].max():.2f})")
print("Units with r2 > 0.25 under each O11 reading and filter:", vs.drop_duplicates(["O11", "filter"]).set_index(["O11", "filter"])["r2_above_0.25"].to_dict())
nv = align[align.kind == "VPM"]
print(f"Reference (not part of the criterion): with spikes moved to the next trial, {int((nv.r2_shift > 0.25).sum())} of {len(nv)} units "
"have r2 > 0.25 (filtfilt)")
print("By mouse (filtfilt):", v.assign(c=v["filtfilt:r2"] > 0.25).groupby("subject").c.agg(["sum", "size"]).to_dict("index"))
filtfilt: VPM units with r2 > 0.25: 25 of 29; r2 median 0.58; coupling strength of coupled units median 1.41 spikes/s (range 0.12 to 7.18)
lfilter: VPM units with r2 > 0.25: 27 of 29; r2 median 0.65; coupling strength of coupled units median 0.91 spikes/s (range 0.10 to 3.28)
Units with r2 > 0.25 under each O11 reading and filter: {('a-i', 'filtfilt'): 25, ('a-i', 'lfilter'): 27, ('a-ii', 'filtfilt'): 26, ('a-ii', 'lfilter'): 26, ('b-i', 'filtfilt'): 25, ('b-i', 'lfilter'): 26, ('b-ii', 'filtfilt'): 26, ('b-ii', 'lfilter'): 26}
Reference (not part of the criterion): with spikes moved to the next trial, 16 of 29 units have r2 > 0.25 (filtfilt)
By mouse (filtfilt): {'anm184389': {'sum': 10, 'size': 12}, 'anm186997': {'sum': 1, 'size': 2}, 'anm199549': {'sum': 5, 'size': 5}, 'anm199551': {'sum': 1, 'size': 1}, 'anm199552': {'sum': 7, 'size': 7}, 'anm203464': {'sum': 1, 'size': 2}}
Result: reproduced. 25 to 27 of the 29 VPM units have a sinusoid fit of rate against phase with r2 > 0.25 under both filters and every O11 reading, more than half. The measure is weak as a test: with spikes moved to the next trial, 16 of 29 units also pass, which the criterion does not ask for; the units' own spikes pass far more often (25 against 16, and r2 is higher in their own trial, p = 0.0015 in the data check).
exc-vm-touch-whisking¶
In L4 excitatory neurons (37 whole-cell), resting Vm spans -76 to -45 mV (-63 +- 8 mV); every neuron depolarizes after touch (latency 4.8 +- 0.8 ms, amplitude 6.0 +- 2.9 mV), while whisking causes only a small depolarization or hyperpolarization (1.2 +- 2.1 mV).
def vm_table(cls, sel, col="cls", filt="filtfilt", o3="a"):
paths = group(cls, "whole-cell", sel, col).path
return vmm[vmm.path.isin(paths) & (vmm.filt == filt) & (vmm.O3 == o3)].set_index("path")
for sel in ("all", "no_flagged"):
e = vm_table("E", sel)
pos = e[e.amp_mv > 0]
print(f"[{sel}] whole-cell E cells: {len(e)} (paper 37); average touch PSP from a median of {int(e.touch_avg_n.median())} touches "
f"(range {int(e.touch_avg_n.min())} to {int(e.touch_avg_n.max())}); amplitude > 0 in {len(pos)}, latency found in "
f"{int(pos.latency_ms.notna().sum())} of them")
print(f" touch latency (expected mean 4.8 +- 0.48 ms), dVm/dt at the sampling rate (the criterion): mean {e.latency_ms.mean():.2f} "
f"(s.d. {e.latency_ms.std():.2f}, median {e.latency_ms.median():.2f}, range {e.latency_ms.min():.2f} to {e.latency_ms.max():.2f}); "
f"for comparison only, 1 ms bins (no longer a reading): left edge mean {e.latency_1ms_edge.mean():.2f}, centre mean "
f"{e.latency_1ms_centre.mean():.2f}; amplitude mean {e.amp_mv.mean():.2f} mV (expected 6.0 +- 0.6; s.d. {e.amp_mv.std():.2f}, "
f"median {e.amp_mv.median():.2f}, range {e.amp_mv.min():.2f} to {e.amp_mv.max():.2f})")
print(f" resting Vm O4a mean {e.rest_mean.mean():.2f} mV (s.d. {e.rest_mean.std():.2f}, range {e.rest_mean.min():.1f} to "
f"{e.rest_mean.max():.1f}); O4b (median per cell) mean {e.rest_median.mean():.2f} (expected -63 +- 6.3)")
for filt in yu2016.FILTERS:
ef = vm_table("E", sel, filt=filt)
print(f" {filt}, earlier rule of this attempt (epochs O11 a-i, 250 ms rule on principal-whisker touches): whisking change (a) onset windows mean {ef.whisk_onset_change.mean():.2f} mV (s.d. {ef.whisk_onset_change.std():.2f}; "
f"cells with onsets {int(ef.whisk_onset_change.notna().sum())}, onsets per cell median {int(ef.n_onsets.median())}); "
f"(b) epochs mean {ef.whisk_epoch_change.mean():.2f} mV (s.d. {ef.whisk_epoch_change.std():.2f}); expected 1.2 +- 0.35")
e = vm_table("E", "all")
print("Cells without a positive touch PSP or a latency:", e[(e.amp_mv <= 0) | e.latency_ms.isna()][["subject", "amp_mv", "latency_ms", "touch_avg_n"]]
.round(2).reset_index(drop=True).to_dict("records"))
print("By mouse line:", e.assign(line=e.subject.map(line_of)).groupby("line")[["amp_mv", "latency_ms", "rest_mean"]].mean().round(2).to_dict("index"))
# Whisking onsets and epochs under readings O10 (windows of the 2 degree rise: a, Supp. Fig. 4 windows; b, 0 to 250 ms
# after against the whole nonwhisking period) and O11 (touches bounding epochs and onsets: a, every tracked whisker;
# b, the principal whisker; i, 10 ms before onset to 100 ms after offset; ii, to 100 ms after onset). Answer of 2026-10-20.
o_rows = []
for cls_, p in [("E", p) for p in group("E", "whole-cell").path] + [("FS", p) for p in group("FS", "whole-cell").path]:
r = by_path[p]
m = ctl_mask(r)
vm = r["vm1k"].astype(float)
tri = r["beh"]["trial"]
use = (tri >= 0) & m[np.clip(tri, 0, None)] & np.isfinite(vm)
for filt in yu2016.FILTERS:
for (o10, o11) in r["beh"][filt]["readings"]:
e_ = yu2016.beh_reading(r["beh"], filt, o10, o11)[filt]
res_, pre_, d_ = yu2016.vm_epoch_measures(vm, use, e_)
o_rows.append({"cls": cls_, "path": p, "subject": r["subject"], "filter": filt, "O10": o10, "O11": o11, **res_,
"whisk_pre": np.array(pre_), "whisk_dvm": np.array(d_)})
ovm_all = pd.DataFrame(o_rows)
ovm = ovm_all[ovm_all.cls == "E"]
rows = []
for sel in ("all", "no_flagged"):
g0 = ovm if sel == "all" else ovm[~ovm.subject.isin(flagged)]
for (filt, o10, o11), g in g0.groupby(["filter", "O10", "O11"]):
rows.append({"cells": sel, "filter": filt, "O10": o10, "O11": o11, "n": len(g), "rest_O4a": g.rest_mean.mean(),
"rest_O4b": g.rest_median.mean(), "onset_cells": int(g.whisk_onset_change.notna().sum()),
"onsets_median": g.n_onsets.median(), "whisk_change_a_onsets": g.whisk_onset_change.mean(),
"whisk_change_a_sd": g.whisk_onset_change.std(), "whisk_change_b_epochs": g.whisk_epoch_change.mean(),
"a_within": within(g.whisk_onset_change.mean(), 1.2, 0.35), "b_within": within(g.whisk_epoch_change.mean(), 1.2, 0.35),
"rest_within": within(g.rest_mean.mean(), -63, 6.3)})
oread = pd.DataFrame(rows)
print("\nWhisking change and resting Vm of the whole-cell E cells under readings O10 and O11 (expected whisking change 1.2 +- 0.35 mV, "
"resting Vm -63 +- 6.3 mV; the touch values above do not depend on these readings):")
print(oread.round(2).to_string(index=False))
oa = oread[oread.cells == "all"]
print("Readings (all cells) with the whisking change within tolerance: measure (a)", int(oa.a_within.sum()), "of", len(oa),
"; measure (b)", int(oa.b_within.sum()), "of", len(oa), "; resting Vm within tolerance in", int(oa.rest_within.sum()), "of", len(oa))
print("Range over readings (all cells): whisking change (a)", round(oa.whisk_change_a_onsets.min(), 2), "to", round(oa.whisk_change_a_onsets.max(), 2),
"mV, (b)", round(oa.whisk_change_b_epochs.min(), 2), "to", round(oa.whisk_change_b_epochs.max(), 2), "mV; resting Vm (O4 a)",
round(oa.rest_O4a.min(), 2), "to", round(oa.rest_O4a.max(), 2), "mV")
[all] whole-cell E cells: 51 (paper 37); average touch PSP from a median of 205 touches (range 0 to 662); amplitude > 0 in 48, latency found in 48 of them
touch latency (expected mean 4.8 +- 0.48 ms), dVm/dt at the sampling rate (the criterion): mean 7.68 (s.d. 14.58, median 4.90, range 0.00 to 101.15); for comparison only, 1 ms bins (no longer a reading): left edge mean 14.89, centre mean 15.39; amplitude mean 4.78 mV (expected 6.0 +- 0.6; s.d. 3.67, median 4.69, range 0.01 to 15.64)
resting Vm O4a mean -61.69 mV (s.d. 8.81, range -76.0 to -38.8); O4b (median per cell) mean -61.89 (expected -63 +- 6.3)
filtfilt, earlier rule of this attempt (epochs O11 a-i, 250 ms rule on principal-whisker touches): whisking change (a) onset windows mean 0.86 mV (s.d. 1.73; cells with onsets 51, onsets per cell median 61); (b) epochs mean 1.10 mV (s.d. 1.64); expected 1.2 +- 0.35
lfilter, earlier rule of this attempt (epochs O11 a-i, 250 ms rule on principal-whisker touches): whisking change (a) onset windows mean 0.75 mV (s.d. 1.60; cells with onsets 51, onsets per cell median 62); (b) epochs mean 1.08 mV (s.d. 1.69); expected 1.2 +- 0.35
[no_flagged] whole-cell E cells: 43 (paper 37); average touch PSP from a median of 253 touches (range 3 to 662); amplitude > 0 in 41, latency found in 41 of them
touch latency (expected mean 4.8 +- 0.48 ms), dVm/dt at the sampling rate (the criterion): mean 4.97 (s.d. 3.08, median 4.90, range 0.00 to 17.90); for comparison only, 1 ms bins (no longer a reading): left edge mean 5.83, centre mean 6.33; amplitude mean 5.54 mV (expected 6.0 +- 0.6; s.d. 3.44, median 5.45, range 0.01 to 15.64)
resting Vm O4a mean -60.53 mV (s.d. 9.05, range -76.0 to -38.8); O4b (median per cell) mean -60.73 (expected -63 +- 6.3)
filtfilt, earlier rule of this attempt (epochs O11 a-i, 250 ms rule on principal-whisker touches): whisking change (a) onset windows mean 0.92 mV (s.d. 1.88; cells with onsets 43, onsets per cell median 63); (b) epochs mean 1.24 mV (s.d. 1.74); expected 1.2 +- 0.35
lfilter, earlier rule of this attempt (epochs O11 a-i, 250 ms rule on principal-whisker touches): whisking change (a) onset windows mean 0.80 mV (s.d. 1.73; cells with onsets 43, onsets per cell median 65); (b) epochs mean 1.21 mV (s.d. 1.80); expected 1.2 +- 0.35
Cells without a positive touch PSP or a latency: [{'subject': 'anm223517', 'amp_mv': nan, 'latency_ms': nan, 'touch_avg_n': 3}, {'subject': 'anm307766', 'amp_mv': nan, 'latency_ms': nan, 'touch_avg_n': 4}, {'subject': 'anm318532', 'amp_mv': nan, 'latency_ms': nan, 'touch_avg_n': 0}]
By mouse line: {'C57BL/6': {'amp_mv': 6.5, 'latency_ms': 5.1, 'rest_mean': -62.05}, 'PV-ChR2': {'amp_mv': 8.36, 'latency_ms': 4.5, 'rest_mean': -38.81}, 'PV-IRES-Cre': {'amp_mv': 5.53, 'latency_ms': 4.62, 'rest_mean': -54.17}, 'PV-IRES-Cre x Ai32': {'amp_mv': nan, 'latency_ms': nan, 'rest_mean': -57.76}, 'SOM-IRES-Cre x Ai32': {'amp_mv': 8.35, 'latency_ms': 2.6, 'rest_mean': -71.76}, 'VGAT-ChR2-EYFP': {'amp_mv': 3.91, 'latency_ms': 9.48, 'rest_mean': -64.25}}
Whisking change and resting Vm of the whole-cell E cells under readings O10 and O11 (expected whisking change 1.2 +- 0.35 mV, resting Vm -63 +- 6.3 mV; the touch values above do not depend on these readings):
cells filter O10 O11 n rest_O4a rest_O4b onset_cells onsets_median whisk_change_a_onsets whisk_change_a_sd whisk_change_b_epochs a_within b_within rest_within
all filtfilt a a-i 51 -61.69 -61.89 51 57.0 0.83 1.72 1.10 False True True
all filtfilt a a-ii 51 -61.67 -61.88 51 59.0 0.82 1.73 1.10 False True True
all filtfilt a b-i 51 -61.69 -61.89 51 61.0 0.87 1.73 1.13 True True True
all filtfilt a b-ii 51 -61.67 -61.88 51 61.0 0.86 1.74 1.14 True True True
all filtfilt b a-i 51 -61.69 -61.89 51 58.0 0.78 1.73 1.10 False True True
all filtfilt b a-ii 51 -61.67 -61.88 51 58.0 0.78 1.73 1.10 False True True
all filtfilt b b-i 51 -61.69 -61.89 51 58.0 0.82 1.75 1.13 False True True
all filtfilt b b-ii 51 -61.67 -61.88 51 58.0 0.81 1.75 1.14 False True True
all lfilter a a-i 51 -61.68 -61.88 51 57.0 0.73 1.59 1.08 False True True
all lfilter a a-ii 51 -61.66 -61.87 51 57.0 0.72 1.59 1.08 False True True
all lfilter a b-i 51 -61.68 -61.88 51 62.0 0.76 1.60 1.11 False True True
all lfilter a b-ii 51 -61.66 -61.87 51 62.0 0.74 1.60 1.11 False True True
all lfilter b a-i 51 -61.68 -61.88 51 55.0 0.65 1.56 1.08 False True True
all lfilter b a-ii 51 -61.66 -61.87 51 56.0 0.65 1.56 1.08 False True True
all lfilter b b-i 51 -61.68 -61.88 51 61.0 0.68 1.57 1.11 False True True
all lfilter b b-ii 51 -61.66 -61.87 51 61.0 0.67 1.57 1.11 False True True
no_flagged filtfilt a a-i 43 -60.53 -60.73 43 61.0 0.88 1.87 1.24 True True True
no_flagged filtfilt a a-ii 43 -60.52 -60.72 43 61.0 0.87 1.87 1.24 True True True
no_flagged filtfilt a b-i 43 -60.53 -60.73 43 63.0 0.92 1.88 1.27 True True True
no_flagged filtfilt a b-ii 43 -60.52 -60.72 43 65.0 0.91 1.89 1.28 True True True
no_flagged filtfilt b a-i 43 -60.53 -60.73 43 61.0 0.82 1.87 1.24 False True True
no_flagged filtfilt b a-ii 43 -60.52 -60.72 43 61.0 0.82 1.88 1.24 False True True
no_flagged filtfilt b b-i 43 -60.53 -60.73 43 64.0 0.86 1.90 1.27 True True True
no_flagged filtfilt b b-ii 43 -60.52 -60.72 43 64.0 0.86 1.90 1.28 True True True
no_flagged lfilter a a-i 43 -60.52 -60.71 43 62.0 0.78 1.73 1.21 False True True
no_flagged lfilter a a-ii 43 -60.51 -60.70 43 62.0 0.77 1.73 1.21 False True True
no_flagged lfilter a b-i 43 -60.52 -60.71 43 65.0 0.81 1.74 1.24 False True True
no_flagged lfilter a b-ii 43 -60.51 -60.70 43 65.0 0.79 1.74 1.24 False True True
no_flagged lfilter b a-i 43 -60.52 -60.71 43 59.0 0.67 1.69 1.21 False True True
no_flagged lfilter b a-ii 43 -60.51 -60.70 43 59.0 0.67 1.69 1.21 False True True
no_flagged lfilter b b-i 43 -60.52 -60.71 43 67.0 0.70 1.71 1.24 False True True
no_flagged lfilter b b-ii 43 -60.51 -60.70 43 67.0 0.70 1.71 1.24 False True True
Readings (all cells) with the whisking change within tolerance: measure (a) 2 of 16 ; measure (b) 16 of 16 ; resting Vm within tolerance in 16 of 16
Range over readings (all cells): whisking change (a) 0.65 to 0.87 mV, (b) 1.08 to 1.14 mV; resting Vm (O4 a) -61.69 to -61.66 mV
Result: partially reproduced. Every one of the 48 whole-cell excitatory cells with enough touches depolarizes after touch and has a latency (3 cells have fewer than 5 touches). Resting Vm (mean -61.69 mV, or -61.89 with the median per cell) is within tolerance under every reading, but the touch amplitude (4.78 mV against 6.0 +- 0.6) and the latency (mean 7.68 ms with dVm/dt at the sampling rate, median 4.90, against 4.8 +- 0.48) are not, and neither depends on the whisking readings. The latency diagnostic below shows that the criterion's rule picks single-sample excursions of dVm/dt in 5 cells (under 1 ms) and is pulled up by late crossings in others (up to 101 ms). Under the 16 combinations of readings O10, O11 and the two filters, the whisking change with the onset windows ranges from 0.65 to 0.87 mV and is within 1.2 +- 0.35 in 2 (zero-phase filter, O10 a, O11 b-i and b-ii, 0.87 and 0.86 mV); with the epochs it is 1.08 to 1.14 mV and within tolerance in all 16. The group holds 51 cells against the paper's 37 and includes the 8 cells of the flagged mice; without them the amplitude is 5.54 mV and the latency 4.97 ms, both within tolerance, but that variant does not decide. Amplitudes go down to 0.01 mV: the peak of the average minus the pre-touch mean is positive for nearly any average, so "every neuron depolarizes" is a weak test.
exc-threshold-touch-vs-whisking¶
Touch moves the membrane potential of L4 excitatory neurons much closer to spike threshold than whisking does (shift 6.1 +- 3.2 mV against 1.1 +- 2.0 mV).
rows = []
for sel in ("all", "no_flagged"):
for filt in yu2016.FILTERS:
for o3 in ("a", "b"):
e = vm_table("E", sel, filt=filt, o3=o3).dropna(subset=["shift_whisk", "shift_touch"])
rows.append({"cells": sel, "filter": filt, "O3": o3, "n": len(e), "shift_whisk_mean": e.shift_whisk.mean(),
"shift_whisk_sd": e.shift_whisk.std(), "shift_touch_mean": e.shift_touch.mean(), "shift_touch_sd": e.shift_touch.std(),
"touch_larger": int((e.shift_touch > e.shift_whisk).sum()),
"p_touch_gt_whisk": "%.2g" % wilcox(e.shift_touch, e.shift_whisk, "greater"),
"threshold_median_mv": e.threshold_median.median(), "silent_cells": int((e.threshold_median == -42).sum())})
print("Shift toward threshold (10th percentile of distance to threshold, nonwhisking minus whisking or touch epochs); "
"expected whisking 1.1 +- 0.33 mV, touch 6.1 +- 0.61 mV, touch > whisking by signed-rank p < 0.05")
print(pd.DataFrame(rows).round(2).to_string(index=False))
ta = vmm[vmm.O3 == "a"].set_index(["path", "filt"]).threshold_median
tb = vmm[vmm.O3 == "b"].set_index(["path", "filt"]).threshold_median
print("O3 a against b: largest difference of a cell's median threshold", round(float((ta - tb).abs().max()), 4), "mV; spikes whose threshold differs:",
sum(int(np.sum(~np.isclose(r["spike_thr"]["a"], r["spike_thr"]["b"], equal_nan=True))) for r in l4 if r["mode"] == "whole-cell"), "of",
sum(len(r["spike_thr"]["a"]) for r in l4 if r["mode"] == "whole-cell"),
"(reading a adds the trial's mean dVm/dt to the crossing level of reading b)")
trows = []
for p in group("E", "whole-cell").path:
r = by_path[p]
m = ctl_mask(r)
for o11 in yu2016.O11:
rr = {**r, "beh": yu2016.beh_o11(r["beh"], o11)}
for filt in yu2016.FILTERS:
x = yu2016.vm_measures(rr, m, filt, "a")
trows.append({"O11": o11, "filter": filt, "shift_whisk": x["shift_whisk"], "shift_touch": x["shift_touch"]})
tr11 = pd.DataFrame(trows).dropna()
t11 = tr11.groupby(["O11", "filter"]).apply(lambda g: pd.Series({"n": len(g), "shift_whisk_mean": g.shift_whisk.mean(), "shift_touch_mean": g.shift_touch.mean(),
"touch_larger": int((g.shift_touch > g.shift_whisk).sum()),
"p_touch_gt_whisk": wilcox(g.shift_touch, g.shift_whisk, "greater")})).reset_index()
print("\nUnder each O11 reading (threshold O3 a):")
print(fmt_p(t11).round(3).to_string(index=False))
Shift toward threshold (10th percentile of distance to threshold, nonwhisking minus whisking or touch epochs); expected whisking 1.1 +- 0.33 mV, touch 6.1 +- 0.61 mV, touch > whisking by signed-rank p < 0.05
cells filter O3 n shift_whisk_mean shift_whisk_sd shift_touch_mean shift_touch_sd touch_larger p_touch_gt_whisk threshold_median_mv silent_cells
all filtfilt a 50 0.84 2.75 4.62 3.50 44 1e-10 -42.0 17
all filtfilt b 50 0.84 2.75 4.62 3.50 44 1e-10 -42.0 17
all lfilter a 50 1.10 2.01 4.67 3.54 43 9e-11 -42.0 17
all lfilter b 50 1.10 2.01 4.67 3.54 43 9e-11 -42.0 17
no_flagged filtfilt a 43 0.76 2.78 5.24 3.16 40 8e-12 -42.0 14
no_flagged filtfilt b 43 0.76 2.78 5.24 3.16 40 8e-12 -42.0 14
no_flagged lfilter a 43 1.03 1.85 5.29 3.20 40 3.8e-12 -42.0 14
no_flagged lfilter b 43 1.03 1.85 5.29 3.20 40 3.8e-12 -42.0 14
O3 a against b: largest difference of a cell's median threshold 0.0089 mV; spikes whose threshold differs: 8 of 41620 (reading a adds the trial's mean dVm/dt to the crossing level of reading b)
Under each O11 reading (threshold O3 a): O11 filter n shift_whisk_mean shift_touch_mean touch_larger p_touch_gt_whisk a-i filtfilt 50.0 0.845 4.621 44.0 1e-10 a-i lfilter 50.0 1.096 4.672 43.0 9e-11 a-ii filtfilt 50.0 1.063 4.598 43.0 1.1e-10 a-ii lfilter 50.0 1.102 4.649 43.0 9e-11 b-i filtfilt 50.0 0.898 4.617 44.0 1.1e-10 b-i lfilter 50.0 1.145 4.667 43.0 1.1e-10 b-ii filtfilt 50.0 1.123 4.597 43.0 1.3e-10 b-ii lfilter 50.0 1.156 4.646 43.0 1.1e-10
Result: partially reproduced. Touch moves the 50 excitatory cells closer to threshold than whisking does in 43 or 44 of them under every filter and O11 reading (signed-rank p = 9e-11 to 1.3e-10). The touch shift averages 4.60 to 4.67 mV, outside the tolerance of 6.1 +- 0.61, and the whisking shift 0.84 to 1.16 mV, within 1.1 +- 0.33 under every reading. The two threshold readings O3 give the same values: a cell's median threshold differs by at most 0.0089 mV and 8 of 41,620 spikes change threshold, since a trial's mean dVm/dt is close to zero. 17 of the 50 cells fire no spike, so their threshold is the default -42 mV.
exc-effective-reversal¶
The effective reversal potential of the touch response of L4 excitatory neurons is more depolarized than resting Vm, while that of whisking is close to rest and more hyperpolarized than that of touch.
rows = []
for sel in ("all", "no_flagged"):
for filt in yu2016.FILTERS:
e = vm_table("E", sel, filt=filt)
rt = e.apply(lambda x: yu2016.reversal(x.touch_pre, x.touch_dvm)[0], axis=1)
rw = e.apply(lambda x: yu2016.reversal(x.whisk_pre, x.whisk_dvm)[0], axis=1)
ok = rt.notna() & rw.notna()
pt = yu2016.reversal(np.concatenate(e.touch_pre.to_list()), np.concatenate(e.touch_dvm.to_list()))
pw_ = yu2016.reversal(np.concatenate(e.whisk_pre.to_list()), np.concatenate(e.whisk_dvm.to_list()))
rows.append({"cells": sel, "filter": filt, "n_both": int(ok.sum()), "touch_rev_median": rt[ok].median(), "whisk_rev_median": rw[ok].median(),
"touch_above_whisk": int((rt[ok] > rw[ok]).sum()), "rest_median": e.rest_mean.median(),
"pooled_touch_rev": pt[0], "pooled_touch_slope": pt[1], "pooled_whisk_rev": pw_[0], "pooled_whisk_slope": pw_[1]})
print("Effective reversal potentials (mV): (a) per neuron, medians over neurons with both; (b) one pooled fit per event type")
print(pd.DataFrame(rows).round(2).to_string(index=False))
e = vm_table("E", "all")
rt_ = e.apply(lambda x: yu2016.reversal(x.touch_pre, x.touch_dvm)[0], axis=1).dropna().sort_values()
print("Per-neuron touch reversals (mV), the middle six of", len(rt_), ":", rt_.iloc[len(rt_) // 2 - 3:len(rt_) // 2 + 3].round(3).tolist(),
"; every value:", rt_.round(1).tolist())
rrows = []
for (filt, o10, o11), g in ovm.groupby(["filter", "O10", "O11"]):
rw = g.set_index("path").apply(lambda x: yu2016.reversal(x.whisk_pre, x.whisk_dvm)[0], axis=1)
both = rw.dropna().index.intersection(rt_.index)
rrows.append({"filter": filt, "O10": o10, "O11": o11, "n_both": len(both), "touch_rev_median": rt_[both].median(),
"whisk_rev_median": rw[both].median(), "touch_above_whisk": int((rt_[both] > rw[both]).sum()),
"pooled_whisk_rev": yu2016.reversal(np.concatenate(g.whisk_pre.to_list()), np.concatenate(g.whisk_dvm.to_list()))[0]})
print("\nWhisking reversal under each onset reading O10 and O11 (touch reversals do not depend on them):")
print(pd.DataFrame(rrows).round(2).to_string(index=False))
print("Per-neuron touch events median", int(e.touch_dvm.map(len).median()), ", whisking onsets median", int(e.whisk_dvm.map(len).median()))
Effective reversal potentials (mV): (a) per neuron, medians over neurons with both; (b) one pooled fit per event type
cells filter n_both touch_rev_median whisk_rev_median touch_above_whisk rest_median pooled_touch_rev pooled_touch_slope pooled_whisk_rev pooled_whisk_slope
all filtfilt 48 -50.00 -58.49 39 -63.90 -31.90 -0.19 -55.30 -0.18
all lfilter 48 -50.00 -59.78 38 -63.91 -31.90 -0.19 -56.27 -0.19
no_flagged filtfilt 41 -49.09 -57.25 37 -62.36 -34.45 -0.22 -54.91 -0.20
no_flagged lfilter 41 -49.09 -58.21 36 -62.32 -34.45 -0.22 -55.85 -0.20
Per-neuron touch reversals (mV), the middle six of 48 : [-51.525, -50.965, -50.231, -49.769, -49.595, -49.467] ; every value: [-215.7, -70.2, -70.0, -67.2, -65.7, -64.4, -63.1, -62.9, -62.1, -60.4, -60.2, -59.5, -56.0, -55.8, -55.5, -54.8, -53.4, -52.7, -52.4, -52.0, -51.8, -51.5, -51.0, -50.2, -49.8, -49.6, -49.5, -49.1, -48.8, -46.0, -45.7, -45.3, -45.0, -44.8, -39.5, -39.4, -38.8, -38.4, -36.7, -36.6, -33.7, -29.0, -27.0, -19.2, -16.4, -6.7, 16.6, 18.3]
Whisking reversal under each onset reading O10 and O11 (touch reversals do not depend on them): filter O10 O11 n_both touch_rev_median whisk_rev_median touch_above_whisk pooled_whisk_rev filtfilt a a-i 48 -50.00 -57.43 38 -55.41 filtfilt a a-ii 48 -50.00 -57.43 39 -55.49 filtfilt a b-i 48 -50.00 -58.50 39 -55.27 filtfilt a b-ii 48 -50.00 -58.50 40 -55.34 filtfilt b a-i 48 -50.00 -58.35 38 -51.62 filtfilt b a-ii 48 -50.00 -58.35 38 -51.67 filtfilt b b-i 48 -50.00 -58.64 40 -51.59 filtfilt b b-ii 48 -50.00 -58.64 40 -51.63 lfilter a a-i 48 -50.00 -60.28 38 -56.24 lfilter a a-ii 48 -50.00 -60.28 38 -56.30 lfilter a b-i 48 -50.00 -59.78 38 -56.22 lfilter a b-ii 48 -50.00 -59.78 38 -56.29 lfilter b a-i 47 -49.77 -60.91 39 -53.32 lfilter b a-ii 47 -49.77 -60.91 39 -53.27 lfilter b b-i 47 -49.77 -60.91 39 -53.20 lfilter b b-ii 47 -49.77 -60.91 39 -53.19 Per-neuron touch events median 205 , whisking onsets median 61
Result: reproduced. Fitted per neuron, the touch reversal (median -50.0 mV) lies above the whisking reversal (median -57.4 to -60.9 mV over the filters and the O10 and O11 readings) in 38 to 40 of 47 or 48 neurons and above the median resting Vm (-63.9 mV); one pooled fit per event type gives -31.90 mV for touch and -51.6 to -56.3 mV for whisking, both above rest as well. The median of exactly -50.00 mV is the mean of the two middle neurons, -50.231 and -49.769 mV.
fs-touch-response¶
L4 FS interneurons fire at 9.3 +- 8.5 spikes/s without whisker movement and fire 1.9 +- 1.0 spikes per touch, with a spike latency of 5.3 +- 2.1 ms.
# FS readings of the criterion (answer of 2026-10-20): driven at (a) p < 0.001 or (b) p < 0.01, each (i) with or
# (ii) without the approximate waveform boundary. The paper's counts are the check: 19 ChR2-tagged control FS cells
# (5 whole-cell, 14 cell-attached), and 14 control plus 10 ION-cut cell-attached FS cells in VGAT-ChR2-EYFP mice.
cnt = []
for rname, col in FS_READINGS_TOUCH.items():
vgat = cells[(cells.line == "VGAT-ChR2-EYFP") & (cells[col] == "FS")]
cnt.append({"FS reading": rname, "VGAT_FS_whole_cell": int((vgat["mode"] == "whole-cell").sum()),
"VGAT_FS_cell_attached": int((vgat["mode"] == "cell-attached").sum()),
"VGAT_FS_cell_attached_no_flagged": int(((vgat["mode"] == "cell-attached") & ~vgat.subject.isin(flagged)).sum()),
"FS_all_lines": int((cells[col] == "FS").sum()), "FS_with_principal_whisker": int((meas[col] == "FS").sum())})
print("FS cells per reading; Supplementary Table 2: VGAT-ChR2 whole-cell 5 (control), cell-attached 24 (14 control + 10 ION-cut); "
"control ChR2-tagged 19; control FS of all lines 26 (text) or 25 (Supp. Fig. 7)")
print(pd.DataFrame(cnt).to_string(index=False))
rows = []
for rname, col in FS_READINGS_TOUCH.items():
for sel in ("all", "no_flagged"):
g = group("FS", None, sel, col)
lat = g.dropna(subset=["lat_centre"])
row = {"FS reading": rname, "cells": sel, "n": len(g), "nonwhisk_mean": g["filtfilt:rate_nonwhisk"].mean(),
"nonwhisk_mean_lfilter": g["lfilter:rate_nonwhisk"].mean(), "nonwhisk_sd": g["filtfilt:rate_nonwhisk"].std(),
"spt_mean": g.spt.mean(), "spt_sd": g.spt.std(), "spt_median": g.spt.median(), "lat_n": len(lat),
"lat_O9a_edge_mean": lat.lat_edge.mean(), "lat_O9b_centre_mean": lat.lat_centre.mean(), "lat_centre_median": lat.lat_centre.median(),
"lat_sd": lat.lat_centre.std(), "lat_max": lat.lat_centre.max()}
row["within_of_3"] = (within(row["nonwhisk_mean"], 9.3, 1.67) + within(row["spt_mean"], 1.9, 0.2)
+ max(within(row["lat_O9a_edge_mean"], 5.3, 0.53), within(row["lat_O9b_centre_mean"], 5.3, 0.53)))
rows.append(row)
fsr = pd.DataFrame(rows)
print("\nFS cells, trials without light; expected nonwhisking 9.3 +- 1.67 spikes/s, spikes per touch 1.9 +- 0.2, latency 5.3 +- 0.53 ms (24 cells); "
"within_of_3 counts the values within tolerance (latency under either O9 reading; nonwhisking with the zero-phase filter)")
print(fsr.round(2).to_string(index=False))
nw11 = []
for rname, col in FS_READINGS_TOUCH.items():
t = rate_summary(group("FS", None, "all", col))
nw11.append({"FS reading": rname, "nonwhisk_mean_min": t.nonwhisk_mean.min(), "nonwhisk_mean_max": t.nonwhisk_mean.max(),
"within_under_all": bool(t.nonwhisk_mean.sub(9.3).abs().le(1.67).all())})
print("\nNonwhisking rate over the O11 readings and both filters (expected 9.3 +- 1.67):")
print(pd.DataFrame(nw11).round(2).to_string(index=False))
g = group("FS")
print("Main group (a)(i) by line and mode:", g.assign(line=g.subject.map(line_of)).groupby(["line", "mode"]).size().to_dict())
print("FS cells left out of the latency, (a)(i):", g[g.lat_centre.isna()].lat_excluded.value_counts().to_dict())
FS cells per reading; Supplementary Table 2: VGAT-ChR2 whole-cell 5 (control), cell-attached 24 (14 control + 10 ION-cut); control ChR2-tagged 19; control FS of all lines 26 (text) or 25 (Supp. Fig. 7)
FS reading VGAT_FS_whole_cell VGAT_FS_cell_attached VGAT_FS_cell_attached_no_flagged FS_all_lines FS_with_principal_whisker
(a)(i) p<0.001, boundary 4 12 8 20 17
(a)(ii) p<0.001, no boundary 4 20 13 28 25
(b)(i) p<0.01, boundary 4 12 8 20 17
(b)(ii) p<0.01, no boundary 4 23 16 31 28
FS cells, trials without light; expected nonwhisking 9.3 +- 1.67 spikes/s, spikes per touch 1.9 +- 0.2, latency 5.3 +- 0.53 ms (24 cells); within_of_3 counts the values within tolerance (latency under either O9 reading; nonwhisking with the zero-phase filter)
FS reading cells n nonwhisk_mean nonwhisk_mean_lfilter nonwhisk_sd spt_mean spt_sd spt_median lat_n lat_O9a_edge_mean lat_O9b_centre_mean lat_centre_median lat_sd lat_max within_of_3
(a)(i) p<0.001, boundary all 17 8.53 8.64 8.59 1.50 1.07 1.93 13 5.23 5.73 4.5 3.54 15.5 2
(a)(i) p<0.001, boundary no_flagged 15 8.55 8.70 9.07 1.69 0.99 1.98 13 5.23 5.73 4.5 3.54 15.5 2
(a)(ii) p<0.001, no boundary all 25 8.73 8.81 8.49 1.48 1.23 1.67 19 6.16 6.66 4.5 5.23 24.5 1
(a)(ii) p<0.001, no boundary no_flagged 20 8.04 8.18 8.13 1.82 1.15 1.96 18 5.17 5.67 4.5 3.03 15.5 3
(b)(i) p<0.01, boundary all 17 8.53 8.64 8.59 1.50 1.07 1.93 13 5.23 5.73 4.5 3.54 15.5 2
(b)(i) p<0.01, boundary no_flagged 15 8.55 8.70 9.07 1.69 0.99 1.98 13 5.23 5.73 4.5 3.54 15.5 2
(b)(ii) p<0.01, no boundary all 28 8.71 8.82 8.29 1.39 1.21 1.28 22 10.64 11.14 5.5 16.67 78.5 1
(b)(ii) p<0.01, no boundary no_flagged 23 8.12 8.27 7.92 1.66 1.16 1.78 21 10.00 10.50 5.5 16.80 78.5 1
Nonwhisking rate over the O11 readings and both filters (expected 9.3 +- 1.67):
FS reading nonwhisk_mean_min nonwhisk_mean_max within_under_all
(a)(i) p<0.001, boundary 8.52 8.72 True
(a)(ii) p<0.001, no boundary 8.71 8.88 True
(b)(i) p<0.01, boundary 8.52 8.72 True
(b)(ii) p<0.01, no boundary 8.70 8.88 True
Main group (a)(i) by line and mode: {('C57BL/6', 'whole-cell'): 1, ('PV-IRES-Cre', 'cell-attached'): 3, ('VGAT-ChR2-EYFP', 'cell-attached'): 9, ('VGAT-ChR2-EYFP', 'whole-cell'): 4}
FS cells left out of the latency, (a)(i): {'no crossing': 4}
Result: blocked (methods underspecified). The FS readings of the criterion give different outcomes. With the boundary, (a)(i) and (b)(i) give the same 17 cells with a principal whisker (the paper has 26): the nonwhisking rate (8.53 spikes/s; 8.52 to 8.72 over the O11 readings and filters) and the latency (5.23 ms with the bin edge, 5.73 ms with the centre, over 13 cells) are within tolerance and spikes per touch (1.50) is not, so two of three values match (partially reproduced). Without the boundary, (a)(ii) has 25 cells and (b)(ii) 28, with 1.48 and 1.39 spikes per touch and latencies of 6.16 to 6.66 and 10.64 to 11.14 ms, so only the nonwhisking rate is within tolerance (not reproduced). Against the paper's counts, the dandiset's VGAT-ChR2 mice give 4 whole-cell FS cells (paper 5) under every reading, and 12, 20, 12 and 23 cell-attached FS cells under (a)(i), (a)(ii), (b)(i) and (b)(ii), against 24 (14 control and 10 ION-cut), so the readings without the boundary come closer to the paper's count. Spikes per touch is 1.39 to 1.50 under every reading, below 1.9 +- 0.2.
fs-vm-touch-whisking¶
In whole-cell L4 FS interneurons (n = 7), touch depolarizes the membrane by 7.0 +- 5.2 mV with onset 4.0 +- 0.6 ms, and whisking depolarizes it by 2.7 +- 2.1 mV toward threshold.
for sel in ("all", "no_flagged"):
for filt in yu2016.FILTERS:
f_ = vm_table("FS", sel, filt=filt)
print(f"[{sel}, {filt}] whole-cell FS cells: {len(f_)} (paper 7): touch amplitude mean {f_.amp_mv.mean():.2f} mV (expected 7.0 +- 1.97), "
f"latency mean (expected 4.0 +- 0.4 ms) at the sampling rate {f_.latency_ms.mean():.2f} (median {f_.latency_ms.median():.2f}); for "
f"comparison only (no longer a reading), 1 ms edge {f_.latency_1ms_edge.mean():.2f}, centre {f_.latency_1ms_centre.mean():.2f}; whisking (a) onset change mean {f_.whisk_onset_change.mean():.2f} mV, "
f"(b) threshold-distance shift mean {f_.shift_whisk.mean():.2f} mV (expected 2.7 +- 0.79)")
print(vm_table("FS", "all")[["subject", "amp_mv", "latency_ms", "latency_1ms_centre", "whisk_onset_change", "shift_whisk", "n_onsets", "touch_avg_n"]]
.assign(line=lambda d: d.subject.map(line_of)).round(2).reset_index(drop=True).to_string())
fo = ovm_all[ovm_all.cls == "FS"].groupby(["filter", "O10", "O11"]).whisk_onset_change.agg(["size", "mean"]).reset_index()
print("Whisking change (a), onset windows, of the whole-cell FS cells under each O10 and O11 reading: range", round(fo["mean"].min(), 2), "to",
round(fo["mean"].max(), 2), "mV; within 2.7 +- 0.79 in", int(fo["mean"].sub(2.7).abs().le(0.79).sum()), "of", len(fo))
print("Variant (not a reading of the claims file): the PV-ChR2 cell, FS in Supplementary Table 2, added:",
vmm[(vmm.subject == "anm250509") & (vmm.filt == "filtfilt") & (vmm.O3 == "a")][["amp_mv", "latency_ms", "latency_1ms_centre", "whisk_onset_change", "shift_whisk"]].round(2).to_dict("records"))
[all, filtfilt] whole-cell FS cells: 5 (paper 7): touch amplitude mean 5.56 mV (expected 7.0 +- 1.97), latency mean (expected 4.0 +- 0.4 ms) at the sampling rate 1.88 (median 1.35); for comparison only (no longer a reading), 1 ms edge 3.67, centre 4.17; whisking (a) onset change mean 2.54 mV, (b) threshold-distance shift mean 1.01 mV (expected 2.7 +- 0.79)
[all, lfilter] whole-cell FS cells: 5 (paper 7): touch amplitude mean 5.56 mV (expected 7.0 +- 1.97), latency mean (expected 4.0 +- 0.4 ms) at the sampling rate 1.88 (median 1.35); for comparison only (no longer a reading), 1 ms edge 3.67, centre 4.17; whisking (a) onset change mean 2.24 mV, (b) threshold-distance shift mean 0.82 mV (expected 2.7 +- 0.79)
[no_flagged, filtfilt] whole-cell FS cells: 5 (paper 7): touch amplitude mean 5.56 mV (expected 7.0 +- 1.97), latency mean (expected 4.0 +- 0.4 ms) at the sampling rate 1.88 (median 1.35); for comparison only (no longer a reading), 1 ms edge 3.67, centre 4.17; whisking (a) onset change mean 2.54 mV, (b) threshold-distance shift mean 1.01 mV (expected 2.7 +- 0.79)
[no_flagged, lfilter] whole-cell FS cells: 5 (paper 7): touch amplitude mean 5.56 mV (expected 7.0 +- 1.97), latency mean (expected 4.0 +- 0.4 ms) at the sampling rate 1.88 (median 1.35); for comparison only (no longer a reading), 1 ms edge 3.67, centre 4.17; whisking (a) onset change mean 2.24 mV, (b) threshold-distance shift mean 0.82 mV (expected 2.7 +- 0.79)
subject amp_mv latency_ms latency_1ms_centre whisk_onset_change shift_whisk n_onsets touch_avg_n line
0 anm220466 2.90 0.00 NaN 2.06 1.32 175 134 VGAT-ChR2-EYFP
1 anm220470 1.20 0.30 NaN 0.76 1.60 147 58 VGAT-ChR2-EYFP
2 anm236461 10.44 5.10 5.5 7.32 4.02 18 131 C57BL/6
3 anm305599 4.90 2.65 4.5 2.31 1.39 18 88 VGAT-ChR2-EYFP
4 anm307766 8.38 1.35 2.5 0.25 -3.29 7 11 VGAT-ChR2-EYFP
Whisking change (a), onset windows, of the whole-cell FS cells under each O10 and O11 reading: range 1.47 to 2.55 mV; within 2.7 +- 0.79 in 12 of 16
Variant (not a reading of the claims file): the PV-ChR2 cell, FS in Supplementary Table 2, added: [{'amp_mv': 8.36, 'latency_ms': 4.5, 'latency_1ms_centre': 3.5, 'whisk_onset_change': 1.7, 'shift_whisk': 2.05}]
# Cause of the near-zero Vm latencies (the verifier's request). For each whole-cell E and FS cell, the touch-aligned average
# (full rate) and its first difference dVm/dt, with the criterion's threshold (3 s.d. of dVm/dt 0 to 25 ms before onset). Also,
# as a diagnostic that decides nothing, the latency with the baseline's mean dVm/dt subtracted before the comparison.
def lat_diag(rec):
fs = rec["vm_hz"]
a = rec["touch_avg"].astype(float)
i0 = int(yu2016.EVENT_PRE_S * fs)
d = np.diff(a) * fs / 1000.0
base = d[i0 - int(0.025 * fs):i0]
thr = 3 * base.std()
hit = np.flatnonzero(d[i0:] > thr)
hit_m = np.flatnonzero(d[i0:] - base.mean() > thr)
pre = a[i0 - int(0.005 * fs):i0].mean()
return {"fs": fs, "base_mean": base.mean(), "base_sd": base.std(), "thr": thr, "d_first_0.5ms_max": d[i0:i0 + int(0.0005 * fs)].max(),
"latency_ms": hit[0] / fs * 1000 if len(hit) else np.nan, "latency_mean_subtracted_ms": hit_m[0] / fs * 1000 if len(hit_m) else np.nan,
"vm_0_to_1ms_minus_pre": a[i0:i0 + int(0.001 * fs)].mean() - pre, "d": d, "a": a - pre, "i0": i0}
ld = []
for cls_ in ("E", "FS"):
for p in group(cls_, "whole-cell").path:
r = by_path[p]
if r.get("touch_avg") is None or r["touch_avg_n"] < 5:
continue
x = lat_diag(r)
ld.append({"cls": cls_, "path": p, "subject": r["subject"], "touches": r["touch_avg_n"], **{k: v for k, v in x.items() if np.ndim(v) == 0}})
ld = pd.DataFrame(ld)
print("Cells with the criterion's latency under 2 ms:")
print(ld[ld.latency_ms < 2].drop(columns="path").round(3).to_string(index=False))
print("All cells: dVm/dt baseline mean against its s.d., median ratio", round(float((ld.base_mean / ld.base_sd).median()), 3))
for cls_, g in ld.groupby("cls"):
print(f"{cls_}: {len(g)} cells; latency mean {g.latency_ms.mean():.2f} ms (median {g.latency_ms.median():.2f}); under 1 ms {int((g.latency_ms < 1).sum())}; "
f"diagnostic with the baseline mean subtracted, mean {g.latency_mean_subtracted_ms.mean():.2f} ms")
fig, ax = plt.subplots(2, max(1, int((ld.latency_ms < 2).sum())), figsize=(2.4 * max(1, int((ld.latency_ms < 2).sum())), 4), squeeze=False)
for j, p in enumerate(ld[ld.latency_ms < 2].path):
x = lat_diag(by_path[p])
fs, i0 = x["fs"], x["i0"]
sl = slice(i0 - int(0.002 * fs), i0 + int(0.002 * fs))
tms = (np.arange(len(x["a"]))[sl] - i0) / fs * 1000
ax[0, j].plot(tms, x["a"][sl], color="k", lw=0.8); ax[0, j].axvline(0, color="C0", lw=0.5)
ax[0, j].set_title(by_path[p]["subject"], fontsize=7)
ax[1, j].plot(tms, x["d"][sl], color="k", lw=0.8); ax[1, j].axhline(x["thr"], color="C3", lw=0.6); ax[1, j].axvline(0, color="C0", lw=0.5)
ax[1, j].set_xlabel("ms from touch onset", fontsize=7)
print(f"{by_path[p]['subject']} ({ld.set_index('path').cls[p]}): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms:",
np.round(x["a"][sl][::max(1, int(fs // 5000))], 3).tolist())
print(" dVm/dt (mV/ms), same samples:", np.round(x["d"][sl][::max(1, int(fs // 5000))], 2).tolist(), "; threshold", round(x["thr"], 2))
ax[0, 0].set_ylabel("Vm - pre (mV)"); ax[1, 0].set_ylabel("dVm/dt (mV/ms)")
plt.tight_layout(); plt.show()
print("Each panel shows -2 to +2 ms; every plotted value lies within the axes.")
Cells with the criterion's latency under 2 ms: cls subject touches fs base_mean base_sd thr d_first_0.5ms_max latency_ms latency_mean_subtracted_ms vm_0_to_1ms_minus_pre i0 E anm190964 59 10000.0 -0.008 0.117 0.351 0.316 1.40 1.40 0.171 500 E anm220466 76 10000.0 -0.018 0.142 0.426 0.562 0.00 0.00 -0.064 500 E anm231235 253 10000.0 -0.012 0.089 0.268 0.276 0.30 0.30 -0.083 500 E anm234412 301 10000.0 -0.019 0.136 0.407 0.217 1.00 1.00 -0.045 500 E anm253096 239 10000.0 -0.075 0.105 0.314 0.043 1.80 1.20 0.016 500 E anm266945 303 10000.0 -0.003 0.124 0.372 0.210 0.90 0.90 -0.035 500 E anm318532 116 20000.0 0.020 0.290 0.870 2.143 0.45 0.45 0.098 1000 E anm318532 40 20000.0 -0.001 0.451 1.352 8.400 0.35 0.35 0.081 1000 FS anm220466 134 10000.0 -0.007 0.237 0.711 0.958 0.00 0.00 0.592 500 FS anm220470 58 10000.0 0.016 0.616 1.847 3.053 0.30 0.30 0.597 500 FS anm307766 11 20000.0 -0.028 0.413 1.240 1.069 1.35 1.35 0.429 1000 All cells: dVm/dt baseline mean against its s.d., median ratio -0.243 E: 48 cells; latency mean 7.68 ms (median 4.90); under 1 ms 5; diagnostic with the baseline mean subtracted, mean 7.48 ms FS: 5 cells; latency mean 1.88 ms (median 1.35); under 1 ms 2; diagnostic with the baseline mean subtracted, mean 1.76 ms anm190964 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [-0.02, -0.012, -0.023, -0.014, -0.009, -0.003, 0.014, 0.024, 0.041, 0.056, 0.117, 0.147, 0.168, 0.211, 0.189, 0.194, 0.211, 0.264, 0.364, 0.476] dVm/dt (mV/ms), same samples: [0.07, -0.06, 0.05, 0.04, 0.01, 0.07, 0.08, 0.1, 0.01, 0.3, 0.2, 0.05, 0.32, -0.13, -0.05, -0.12, 0.26, 0.42, 0.62, 0.38] ; threshold 0.35 anm220466 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [-0.017, -0.044, -0.073, -0.082, -0.089, -0.049, -0.004, -0.039, -0.077, -0.114, -0.112, -0.003, -0.005, -0.054, -0.13, -0.165, -0.177, -0.169, -0.142, -0.142] dVm/dt (mV/ms), same samples: [-0.03, -0.27, -0.09, -0.09, 0.11, 0.62, -0.38, -0.24, -0.24, -0.1, 0.56, 0.08, -0.26, -0.48, -0.24, -0.02, -0.08, 0.09, -0.02, 0.14] ; threshold 0.43 anm231235 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [0.025, 0.023, 0.014, 0.022, -0.002, -0.002, -0.016, -0.04, -0.05, -0.07, -0.068, -0.074, -0.08, -0.09, -0.095, -0.09, -0.103, -0.113, -0.125, -0.106] dVm/dt (mV/ms), same samples: [0.09, -0.04, 0.05, -0.16, -0.06, 0.08, -0.18, -0.06, -0.13, -0.06, 0.07, -0.34, 0.04, 0.01, 0.02, 0.1, -0.06, 0.04, 0.2, 0.01] ; threshold 0.27 anm234412 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [-0.023, -0.039, -0.033, -0.018, -0.023, -0.012, -0.019, -0.025, -0.021, -0.061, -0.051, -0.044, -0.053, -0.039, -0.043, -0.087, -0.037, -0.054, -0.041, -0.042] dVm/dt (mV/ms), same samples: [0.16, -0.16, 0.11, -0.06, 0.01, 0.09, -0.06, 0.01, -0.43, -0.31, 0.22, -0.17, 0.05, -0.04, 0.02, 0.5, -0.18, 0.09, -0.01, 0.02] ; threshold 0.41 anm253096 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [0.011, 0.003, 0.006, -0.001, -0.009, -0.007, -0.012, -0.011, -0.015, -0.0, 0.009, 0.011, 0.016, 0.018, 0.019, 0.028, 0.002, 0.036, 0.034, 0.0] dVm/dt (mV/ms), same samples: [-0.08, 0.02, -0.03, -0.03, 0.01, -0.02, 0.0, -0.04, 0.08, 0.02, 0.03, 0.04, 0.04, 0.02, 0.03, 0.04, 0.3, -0.01, 0.03, 0.34] ; threshold 0.31 anm266945 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [0.006, -0.009, -0.015, -0.019, -0.024, -0.025, -0.049, -0.025, -0.019, -0.028, -0.029, -0.045, -0.021, -0.043, -0.037, -0.047, -0.056, -0.054, -0.07, -0.056] dVm/dt (mV/ms), same samples: [0.0, -0.08, -0.03, -0.13, -0.16, -0.1, -0.04, 0.07, -0.22, 0.08, 0.21, 0.18, -0.05, 0.1, -0.49, -0.35, -0.2, 0.09, -0.01, -0.07] ; threshold 0.37
anm318532 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [-0.002, 0.015, 0.05, 0.153, 0.189, 0.19, 0.163, 0.152, 0.136, 0.167, 0.124, 0.088, 0.072, 0.139, 0.097, 0.054, -0.002, -0.06, -0.114, -0.114] dVm/dt (mV/ms), same samples: [0.18, 0.55, 0.13, 0.16, -0.26, -0.12, 0.08, 0.03, 0.83, -0.39, -0.09, 0.25, -1.19, -0.34, -0.36, -0.35, -0.12, -0.29, -1.19, 1.6] ; threshold 0.87
anm318532 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [0.061, 0.058, 0.057, 0.069, 0.02, 0.004, 0.027, 0.029, 0.042, 0.023, 0.028, 0.036, 0.176, 0.152, 0.115, 0.068, 0.063, 0.067, 0.082, 0.073] dVm/dt (mV/ms), same samples: [-0.1, 0.3, 0.12, -0.6, -0.18, 0.63, 0.44, 0.19, -0.29, -0.26, 0.23, 0.65, 0.32, -0.37, -0.12, 0.04, 0.25, -0.19, -0.26, -0.39] ; threshold 1.35 anm220466 (FS): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [0.289, 0.383, 0.394, 0.379, 0.36, 0.382, 0.354, 0.33, 0.332, 0.35, 0.462, 0.625, 0.657, 0.592, 0.568, 0.634, 0.544, 0.45, 0.396, 0.381] dVm/dt (mV/ms), same samples: [0.66, 0.14, -0.12, -0.06, 0.03, 0.03, -0.27, -0.0, 0.13, 0.4, 0.96, 0.16, -0.24, -0.26, 0.5, -0.32, -0.57, -0.4, -0.08, 0.16] ; threshold 0.71 anm220470 (FS): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [-0.083, 0.088, 0.295, 0.212, 0.246, 0.278, 0.421, 0.45, 0.352, 0.255, 0.257, 0.281, 0.77, 0.797, 0.76, 0.733, 0.542, 0.756, 0.655, 0.671] dVm/dt (mV/ms), same samples: [1.23, 1.1, -0.15, -0.41, 0.46, 0.74, 0.25, -0.45, -0.66, -0.39, 0.05, 1.84, 0.99, -1.04, 0.53, -1.09, 0.46, 0.1, -0.61, 0.12] ; threshold 1.85 anm307766 (FS): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [-0.278, -0.267, -0.297, -0.288, -0.226, -0.105, 0.004, 0.097, 0.13, 0.132, 0.215, 0.281, 0.382, 0.516, 0.594, 0.637, 0.719, 0.93, 1.152, 1.325] dVm/dt (mV/ms), same samples: [0.23, -0.32, 0.33, 0.25, 0.51, 0.61, 0.86, -0.28, -0.07, 0.63, 0.43, 0.25, 0.35, 0.69, 0.1, 0.7, 1.09, 1.33, 1.24, 0.5] ; threshold 1.24
Each panel shows -2 to +2 ms; every plotted value lies within the axes.
The latencies under 2 ms in E and FS cells come from single samples of dVm/dt, at 10 or 20 kHz, that just cross 3 s.d. of the baseline before any sustained rise; the Vm of the E cells is within 0.17 mV of its pre-touch mean in the first millisecond (the values above). A 3 s.d. threshold on an unsmoothed derivative is crossed by single samples of noise; in the laser-onset averages of exc-thalamus-photoinhibition 1 and 3 of the 250 baseline samples already cross it. This is the rule the criterion fixes (O8, answer of 2026-10-20), so the latencies stand as measured; the diagnostic decides nothing.
Result: not reproduced. The dandiset holds 5 whole-cell FS cells by the claims file's rules
(the paper has 7). Their touch amplitude averages 5.56 mV (within 7.0 +- 1.97) and the whisking
depolarization 2.54 mV with the onset windows (within 2.7 +- 0.79; 1.47 to 2.55 mV over the O10 and
O11 readings, within tolerance in 12 of 16; 1.01 mV with the threshold distance), but the touch
latency, with dVm/dt at the sampling rate as the criterion says, averages 1.88 ms (median 1.35)
against 4.0 +- 0.4. The latency diagnostic above (after exc-vm-touch-whisking) shows why: in anm220466 (0.00 ms) and anm220470 (0.30 ms)
the first crossing is a single sample of dVm/dt just above the threshold (0.96 against 0.71 and 3.05
against 1.85 mV/ms), in averages whose Vm already lies 0.26 to 0.46 mV above its pre-touch mean
at onset. Subtracting the baseline's mean dVm/dt, a diagnostic that decides nothing, does not move
these crossings (mean 1.76 ms), so the rule as the criterion gives it, not an offset, yields these
latencies.
fs-whisking-rate¶
Most L4 FS interneurons (23 of 26) increase their spike rate during whisking without touch and 3 of 26 decrease it; over the population the rate roughly doubles (to 21.1 +- 15.8 spikes/s).
def fs_rate_table(groups):
rows = []
for rname, col in groups.items():
for sel in ("all", "no_flagged"):
g = group("FS", None, sel, col)
for f in yu2016.FILTERS:
for o2 in ("O2a", "O2b"):
rows.append({"FS reading": rname, "cells": sel, "filter": f, "samples": o2, "n": len(g),
"increase": int((g[f"{f}:{o2}"] == 1).sum()), "decrease": int((g[f"{f}:{o2}"] == -1).sum()),
"nonwhisk_mean": g[f"{f}:rate_nonwhisk"].mean(), "whisk_mean": g[f"{f}:rate_whisk"].mean(),
"whisk_sd": g[f"{f}:rate_whisk"].std(), "whisk_median": g[f"{f}:rate_whisk"].median(),
"signed_rank_p": "%.2g" % wilcox(g[f"{f}:rate_nonwhisk"], g[f"{f}:rate_whisk"])})
return pd.DataFrame(rows)
print("FS cells, trials without light; expected increases 23 +- 2.3 and decreases 3 +- 0.3 of 26, whisking mean 21.1 +- 3.1 spikes/s")
print(fs_rate_table({**FS_READINGS, **FS_VARIANT}).round(2).to_string(index=False))
fs11 = rate_summary(group("FS"))
print("\nMain FS group under every O11 reading, filter and sample reading:")
print(fmt_p(fs11.drop(columns=["r2_above_0.25", "multiplex"])).round(4).to_string(index=False))
print("Ranges over the 16 readings:", ranges(fs11, ["increase", "decrease", "whisk_mean", "signed_rank_p"]))
FS cells, trials without light; expected increases 23 +- 2.3 and decreases 3 +- 0.3 of 26, whisking mean 21.1 +- 3.1 spikes/s
FS reading cells filter samples n increase decrease nonwhisk_mean whisk_mean whisk_sd whisk_median signed_rank_p
tag p<0.001, boundary all filtfilt O2a 17 13 2 8.53 19.02 14.34 16.20 0.00029
tag p<0.001, boundary all filtfilt O2b 17 13 1 8.53 19.02 14.34 16.20 0.00029
tag p<0.001, boundary all lfilter O2a 17 13 1 8.64 17.77 12.95 14.90 0.0005
tag p<0.001, boundary all lfilter O2b 17 13 1 8.64 17.77 12.95 14.90 0.0005
tag p<0.001, boundary no_flagged filtfilt O2a 15 11 2 8.55 20.22 14.84 17.28 0.00061
tag p<0.001, boundary no_flagged filtfilt O2b 15 13 1 8.55 20.22 14.84 17.28 0.00061
tag p<0.001, boundary no_flagged lfilter O2a 15 11 1 8.70 18.81 13.41 16.29 0.0012
tag p<0.001, boundary no_flagged lfilter O2b 15 13 1 8.70 18.81 13.41 16.29 0.0012
tag p<0.01, boundary all filtfilt O2a 17 13 2 8.53 19.02 14.34 16.20 0.00029
tag p<0.01, boundary all filtfilt O2b 17 13 1 8.53 19.02 14.34 16.20 0.00029
tag p<0.01, boundary all lfilter O2a 17 13 1 8.64 17.77 12.95 14.90 0.0005
tag p<0.01, boundary all lfilter O2b 17 13 1 8.64 17.77 12.95 14.90 0.0005
tag p<0.01, boundary no_flagged filtfilt O2a 15 11 2 8.55 20.22 14.84 17.28 0.00061
tag p<0.01, boundary no_flagged filtfilt O2b 15 13 1 8.55 20.22 14.84 17.28 0.00061
tag p<0.01, boundary no_flagged lfilter O2a 15 11 1 8.70 18.81 13.41 16.29 0.0012
tag p<0.01, boundary no_flagged lfilter O2b 15 13 1 8.70 18.81 13.41 16.29 0.0012
tag p<0.001, any waveform (variant) all filtfilt O2a 25 17 4 8.73 16.53 12.73 13.47 0.00019
tag p<0.001, any waveform (variant) all filtfilt O2b 25 15 3 8.73 16.53 12.73 13.47 0.00019
tag p<0.001, any waveform (variant) all lfilter O2a 25 17 3 8.81 15.63 11.53 12.61 0.00029
tag p<0.001, any waveform (variant) all lfilter O2b 25 15 3 8.81 15.63 11.53 12.61 0.00029
tag p<0.001, any waveform (variant) no_flagged filtfilt O2a 20 14 3 8.04 17.78 13.54 13.96 0.00013
tag p<0.001, any waveform (variant) no_flagged filtfilt O2b 20 15 2 8.04 17.78 13.54 13.96 0.00013
tag p<0.001, any waveform (variant) no_flagged lfilter O2a 20 14 2 8.18 16.58 12.24 13.19 0.00039
tag p<0.001, any waveform (variant) no_flagged lfilter O2b 20 15 2 8.18 16.58 12.24 13.19 0.00039
Main FS group under every O11 reading, filter and sample reading:
O11 filter samples n increase decrease nonwhisk_mean nonwhisk_median whisk_mean signed_rank_p whisk_higher
a-i filtfilt O2a 17 13 2 8.5279 4.5529 19.0164 0.00029 16
a-i filtfilt O2b 17 13 1 8.5279 4.5529 19.0164 0.00029 16
a-i lfilter O2a 17 13 1 8.6430 4.4015 17.7652 0.0005 16
a-i lfilter O2b 17 13 1 8.6430 4.4015 17.7652 0.0005 16
a-ii filtfilt O2a 17 13 2 8.5849 4.5903 19.0370 0.00029 16
a-ii filtfilt O2b 17 13 1 8.5849 4.5903 19.0370 0.00029 16
a-ii lfilter O2a 17 13 1 8.7128 4.4859 17.7978 0.00038 16
a-ii lfilter O2b 17 13 1 8.7128 4.4859 17.7978 0.00038 16
b-i filtfilt O2a 17 14 2 8.5205 4.5648 19.1368 0.00021 16
b-i filtfilt O2b 17 13 1 8.5205 4.5648 19.1368 0.00021 16
b-i lfilter O2a 17 13 1 8.6355 4.4180 17.8717 0.0005 16
b-i lfilter O2b 17 13 1 8.6355 4.4180 17.8717 0.0005 16
b-ii filtfilt O2a 17 13 3 8.5909 4.6185 19.1680 0.00021 16
b-ii filtfilt O2b 17 13 1 8.5909 4.6185 19.1680 0.00021 16
b-ii lfilter O2a 17 13 1 8.7225 4.5005 17.9122 0.0005 15
b-ii lfilter O2b 17 13 1 8.7225 4.5005 17.9122 0.0005 15
Ranges over the 16 readings: {'increase': (13.0, 14.0), 'decrease': (1.0, 3.0), 'whisk_mean': (17.8, 19.2), 'signed_rank_p': (0.000214, 0.000504)}
Result: partially reproduced. The 17 FS cells fire more during whisking under every filter, sample and O11 reading (signed-rank p = 0.0002 to 0.0005), with a mean whisking rate of 17.77 to 19.17 spikes/s (within 21.1 +- 3.1 with the zero-phase filter; outside with the forward one). 13 or 14 cells increase and 1 to 3 decrease significantly against the paper's 23 and 3 of 26, so the increase count is outside its tolerance under every reading; the group is smaller than the paper's.
fs-whisking-phase¶
Spikes of L4 FS interneurons are coupled to whisking phase.
rows = []
for rname, col in {**FS_READINGS, **FS_VARIANT}.items():
for sel in ("all", "no_flagged"):
g = group("FS", None, sel, col)
for f in yu2016.FILTERS:
rows.append({"FS reading": rname, "cells": sel, "filter": f, "n": len(g), "coupled": int((g[f"{f}:r2"] > 0.25).sum()),
"double_fits": int((g[f"{f}:fit"] == "double").sum()), "r2_median": g[f"{f}:r2"].median(),
"strength_median": g[f"{f}:strength"].median(), "whisk_spikes_median": g[f"{f}:whisk_spikes"].median()})
print("FS cells coupled to whisking phase (r2 > 0.25; sinusoid, or with the second harmonic when its r2 is 1.5 times higher):")
print(pd.DataFrame(rows).round(2).to_string(index=False))
print("Main FS group, cells with r2 > 0.25 under each O11 reading and filter:",
fs11.drop_duplicates(["O11", "filter"]).set_index(["O11", "filter"])["r2_above_0.25"].to_dict(), "of", len(group("FS")))
na = align[align.path.isin(group("FS").path)]
print(f"Reference (not part of the criterion): with spikes moved to the next trial (single sinusoid), {int((na.r2_shift > 0.25).sum())} "
f"of {len(na)} FS cells have r2 > 0.25")
FS cells coupled to whisking phase (r2 > 0.25; sinusoid, or with the second harmonic when its r2 is 1.5 times higher):
FS reading cells filter n coupled double_fits r2_median strength_median whisk_spikes_median
tag p<0.001, boundary all filtfilt 17 16 5 0.73 5.37 769.0
tag p<0.001, boundary all lfilter 17 16 3 0.61 3.01 709.0
tag p<0.001, boundary no_flagged filtfilt 15 15 5 0.74 5.43 769.0
tag p<0.001, boundary no_flagged lfilter 15 14 3 0.61 3.33 709.0
tag p<0.01, boundary all filtfilt 17 16 5 0.73 5.37 769.0
tag p<0.01, boundary all lfilter 17 16 3 0.61 3.01 709.0
tag p<0.01, boundary no_flagged filtfilt 15 15 5 0.74 5.43 769.0
tag p<0.01, boundary no_flagged lfilter 15 14 3 0.61 3.33 709.0
tag p<0.001, any waveform (variant) all filtfilt 25 22 7 0.66 2.63 750.0
tag p<0.001, any waveform (variant) all lfilter 25 23 7 0.58 2.48 698.0
tag p<0.001, any waveform (variant) no_flagged filtfilt 20 19 6 0.74 4.33 759.5
tag p<0.001, any waveform (variant) no_flagged lfilter 20 18 5 0.61 2.55 703.5
Main FS group, cells with r2 > 0.25 under each O11 reading and filter: {('a-i', 'filtfilt'): 16, ('a-i', 'lfilter'): 16, ('a-ii', 'filtfilt'): 16, ('a-ii', 'lfilter'): 15, ('b-i', 'filtfilt'): 16, ('b-i', 'lfilter'): 15, ('b-ii', 'filtfilt'): 16, ('b-ii', 'lfilter'): 15} of 17
Reference (not part of the criterion): with spikes moved to the next trial (single sinusoid), 9 of 17 FS cells have r2 > 0.25
Result: reproduced. 15 or 16 of 17 FS cells are coupled to whisking phase (r2 > 0.25) under both filters and every O11 reading. As for VPM, 9 of the 17 also pass with spikes moved to the next trial, which the criterion does not ask for.
fs-touch-and-whisking¶
Touch- and whisking-evoked spikes are typically multiplexed in individual L4 FS neurons, as in VPM neurons.
rows = []
for rname, col in {**FS_READINGS, **FS_VARIANT, "VPM units": None}.items():
g = group("VPM") if col is None else group("FS", None, "all", col)
for f in yu2016.FILTERS:
for o2 in ("O2a", "O2b"):
mult = (g.spt_lo > 0) & (g[f"{f}:{o2}"] != 0)
rows.append({"group": rname, "filter": f, "samples": o2, "n": len(g), "touch": int((g.spt_lo > 0).sum()),
"whisking": int((g[f"{f}:{o2}"] != 0).sum()), "both": int(mult.sum()), "fraction": mult.mean()})
print("Cells whose spikes per touch has a 95% interval above zero and whose whisking rate differs from the nonwhisking rate "
"(rank-sum p < 0.05); more than half needed under every reading")
print(pd.DataFrame(rows).round(2).to_string(index=False))
print("Main FS group, cells that multiplex under each O11, filter and sample reading: range", int(fs11.multiplex.min()), "to",
int(fs11.multiplex.max()), "of", len(group("FS")), "; VPM units:", int(vs.multiplex.min()), "to", int(vs.multiplex.max()), "of", len(v))
for sel in ("no_flagged",):
g = group("FS", None, sel)
print(f"[{sel}] FS: both in", [int(((g.spt_lo > 0) & (g[f'{f}:{o}'] != 0)).sum()) for f in yu2016.FILTERS for o in ("O2a", "O2b")], "of", len(g))
Cells whose spikes per touch has a 95% interval above zero and whose whisking rate differs from the nonwhisking rate (rank-sum p < 0.05); more than half needed under every reading
group filter samples n touch whisking both fraction
tag p<0.001, boundary filtfilt O2a 17 12 15 11 0.65
tag p<0.001, boundary filtfilt O2b 17 12 14 12 0.71
tag p<0.001, boundary lfilter O2a 17 12 14 11 0.65
tag p<0.001, boundary lfilter O2b 17 12 14 12 0.71
tag p<0.01, boundary filtfilt O2a 17 12 15 11 0.65
tag p<0.01, boundary filtfilt O2b 17 12 14 12 0.71
tag p<0.01, boundary lfilter O2a 17 12 14 11 0.65
tag p<0.01, boundary lfilter O2b 17 12 14 12 0.71
tag p<0.001, any waveform (variant) filtfilt O2a 25 18 21 16 0.64
tag p<0.001, any waveform (variant) filtfilt O2b 25 18 18 16 0.64
tag p<0.001, any waveform (variant) lfilter O2a 25 18 20 16 0.64
tag p<0.001, any waveform (variant) lfilter O2b 25 18 18 16 0.64
VPM units filtfilt O2a 29 23 25 21 0.72
VPM units filtfilt O2b 29 23 26 22 0.76
VPM units lfilter O2a 29 23 22 18 0.62
VPM units lfilter O2b 29 23 25 21 0.72
Main FS group, cells that multiplex under each O11, filter and sample reading: range 11 to 12 of 17 ; VPM units: 18 to 22 of 29
[no_flagged] FS: both in [11, 12, 11, 12] of 15
Result: reproduced. 11 or 12 of the 17 FS cells (0.65 to 0.71) both respond to touch and change their rate with whisking under every filter, sample and O11 reading, more than half; the VPM units give 18 to 22 of 29 (0.62 to 0.76).
exc-mainly-touch¶
L4 excitatory neurons respond mostly to touch (0.3 +- 0.4 spikes per touch, n = 85) and only weakly to whisker movement; their rate does not change significantly between nonwhisking and whisking (0.4 to 0.6 spikes/s, P = 0.20).
rows = []
for sel in ("all", "no_flagged"):
g = group("E", None, sel)
for f in yu2016.FILTERS:
rows.append({"cells": sel, "filter": f, "n": len(g), "whole_cell": int((g["mode"] == "whole-cell").sum()), "spt_mean": g.spt.mean(),
"spt_sd": g.spt.std(), "spt_median": g.spt.median(), "nonwhisk_mean": g[f"{f}:rate_nonwhisk"].mean(),
"nonwhisk_median": g[f"{f}:rate_nonwhisk"].median(), "whisk_mean": g[f"{f}:rate_whisk"].mean(),
"whisk_median": g[f"{f}:rate_whisk"].median(), "whisk_minus_nonwhisk": (g[f"{f}:rate_whisk"] - g[f"{f}:rate_nonwhisk"]).mean(),
"signed_rank_p": "%.2g" % wilcox(g[f"{f}:rate_nonwhisk"], g[f"{f}:rate_whisk"]),
"whisk_higher": int((g[f"{f}:rate_whisk"] > g[f"{f}:rate_nonwhisk"]).sum())})
print("L4 E cells, trials without light; expected spikes per touch 0.3 +- 0.043, nonwhisking 0.4 +- 0.065, whisking 0.6 +- 0.098 spikes/s; "
"absence judged by the signed-rank test at 0.05")
print(pd.DataFrame(rows).round(3).to_string(index=False))
e11 = rate_summary(group("E")).drop_duplicates(["O11", "filter"])
print("\nUnder every O11 reading and filter:")
print(fmt_p(e11[["O11", "filter", "n", "nonwhisk_mean", "whisk_mean", "signed_rank_p", "whisk_higher"]]).round(4).to_string(index=False))
g = group("E")
print("Spike detection by mode (see the data check): fraction of interspike intervals under 1.5 ms, median over cells",
g.path.map(spikeq.set_index("path").isi_viol).groupby(g["mode"]).median().round(4).to_dict())
print("E cells by line and mode:", g.assign(line=g.subject.map(line_of)).groupby(["line", "mode"]).size().to_dict())
print("Rates by mode (filtfilt), means:", g.groupby("mode")[["spt", "filtfilt:rate_nonwhisk", "filtfilt:rate_whisk"]].mean().round(3).to_dict("index"))
L4 E cells, trials without light; expected spikes per touch 0.3 +- 0.043, nonwhisking 0.4 +- 0.065, whisking 0.6 +- 0.098 spikes/s; absence judged by the signed-rank test at 0.05
cells filter n whole_cell spt_mean spt_sd spt_median nonwhisk_mean nonwhisk_median whisk_mean whisk_median whisk_minus_nonwhisk signed_rank_p whisk_higher
all filtfilt 97 51 0.320 0.643 0.048 1.529 0.237 3.551 0.589 2.022 9.1e-07 55
all lfilter 97 51 0.320 0.643 0.048 1.567 0.257 3.382 0.593 1.815 8.7e-06 51
no_flagged filtfilt 89 43 0.344 0.662 0.075 1.666 0.399 3.866 0.707 2.200 1.5e-06 53
no_flagged lfilter 89 43 0.344 0.662 0.075 1.708 0.430 3.681 0.675 1.973 1.5e-05 49
Under every O11 reading and filter:
O11 filter n nonwhisk_mean whisk_mean signed_rank_p whisk_higher
a-i filtfilt 97 1.5290 3.5510 9.1e-07 55
a-i lfilter 97 1.5670 3.3821 8.7e-06 51
a-ii filtfilt 97 1.5495 3.5983 3.8e-07 56
a-ii lfilter 97 1.5905 3.4265 8.5e-06 50
b-i filtfilt 97 1.5348 3.5962 4.6e-07 57
b-i lfilter 97 1.5736 3.4304 4.2e-06 53
b-ii filtfilt 97 1.5512 3.6437 1.8e-07 58
b-ii lfilter 97 1.5925 3.4768 3.5e-06 53
Spike detection by mode (see the data check): fraction of interspike intervals under 1.5 ms, median over cells {'cell-attached': 0.0504, 'whole-cell': 0.0}
E cells by line and mode: {('C57BL/6', 'whole-cell'): 7, ('PV-ChR2', 'whole-cell'): 1, ('PV-IRES-Cre', 'cell-attached'): 34, ('PV-IRES-Cre', 'whole-cell'): 9, ('PV-IRES-Cre x Ai32', 'whole-cell'): 1, ('SOM-IRES-Cre x Ai32', 'cell-attached'): 2, ('SOM-IRES-Cre x Ai32', 'whole-cell'): 1, ('VGAT-ChR2-EYFP', 'cell-attached'): 10, ('VGAT-ChR2-EYFP', 'whole-cell'): 32}
Rates by mode (filtfilt), means: {'cell-attached': {'spt': 0.534, 'filtfilt:rate_nonwhisk': 2.451, 'filtfilt:rate_whisk': 6.193}, 'whole-cell': {'spt': 0.122, 'filtfilt:rate_nonwhisk': 0.698, 'filtfilt:rate_whisk': 1.168}}
Result: not reproduced. The 97 excitatory cells fire more during whisking than without it under every filter and O11 reading (signed-rank p = 9.1e-07 for the main epochs, at most 8.7e-06 for the forward filter), so the paper's absence of a change is not found. Spikes per touch (0.320) is within tolerance of 0.3, but the nonwhisking (1.53 to 1.59 spikes/s) and whisking (3.38 to 3.64) rates are well above 0.4 and 0.6; the 46 cell-attached cells fire more than the 51 whole-cell ones (2.45 against 0.70 spikes/s without whisking). The data check shows that double detection removes only 8.1% of the cell-attached spikes, so it does not explain the high rates.
Light experiments¶
The functions below select the light trials and the windows used by the thalamic photoinhibition and PV suppression claims (windows O5, fractions O6, laser powers O7).
# Light designs. Thalamic photoinhibition: trials lit at the site labelled VPM and at no other site (VGAT-ChR2 mice).
# PV suppression: trials lit at the vS1 site of PV-IRES-Cre mice (constant light). Control trials have no light at
# any site. Windows O5: (a) control samples inside the median light span of the lit trials, (b) anywhere.
def light_design(r, region):
tr = r["trials"]
sites = [s for s, L in r["light"].items() if L["region"] == region and len(L["pulses"])]
if not sites:
return None
s = sites[0]
w = yu2016.light_windows(r["light"], s, tr)
other = set()
for s2 in r["light"]:
if s2 != s:
other |= set(yu2016.light_windows(r["light"], s2, tr).trial.astype(int))
w = w[~w.trial.isin(other)]
if len(w) == 0:
return None
P = r["light"][s]["pulses"]
pk = yu2016.per_trial_index(P[:, 0], tr)
pw = r["light"][s].get("pulse_power", np.ones(len(P)))
power = {int(t): float(pw[pk == t].max()) for t in w.trial}
return {"site": s, "windows": {int(t): (a, b) for t, a, b in zip(w.trial, w.rel_start, w.rel_stop)},
"span": (float(w.rel_start.median()), float(w.rel_stop.median())), "power": power,
"ctl": ~yu2016.lit_trials(r["light"], tr)}
def masks(r, d, reading, top_power=False):
b, tr = r["beh"], r["trials"]
tri = b["trial"]
rel = b["t"] - tr.start.to_numpy()[np.clip(tri, 0, None)]
lo, hi = np.full(len(tr), np.inf), np.full(len(tr), -np.inf)
pmax = max(d["power"].values())
for t, (a, c) in d["windows"].items():
if not top_power or d["power"][t] == pmax:
lo[t], hi[t] = a, c
k = np.clip(tri, 0, None)
use_l = (tri >= 0) & (rel >= lo[k]) & (rel < hi[k])
use_c = (tri >= 0) & d["ctl"][k]
if reading == "a":
use_c &= (rel >= d["span"][0]) & (rel < d["span"][1])
return use_l, use_c
def light_compare(r, spikes, d, filt="filtfilt", reading="a", definition="cortex5", min_iti=0.025, top_power=False, o11="a-i"):
b = r["beh"] if o11 == "a-i" else yu2016.beh_o11(r["beh"], o11)
use_l, use_c = masks(r, d, reading, top_power)
count = yu2016.counts_1k(spikes, b)
out = {}
on = yu2016.touches(b, None, min_iti)
for tag, u in (("light", use_l), ("ctl", use_c)):
er = yu2016.epoch_rates(np.where(u, count, 0), b, filt, u)
out[f"{tag}_nonwhisk"], out[f"{tag}_whisk"] = er["rate_nonwhisk"], er["rate_whisk"]
out[f"{tag}_t_nonwhisk"], out[f"{tag}_t_whisk"] = er["time_nonwhisk"], er["time_whisk"]
s = yu2016.spikes_per_touch(count, b["trial"], on[u[on]], definition)
out[f"{tag}_spt"], out[f"{tag}_touches"] = s["spt"], s["n_touches"]
out[f"{tag}_rate"] = count[u].sum() / max(u.sum(), 1) * 1000
return out
def frac(light, ctl, how):
light, ctl = np.asarray(light, float), np.asarray(ctl, float)
ok = np.isfinite(light) & np.isfinite(ctl)
if how == "a":
return light[ok].mean() / ctl[ok].mean()
ok &= ctl > 0
return float(np.median(light[ok] / ctl[ok]))
thalamus-drives-fs¶
Photoinhibiting the thalamus reduces the spike rates of L4 FS interneurons during nonwhisking (to 4% of control), whisking (to 13%) and touch (to 32%), so FS interneurons are driven by thalamic input.
def thal_rows(col):
out = []
for r in l4:
c = cells.set_index("path").loc[r["path"]]
if c[col] != "FS" or c.line != "VGAT-ChR2-EYFP" or r["beh"]["principal"] is None:
continue
d = light_design(r, "VPM")
if d is None:
continue
for filt in yu2016.FILTERS:
for reading in ("a", "b"):
for top in (False, True):
for o11 in yu2016.O11:
out.append({"path": r["path"], "subject": r["subject"], "filter": filt, "O5": reading, "O7": "b" if top else "a",
"O11": o11, "lit_trials": len(d["windows"]), "powers": sorted(set(d["power"].values())),
**light_compare(r, r["spikes"], d, filt, reading, top_power=top, o11=o11)})
return pd.DataFrame(out)
def thal_summary(t):
rows = []
for (filt, o5, o7, o11), g in t.groupby(["filter", "O5", "O7", "O11"]):
row = {"filter": filt, "O5": o5, "O7": o7, "O11": o11, "cells": len(g)}
for m in ("nonwhisk", "whisk", "spt"):
gg = g.dropna(subset=[f"light_{m}", f"ctl_{m}"])
row.update({f"{m}_n": len(gg), f"{m}_O6a": frac(gg[f"light_{m}"], gg[f"ctl_{m}"], "a"),
f"{m}_O6b": frac(gg[f"light_{m}"], gg[f"ctl_{m}"], "b"),
f"{m}_lower": int((gg[f"light_{m}"] < gg[f"ctl_{m}"]).sum()), f"{m}_p": "%.2g" % wilcox(gg[f"light_{m}"], gg[f"ctl_{m}"]),
f"{m}_p_one_sided": "%.2g" % wilcox(gg[f"light_{m}"], gg[f"ctl_{m}"], "less")})
rows.append(row)
return pd.DataFrame(rows)
th = thal_rows("cls")
print("FS cells with thalamic light:", th.path.nunique(), "from mice", sorted(th.subject.unique()), "; lit trials per cell",
th.drop_duplicates("path").lit_trials.tolist(), "; laser powers (stored values)", th.drop_duplicates("path").powers.tolist())
print("Expected fractions of control: nonwhisking 0.04 +- 0.004, whisking 0.13 +- 0.013, spikes per touch 0.32 +- 0.032; each two-sided "
"signed-rank test p < 0.05 with light lower (answer of 2026-10-20); the one-sided p (light lower) is a variant that does not decide")
n5 = th.path.nunique()
print(f"Smallest exact signed-rank p with {n5} cells: two-sided {2 * 0.5 ** n5:.4f}, one-sided {0.5 ** n5:.4f}")
ths = thal_summary(th)
print(ths[ths.O11 == "a-i"].round(3).to_string(index=False))
print("Over the O11 readings (32 combinations): fractions nonwhisking", ranges(ths, ["nonwhisk_O6a", "nonwhisk_O6b", "whisk_O6a", "whisk_O6b"]),
"; two-sided p", {m: (ths[f"{m}_p"].astype(float).min(), ths[f"{m}_p"].astype(float).max()) for m in ("nonwhisk", "whisk")})
tv = thal_rows("cls_tag")
print(f"\nVariant (tag only, any waveform): {tv.path.nunique()} cells")
print(thal_summary(tv)[lambda d: (d.O7 == "a") & (d.O11 == "a-i")].round(3).to_string(index=False))
print("Per cell (filtfilt, O5 a, O7 a):")
print(th[(th["filter"] == "filtfilt") & (th.O5 == "a") & (th.O7 == "a") & (th.O11 == "a-i")][["subject", "ctl_nonwhisk", "light_nonwhisk", "ctl_whisk", "light_whisk",
"ctl_spt", "light_spt", "light_touches", "light_t_nonwhisk", "light_t_whisk"]].round(3).to_string(index=False))
FS cells with thalamic light: 5 from mice ['anm260070'] ; lit trials per cell [31, 38, 23, 27, 24] ; laser powers (stored values) [[15.0], [3.75, 15.0], [3.75, 15.0], [3.75, 15.0], [3.75, 15.0]] Expected fractions of control: nonwhisking 0.04 +- 0.004, whisking 0.13 +- 0.013, spikes per touch 0.32 +- 0.032; each two-sided signed-rank test p < 0.05 with light lower (answer of 2026-10-20); the one-sided p (light lower) is a variant that does not decide Smallest exact signed-rank p with 5 cells: two-sided 0.0625, one-sided 0.0312
filter O5 O7 O11 cells nonwhisk_n nonwhisk_O6a nonwhisk_O6b nonwhisk_lower nonwhisk_p nonwhisk_p_one_sided whisk_n whisk_O6a whisk_O6b whisk_lower whisk_p whisk_p_one_sided spt_n spt_O6a spt_O6b spt_lower spt_p spt_p_one_sided
filtfilt a a a-i 5 5 0.204 0.200 5 0.062 0.031 5 0.273 0.231 4 0.12 0.062 5 0.410 0.359 5 0.062 0.031
filtfilt a b a-i 5 5 0.022 0.000 5 0.062 0.031 5 0.203 0.163 4 0.12 0.062 5 0.400 0.381 5 0.062 0.031
filtfilt b a a-i 5 5 0.266 0.194 5 0.062 0.031 5 0.287 0.240 4 0.12 0.062 5 0.401 0.369 5 0.062 0.031
filtfilt b b a-i 5 5 0.028 0.000 5 0.062 0.031 5 0.214 0.177 4 0.12 0.062 5 0.391 0.392 5 0.062 0.031
lfilter a a a-i 5 5 0.241 0.238 5 0.062 0.031 5 0.262 0.235 4 0.12 0.062 5 0.410 0.359 5 0.062 0.031
lfilter a b a-i 5 5 0.030 0.012 5 0.062 0.031 5 0.208 0.168 4 0.12 0.062 5 0.400 0.381 5 0.062 0.031
lfilter b a a-i 5 5 0.322 0.233 4 0.12 0.062 5 0.279 0.244 4 0.12 0.062 5 0.401 0.369 5 0.062 0.031
lfilter b b a-i 5 5 0.040 0.017 5 0.062 0.031 5 0.221 0.179 4 0.12 0.062 5 0.391 0.392 5 0.062 0.031
Over the O11 readings (32 combinations): fractions nonwhisking {'nonwhisk_O6a': (0.0217, 0.324), 'nonwhisk_O6b': (0.0, 0.238), 'whisk_O6a': (0.197, 0.287), 'whisk_O6b': (0.146, 0.244)} ; two-sided p {'nonwhisk': (np.float64(0.062), np.float64(0.12)), 'whisk': (np.float64(0.12), np.float64(0.12))}
Variant (tag only, any waveform): 9 cells
filter O5 O7 O11 cells nonwhisk_n nonwhisk_O6a nonwhisk_O6b nonwhisk_lower nonwhisk_p nonwhisk_p_one_sided whisk_n whisk_O6a whisk_O6b whisk_lower whisk_p whisk_p_one_sided spt_n spt_O6a spt_O6b spt_lower spt_p spt_p_one_sided filtfilt a a a-i 9 9 0.199 0.109 8 0.012 0.0059 9 0.292 0.231 8 0.0078 0.0039 9 0.462 0.420 9 0.0039 0.002 filtfilt b a a-i 9 9 0.229 0.139 8 0.02 0.0098 9 0.302 0.240 8 0.0078 0.0039 9 0.453 0.396 9 0.0039 0.002 lfilter a a a-i 9 9 0.222 0.127 8 0.012 0.0059 9 0.277 0.235 8 0.0078 0.0039 9 0.462 0.420 9 0.0039 0.002 lfilter b a a-i 9 9 0.264 0.179 7 0.027 0.014 9 0.290 0.244 8 0.0078 0.0039 9 0.453 0.396 9 0.0039 0.002 Per cell (filtfilt, O5 a, O7 a): subject ctl_nonwhisk light_nonwhisk ctl_whisk light_whisk ctl_spt light_spt light_touches light_t_nonwhisk light_t_whisk anm260070 3.907 0.000 26.133 4.268 2.170 0.910 145 4.877 13.589 anm260070 3.985 0.797 8.353 9.715 2.660 0.887 177 32.620 12.970 anm260070 5.222 2.850 18.536 4.275 2.295 0.824 34 17.895 7.251 anm260070 4.805 2.085 13.847 3.401 1.448 1.089 112 2.398 11.762 anm260070 20.115 2.039 43.661 8.544 2.201 0.706 126 19.621 7.725
Result: blocked (too few cells). Only 5 FS cells by the claims file's rule were recorded with thalamic light, all from one mouse (anm260070), and with 5 cells a two-sided signed-rank test, which the criterion names, cannot reach p < 0.05 (smallest p 0.0625; 0.062 to 0.12 over every reading here). The rates fall with the light in all 5 cells without whisking and in 4 of 5 during whisking, to 0.20 and 0.27 of control (population means, all powers, main epochs), and spikes per touch to 0.41; at the highest power only the nonwhisking rate falls to 0.022 of control. Over the 32 combinations of filter, window, power and O11 reading the fractions are 0 to 0.32 without whisking and 0.15 to 0.29 with it, above the paper's 0.13 for whisking under every reading. The one-sided variant, which does not decide, gives p = 0.031 for the nonwhisking rate and spikes per touch and 0.062 for the whisking rate. The variant without the waveform boundary (9 cells) passes all three two-sided tests (p = 0.012, 0.0078 and 0.0039) with fractions of 0.20, 0.29 and 0.46, outside tolerance, so the best it could give is partially reproduced.
exc-thalamus-photoinhibition¶
Thalamic photoinhibition hyperpolarizes L4 excitatory neurons and reduces their Vm fluctuations, with an onset latency of 4.2 ms, longer than the 2.8 ms latency of direct cortical GABAergic activation.
from scipy import signal as sg
bh, ah = sg.butter(2, 10 / 500, "high")
def vm_light(r, d, reading):
b = r["beh"]
use_l, use_c = masks(r, d, reading)
vm = r["vm1k"].astype(float)
hp = np.full(len(vm), np.nan)
for k in np.unique(b["trial"][b["trial"] >= 0]):
i = np.flatnonzero(b["trial"] == k)
x = vm[i]
g = np.isfinite(x)
if g.sum() > 100:
hp[i] = sg.filtfilt(bh, ah, np.interp(np.arange(len(x)), np.flatnonzero(g), x[g]))
hp[i[~g]] = np.nan
keep = ~b["excl"] & np.isfinite(vm)
return {f"{t}_{q}": f(x[u & keep]) for t, u in (("light", use_l), ("ctl", use_c)) for q, x, f in (("vm", vm, np.mean), ("var", hp, np.nanvar))}
def resample(avg, fs):
"""20 kHz averages to 10 kHz by the mean of each pair of samples (an anti-alias step before decimation)."""
k = int(fs // 10000)
return avg if k == 1 else avg[: len(avg) // k * k].reshape(-1, k).mean(axis=1)
rows, avgs = [], []
for r in l4:
c = cells.set_index("path").loc[r["path"]]
if c.cls != "E" or r["mode"] != "whole-cell" or c.line != "VGAT-ChR2-EYFP" or "vm1k" not in r:
continue
d = light_design(r, "VPM")
if d is None:
continue
for reading in ("a", "b"):
rows.append({"path": r["path"], "subject": r["subject"], "O5": reading, "lit_trials": len(d["windows"]), **vm_light(r, d, reading)})
la = r["light_avg"].get(d["site"], {}).get("first", (None, 0))
if la[0] is not None:
avgs.append(resample(la[0], r["vm_hz"]) - resample(la[0], r["vm_hz"])[:int(0.05 * 10000)].mean())
tp = pd.DataFrame(rows)
print("Whole-cell E cells with thalamic light:", tp.path.nunique(), "(paper 12), from", tp.subject.nunique(), "mice; lit trials per cell",
tp.drop_duplicates("path").lit_trials.tolist())
for reading, g in tp.groupby("O5"):
print(f"O5 {reading}: Vm lower with light in {(g.light_vm < g.ctl_vm).sum()} of {len(g)} (median change {np.median(g.light_vm - g.ctl_vm):.2f} mV, "
f"signed-rank p = {wilcox(g.light_vm, g.ctl_vm):.2g}); variance (>10 Hz) lower in {(g.light_var < g.ctl_var).sum()} "
f"(median ratio {np.median(g.light_var / g.ctl_var):.2f}, p = {wilcox(g.light_var, g.ctl_var):.2g})")
ga = np.mean(avgs, axis=0) if avgs else None
lat_thal = yu2016.neg_onset_latency(ga, len(avgs), 10000)
lat_thal_1ms = yu2016.neg_onset_latency(ga, len(avgs), 10000, binned=True)
print(f"Grand average aligned to thalamic laser onset ({len(avgs)} cells): onset latency of the hyperpolarization {lat_thal:.2f} ms at the sampling rate "
f"(the criterion; expected 4.2 +- 0.42); for comparison only, {lat_thal_1ms:.1f} ms in 1 ms bins (no longer a reading); "
f"mean change 10 to 50 ms after onset {ga[int(0.06 * 1e4):int(0.1 * 1e4)].mean():.2f} mV")
cav = []
for r in l4:
c = cells.set_index("path").loc[r["path"]]
if c.cls != "E" or r["mode"] != "whole-cell" or c.line != "VGAT-ChR2-EYFP" or "vm1k" not in r:
continue
for s, L in r["light"].items():
if "vS1" in L["region"]:
a, n = r["light_avg"].get(s, {}).get("isolated", (None, 0))
if a is not None and n >= 5:
x = resample(a, r["vm_hz"])
cav.append(x - x[:int(0.05 * 1e4)].mean())
gc = np.mean(cav, axis=0) if cav else None
lat_ctx = yu2016.neg_onset_latency(gc, len(cav), 10000)
lat_ctx_1ms = yu2016.neg_onset_latency(gc, len(cav), 10000, binned=True)
print(f"Grand average aligned to cortical tag pulses ({len(cav)} whole-cell E cells in VGAT-ChR2 mice; paper 7): onset latency {lat_ctx:.2f} ms at the sampling rate "
f"(expected 2.8 +- 0.28; 1 ms bins, comparison only, {lat_ctx_1ms:.1f} ms); mean change 5 to 20 ms after onset {gc[int(0.055 * 1e4):int(0.07 * 1e4)].mean():.2f} mV")
print("Grand averages 0 to 8 ms after onset, 0.5 ms steps (mV): thalamic", np.round(ga[500:581:5].astype(float), 2).tolist())
print(" cortical", np.round(gc[500:581:5].astype(float), 2).tolist())
fig, ax = plt.subplots(1, 2, figsize=(9, 2.8), sharey=True)
tt = (np.arange(len(ga)) / 1e4 - 0.05) * 1000
ax[0].plot(tt, ga, color="k", lw=0.8); ax[0].axvline(0, color="C0"); ax[0].set_title("thalamic light", fontsize=8)
ax[1].plot(tt, gc, color="k", lw=0.8); ax[1].axvline(0, color="C0"); ax[1].set_title("cortical tag pulse", fontsize=8)
for a in ax:
a.set_xlim(-20, 60); a.set_xlabel("ms from laser onset")
ax[0].set_ylabel("Vm change (mV)"); plt.tight_layout(); plt.show()
print("Plotted range -20 to 60 ms; values (mV):", round(float(ga[300:1100].min()), 2), "to", round(float(ga[300:1100].max()), 2), "and",
round(float(gc[300:1100].min()), 2), "to", round(float(gc[300:1100].max()), 2))
Whole-cell E cells with thalamic light: 13 (paper 12), from 8 mice; lit trials per cell [24, 25, 42, 41, 20, 30, 20, 22, 19, 9, 16, 18, 28] O5 a: Vm lower with light in 12 of 13 (median change -4.02 mV, signed-rank p = 0.00049); variance (>10 Hz) lower in 10 (median ratio 0.55, p = 0.0046) O5 b: Vm lower with light in 12 of 13 (median change -4.15 mV, signed-rank p = 0.00049); variance (>10 Hz) lower in 10 (median ratio 0.57, p = 0.0046) Grand average aligned to thalamic laser onset (13 cells): onset latency of the hyperpolarization 0.10 ms at the sampling rate (the criterion; expected 4.2 +- 0.42); for comparison only, 5.0 ms in 1 ms bins (no longer a reading); mean change 10 to 50 ms after onset -3.70 mV Grand average aligned to cortical tag pulses (14 whole-cell E cells in VGAT-ChR2 mice; paper 7): onset latency 0.10 ms at the sampling rate (expected 2.8 +- 0.28; 1 ms bins, comparison only, 3.0 ms); mean change 5 to 20 ms after onset -1.86 mV Grand averages 0 to 8 ms after onset, 0.5 ms steps (mV): thalamic [-0.21, -0.22, -0.22, -0.21, -0.21, -0.21, -0.2, -0.21, -0.21, -0.23, -0.31, -0.4, -0.52, -0.65, -0.79, -0.91, -1.04] cortical [0.59, 0.59, 0.62, 0.63, 0.6, 0.6, 0.52, 0.36, 0.19, 0.04, -0.08, -0.23, -0.36, -0.53, -0.78, -0.99, -1.19]
Plotted range -20 to 60 ms; values (mV): -4.63 to 0.02 and -3.73 to 0.67
# The 0.1 ms laser latencies (the verifier's request): the grand averages -2 to +2 ms from laser onset, their first difference
# and the criterion's threshold (3 s.d. of the first difference 0 to 25 ms before onset, turned downward), at 10 kHz.
for name, avg_ in (("thalamic light", ga), ("cortical tag pulses", gc)):
avg_ = np.asarray(avg_, float)
i0 = int(0.05 * 1e4)
d = np.diff(avg_) # d[i] is the change into sample i + 1
base = d[i0 - 251:i0 - 1]
thr = 3 * base.std()
hit = np.flatnonzero(d[i0 - 1:] < -thr)
print(f"{name}: Vm (mV) -2 to +2 ms in 0.1 ms steps:", np.round(avg_[i0 - 20:i0 + 21], 3).tolist())
print(" first difference (mV per 0.1 ms) into each of those samples:", np.round(d[i0 - 21:i0 + 20], 4).tolist(), "; threshold", round(-thr, 4))
print(f" baseline samples beyond the threshold: {int(np.sum(base < -thr))} of {len(base)}; first sample beyond it after onset "
f"{hit[0] / 10:.1f} ms, a change of {d[i0 - 1 + hit[0]]:.4f} mV; step across onset (mean 0 to 1 ms minus mean -1 to 0 ms) "
f"{avg_[i0:i0 + 10].mean() - avg_[i0 - 10:i0].mean():.3f} mV")
later = hit[hit >= 20]
print(f" first sample beyond it 2 ms or more after onset (diagnostic, decides nothing): {later[0] / 10 if len(later) else np.nan:.1f} ms")
thalamic light: Vm (mV) -2 to +2 ms in 0.1 ms steps: [-0.176, -0.179, -0.181, -0.179, -0.192, -0.188, -0.187, -0.19, -0.19, -0.191, -0.192, -0.197, -0.206, -0.194, -0.196, -0.199, -0.2, -0.202, -0.199, -0.212, -0.214, -0.275, -0.226, -0.221, -0.223, -0.22, -0.226, -0.229, -0.22, -0.218, -0.218, -0.212, -0.213, -0.207, -0.213, -0.213, -0.212, -0.217, -0.214, -0.22, -0.214] first difference (mV per 0.1 ms) into each of those samples: [-0.0039, -0.0031, -0.0017, 0.0015, -0.0128, 0.0039, 0.0011, -0.0025, -0.0008, -0.0005, -0.001, -0.0052, -0.0092, 0.0121, -0.0019, -0.0024, -0.0012, -0.0024, 0.0028, -0.0122, -0.0019, -0.0612, 0.0484, 0.0057, -0.0018, 0.0022, -0.0053, -0.0033, 0.0086, 0.0025, 0.0002, 0.0061, -0.0015, 0.006, -0.0056, -0.0008, 0.0019, -0.0053, 0.0024, -0.0052, 0.0057] ; threshold -0.0206 baseline samples beyond the threshold: 1 of 250; first sample beyond it after onset 0.1 ms, a change of -0.0612 mV; step across onset (mean 0 to 1 ms minus mean -1 to 0 ms) -0.027 mV first sample beyond it 2 ms or more after onset (diagnostic, decides nothing): 4.9 ms cortical tag pulses: Vm (mV) -2 to +2 ms in 0.1 ms steps: [0.667, 0.657, 0.669, 0.669, 0.658, 0.656, 0.654, 0.655, 0.654, 0.654, 0.654, 0.645, 0.628, 0.629, 0.615, 0.625, 0.617, 0.615, 0.605, 0.593, 0.589, 0.526, 0.596, 0.591, 0.585, 0.594, 0.59, 0.594, 0.606, 0.602, 0.616, 0.617, 0.625, 0.629, 0.627, 0.626, 0.613, 0.609, 0.603, 0.598, 0.603] first difference (mV per 0.1 ms) into each of those samples: [0.0121, -0.0095, 0.0114, 0.0001, -0.0107, -0.0017, -0.0027, 0.0018, -0.001, -0.0006, 0.0001, -0.0089, -0.0166, 0.0005, -0.0139, 0.0102, -0.0086, -0.0015, -0.0105, -0.0119, -0.0033, -0.0638, 0.0703, -0.0046, -0.0065, 0.0089, -0.0036, 0.0043, 0.0115, -0.0044, 0.0148, 0.0007, 0.0082, 0.0033, -0.0018, -0.0007, -0.0129, -0.0043, -0.0055, -0.0055, 0.0053] ; threshold -0.0557 baseline samples beyond the threshold: 3 of 250; first sample beyond it after onset 0.1 ms, a change of -0.0638 mV; step across onset (mean 0 to 1 ms minus mean -1 to 0 ms) -0.035 mV first sample beyond it 2 ms or more after onset (diagnostic, decides nothing): 5.2 ms
Result: partially reproduced. Thalamic light hyperpolarizes 12 of 13 whole-cell excitatory cells (median -4.02 mV) and lowers their Vm variance above 10 Hz in 10 (median ratio 0.55), under both window readings. With dVm/dt at the sampling rate, as the criterion says, the onset latency is 0.10 ms for both the thalamic light (13 cells) and the cortical tag pulses (14 cells, now that every pulse of a train counts), against 4.2 and 2.8 ms, so the thalamic latency is not longer. The diagnostic above shows the cause: one sample 0.1 ms after light onset drops by 0.061 mV (thalamic) and 0.064 mV (cortical) and the next sample returns, a one-sample transient at light onset, likely an artifact, which crosses the threshold. The first crossing 2 ms or more after onset, a diagnostic that decides nothing, is at 4.9 ms (thalamic) and 5.2 ms (cortical), but the two thresholds differ (-0.0206 and -0.0557 mV per sample; 1 and 3 of the 250 baseline samples already cross them), so this does not compare the two latencies. The other outputs above put the thalamic onset later: 5.0 against 3.0 ms in 1 ms bins, and in 0.5 ms steps the thalamic average starts to fall at about 4.5 to 5 ms and the cortical one at about 3 ms. None of these is the criterion's rule, so they decide nothing; by the rule the thalamic latency is not longer. The 20 kHz averages are now brought to 10 kHz by averaging pairs of samples.
pv-enphr-suppression¶
Yellow light in PV-IRES-Cre mice expressing eNpHR3.0 strongly inhibits most L4 PV interneurons (3 of 4); their rate falls 3 ms after laser onset and L4 excitatory activity rises 2 ms later.
def boot_rate_ci(count, b, filt, use, rng):
"""95% bootstrap interval (1,000 draws over trials) of the rate in nonwhisking and whisking samples."""
out = {}
tri = b["trial"]
for name in ("nonwhisk", "whisk"):
m = b[filt][name] & use & (tri >= 0)
ks = tri[m]
n = tri.max() + 1
c = np.bincount(ks, weights=count[m], minlength=n)
t = np.bincount(ks, minlength=n) / 1000
keep = t > 0
c, t = c[keep], t[keep]
if len(c) < 2:
out[name] = (np.nan, np.nan)
continue
idx = rng.integers(0, len(c), (yu2016.N_BOOT, len(c)))
bs = c[idx].sum(axis=1) / t[idx].sum(axis=1)
out[name] = (np.percentile(bs, 2.5), np.percentile(bs, 97.5))
return out
def onset_psth(r, d):
"""Rate (spikes/s) in 1 ms bins from 25 ms before to 100 ms after the first light sample of each lit trial."""
sp = np.sort(r["spikes"])
st = r["trials"].start.to_numpy()
on = np.array([st[t] + w[0] for t, w in d["windows"].items()])
edges = np.arange(-25, 101) / 1000
h = np.zeros(len(edges) - 1)
for o in on:
h += np.histogram(sp[(sp >= o - 0.025) & (sp < o + 0.1)] - o, edges)[0]
return h / len(on) * 1000
def psth_crossing(m, sign):
lags = np.arange(-25, 100)
dd = np.diff(m)
lag_d = lags[1:]
thr = 3 * dd[(lag_d >= -25) & (lag_d < 0)].std()
hit = np.flatnonzero((lag_d >= 0) & (sign * dd > thr))
return (float(lag_d[hit[0]]), float(lag_d[hit[0]] + 0.5)) if len(hit) else (np.nan, np.nan)
rng = np.random.default_rng(13)
rows, ps_pv, ps_e = [], [], []
for r in l4:
c = cells.set_index("path").loc[r["path"]]
if c.line != "PV-IRES-Cre" or r["beh"]["principal"] is None:
continue
d = light_design(r, "vS1")
if d is None:
continue
ps = onset_psth(r, d)
if c.cls == "FS":
ps_pv.append(ps)
b = r["beh"]
use_l, use_c = masks(r, d, "a")
count = yu2016.counts_1k(r["spikes"], b)
ci = boot_rate_ci(count, b, "filtfilt", use_c, rng)
lr = light_compare(r, r["spikes"], d, "filtfilt", "a")
rows.append({"path": r["path"].split("/")[1][:40], "ctl_nonwhisk": lr["ctl_nonwhisk"], "ctl_nonwhisk_lo": ci["nonwhisk"][0],
"light_nonwhisk": lr["light_nonwhisk"], "ctl_whisk": lr["ctl_whisk"], "ctl_whisk_lo": ci["whisk"][0],
"light_whisk": lr["light_whisk"],
"strongly_inhibited": lr["light_nonwhisk"] < ci["nonwhisk"][0] and lr["light_whisk"] < ci["whisk"][0]})
elif c.cls == "E":
ps_e.append(ps)
pvr = pd.DataFrame(rows)
print("PV cells identified (PV-IRES-Cre, rate lower with the light):", len(pvr), "(paper 4); strongly inhibited (light rate below the lower "
"bound of the control 95% interval in nonwhisking and whisking periods):", int(pvr.strongly_inhibited.sum()))
print(pvr.round(2).to_string(index=False))
gpv, ge = np.mean(ps_pv, axis=0), np.mean(ps_e, axis=0)
lpv, le_ = psth_crossing(gpv, -1), psth_crossing(ge, +1)
print(f"Grand average PSTH aligned to laser onset: PV cells ({len(ps_pv)}) rate falls at {lpv[0]:.1f} ms (O9 a, bin edge) / {lpv[1]:.1f} ms (O9 b, centre), "
f"expected 3 +- 0.3; E cells ({len(ps_e)}) rate rises at {le_[0]:.1f} / {le_[1]:.1f} ms, a further delay of {le_[0] - lpv[0]:.1f} ms "
f"(expected 2 +- 0.2)")
print("PV grand average, 0 to 10 ms (spikes/s):", np.round(gpv[25:36], 1).tolist(), "; 25 ms before: mean", round(gpv[:25].mean(), 1))
print("E grand average, 0 to 10 ms (spikes/s):", np.round(ge[25:36], 2).tolist(), "; 25 ms before: mean", round(ge[:25].mean(), 2))
PV cells identified (PV-IRES-Cre, rate lower with the light): 3 (paper 4); strongly inhibited (light rate below the lower bound of the control 95% interval in nonwhisking and whisking periods): 3
path ctl_nonwhisk ctl_nonwhisk_lo light_nonwhisk ctl_whisk ctl_whisk_lo light_whisk strongly_inhibited
sub-anm244024_ses-20141112_obj-vigb9a_be 13.06 11.01 3.30 21.32 18.28 6.33 True
sub-anm266945_ses-20141220_obj-7o69ev_be 41.73 31.96 2.00 56.58 48.57 4.28 True
sub-anm300169_ses-20150926_obj-rp0tw4_be 27.55 24.67 1.74 18.85 15.53 0.63 True
Grand average PSTH aligned to laser onset: PV cells (3) rate falls at nan ms (O9 a, bin edge) / nan ms (O9 b, centre), expected 3 +- 0.3; E cells (42) rate rises at 48.0 / 48.5 ms, a further delay of nan ms (expected 2 +- 0.2)
PV grand average, 0 to 10 ms (spikes/s): [26.7, 66.5, 49.0, 17.5, 0.0, 0.0, 0.0, 0.0, 0.0, 12.8, 0.0] ; 25 ms before: mean 37.1
E grand average, 0 to 10 ms (spikes/s): [7.76, 2.02, 3.24, 3.18, 2.32, 6.61, 5.08, 2.17, 7.85, 4.02, 7.89] ; 25 ms before: mean 3.1
Result: partially reproduced. 3 PV cells are identified by their inactivation, and all 3 are strongly inhibited (their light rate below the control interval in both periods); the paper's fourth PV cell cannot be found. Since the cells are found by their inactivation, the count cannot fail, and a fourth PV cell that the light did not inhibit would be classed excitatory. The parts that could fail fail: the dPSTH/dt rule finds no fall in the grand average of the 3 PV cells, and the excitatory cells' rise is found at 48.0 ms with the bin edge (O9 a) and 48.5 ms with its centre (O9 b), so neither latency is within tolerance under either bin position.
fs-suppress-movement-spiking¶
Partially suppressing PV (FS) interneurons increases the spike rate of L4 excitatory neurons during nonwhisking (0.6 to 1.6 spikes/s) and whisking (0.8 to 2.7 spikes/s).
def pv_rows(min_iti=0.025, definition="cortex5", o11s=yu2016.O11):
out = []
for r in l4:
c = cells.set_index("path").loc[r["path"]]
if c.line != "PV-IRES-Cre" or c.cls != "E" or r["beh"]["principal"] is None:
continue
d = light_design(r, "vS1")
if d is None:
continue
for filt in yu2016.FILTERS:
for reading in ("a", "b"):
for o11 in o11s:
out.append({"path": r["path"], "subject": r["subject"], "mode": r["mode"], "filter": filt, "O5": reading, "O11": o11,
"lit_trials": len(d["windows"]), **light_compare(r, r["spikes"], d, filt, reading, definition, min_iti, o11=o11)})
return pd.DataFrame(out)
pvE = pv_rows()
pvE8 = pv_rows(0.010, "cortex8", ("a-i",))
print("E cells in PV-IRES-Cre mice with the eNpHR light:", pvE.path.nunique(), "(paper 40 of 43), from", pvE.subject.nunique(), "mice;",
pvE.drop_duplicates("path")["mode"].value_counts().to_dict())
rows = []
for (filt, o5, o11), g in pvE.groupby(["filter", "O5", "O11"]):
g = g[(g.light_t_nonwhisk > 0) & (g.light_t_whisk > 0) & (g.ctl_t_nonwhisk > 0) & (g.ctl_t_whisk > 0)]
kw = stats.kruskal(g.ctl_nonwhisk, g.ctl_whisk, g.light_nonwhisk, g.light_whisk).pvalue
rows.append({"filter": filt, "O5": o5, "O11": o11, "n": len(g), "ctl_nonwhisk": g.ctl_nonwhisk.mean(), "light_nonwhisk": g.light_nonwhisk.mean(),
"ctl_whisk": g.ctl_whisk.mean(), "light_whisk": g.light_whisk.mean(),
"sem_light_nonwhisk": g.light_nonwhisk.sem(), "sem_light_whisk": g.light_whisk.sem(),
"nonwhisk_higher": int((g.light_nonwhisk > g.ctl_nonwhisk).sum()), "p_nonwhisk": "%.2g" % wilcox(g.light_nonwhisk, g.ctl_nonwhisk),
"whisk_higher": int((g.light_whisk > g.ctl_whisk).sum()), "p_whisk": "%.2g" % wilcox(g.light_whisk, g.ctl_whisk),
"kruskal_wallis_p": "%.2g" % kw})
print("Expected means: nonwhisking 0.6 +- 0.1 -> 1.6 +- 0.3, whisking 0.8 +- 0.2 -> 2.7 +- 0.5 spikes/s; both paired tests p < 0.05 with light higher")
fsm = pd.DataFrame(rows)
print(fsm.round(3).to_string(index=False))
print("Ranges over the 16 readings:", ranges(fsm, ["ctl_nonwhisk", "light_nonwhisk", "ctl_whisk", "light_whisk"]),
"; largest paired p:", max(float(x) for x in fsm.p_nonwhisk), "and", max(float(x) for x in fsm.p_whisk))
g = pvE[(pvE["filter"] == "filtfilt") & (pvE.O5 == "a") & (pvE.O11 == "a-i")]
print("Cells dropped for lack of an epoch of each kind (filtfilt, O5 a):",
int(((g.light_t_nonwhisk == 0) | (g.light_t_whisk == 0) | (g.ctl_t_nonwhisk == 0) | (g.ctl_t_whisk == 0)).sum()))
print("By mouse (filtfilt, O5 a), mean rates:", g.groupby("subject")[["ctl_nonwhisk", "light_nonwhisk", "ctl_whisk", "light_whisk"]].mean().round(2).to_dict("index"))
print("Seconds per cell inside the light, median: nonwhisking", round(g.light_t_nonwhisk.median(), 1), ", whisking", round(g.light_t_whisk.median(), 1))
E cells in PV-IRES-Cre mice with the eNpHR light: 42 (paper 40 of 43), from 9 mice; {'cell-attached': 34, 'whole-cell': 8}
Expected means: nonwhisking 0.6 +- 0.1 -> 1.6 +- 0.3, whisking 0.8 +- 0.2 -> 2.7 +- 0.5 spikes/s; both paired tests p < 0.05 with light higher
filter O5 O11 n ctl_nonwhisk light_nonwhisk ctl_whisk light_whisk sem_light_nonwhisk sem_light_whisk nonwhisk_higher p_nonwhisk whisk_higher p_whisk kruskal_wallis_p
filtfilt a a-i 42 1.256 2.263 3.953 6.135 0.374 1.145 33 6.2e-05 33 7.4e-05 0.00011
filtfilt a a-ii 42 1.271 2.297 4.067 6.272 0.374 1.164 32 7.4e-05 33 9.9e-05 8.2e-05
filtfilt a b-i 42 1.269 2.288 4.214 6.551 0.373 1.172 33 7e-05 33 4.9e-05 5e-05
filtfilt a b-ii 42 1.279 2.317 4.307 6.633 0.373 1.189 32 6.6e-05 33 7.4e-05 4.1e-05
filtfilt b a-i 42 1.179 2.263 3.583 6.135 0.374 1.145 33 3.7e-05 33 1.1e-05 6.2e-05
filtfilt b a-ii 42 1.193 2.297 3.635 6.272 0.374 1.164 33 3.7e-05 34 4.4e-06 5.2e-05
filtfilt b b-i 42 1.184 2.288 3.665 6.551 0.373 1.172 33 3.3e-05 33 4.6e-06 2.9e-05
filtfilt b b-ii 42 1.196 2.317 3.717 6.633 0.373 1.189 33 3.5e-05 34 3e-06 2.7e-05
lfilter a a-i 42 1.291 2.504 3.720 5.690 0.378 1.054 33 4.1e-05 33 0.00018 0.00035
lfilter a a-ii 42 1.307 2.539 3.838 5.815 0.378 1.069 33 3.3e-05 31 0.0003 0.0003
lfilter a b-i 42 1.307 2.517 4.001 6.030 0.377 1.069 33 4.4e-05 33 0.00011 0.00016
lfilter a b-ii 42 1.315 2.552 4.099 6.154 0.377 1.089 33 3.5e-05 31 0.00019 0.00014
lfilter b a-i 42 1.212 2.504 3.364 5.690 0.378 1.054 36 6.7e-06 32 7.1e-06 0.00018
lfilter b a-ii 42 1.219 2.539 3.414 5.815 0.378 1.069 36 6.3e-06 33 4.4e-06 0.00013
lfilter b b-i 42 1.217 2.517 3.453 6.030 0.377 1.069 36 5.9e-06 33 3.2e-06 8e-05
lfilter b b-ii 42 1.222 2.552 3.505 6.154 0.377 1.089 36 5.2e-06 33 2.6e-06 7e-05
Ranges over the 16 readings: {'ctl_nonwhisk': (1.18, 1.32), 'light_nonwhisk': (2.26, 2.55), 'ctl_whisk': (3.36, 4.31), 'light_whisk': (5.69, 6.63)} ; largest paired p: 7.4e-05 and 0.0003
Cells dropped for lack of an epoch of each kind (filtfilt, O5 a): 0
By mouse (filtfilt, O5 a), mean rates: {'anm244024': {'ctl_nonwhisk': 1.19, 'light_nonwhisk': 1.98, 'ctl_whisk': 3.71, 'light_whisk': 4.97}, 'anm244025': {'ctl_nonwhisk': 1.22, 'light_nonwhisk': 1.68, 'ctl_whisk': 2.69, 'light_whisk': 4.9}, 'anm244028': {'ctl_nonwhisk': 0.59, 'light_nonwhisk': 1.25, 'ctl_whisk': 1.38, 'light_whisk': 2.83}, 'anm266945': {'ctl_nonwhisk': 0.77, 'light_nonwhisk': 1.46, 'ctl_whisk': 2.8, 'light_whisk': 6.73}, 'anm266951': {'ctl_nonwhisk': 0.6, 'light_nonwhisk': 0.0, 'ctl_whisk': 0.0, 'light_whisk': 0.0}, 'anm300169': {'ctl_nonwhisk': 1.41, 'light_nonwhisk': 2.8, 'ctl_whisk': 1.86, 'light_whisk': 4.83}, 'anm300170': {'ctl_nonwhisk': 1.62, 'light_nonwhisk': 5.34, 'ctl_whisk': 4.49, 'light_whisk': 13.16}, 'anm300171': {'ctl_nonwhisk': 2.8, 'light_nonwhisk': 4.32, 'ctl_whisk': 6.57, 'light_whisk': 7.52}, 'anm300172': {'ctl_nonwhisk': 1.56, 'light_nonwhisk': 2.51, 'ctl_whisk': 9.47, 'light_whisk': 7.88}}
Seconds per cell inside the light, median: nonwhisking 11.7 , whisking 10.5
Result: partially reproduced. In the 42 excitatory cells of PV-IRES-Cre mice the light raises the nonwhisking rate (32 to 36 cells, p at most 7.4e-05) and the whisking rate (31 to 34 cells, p at most 0.0003) under every filter, window and O11 reading, but the rates are higher than the paper's: 1.18 to 1.32 rising to 2.26 to 2.55 spikes/s without whisking (paper 0.6 to 1.6) and 3.36 to 4.31 rising to 5.69 to 6.63 with it (paper 0.8 to 2.7), outside the tolerances. Most of these cells are cell-attached (34 of 42), whose rates are high throughout this dandiset (data check).
fs-enhance-touch-selectivity¶
With PV interneurons suppressed, L4 excitatory neurons become driven by whisker movement (whisking rate above nonwhisking, P = 0.02), while in control trials they are not (P = 0.55); their selectivity for touch is normally maintained in part by FS interneurons.
rows = []
for (filt, o5, o11), g in pvE.groupby(["filter", "O5", "O11"]):
g = g[(g.light_t_nonwhisk > 0) & (g.light_t_whisk > 0) & (g.ctl_t_nonwhisk > 0) & (g.ctl_t_whisk > 0)]
rows.append({"filter": filt, "O5": o5, "O11": o11, "n": len(g), "light_whisk_minus_nonwhisk": (g.light_whisk - g.light_nonwhisk).mean(),
"light_whisk_higher": int((g.light_whisk > g.light_nonwhisk).sum()), "p_light": "%.2g" % wilcox(g.light_whisk, g.light_nonwhisk),
"ctl_whisk_minus_nonwhisk": (g.ctl_whisk - g.ctl_nonwhisk).mean(), "ctl_whisk_higher": int((g.ctl_whisk > g.ctl_nonwhisk).sum()),
"p_ctl": "%.2g" % wilcox(g.ctl_whisk, g.ctl_nonwhisk)})
print("Whisking against nonwhisking rate within light trials (paper P = 0.02) and within control trials (paper P = 0.55):")
fse = pd.DataFrame(rows)
print(fse.round(3).to_string(index=False))
print("Ranges over the 16 readings: p with light", (min(float(x) for x in fse.p_light), max(float(x) for x in fse.p_light)),
"; p in control trials", (min(float(x) for x in fse.p_ctl), max(float(x) for x in fse.p_ctl)))
Whisking against nonwhisking rate within light trials (paper P = 0.02) and within control trials (paper P = 0.55): filter O5 O11 n light_whisk_minus_nonwhisk light_whisk_higher p_light ctl_whisk_minus_nonwhisk ctl_whisk_higher p_ctl filtfilt a a-i 42 3.872 33 2.2e-06 2.697 31 3.3e-05 filtfilt a a-ii 42 3.975 33 1.9e-06 2.796 31 2.7e-05 filtfilt a b-i 42 4.263 33 2.1e-06 2.945 32 2e-05 filtfilt a b-ii 42 4.317 33 1.7e-06 3.028 32 1.7e-05 filtfilt b a-i 42 3.872 33 2.2e-06 2.404 35 7.6e-06 filtfilt b a-ii 42 3.975 33 1.9e-06 2.441 35 7.1e-06 filtfilt b b-i 42 4.263 33 2.1e-06 2.481 35 7.6e-06 filtfilt b b-ii 42 4.317 33 1.7e-06 2.521 35 6.7e-06 lfilter a a-i 42 3.186 32 3.3e-05 2.429 29 0.00013 lfilter a a-ii 42 3.276 31 3.7e-05 2.531 27 0.00011 lfilter a b-i 42 3.513 32 2.2e-05 2.695 30 7.4e-05 lfilter a b-ii 42 3.602 32 2e-05 2.784 28 5.8e-05 lfilter b a-i 42 3.186 32 3.3e-05 2.152 30 7.5e-05 lfilter b a-ii 42 3.276 31 3.7e-05 2.196 30 7.5e-05 lfilter b b-i 42 3.513 32 2.2e-05 2.236 31 5.4e-05 lfilter b b-ii 42 3.602 32 2e-05 2.283 31 5.1e-05 Ranges over the 16 readings: p with light (1.7e-06, 3.7e-05) ; p in control trials (6.7e-06, 0.00013)
Result: partially reproduced. With the light, whisking rates exceed nonwhisking rates in 31 to 33 of 42 cells (p = 1.7e-06 to 3.7e-05 over the 16 readings), as the paper reports, but in control trials they do too (27 to 35 of 42, p = 6.7e-06 to 0.00013), where the paper found no difference (P = 0.55).
exc-touch-spikes-enphr¶
PV interneuron suppression does not change mean touch-evoked spiking of L4 excitatory neurons significantly (0.27 to 0.32 spikes per touch, P = 0.09); its effect is heterogeneous, with weakly responding neurons responding more and strongly responding neurons unchanged or less.
rows = []
for (filt, o5), g in pvE8[pvE8["filter"] == "filtfilt"].groupby(["filter", "O5"]):
g = g.dropna(subset=["ctl_spt", "light_spt"])
rows.append({"O5": o5, "n": len(g), "ctl_mean": g.ctl_spt.mean(), "ctl_sem": g.ctl_spt.sem(), "light_mean": g.light_spt.mean(),
"light_sem": g.light_spt.sem(), "higher": int((g.light_spt > g.ctl_spt).sum()), "p": "%.2g" % wilcox(g.light_spt, g.ctl_spt),
"touches_light_median": g.light_touches.median()})
print("Spikes per touch (Fig. 8e definition, ITI >= 10 ms), light against control; expected 0.27 +- 0.06 -> 0.32 +- 0.06, P = 0.09 (absence at 0.05)")
print(pd.DataFrame(rows).round(3).to_string(index=False))
g = pvE8[(pvE8["filter"] == "filtfilt") & (pvE8.O5 == "a")].dropna(subset=["ctl_spt", "light_spt"])
lo = g.ctl_spt < g.ctl_spt.median()
print(f"Heterogeneity (described, not tested): cells below the median control response change by {np.mean(g.light_spt[lo] - g.ctl_spt[lo]):+.3f} on average "
f"({int((g.light_spt[lo] > g.ctl_spt[lo]).sum())} of {int(lo.sum())} rise); cells above it by {np.mean(g.light_spt[~lo] - g.ctl_spt[~lo]):+.3f} "
f"({int((g.light_spt[~lo] > g.ctl_spt[~lo]).sum())} of {int((~lo).sum())} rise)")
Spikes per touch (Fig. 8e definition, ITI >= 10 ms), light against control; expected 0.27 +- 0.06 -> 0.32 +- 0.06, P = 0.09 (absence at 0.05) O5 n ctl_mean ctl_sem light_mean light_sem higher p touches_light_median a 40 0.339 0.079 0.448 0.102 25 0.044 81.5 b 41 0.304 0.077 0.442 0.100 28 0.0091 80.0 Heterogeneity (described, not tested): cells below the median control response change by +0.123 on average (14 of 20 rise); cells above it by +0.095 (11 of 20 rise)
Result: not reproduced. Spikes per touch (Fig. 8e definition) rise with the light from 0.339 to 0.448 (40 cells, p = 0.044) or from 0.304 to 0.442 (41 cells, p = 0.0091), so the paper's absence of a significant change is not found. Cells below the median control response rise by 0.123 on average and cells above it by 0.095.
auc-weak-strong¶
Suppressing PV interneurons improves touch detection (AUC) for L4 excitatory neurons with weak touch responses and diminishes it for neurons with robust touch responses.
def auc_inputs(r, d, rng, reading):
"""Per trial, spike counts in the 250 ms before and after the first principal-whisker touch, or (no touch) in two
adjacent 250 ms windows of the sample epoch: reading (a) at a random time, (b) around the median first-touch time
of the touch trials. Light trials count when the 500 ms lie inside the light."""
b, tr = r["beh"], r["trials"]
sp = np.sort(r["spikes"])
st = tr.start.to_numpy()
on_t = b["t"][b["touch_on"]]
k = yu2016.per_trial_index(on_t, tr)
first = {}
for t, kk in zip(on_t, k):
if kk >= 0 and kk not in first:
first[kk] = t
med = np.median([first[kk] - st[kk] for kk in first]) if first else np.nan
out = {"ctl": {"t": [[], []], "n": [[], []]}, "light": {"t": [[], []], "n": [[], []]}}
cnt = lambda a, z: int(np.searchsorted(sp, z) - np.searchsorted(sp, a))
for kk in range(len(tr)):
if d["ctl"][kk]:
cond, win = "ctl", None
elif kk in d["windows"]:
cond, win = "light", d["windows"][kk]
else:
continue
if kk in first:
t0, key = first[kk], "t"
else:
p0, p1 = tr.pole_in_time.iloc[kk], tr.pole_out_time.iloc[kk]
if not (np.isfinite(p0) and np.isfinite(p1)) or p1 - p0 < 0.5:
continue
t0 = rng.uniform(p0 + 0.25, p1 - 0.25) if reading == "a" else st[kk] + med
key = "n"
if win is not None and not (st[kk] + win[0] <= t0 - 0.25 and t0 + 0.25 <= st[kk] + win[1]):
continue
if t0 - 0.25 < st[kk] or t0 + 0.25 > st[kk] + yu2016.ANALYSIS_END_S:
continue
out[cond][key][0].append(cnt(t0 - 0.25, t0))
out[cond][key][1].append(cnt(t0, t0 + 0.25))
return out
def auc_table(reading, rng):
rows_ = []
for r in l4:
c = cells.set_index("path").loc[r["path"]]
if c.line != "PV-IRES-Cre" or c.cls != "E" or r["beh"]["principal"] is None:
continue
d = light_design(r, "vS1")
if d is None:
continue
x = auc_inputs(r, d, rng, reading)
row = {"path": r["path"], "subject": r["subject"], "reading": reading}
for cond in ("ctl", "light"):
tb, ta = np.array(x[cond]["t"][0]), np.array(x[cond]["t"][1])
nb, na = np.array(x[cond]["n"][0]), np.array(x[cond]["n"][1])
row[f"{cond}_touch_trials"], row[f"{cond}_notouch_trials"] = len(tb), len(nb)
row[f"{cond}_auc"] = yu2016.roc_auc((tb, nb), (ta, na), rng)
row[f"{cond}_after"] = ta.mean() / 0.25 if len(ta) else np.nan
row[f"{cond}_before"] = tb.mean() / 0.25 if len(tb) else np.nan
rows_.append(row)
t = pd.DataFrame(rows_)
t["evoked_ctl"] = t.ctl_after - t.ctl_before
t["auc_change"] = t.light_auc - t.ctl_auc
return t
auc = pd.concat([auc_table(reading, np.random.default_rng(29 if reading == "a" else 31)) for reading in ("a", "b")], ignore_index=True)
for reading, g in auc.groupby("reading"):
g = g.dropna(subset=["ctl_auc", "light_auc"])
up, dn = g[g.auc_change > 0], g[g.auc_change < 0]
print(f"Reading {reading}: {len(g)} cells with an AUC in both conditions (trials per cell, median: control {int(g.ctl_touch_trials.median())} touch, "
f"{int(g.ctl_notouch_trials.median())} no-touch; light {int(g.light_touch_trials.median())} touch, {int(g.light_notouch_trials.median())} no-touch). "
f"AUC rises in {len(up)}, falls in {len(dn)}, unchanged in {int((g.auc_change == 0).sum())}. Control touch-evoked rate (0 to 250 ms after minus "
f"before, spikes/s): rising group mean {up.evoked_ctl.mean():.2f}, falling group mean {dn.evoked_ctl.mean():.2f} "
f"(rank-sum p = {stats.ranksums(up.evoked_ctl, dn.evoked_ctl).pvalue:.2g}, reported only)")
print(f" AUC control median {g.ctl_auc.median():.3f}, light median {g.light_auc.median():.3f}")
print(f" Spearman correlation of AUC change with the control touch-evoked rate: rho = {stats.spearmanr(g.evoked_ctl, g.auc_change)[0]:.2f} (reported only)")
print("Cells without an AUC in a condition:", auc[auc.ctl_auc.isna() | auc.light_auc.isna()].groupby("reading").size().to_dict())
Reading a: 36 cells with an AUC in both conditions (trials per cell, median: control 23 touch, 20 no-touch; light 12 touch, 10 no-touch). AUC rises in 23, falls in 12, unchanged in 1. Control touch-evoked rate (0 to 250 ms after minus before, spikes/s): rising group mean 5.93, falling group mean 10.87 (rank-sum p = 0.37, reported only)
AUC control median 0.660, light median 0.690
Spearman correlation of AUC change with the control touch-evoked rate: rho = -0.23 (reported only)
Reading b: 34 cells with an AUC in both conditions (trials per cell, median: control 24 touch, 20 no-touch; light 12 touch, 15 no-touch). AUC rises in 17, falls in 16, unchanged in 1. Control touch-evoked rate (0 to 250 ms after minus before, spikes/s): rising group mean 8.14, falling group mean 8.18 (rank-sum p = 1, reported only)
AUC control median 0.690, light median 0.746
Spearman correlation of AUC change with the control touch-evoked rate: rho = 0.04 (reported only)
Cells without an AUC in a condition: {'a': 6, 'b': 8}
Result: not reproduced. With the fixed seeds, cells whose AUC rises with the light have a lower control touch-evoked rate than those whose AUC falls under both readings: 5.93 against 10.87 spikes/s (a) and 8.14 against 8.18 (b). The seed check below shows that reading (b) does not decide: over 20 further seeds the rising group's rate is lower in 8 and higher in 12 (8.14 to 8.53 against 7.86 to 8.18 spikes/s), while reading (a) holds in 19 of 20. The criterion needs both readings, and under (b) the sign of a 0.04 spikes/s difference is set by the resampling, so the result is not reproduced. No test separates the groups under either reading with the fixed seeds (rank-sum p = 0.37 and 1, reported only).
auc-groups¶
Neurons whose AUC rose with PV suppression gained touch-evoked spiking (3.4 to 8.7 spikes/s); neurons whose AUC fell kept their touch-evoked response (7.3 to 7.0 spikes/s) but had higher baseline rates (0.9 to 2.6 spikes/s).
rows = []
for reading, g in auc.groupby("reading"):
g = g.dropna(subset=["ctl_auc", "light_auc"])
up, dn = g[g.auc_change > 0], g[g.auc_change < 0]
rows.append({"reading": reading, "n_up": len(up), "n_down": len(dn),
"up_after_ctl": up.ctl_after.mean(), "up_after_light": up.light_after.mean(), "p_up_after": "%.2g" % wilcox(up.ctl_after, up.light_after),
"down_after_ctl": dn.ctl_after.mean(), "down_after_light": dn.light_after.mean(), "p_down_after": "%.2g" % wilcox(dn.ctl_after, dn.light_after),
"down_before_ctl": dn.ctl_before.mean(), "down_before_light": dn.light_before.mean(), "p_down_before": "%.2g" % wilcox(dn.ctl_before, dn.light_before)})
print("Expected: increased-AUC group (19 +- 1.9) 3.4 +- 0.8 -> 8.7 +- 1.9 spikes/s, P = 0.0001; reduced-AUC group (21 +- 2.1) 7.3 +- 1.5 -> 7.0 +- 1.4 "
"(no change, P = 0.98); its baseline 0.9 +- 0.2 -> 2.6 +- 0.6 (P = 0.0002)")
print(pd.DataFrame(rows).round(3).T.to_string())
Expected: increased-AUC group (19 +- 1.9) 3.4 +- 0.8 -> 8.7 +- 1.9 spikes/s, P = 0.0001; reduced-AUC group (21 +- 2.1) 7.3 +- 1.5 -> 7.0 +- 1.4 (no change, P = 0.98); its baseline 0.9 +- 0.2 -> 2.6 +- 0.6 (P = 0.0002)
0 1
reading a b
n_up 23 17
n_down 12 16
up_after_ctl 9.991 12.642
up_after_light 15.157 18.101
p_up_after 4.9e-05 0.0026
down_after_ctl 15.982 12.366
down_after_light 17.854 14.946
p_down_after 0.11 0.021
down_before_ctl 5.108 4.187
down_before_light 12.076 11.276
p_down_before 0.0049 0.00065
# Stability over seeds (the verifier's request): the AUC resampling and the random no-touch windows of reading (a) depend on the
# generator. 20 further seeds per reading; for each, the group sizes, the two groups' control touch-evoked rates (auc-weak-strong)
# and the three tests of auc-groups. The outcomes above use the fixed seeds 29 and 31.
srows = []
for reading in ("a", "b"):
for seed in range(20):
t = auc_table(reading, np.random.default_rng(1000 + seed)).dropna(subset=["ctl_auc", "light_auc"])
up, dn = t[t.auc_change > 0], t[t.auc_change < 0]
srows.append({"reading": reading, "seed": 1000 + seed, "n_up": len(up), "n_down": len(dn), "up_evoked": up.evoked_ctl.mean(),
"down_evoked": dn.evoked_ctl.mean(), "weak_strong_holds": bool(up.evoked_ctl.mean() < dn.evoked_ctl.mean()),
"p_up_after": wilcox(up.ctl_after, up.light_after), "p_down_after": wilcox(dn.ctl_after, dn.light_after),
"p_down_before": wilcox(dn.ctl_before, dn.light_before)})
st_ = pd.DataFrame(srows)
st_["groups_tests_hold"] = (st_.p_up_after < 0.05) & (st_.p_down_after >= 0.05) & (st_.p_down_before < 0.05)
print("Over 20 seeds per reading:")
print(st_.groupby("reading").agg(n_up_min=("n_up", "min"), n_up_max=("n_up", "max"), n_down_min=("n_down", "min"), n_down_max=("n_down", "max"),
up_evoked_mean=("up_evoked", "mean"), down_evoked_mean=("down_evoked", "mean"),
weak_strong_holds=("weak_strong_holds", "sum"), groups_tests_hold=("groups_tests_hold", "sum"),
p_down_after_min=("p_down_after", "min"), p_down_after_max=("p_down_after", "max")).round(3).T.to_string())
print("Reading (b), the two groups' control touch-evoked rates per seed (up, down):",
st_[st_.reading == "b"][["up_evoked", "down_evoked"]].round(2).values.tolist())
Over 20 seeds per reading: reading a b n_up_min 15.000 15.000 n_up_max 22.000 17.000 n_down_min 11.000 16.000 n_down_max 19.000 18.000 up_evoked_mean 6.323 8.283 down_evoked_mean 9.119 8.048 weak_strong_holds 19.000 8.000 groups_tests_hold 9.000 0.000 p_down_after_min 0.005 0.015 p_down_after_max 0.241 0.045 Reading (b), the two groups' control touch-evoked rates per seed (up, down): [[8.14, 8.18], [8.4, 7.93], [8.4, 7.93], [8.14, 8.18], [8.14, 8.18], [8.33, 8.0], [8.53, 7.86], [8.14, 8.18], [8.4, 7.93], [8.4, 7.93], [8.14, 8.18], [8.4, 7.93], [8.31, 8.02], [8.14, 8.18], [8.14, 8.18], [8.4, 7.93], [8.31, 8.02], [8.31, 8.02], [8.31, 8.02], [8.14, 8.18]]
Seeds. The AUC resampling and the random no-touch windows of reading (a) depend on the generator. A reading is taken to match only when it matches under most of the 20 seeds, since an outcome that changes with the seed is not a reproduction; this rule is this attempt's, recorded as a deviation.
Result: not reproduced. With the fixed seeds the readings differ: under (a) the three tests hold (rise in the increased-AUC group, p = 4.9e-05; no change in the reduced-AUC group's touch response, p = 0.11; rise of its baseline, p = 0.0049) with group sizes (23 and 12) and rates outside tolerance, and under (b) the reduced-AUC group's touch response changes (p = 0.021). Over 20 further seeds, the three tests hold in 9 seeds under (a) and in none under (b), where the reduced-AUC group's touch response changes in every seed (p = 0.015 to 0.045). The partial result of (a) with seed 29 is the minority outcome, so neither reading matches the claim and it is not reproduced. Group sizes range from 15 to 22 (rising) and 11 to 19 (falling) over seeds.
behavior-false-alarms¶
With PV interneurons suppressed, most mice (6 of 8) are more likely to report touch with the pole out of reach (false alarms on no-lick trials).
rows = []
for r in l4:
if line_of[r["subject"]] != "PV-IRES-Cre":
continue
d = light_design(r, "vS1")
if d is None:
continue
tr = r["trials"]
lit = np.zeros(len(tr), bool)
lit[list(d["windows"])] = True
nogo = tr.type.str.lower().to_numpy() == "nogo"
cr = tr.response.to_numpy() == "CR"
for cond, m in (("light", lit), ("ctl", d["ctl"])):
rows.append({"subject": r["subject"], "path": r["path"], "cond": cond, "nogo": int((m & nogo).sum()), "correct": int((m & nogo & cr).sum())})
bt = pd.DataFrame(rows)
def chi(g):
t = g.groupby("cond")[["nogo", "correct"]].sum()
if not {"light", "ctl"} <= set(t.index):
return pd.Series({"ctl_correct": np.nan})
tab = np.array([[t.loc[c, "correct"], t.loc[c, "nogo"] - t.loc[c, "correct"]] for c in ("ctl", "light")])
ok = tab.sum(axis=1).min() > 0 and tab.sum(axis=0).min() > 0
p_y = stats.chi2_contingency(tab)[1] if ok else np.nan
p_n = stats.chi2_contingency(tab, correction=False)[1] if ok else np.nan
return pd.Series({"ctl_nogo": int(tab[0].sum()), "ctl_correct": tab[0, 0] / tab[0].sum(), "light_nogo": int(tab[1].sum()),
"light_correct": tab[1, 0] / tab[1].sum() if tab[1].sum() else np.nan, "p_yates": p_y, "p_no_correction": p_n})
pooled = bt.groupby("subject").apply(chi)
pooled["decrease_yates"] = (pooled.p_yates < 0.05) & (pooled.light_correct < pooled.ctl_correct)
pooled["decrease_plain"] = (pooled.p_no_correction < 0.05) & (pooled.light_correct < pooled.ctl_correct)
pooled["increase_yates"] = (pooled.p_yates < 0.05) & (pooled.light_correct > pooled.ctl_correct)
pooled["increase_plain"] = (pooled.p_no_correction < 0.05) & (pooled.light_correct > pooled.ctl_correct)
print("PV-IRES-Cre mice with the eNpHR light:", len(pooled), "(paper 8 tested). Reading (a), each mouse's trials pooled; correct = CR on no-lick trials:")
print(pooled.round(3).to_string())
print("Reading (a), mice with a significant decrease: (i) without the correction", int(pooled.decrease_plain.sum()), "; (ii) Yates",
int(pooled.decrease_yates.sum()), "; significant increase: (i)", int(pooled.increase_plain.sum()), ", (ii)", int(pooled.increase_yates.sum()))
per = bt.groupby(["subject", "path"]).apply(chi).reset_index()
per["decrease"] = (per.p_yates < 0.05) & (per.light_correct < per.ctl_correct)
per["decrease_plain"] = (per.p_no_correction < 0.05) & (per.light_correct < per.ctl_correct)
ps = per.groupby("subject").agg(sessions=("path", "size"), any_decrease=("decrease", "any"), any_decrease_plain=("decrease_plain", "any"))
print("Reading (b), per session (file), a mouse counting when any session shows a significant decrease: (i) without the correction",
int(ps.any_decrease_plain.sum()), "; (ii) Yates", int(ps.any_decrease.sum()), "of", len(ps))
print("Mice counted under (b)(i):", sorted(ps[ps.any_decrease_plain].index), "; sessions per mouse:", ps.sessions.to_dict())
print("Per session, without the correction:")
print(per[["subject", "ctl_nogo", "ctl_correct", "light_nogo", "light_correct", "p_no_correction", "p_yates"]].round(3).to_string(index=False))
print("Expected 6 of 8 mice (reproduced at 6 under a reading, partially at 5 to 7, not reproduced below 5)")
PV-IRES-Cre mice with the eNpHR light: 9 (paper 8 tested). Reading (a), each mouse's trials pooled; correct = CR on no-lick trials:
ctl_nogo ctl_correct light_nogo light_correct p_yates p_no_correction decrease_yates decrease_plain increase_yates increase_plain
subject
anm244024 200.0 0.800 181.0 0.691 0.019 0.014 True True False False
anm244025 116.0 0.638 85.0 0.671 0.741 0.631 False False False False
anm244028 154.0 0.896 67.0 0.627 0.000 0.000 True True False False
anm266945 340.0 0.568 188.0 0.521 0.350 0.305 False False False False
anm266951 5.0 0.800 2.0 0.500 1.000 0.427 False False False False
anm300169 59.0 0.915 18.0 0.722 0.083 0.033 False True False False
anm300170 86.0 0.895 32.0 0.375 0.000 0.000 True True False False
anm300171 77.0 0.792 66.0 0.879 0.247 0.167 False False False False
anm300172 137.0 0.891 96.0 0.750 0.008 0.005 True True False False
Reading (a), mice with a significant decrease: (i) without the correction 5 ; (ii) Yates 4 ; significant increase: (i) 0 , (ii) 0
Reading (b), per session (file), a mouse counting when any session shows a significant decrease: (i) without the correction 6 ; (ii) Yates 5 of 9
Mice counted under (b)(i): ['anm244024', 'anm244028', 'anm266945', 'anm300169', 'anm300170', 'anm300172'] ; sessions per mouse: {'anm244024': 9, 'anm244025': 5, 'anm244028': 5, 'anm266945': 11, 'anm266951': 1, 'anm300169': 3, 'anm300170': 3, 'anm300171': 4, 'anm300172': 6}
Per session, without the correction:
subject ctl_nogo ctl_correct light_nogo light_correct p_no_correction p_yates
anm244024 26.0 0.962 25.0 0.960 0.977 1.000
anm244024 66.0 0.939 42.0 0.786 0.017 0.037
anm244024 8.0 0.500 19.0 0.316 0.365 0.639
anm244024 13.0 0.308 16.0 0.688 0.042 0.097
anm244024 23.0 0.739 11.0 0.636 0.538 0.831
anm244024 10.0 1.000 19.0 1.000 NaN NaN
anm244024 16.0 0.562 12.0 0.250 0.098 0.205
anm244024 12.0 0.333 11.0 0.091 0.159 0.367
anm244024 26.0 0.962 26.0 0.808 0.083 0.193
anm244025 5.0 0.400 9.0 0.556 0.577 1.000
anm244025 33.0 0.364 14.0 0.429 0.675 0.928
anm244025 10.0 1.000 9.0 1.000 NaN NaN
anm244025 29.0 0.621 24.0 0.625 0.974 1.000
anm244025 39.0 0.821 29.0 0.759 0.532 0.748
anm244028 25.0 0.920 10.0 0.700 0.093 0.252
anm244028 28.0 0.893 12.0 0.917 0.818 1.000
anm244028 20.0 0.850 10.0 0.700 0.333 0.628
anm244028 31.0 0.968 11.0 0.636 0.004 0.018
anm244028 50.0 0.860 24.0 0.417 0.000 0.000
anm266945 42.0 0.762 15.0 0.667 0.472 0.706
anm266945 11.0 0.364 7.0 0.143 0.308 0.631
anm266945 37.0 0.919 23.0 1.000 0.161 0.428
anm266945 66.0 0.636 39.0 0.333 0.003 0.005
anm266945 21.0 0.238 8.0 0.375 0.461 0.785
anm266945 31.0 0.548 15.0 0.333 0.171 0.292
anm266945 30.0 0.367 24.0 0.500 0.325 0.479
anm266945 4.0 0.250 2.0 0.000 0.439 1.000
anm266945 25.0 0.800 20.0 0.800 1.000 1.000
anm266945 20.0 0.250 10.0 0.400 0.398 0.673
anm266945 53.0 0.415 25.0 0.440 0.835 1.000
anm266951 5.0 0.800 2.0 0.500 0.427 1.000
anm300169 14.0 0.714 5.0 0.800 0.709 1.000
anm300169 14.0 0.929 3.0 0.333 0.014 0.105
anm300169 31.0 1.000 10.0 0.800 0.011 0.087
anm300170 44.0 0.795 20.0 0.050 0.000 0.000
anm300170 33.0 1.000 8.0 1.000 NaN NaN
anm300170 9.0 1.000 4.0 0.750 0.118 0.665
anm300171 18.0 0.778 16.0 0.938 0.189 0.408
anm300171 15.0 0.800 15.0 0.933 0.283 0.591
anm300171 11.0 0.636 18.0 0.778 0.408 0.690
anm300171 33.0 0.848 17.0 0.882 0.744 1.000
anm300172 22.0 0.818 13.0 0.846 0.832 1.000
anm300172 18.0 1.000 3.0 1.000 NaN NaN
anm300172 43.0 0.930 29.0 0.828 0.174 0.329
anm300172 17.0 0.941 17.0 0.588 0.015 0.043
anm300172 22.0 0.818 20.0 0.750 0.591 0.872
anm300172 15.0 0.800 14.0 0.643 0.344 0.596
Expected 6 of 8 mice (reproduced at 6 under a reading, partially at 5 to 7, not reproduced below 5)
Result: reproduced. Of 9 PV-IRES-Cre mice with the light (the paper tested 8), 6 show a significant decrease of no-lick performance under reading (b)(i), per session without the continuity correction, as the criterion asks (6 of 8). The other readings give 5 ((a)(i), trials pooled, no correction), 4 ((a)(ii), pooled, Yates) and 5 ((b)(ii), per session, Yates); no mouse shows a significant increase when its trials are pooled. Reading (b) counts a mouse when any of its sessions is significant, and mice have 1 to 11 sessions, so it gives more chances to a mouse with many sessions; one session of anm244024 shows a significant rise (p = 0.042) while another shows a fall. Before the answer of 2026-10-20 the claim was blocked because the readings with and without the correction disagreed; the criterion now names both, and the reading without it matches.
Blocked claims¶
exc-inhibition-timing: With L4 excitatory neurons depolarized near 0 mV to reveal inhibition, touch evokes strong, transient inhibition that follows excitation by 0.4 +- 0.8 ms at half maximum.
exc-inhibition-whisking: In L4 excitatory neurons held near 0 mV, inhibitory inputs occur spontaneously but are more prevalent at the onset of whisker movement; after whisking onset, excitation and inhibition increase gradually, with indistinguishable time courses, over about 100 ms.
vpm-photoinhibition: Photoactivating TRN axons in VPM of VGAT-ChR2-EYFP mice reduces thalamic activity to 17% of control.
rabies-vpm-input: Monosynaptic rabies tracing from L4 PV interneurons labels thalamic neurons mostly in VPM (>90%) and few in POM (<10%).
ion-cut-exc-no-touch,ion-cut-exc-whisking,ion-cut-exc-phase,ion-cut-fs-no-touch,sensory-input-drives-fs,ion-cut-fs-phase,ion-cut-behavior: after infraorbital nerve transection, L4 excitatory neurons do not depolarize after touch, their whisking depolarization and phase locking are reduced, touch does not evoke spikes in FS cells, their whisking activation is absent in most cells and their phase locking reduced, and mice perform at chance.
# What the files hold for the blocked claims (see the markdown above).
print("Files with a recording outside barrel cortex L4 (thalamic pipette multiunits of Fig. 6b,c would be):",
int((meta.region != "barrel cortex").sum() + (meta.layer != "4").sum()), "of", len(meta), "L4 files; VPM files with light:",
sum(r["has_light"] for r in vpm))
img = [p for r in recs for p in ([] if r["kind"] == "VPM" else r["general_fields"]) if "image" in p.lower()]
print("Files with an image or histology group under general/ (rabies tracing, Supp. Fig. 10):", len(img))
held = mode[(mode.from_trace == "whole-cell") & (mode.trials_near_0mV > 0)]
print("Whole-cell cells with trials held near 0 mV (the 6 E cells of Fig. 3 would be):", len(held), held[["path", "line", "trials_near_0mV"]]
.assign(path=lambda d: d.path.str.split("/").str[1]).to_dict("records"), "; its class here:",
cells.set_index("path").cls.get(held.path.iloc[0]) if len(held) else None, "; Supplementary Table 2 gives the one PV-ChR2 cell as FS")
# Descriptive only, not a test: the cells of the flagged mice, which may be among the ION-cut mice
fl = meas[meas.subject.isin(flagged) & (meas.cls != "VPM")]
fv = vm0[vm0.index.isin(fl.path)]
print(f"\nNot a test. Flagged mice {flagged}: {int((fl.cls == 'E').sum())} E cells ({int(((fl.cls == 'E') & (fl['mode'] == 'whole-cell')).sum())} whole-cell), "
f"{int((fl.cls == 'FS').sum())} FS cells with a principal whisker. Whole-cell touch PSP amplitude mean {fv.amp_mv.mean():.2f} mV "
f"(other E cells {vm0[~vm0.index.isin(fl.path) & vm0.index.isin(group('E').path)].amp_mv.mean():.2f}); whisking change (b) mean "
f"{fv.whisk_epoch_change.mean():.2f} mV; Vm phase coupling mean {fv.vm_phase_strength.mean():.2f} mV")
ff = fl[fl.cls == "FS"]
if len(ff):
print(f" FS cells: spikes per touch mean {ff.spt.mean():.2f}; nonwhisking {ff['filtfilt:rate_nonwhisk'].mean():.2f}, whisking "
f"{ff['filtfilt:rate_whisk'].mean():.2f} spikes/s; r2 {ff['filtfilt:r2'].round(2).tolist()}")
Files with a recording outside barrel cortex L4 (thalamic pipette multiunits of Fig. 6b,c would be): 0 of 130 L4 files; VPM files with light: 0
Files with an image or histology group under general/ (rabies tracing, Supp. Fig. 10): 0
Whole-cell cells with trials held near 0 mV (the 6 E cells of Fig. 3 would be): 1 [{'path': 'sub-anm250509_ses-20140817_behavior+icephys+ogen.nwb', 'line': 'PV-ChR2', 'trials_near_0mV': 13}] ; its class here: E ; Supplementary Table 2 gives the one PV-ChR2 cell as FS
Not a test. Flagged mice ['anm273341', 'anm275231', 'anm318530', 'anm318532']: 8 E cells (8 whole-cell), 2 FS cells with a principal whisker. Whole-cell touch PSP amplitude mean 0.35 mV (other E cells 5.54); whisking change (b) mean 0.39 mV; Vm phase coupling mean 0.11 mV
FS cells: spikes per touch mean 0.09; nonwhisking 8.37, whisking 9.98 spikes/s; r2 [0.06, 0.43]
Result: blocked (data missing): exc-inhibition-timing and exc-inhibition-whisking. Only one
whole-cell file has trials held near 0 mV, 13 trials of the PV-ChR2 cell, which Supplementary Table 2
counts as FS, and no other file pairs a whole-cell trace with a depolarizing current (data check); the 6
excitatory neurons recorded at rest and with depolarizing current are not in the dandiset.
Result: blocked (data missing): vpm-photoinhibition. No file holds a thalamic recording made
with a pipette during light: the 130 single-cell files are all in barrel cortex layer 4, and the 18
VPM silicon probe files have no photostimulation series.
Result: blocked (data missing): rabies-vpm-input. The dandiset holds no histology or image data.
Result: blocked (metadata insufficient): the seven ION-cut claims. No file marks the ION-cut mice. The 4 flagged mice are consistent with them (their whole-cell excitatory cells have touch PSPs of 0.35 mV on average against 5.54 mV in the others, and their 2 FS cells 0.09 spikes per touch), but they were found by these very properties, so they cannot test the claims.
Summary¶
| Claim | Outcome | Earlier outcome (000005-yu-2016-01) |
|---|---|---|
behavior-touches |
partially reproduced | |
vpm-touch |
not reproduced | reproduced |
vpm-whisking |
partially reproduced | reproduced |
vpm-whisking-phase |
reproduced | |
exc-vm-touch-whisking |
partially reproduced | |
exc-threshold-touch-vs-whisking |
partially reproduced | |
exc-effective-reversal |
reproduced | |
exc-inhibition-timing |
blocked (data missing) | |
exc-inhibition-whisking |
blocked (data missing) | |
fs-touch-response |
blocked (methods underspecified) | |
fs-vm-touch-whisking |
not reproduced | |
fs-whisking-rate |
partially reproduced | |
fs-whisking-phase |
reproduced | |
fs-touch-and-whisking |
reproduced | reproduced |
exc-mainly-touch |
not reproduced | not reproduced |
vpm-photoinhibition |
blocked (data missing) | |
exc-thalamus-photoinhibition |
partially reproduced | |
thalamus-drives-fs |
blocked (other: too few cells) | reproduced |
rabies-vpm-input |
blocked (data missing) | |
ion-cut-exc-no-touch |
blocked (metadata insufficient) | |
ion-cut-exc-whisking |
blocked (metadata insufficient) | |
ion-cut-exc-phase |
blocked (metadata insufficient) | |
ion-cut-fs-no-touch |
blocked (metadata insufficient) | |
sensory-input-drives-fs |
blocked (metadata insufficient) | blocked (metadata insufficient) |
ion-cut-fs-phase |
blocked (metadata insufficient) | |
ion-cut-behavior |
blocked (metadata insufficient) | |
pv-enphr-suppression |
partially reproduced | |
fs-suppress-movement-spiking |
partially reproduced | reproduced |
fs-enhance-touch-selectivity |
partially reproduced | not reproduced |
exc-touch-spikes-enphr |
not reproduced | |
auc-weak-strong |
not reproduced | |
auc-groups |
not reproduced | |
behavior-false-alarms |
reproduced |
The eight claims that keep their ids have new criteria, written from the full text, so their outcomes are not the same test as before; the attempt record says for each why the outcome differs.
After the first verification round three outcomes changed. fs-touch-response is blocked as
methods underspecified, since its FS readings give different outcomes (two of three values within
tolerance with the waveform boundary, one without it). auc-weak-strong and auc-groups are not
reproduced: over 20 further seeds, reading (b) of auc-weak-strong holds in 8, and the tests of
auc-groups hold in 9 seeds under reading (a) and in none under (b). The other outcomes hold under
the added readings (O11 for every whisking-epoch claim, O10 and O11 for the whisking-onset claims)
and the revised tag test.
A failure to reproduce here may be a failure of this attempt, not of the paper: see the reasoning and the differences from the paper's method.
How the analysis differs from the paper's. The files name no cell type and do not mark the ION-cut mice, so the cell classes are built by the claims file's rules (light tag, the waveform boundary read approximately from Supplementary Fig. 6, PV inactivation, spike width) and every group includes the cells of the ION-cut mice, which the paper left out of its control groups. The rules give 20 FS cells (17 with a principal whisker) against the paper's 26 control FS cells, and 102 excitatory cells (97 measured) against 85; 8 light-driven cells fall outside the boundary as measured here, and 26 VGAT-ChR2 cells have too few cortical light pulses to be tested and count as excitatory; among them are a whole-cell cell with 11,087 spikes 0.37 ms wide and a cell-attached cell with 16,350 spikes, which are likely FS, so the excitatory groups probably hold some FS cells. Only 3 of the paper's 4 PV cells can be found, and they are found by the inactivation the claim tests. The VPM files name one principal whisker per file. Spike detection and the spike-measure windows that the claims file leaves open are standard choices stated in the helper module; the cell-attached detection has 5% of intervals under 1.5 ms in the median E cell, and cell-attached E cells fire at several times the paper's rates; whole-cell E cells, whose detection is clean, also fire 0.70 spikes/s without whisking against the paper's 0.4. The light-driven test (a one-sided binomial test of spikes 0 to 5 ms after each pulse of the 5 and 10 Hz trains against the same 5 ms 50 ms earlier) and the waveform measures are this attempt's; the paper gives neither, and the claims file runs two thresholds and the boundary as readings. Trials of a whole-cell cell held near 0 mV are left out at rest. The derivative-based Vm latencies follow the criterion's rule, which single-sample excursions of dVm/dt and a one-sample transient at light onset cross. Where an outcome depends on a random seed (the AUC claims), a reading counts as matching only when it matches under most of 20 seeds, a rule of this attempt.
What was found about the dandiset. Every finding of the earlier attempt is still present on the same version: no cell types, no ION-cut marks, missing or partial genotypes, layer 4 for every cell, undescribed optogenetic conditions, the wrong recording mode in 24 session descriptions, a first-touch time divided by 1000, a sampling rate stated only by the timestamps, files without a principal whisker and one principal whisker per VPM file, and doubtful light wavelengths. New here: the injected current is stored in nA under a conversion to amperes that assumes uA, and 17 cell-attached files carry a constant stored current of about 1.9; the depolarized recordings of Fig. 3, the thalamic multiunits of Fig. 6b,c and the histology of Supplementary Fig. 10 are not in the dandiset; trains at the VPM site run at 50 Hz in 12 files where the Methods give 100 Hz; and the no-lick trial type is spelled two ways.