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. The full text is not open access, and no open copy was found (PubMed Central, Europe PMC, Unpaywall, OpenAlex, Semantic Scholar). This attempt used the abstract, the eight figure titles, and the supplementary information, which is public on the publisher's site: the legends of Supplementary Figures 1 to 14 and Supplementary Tables 1 and 2 (the mouse line of each mouse, and the cells per line, recording mode and manipulation). The claims were written from the abstract and figure titles before the supplement was read (in the first verification round). The supplement settles some things: FS cells are defined by spike waveform among light-driven cells in VGAT-ChR2 mice; spike-phase modulation is judged by the r² of a sinusoid fit (cut-off 0.25); the PV-Cre mice express eNpHR in PV cells; the VPM light is a 100 Hz train of 5 ms pulses; and Figure 7 tests the sensory origin of FS whisking responses by transecting the infraorbital nerve (ION). It gives no windows, thresholds or waveform boundaries, so the measures here remain choices made in this attempt. No number from the paper's main text is tested; the supplement's cell counts are compared with the files in the data check.
Dandiset. DANDI:000005, version 0.220126.1853. The paper predates the archive and cites no version. This is the only published version; the draft lists the same 148 assets and sizes, so the most recent published version was pinned.
What the dandiset holds. 18 sessions of silicon probe recordings in VPM (6 mice, sorted units), and 130 sessions with one L4 neuron each, recorded whole-cell or juxtacellularly (49 mice), all during a pole localization task with whisker tracking, and most L4 sessions with optogenetic stimulation. No file states the cell type of its L4 neuron, or whether the mouse had its ION transected, so FS interneurons and excitatory neurons are identified here from the data (see the data check).
Code. Neither the paper nor the dandiset links analysis code; none was found.
Outside input. Supplementary Table 1, read at run time from the supplement PDF (part of doi:10.1038/nn.4412), gives the mouse line of each mouse. It is used only in the data check.
| Claim | Source | Tested |
|---|---|---|
vpm-touch: VPM neurons fire in response to touch |
Abstract | yes |
vpm-whisking: VPM neurons fire in response to whisker movement |
Abstract | yes |
fs-touch-and-whisking: L4 FS interneurons respond to touch and whisker movement |
Abstract | yes |
exc-mainly-touch: L4 excitatory neurons respond mainly to touch |
Abstract | yes |
fs-suppress-movement-spiking: silencing FS cells increases movement-related spiking of excitatory cells |
Abstract, Fig. 8 title | yes |
fs-enhance-touch-selectivity: FS cells enhance the touch selectivity of excitatory cells |
Abstract, Fig. 8 title | yes |
thalamus-drives-fs: thalamic activity drives L4 FS interneurons |
Fig. 6 title | yes |
sensory-input-drives-fs: FS movement responses come from sensory input, not corollary discharge |
Abstract, Fig. 7 title, Supp. Figs. 6, 11, 12 | blocked (metadata insufficient) |
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"]
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 156 s
Data check¶
No random numbers are used anywhere in this notebook. Each check of the metadata against the values is stated under its output.
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])
vc = yu2016.versions_check()
print("Published versions:", vc["published"], "; assets in the pinned version:", vc["pinned_assets"],
"; in the draft:", vc["draft_assets"], "; same paths and sizes:", vc["draft_same_paths_and_sizes"])
print("Files by kind:", files.kind.value_counts().to_dict())
print("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("Sessions (files) per subject: median", files.groupby("subject").size().median(),
", max", files.groupby("subject").size().max())
print("\nL4 files by genotype:")
print(files[files.kind == "L4"].genotype.value_counts().to_string())
units = pd.DataFrame([{"path": r["path"], "subject": r["subject"], "unit": u["unit"], "desc": u["desc"],
"spikes": len(u["spikes"]), "location": r["location"][0] if r["location"] else ""}
for r in vpm for u in r["units"]])
print(f"\nVPM: {len(units)} units in {len(vpm)} files; units per file:",
units.groupby("path").size().value_counts().sort_index().to_dict())
print("Unit descriptions start with:", units.desc.str.split(".").str[0].value_counts().to_dict())
print("Electrode group locations:", units.location.str.extract(r"brain_subregion: ([^;]*)")[0].value_counts().to_dict())
print("Unit spike time resolution (s):",
sorted({float(np.round(np.min(np.diff(np.unique(u["spikes"]))), 4)) for r in vpm for u in r["units"]}))
print("Trials: VPM", int(files[files.kind == "VPM"].trials.sum()), ", L4", int(files[files.kind == "L4"].trials.sum()))
Published versions: ['0.220126.1853'] ; 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
Sessions (files) per subject: median 2.0 , max 11
L4 files by genotype:
genotype
VGAT-ChR2-EYFP 66
PV-Cre 48
C57BL6 8
Ai32 5
(none) 3
VPM: 29 units in 18 files; units per file: {1: 12, 2: 3, 3: 2, 5: 1}
Unit descriptions start with: {'thalamic': 29}
Electrode group locations: {'VPM': 29}
Unit spike time resolution (s): [0.001]
Trials: VPM 7656 , L4 11211
0.220126.1853 is the only published version, and the draft holds the same 148 asset paths and sizes. The archive's summary gives 55 subjects, which matches the files. The VPM recordings come from 6 mice and the L4 recordings from 49 others. The units are all described as "thalamic" and their electrode groups as VPM. Spike times of VPM units are stored at 1 ms resolution.
# What the files say about each L4 recording, and the cells per mouse line against Supplementary
# Tables 1 and 2 of the paper (the mouse line of each mouse; the cells per line and recording mode).
meta = pd.DataFrame([{"path": r["path"], "subject": r["subject"], "genotype": r["genotype"] or "(none)", "mode": r["mode"], "label": r["label_mode"],
"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("Fields 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: 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("Last part of the session descriptions:", pd.Series([r["session_description"].split(";")[-1] for r in l4]).value_counts().to_dict())
# Every text value of each file (session and experiment description, keywords, the optogenetics sites, the
# subject and every other string under general/), searched as whole words
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"}
print("Characters of metadata text per L4 file: median", int(np.median([len(r["metadata_text"]) for r in l4])),
"; files whose metadata text contains each term:",
{k: sum(bool(re.search(v, r["metadata_text"])) for r in l4) for k, v in terms.items()})
print("Keyword values:", pd.Series([re.search(r"barrel cortex thalamus.*?perturbations", r["metadata_text"]).group(0)
if re.search(r"barrel cortex thalamus.*?perturbations", r["metadata_text"]) else "(other)"
for r in l4]).value_counts().to_dict())
lines = yu2016.supplement_mouse_lines()
print(f"\nSupplementary Table 1 lists {len(lines)} mice; of the {files.subject.nunique()} mice of the dandiset, "
f"{files.subject.drop_duplicates().isin(lines.subject).sum()} are in it.")
fl = files.merge(lines, on="subject", how="left")
print("Files by genotype in the files against the mouse line in Supplementary Table 1:")
print(pd.crosstab([fl.kind, fl.genotype], fl.line.fillna("(not listed)")).to_string())
print("Mice without a genotype in their files, or given as Ai32:",
fl[fl.genotype.isin(["(none)", "Ai32"])].drop_duplicates("subject").groupby(["genotype", "line"]).subject.apply(list).to_dict())
meta = meta.merge(lines, on="subject", how="left")
# Cells per mouse line and recording mode in Supplementary Table 2 (whole-cell; cell-attached), all cell types together
paper_counts = pd.DataFrame({"line": ["VGAT-ChR2-EYFP", "PV-IRES-Cre", "PV-IRES-Cre x Ai32", "C57BL/6", "PV-ChR2", "SOM-IRES-Cre x Ai32"],
"paper_whole_cell": [23 + 5 + 9, 5, 1, 7 + 1, 1, 1], "paper_cell_attached": [6 + 14 + 10, 38 + 4, 0, 0, 0, 4]})
ours = pd.crosstab(meta.line, meta["mode"]).reindex(paper_counts.line).fillna(0).astype(int)
paper_counts["files_whole_cell"] = ours["whole-cell"].to_numpy()
paper_counts["files_juxtacellular"] = ours["juxtacellular"].to_numpy()
print("\nL4 cells per mouse line: Supplementary Table 2 against the files (mode read from the trace):")
print(paper_counts.to_string(index=False))
print("Totals: paper", int(paper_counts.paper_whole_cell.sum() + paper_counts.paper_cell_attached.sum()), "; files", len(l4))
print("L4 cells by mouse line and session date (year):")
print(pd.crosstab(meta.line, meta.path.str.split("_ses-").str[1].str[:4]).to_string())
print("\nL4 cells by mouse line, mode in the session description (rows) and mode read from the trace (columns):")
print(pd.crosstab([meta.line, meta.label], meta["mode"]).to_string())
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: region {'barrel cortex': 130} ; cortical layer {'4': 130} ; depth (coordinate_dv) 0.42 to 0.62 mm
Last part of the session descriptions: {'whole-cell, acute': 79, 'juxtacellular, acute': 51}
Characters of metadata text per L4 file: median 1186 ; files whose metadata text contains each term: {'ION': 0, 'infraorbital': 0, 'nerve': 0, 'transect': 0, 'lesion': 0, 'cut': 0, 'surgery': 0, 'sham': 0, 'control': 0}
Keyword values: {'barrel cortex thalamus whiskers extracellular electrophysiology intracellular electrophysiology optogenetic perturbations': 130}
Supplementary Table 1 lists 55 mice; of the 55 mice of the dandiset, 55 are in it.
Files by genotype in the files against the mouse line in Supplementary Table 1:
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']}
L4 cells per mouse line: Supplementary Table 2 against the files (mode read from the trace):
line paper_whole_cell paper_cell_attached files_whole_cell files_juxtacellular
VGAT-ChR2-EYFP 37 30 37 30
PV-IRES-Cre 5 42 9 39
PV-IRES-Cre x Ai32 1 0 1 0
C57BL/6 8 0 8 0
PV-ChR2 1 0 1 0
SOM-IRES-Cre x Ai32 1 4 1 4
Totals: paper 129 ; files 130
L4 cells by mouse line and session date (year):
path 2013 2014 2015 2016
line
C57BL/6 3 5 0 0
PV-ChR2 0 1 0 0
PV-IRES-Cre 1 31 16 0
PV-IRES-Cre x Ai32 0 1 0 0
SOM-IRES-Cre x Ai32 0 0 1 4
VGAT-ChR2-EYFP 4 18 45 0
L4 cells by mouse line, mode in the session description (rows) and mode read from the trace (columns):
mode juxtacellular 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
No L4 file records a surgery, lesion, virus, opsin or notes. A search of every text value of each
file (session and experiment description, keywords, optogenetics sites, subject and the rest of
general/) finds none of ION, infraorbital, nerve, transection, lesion, cut, surgery, sham or
control, and every file has the same keywords. The fields under general/ are the
same in every file (9 files have no optogenetics group), the subject has only
genotype, sex, species and ID, and every electrode location reads barrel cortex, layer 4, at 0.42 to
0.62 mm depth. The files therefore cannot say which cells come from ION-transected mice, and they put
every cell in L4 (Supp. Fig. 6 of the paper includes some L5 cells, which cannot be found here).
All 55 mice are in Supplementary Table 1. Where the files give a genotype it agrees with the mouse
line, but it is missing for the 6 VPM mice (VGAT-ChR2-EYFP in the table) and for 3 L4 mice (one each
of VGAT-ChR2-EYFP, PV-IRES-Cre x Ai32 and PV-ChR2), and the SOM-IRES-Cre x Ai32 mice are given as
Ai32. The cells per line and recording mode match Supplementary Table 2 for every line except
PV-IRES-Cre: 48 files against 47 cells, and 9 whole-cell by the trace against 5 in the table. All 24
files whose session description disagrees with the trace are PV-IRES-Cre files. For VGAT-ChR2-EYFP
the table's 37 whole-cell and 30 cell-attached cells include 9 whole-cell excitatory cells and 10
cell-attached FS cells from ION-transected mice; they are in the dandiset, but not marked.
mode = pd.DataFrame([{"path": r["path"], "genotype": r["genotype"], "label": r["label_mode"], "from_data": r["mode"],
"vm_median": r["vm_median"], "vm_hz": r["vm_hz"], "trial_s": r["vm_trial_s"],
"spikes": len(r["spikes"]), "fwhm_ms": r["fwhm_ms"]} for r in l4])
print("Recording mode in session_description against the mode read from the trace "
"(whole-cell when the median trace is below -20 mV):")
print(pd.crosstab(mode.label, mode.from_data).to_string())
print("\nMedian of the trace (stored values, unit 'volts' with conversion 0.001, so mV):")
print(mode.groupby("from_data").vm_median.describe().round(1).to_string())
print("\nTrace sampling rate (Hz) from the timestamps, and trial length of the trace (s):")
print(mode.groupby(["vm_hz", "trial_s"]).size().to_string())
print("First file at 20 kHz (session date):", min(p.split("_ses-")[1][:8] for p in mode[mode.vm_hz == 20000].path))
print("Last file at 10 kHz (session date):", max(p.split("_ses-")[1][:8] for p in mode[mode.vm_hz == 10000].path))
ts = pd.DataFrame([{"outside_trials": r["vm_outside_trials"], "block_start_offset_ms": r["vm_block_first_rel"] * 1000,
"irregular_steps": r["vm_dt_irregular"]} for r in l4])
print("\nTrace samples outside every trial:", int(ts.outside_trials.sum()),
"; largest offset of a trial's first sample from the trial start (ms):", round(ts.block_start_offset_ms.max(), 2),
"; steps within a trial that differ from 1/rate:", int(ts.irregular_steps.sum()))
print("Files with every trace distinct (hash of the whole trace):", mode.shape[0] == len({r['vm_hash'] for r in l4}))
Recording mode in session_description against the mode read from the trace (whole-cell when the median trace is below -20 mV):
from_data juxtacellular whole-cell
label
juxtacellular 50 1
whole-cell 23 56
Median of the trace (stored values, unit 'volts' with conversion 0.001, so mV):
count mean std min 25% 50% 75% max
from_data
juxtacellular 73.0 1.8 5.2 -16.9 0.1 2.3 4.4 13.0
whole-cell 57.0 -60.8 9.1 -76.8 -67.4 -62.5 -56.0 -39.2
Trace sampling rate (Hz) from the timestamps, and trial length of the trace (s):
vm_hz trial_s
10000.0 5.2000 29
5.4999 1
5.5000 62
20000.0 5.9000 38
First file at 20 kHz (session date): 20150827
Last file at 10 kHz (session date): 20150320
Trace samples outside every trial: 0 ; largest offset of a trial's first sample from the trial start (ms): 4.76 ; steps within a trial that differ from 1/rate: 0
Files with every trace distinct (hash of the whole trace): True
The session description says "whole-cell" for 79 files and "juxtacellular" for 51, but the traces disagree for 24 of them: 23 files described as whole-cell have a trace that sits near 0 mV with small extracellular spikes, as juxtacellular (cell-attached) recordings do, and one described as juxtacellular sits near -65 mV. The mode read from the trace is used below. The stored trace values are in mV (conversion 0.001 to volts), which the whole-cell resting potentials confirm. The trace is sampled at 10 kHz (5.2 or 5.5 s per trial) in files up to March 2015 and at 20 kHz (5.9 s per trial) from late August 2015 on; the files do not state the rate except through their timestamps, which are regular within trials and start at the trial start.
wh = []
for r in recs:
for name, w in r["whiskers"].items():
row = {"kind": r["kind"], "path": r["path"], "whisker": name, "principal": w["principal"],
"touches": len(w["touch_on"])}
if w["principal"]:
row.update(dt_ms=w["dt_median"] * 1000, angle_min=w["angle_range"][0], angle_max=w["angle_range"][1],
angle_nan=w["angle_nan"])
wh.append(row)
wh = pd.DataFrame(wh)
p = wh[wh.principal]
print("Files with a whisker named principal:", p.path.nunique(), "of", len(recs),
"; files without one:", sorted(set(wh.path) - set(p.path)))
print("Whisker sampling interval (ms):", p.dt_ms.round(3).unique().tolist())
span = pd.Series([round(float(np.median(np.bincount(w["trial"][w["trial"] >= 0])[np.bincount(w["trial"][w["trial"] >= 0]) > 0]))
* w["dt_median"], 2)
for r in recs for w in r["whiskers"].values() if w["principal"]])
print("Whisker samples per trial times the interval (s), files by value:", span.value_counts().sort_index().to_dict())
print("Principal whisker angle (degrees): 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 on the principal whisker per file: median", int(p.touches.median()), ", range",
int(p.touches.min()), "to", int(p.touches.max()))
# The trial table's first_touch_time against the first touch onset of the principal whisker
rows = []
for r in recs:
if r["principal"] is None:
continue
w = r["whiskers"][r["principal"]]
on_t = w["t"][w["touch_on"]]
tr = r["trials"]
for i, (s, e, ft) in enumerate(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. first_touch_time minus trial start: median {g.table_rel.median():.4f} s; "
f"first touch onset in the series: median {g.series_rel.median():.3f} s; "
f"fraction where the table value equals the series value: {np.mean(np.abs(g.table_rel - g.series_rel) < 0.002):.3f}; "
f"fraction where the table value x 1000 is within 2 ms of it: {np.mean(np.abs(g.table_rel * 1000 - g.series_rel) < 0.002):.3f}")
Files with a whisker named principal: 140 of 148 ; files without one: ['sub-anm230103/sub-anm230103_ses-20140131_behavior+icephys+ogen.nwb', 'sub-anm266945/sub-anm266945_ses-20141222_obj-ianjb9_behavior+icephys+ogen.nwb', 'sub-anm281589/sub-anm281589_ses-20150320_behavior+icephys+ogen.nwb', 'sub-anm300172/sub-anm300172_ses-20150908_obj-n5zk5z_behavior+icephys+ogen.nwb', 'sub-anm318530/sub-anm318530_ses-20151128_behavior+icephys+ogen.nwb', 'sub-anm318532/sub-anm318532_ses-20151125_obj-1kqnryo_behavior+icephys+ogen.nwb', 'sub-anm324650/sub-anm324650_ses-20160422_behavior+icephys+ogen.nwb', 'sub-anm337496/sub-anm337496_ses-20160524_behavior+icephys+ogen.nwb'] Whisker sampling interval (ms): [1.0]
Whisker samples per trial times the interval (s), files by value: {5.0: 140}
Principal whisker angle (degrees): min -98.4 , max 94.4 ; median fraction of NaN samples 0.0
Touch onsets on the principal whisker per file: median 354 , range 0 to 4265
L4: 5627 trials. first_touch_time minus trial start: median 0.0014 s; first touch onset in the series: median 1.373 s; fraction where the table value equals the series value: 0.000; fraction where the table value x 1000 is within 2 ms of it: 0.956
VPM: 3154 trials. first_touch_time minus trial start: median 1.3240 s; first touch onset in the series: median 1.324 s; fraction where the table value equals the series value: 1.000; fraction where the table value x 1000 is within 2 ms of it: 0.000
The whisker variables are sampled at 1 kHz (5 s per trial), in degrees with a plausible range.
In the VPM files, first_touch_time is the first touch onset of the series. In the L4 files it is
not: it lies a few ms after the trial start, and multiplied by 1000 its offset from the trial start
matches the first touch onset in most trials, so the latency was divided by 1000 once too often
when the files were made. The analysis uses the touch onset series only. Files without a named
principal whisker are left out of every measure that needs whisking or touch.
light = []
for r in l4:
tr = r["trials"]
for site, info in r["sites"].items():
w = yu2016.light_windows(r, site)
lt = r["light"][site]
modes = tr.photo_stim_mode.to_numpy()[w.trial.to_numpy(int)] if len(w) else []
light.append({"genotype": r["genotype"], "site_region": info["region"], "lambda_nm": info["lambda_nm"],
"power_max": lt["power_max"], "lit_trials": len(w),
"isolated_pulses": len(yu2016.isolated_pulses(r, site)),
"median_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 genotype and labelled region and wavelength:")
print(light.groupby(["genotype", "site_region", "lambda_nm"]).agg(
files=("lit_trials", "size"), files_with_light=("lit_trials", lambda x: int((x > 0).sum())),
lit_trials=("lit_trials", "sum"), isolated_pulses=("isolated_pulses", "sum"),
pulses_per_trial=("median_pulses_per_lit_trial", "median"), power_max=("power_max", "max")).to_string())
print("\nphoto_stim_mode values of the lit trials of each site region:")
print(light[light.lit_trials > 0].groupby("site_region").modes.value_counts().to_string())
print("Trial table: photo_stim_mode has an empty description; photo_stim_power is NaN in",
sum(int(r["trials"].photo_stim_power.isna().all()) for r in l4), "of", len(l4), "L4 files")
Photostimulation sites, by genotype and labelled region and wavelength:
files files_with_light lit_trials isolated_pulses pulses_per_trial power_max
genotype site_region lambda_nm
VPM 594.0 2 2 76 0 250.0 15.0
VPM and vS1 473.0 1 1 6 0 262.5 15.0
vS1 594.0 1 0 0 0 0.0 0.0
Ai32 vM1 473.0 3 3 54 0 100.0 15.0
vS1 594.0 5 3 16 100 5.0 15.0
PV-Cre vS1 594.0 47 47 1490 0 1.0 3.0
VGAT-ChR2-EYFP VPM 594.0 33 31 742 32 250.0 15.0
VPM and vS1 473.0 23 20 113 689 12.0 15.0
vM1 473.0 25 23 360 25 100.0 15.0
vS1 594.0 33 29 431 1449 10.0 15.0
photo_stim_mode values of the lit trials of each 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
Trial table: photo_stim_mode has an empty description; photo_stim_power is NaN in 130 of 130 L4 files
The trial table's photo_stim_mode has no description, and its values do not map to one kind of
light: in early VGAT-ChR2 files the VPM site gives pulse trains under the label 5ms_pulse, in
later ones under shift. The light epochs are therefore read from the photostimulation series
themselves. Three kinds of light are used below: isolated 5 ms pulses (for tagging), long epochs at
the vS1 site in PV-Cre mice, and pulse trains at the VPM site in VGAT-ChR2 mice. What each light
does is not stated in the files; the claims say what is assumed.
# Alignment of spikes and behaviour. Spikes are compared with the touch onsets and whisking phase
# of their own trial, and with those of the next trial (spikes moved to the next trial at the same
# time after its start). With the true alignment touch responses and phase locking should be
# larger.
rows = []
for r in recs:
if r["principal"] is None:
continue
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) < yu2016.MIN_SPIKES:
continue
a = yu2016.response_measures(sp, r)
b = yu2016.response_measures(yu2016.shift_spikes(sp, r["trials"]), r)
rows.append({"kind": r["kind"], "path": r["path"], "unit": unit,
"touch_true": a["touch_resp_hz"], "touch_shift": b["touch_resp_hz"],
"amp_true": a["whisk_amp_raw_hz"], "amp_shift": b["whisk_amp_raw_hz"],
"vs_true": a["vector_strength"], "vs_shift": b["vector_strength"],
"n_whisk_true": a["n_whisk_spikes"], "n_whisk_shift": b["n_whisk_spikes"]})
align = pd.DataFrame(rows)
for kind, g in align.groupby("kind"):
t = g.dropna(subset=["touch_true", "touch_shift"])
v = g[(g.n_whisk_true >= 20) & (g.n_whisk_shift >= 20)]
print(f"{kind}: {len(t)} cells or units. Touch response (Hz), median true {t.touch_true.median():.2f}, "
f"shifted {t.touch_shift.median():.2f}; true larger in {(t.touch_true > t.touch_shift).sum()} "
f"(Wilcoxon p = {stats.wilcoxon(t.touch_true, t.touch_shift).pvalue:.2g}). "
f"Whisking phase modulation (Hz, occupancy corrected, {len(v)} with 20 whisking spikes), median true "
f"{v.amp_true.median():.3f}, shifted {v.amp_shift.median():.3f} (p = {stats.wilcoxon(v.amp_true, v.amp_shift).pvalue:.2g}); "
f"vector strength of spike phases, true {v.vs_true.median():.3f}, shifted {v.vs_shift.median():.3f}.")
bad = align[(align.touch_true <= align.touch_shift) & (align.amp_true <= align.amp_shift)]
print("Cells or units where neither the touch response nor the phase modulation is larger with the true alignment:",
len(bad), "of", len(align))
print(bad[["kind", "path", "unit"]].to_string(index=False))
L4: 95 cells or units. Touch response (Hz), median true 8.40, shifted 0.34; true larger in 78 (Wilcoxon p = 9e-13). Whisking phase modulation (Hz, occupancy corrected, 84 with 20 whisking spikes), median true 2.160, shifted 0.415 (p = 1.3e-12); vector strength of spike phases, true 0.167, shifted 0.084. VPM: 29 cells or units. Touch response (Hz), median true 6.91, shifted 0.00; true larger in 23 (Wilcoxon p = 1.8e-05). Whisking phase modulation (Hz, occupancy corrected, 29 with 20 whisking spikes), median true 1.236, shifted 0.283 (p = 9.3e-08); vector strength of spike phases, true 0.126, shifted 0.099. Cells or units where neither the touch response nor the phase modulation is larger with the true alignment: 3 of 124 kind path unit L4 sub-anm266945/sub-anm266945_ses-20141220_obj-1g7fwne_behavior+icephys+ogen.nwb 0 L4 sub-anm273341/sub-anm273341_ses-20150308_behavior+icephys+ogen.nwb 0 L4 sub-anm300170/sub-anm300170_ses-20150918_obj-1c4fz8_behavior+icephys+ogen.nwb 0
For both VPM units and L4 cells the touch response and the whisking phase locking are larger with the spikes in their own trial than moved to the next trial, so spikes and behaviour share a clock in the data as a whole. This cannot be shown for a cell that responds to neither; such cells are counted in the last line, and their alignment is consistent with the data, not shown.
# Cell types. FS (proxy): tagged by isolated 5 ms light pulses in VGAT-ChR2 mice.
# Excitatory (proxy): whole-cell, action potentials at least EXC_MIN_FWHM_MS wide, not tagged.
def classify(r, min_diff=yu2016.TAG_MIN_DIFF, min_fwhm=yu2016.EXC_MIN_FWHM_MS):
best = None
for site, info in r["sites"].items():
lr = yu2016.light_pulse_response(r, site)
if lr["n_pulses"] >= yu2016.TAG_MIN_PULSES:
lr["site"] = info["region"]
if best is None or lr["p_evoked"] - lr["p_base"] > best["p_evoked"] - best["p_base"]:
best = lr
tagged = best is not None and best["p_evoked"] - best["p_base"] >= min_diff and best["p_value"] < yu2016.TAG_MAX_P
if r["genotype"] == "VGAT-ChR2-EYFP" and tagged:
cls = "FS"
elif r["mode"] == "whole-cell" and r["fwhm_ms"] >= min_fwhm and not tagged:
cls = "EXC"
else:
cls = "other"
out = {"path": r["path"], "subject": r["subject"], "genotype": r["genotype"], "mode": r["mode"],
"spikes": len(r["spikes"]), "fwhm_ms": r["fwhm_ms"], "tag_tested": best is not None, "tagged": tagged, "cls": cls}
if best is not None:
out.update(tag_site=best["site"], pulses=best["n_pulses"], p_evoked=best["p_evoked"], p_base=best["p_base"],
tag_p=best["p_value"], latency_ms=best["median_latency_ms"])
return out
cl = pd.DataFrame([classify(r) for r in l4])
print("Cells by genotype, mode and class:")
print(pd.crosstab([cl.genotype.replace("", "(none)"), cl["mode"]], cl.cls).to_string())
vg = cl[(cl.genotype == "VGAT-ChR2-EYFP") & cl.tag_tested]
print(f"\nVGAT-ChR2 cells tested with isolated pulses: {len(vg)}; tagged {int(vg.tagged.sum())}")
print("Tagged cells: evoked spike probability median", round(vg[vg.tagged].p_evoked.median(), 2), "(range",
round(vg[vg.tagged].p_evoked.min(), 2), "to", round(vg[vg.tagged].p_evoked.max(), 2), "), baseline median",
round(vg[vg.tagged].p_base.median(), 2), ", first-spike latency median", round(vg[vg.tagged].latency_ms.median(), 1),
"ms (range", round(vg[vg.tagged].latency_ms.min(), 1), "to", round(vg[vg.tagged].latency_ms.max(), 1), ")")
un = vg[~vg.tagged]
print("Untagged tested cells with evoked minus baseline probability of 0.2 or more (too few pulses or p >= 0.001):")
print(un[(un.p_evoked - un.p_base) >= 0.2][["path", "mode", "pulses", "p_evoked", "p_base", "tag_p", "latency_ms"]].round(4).to_string(index=False))
print("Tagged cells by mode:", vg[vg.tagged]["mode"].value_counts().to_dict(), "; by subject:",
vg[vg.tagged].subject.value_counts().to_dict())
wc = cl[(cl["mode"] == "whole-cell")]
print("\nWhole-cell action potential width at half maximum (ms), cells with at least 5 spikes:")
print(wc.dropna(subset=["fwhm_ms"]).groupby("tagged").fwhm_ms.describe().round(2).to_string())
print("Whole-cell cells with fewer than 5 spikes (no width):", int(wc.fwhm_ms.isna().sum()), "of", len(wc))
print("Excitatory cells by subject:", cl[cl.cls == "EXC"].subject.value_counts().to_dict())
for d in (0.1, 0.3):
for f in (0.5, 0.7):
c = pd.DataFrame([classify(r, d, f) for r in l4]).cls.value_counts().to_dict()
print(f"Variant: tag threshold {d}, width threshold {f} ms ->", c)
fig, ax = plt.subplots(1, 2, figsize=(10, 3.2))
ax[0].hist(wc.fwhm_ms.dropna(), bins=np.arange(0, 2.4, 0.1), color="0.6", label="whole-cell, untagged")
ax[0].hist(wc[wc.tagged].fwhm_ms.dropna(), bins=np.arange(0, 2.4, 0.1), color="C3", label="whole-cell, tagged")
ax[0].axvline(yu2016.EXC_MIN_FWHM_MS, color="k", lw=0.8)
ax[0].set_xlabel("AP width at half maximum (ms)"); ax[0].set_ylabel("cells"); ax[0].legend(fontsize=7)
ax[1].scatter(vg.p_base, vg.p_evoked, c=np.where(vg.tagged, "C3", "0.5"), s=14)
ax[1].plot([0, 1], [0, 1], "k:", lw=0.8)
ax[1].set_xlabel("spike probability before pulse"); ax[1].set_ylabel("after pulse (0-6 ms)")
plt.tight_layout(); plt.show()
print("Axes: widths 0 to 2.4 ms hold all", int(wc.fwhm_ms.notna().sum()), "whole-cell widths (max",
round(wc.fwhm_ms.max(), 2), "); probabilities are within 0 to 1.")
print("\nTagged cells, one row each. The tag site is the light site with the largest evoked minus baseline probability:")
print(cl[cl.tagged][["subject", "mode", "spikes", "tag_site", "pulses", "p_evoked", "p_base", "tag_p", "latency_ms"]].round(4).to_string(index=False))
hz = pd.Series({r["path"]: r["vm_hz"] for r in l4})
print("\nWhole-cell action potential width (ms) by trace sampling rate (Hz) and tag:")
print(wc.assign(hz=wc.path.map(hz)).dropna(subset=["fwhm_ms"]).groupby(["hz", "tagged"]).fwhm_ms.describe()[["count", "min", "50%", "max"]].round(2).to_string())
Cells by genotype, mode and class:
cls EXC FS other
genotype mode
(none) whole-cell 1 0 2
Ai32 juxtacellular 0 0 4
whole-cell 0 0 1
C57BL6 whole-cell 4 0 4
PV-Cre juxtacellular 0 0 39
whole-cell 8 0 1
VGAT-ChR2-EYFP juxtacellular 0 19 11
whole-cell 9 4 23
VGAT-ChR2 cells tested with isolated pulses: 38; tagged 23
Tagged cells: evoked spike probability median 1.0 (range 0.37 to 1.0 ), baseline median 0.08 , first-spike latency median 2.3 ms (range 1.6 to 4.6 )
Untagged tested cells with evoked minus baseline probability of 0.2 or more (too few pulses or p >= 0.001):
path mode pulses p_evoked p_base tag_p latency_ms
sub-anm273347/sub-anm273347_ses-20150224_obj-i6vwao_behavior+icephys+ogen.nwb juxtacellular 36.0 0.3333 0.0278 0.0017 2.25
sub-anm273348/sub-anm273348_ses-20150227_behavior+icephys+ogen.nwb juxtacellular 12.0 1.0000 0.2500 0.0020 1.60
sub-anm318531/sub-anm318531_ses-20151230_obj-1jbfh1x_behavior+icephys+ogen.nwb juxtacellular 10.0 1.0000 0.1000 0.0020 1.65
Tagged cells by mode: {'juxtacellular': 19, 'whole-cell': 4} ; by subject: {'anm260070': 6, 'anm273348': 3, 'anm275231': 3, 'anm281589': 2, 'anm281592': 2, 'anm220466': 1, 'anm220470': 1, 'anm273341': 1, 'anm305599': 1, 'anm307766': 1, 'anm318530': 1, 'anm318532': 1}
Whole-cell action potential width at half maximum (ms), cells with at least 5 spikes:
count mean std min 25% 50% 75% max
tagged
False 27.0 0.89 0.41 0.25 0.70 0.83 1.04 2.12
True 4.0 0.51 0.10 0.38 0.46 0.52 0.57 0.61
Whole-cell cells with fewer than 5 spikes (no width): 26 of 57
Excitatory cells by subject: {'anm266945': 4, 'anm214848': 2, 'anm318530': 2, 'anm190964': 1, 'anm196837': 1, 'anm220470': 1, 'anm223517': 1, 'anm234411': 1, 'anm236459': 1, 'anm236462': 1, 'anm244028': 1, 'anm266951': 1, 'anm281592': 1, 'anm300172': 1, 'anm307766': 1, 'anm309008': 1, 'anm318532': 1}
Variant: tag threshold 0.1, width threshold 0.5 ms -> {'other': 85, 'FS': 23, 'EXC': 22}
Variant: tag threshold 0.1, width threshold 0.7 ms -> {'other': 87, 'FS': 23, 'EXC': 20}
Variant: tag threshold 0.3, width threshold 0.5 ms -> {'other': 85, 'FS': 23, 'EXC': 22}
Variant: tag threshold 0.3, width threshold 0.7 ms -> {'other': 87, 'FS': 23, 'EXC': 20}
Axes: widths 0 to 2.4 ms hold all 31 whole-cell widths (max 2.12 ); probabilities are within 0 to 1.
Tagged cells, one row each. The tag site is the light site with the largest evoked minus baseline probability:
subject mode spikes tag_site pulses p_evoked p_base tag_p latency_ms
anm220466 whole-cell 4221 vS1 190.0 0.5474 0.0474 0.0000 3.600
anm220470 whole-cell 2040 vS1 210.0 0.3714 0.0238 0.0000 4.600
anm260070 juxtacellular 10110 VPM and vS1 35.0 1.0000 0.1429 0.0000 1.600
anm260070 juxtacellular 9966 VPM and vS1 48.0 1.0000 0.0833 0.0000 2.100
anm260070 juxtacellular 7678 VPM and vS1 72.0 0.8194 0.1389 0.0000 2.600
anm260070 juxtacellular 6575 VPM and vS1 72.0 0.6944 0.1528 0.0000 2.500
anm260070 juxtacellular 5774 VPM and vS1 24.0 0.5417 0.0833 0.0005 2.300
anm260070 juxtacellular 10209 VPM and vS1 24.0 0.5000 0.0417 0.0005 2.550
anm273341 juxtacellular 2036 vS1 76.0 0.6974 0.0395 0.0000 2.300
anm273348 juxtacellular 3565 VPM and vS1 24.0 1.0000 0.0000 0.0000 2.100
anm273348 juxtacellular 4765 VPM and vS1 48.0 1.0000 0.0417 0.0000 2.500
anm273348 juxtacellular 8884 VPM and vS1 36.0 1.0000 0.1111 0.0000 1.900
anm275231 juxtacellular 2579 vS1 60.0 1.0000 0.0000 0.0000 1.900
anm275231 juxtacellular 4281 vS1 12.0 1.0000 0.0000 0.0002 1.900
anm275231 juxtacellular 18665 vS1 41.0 1.0000 0.0976 0.0000 2.000
anm281589 juxtacellular 16794 VPM and vS1 24.0 1.0000 0.1250 0.0000 2.300
anm281589 juxtacellular 9055 vS1 180.0 1.0000 0.1333 0.0000 2.200
anm281592 juxtacellular 4148 VPM and vS1 120.0 1.0000 0.0833 0.0000 2.300
anm281592 juxtacellular 286 vS1 48.0 1.0000 0.0417 0.0000 2.250
anm305599 whole-cell 5806 vS1 58.0 1.0000 0.2069 0.0000 2.175
anm307766 whole-cell 6411 vS1 108.0 0.9722 0.0926 0.0000 2.150
anm318530 juxtacellular 10401 vS1 20.0 0.7000 0.0000 0.0001 2.500
anm318532 juxtacellular 166 vS1 10.0 1.0000 0.0000 0.0010 2.900
Whole-cell action potential width (ms) by trace sampling rate (Hz) and tag:
count min 50% max
hz tagged
10000.0 False 20.0 0.25 0.82 1.46
True 2.0 0.49 0.55 0.61
20000.0 False 7.0 0.35 1.06 2.12
True 2.0 0.38 0.47 0.55
Nothing in the files names a cell type. In VGAT-ChR2 mice, isolated 5 ms light pulses make some cells spike within a few ms at high probability; these GABAergic cells are the FS proxy. The paper's FS interneurons are cells driven by the light in VGAT-ChR2 mice that also fall inside a boundary of spike peak-to-trough ratio and duration (Supp. Fig. 6), which is not given in numbers, so every tagged cell is used here (in L4, most GABAergic neurons are PV-positive FS cells, but the tag cannot exclude other interneurons). All tags come from the vS1 site or the site labelled "VPM and vS1", at 1.6 to 4.6 ms latency, which suggests direct activation. Three untagged juxtacellular cells spike after the pulses but miss p < 0.001 with 10 to 36 pulses; they are in neither class. Whole-cell cells with broad action potentials that are not tagged are the excitatory proxy. Some tagged whole-cell cells have spikes 0.4 to 0.6 ms wide, so width alone would not separate the classes cleanly. The 85 other cells (juxtacellular cells in PV-Cre, Ai32 and untagged VGAT-ChR2 mice, and whole-cell cells without spikes or with narrow spikes) are not used by the main tests. Many whole-cell recordings have almost no spikes; their membrane potential, which Figures 2 to 4 of the paper are about, is not analysed here.
# Long light epochs at the vS1 site in PV-Cre mice: firing inside the epoch, light trials against
# trials without light, every PV-Cre cell with at least 20 spikes in these trials.
def light_effect(r, site):
w = yu2016.light_windows(r, site)
if len(w) == 0:
return None
lit = np.zeros(len(r["trials"]), bool)
lit[w.trial.to_numpy(int)] = True
dark = ~yu2016.light_trials(r)
win = (round(float(np.median(w.rel_start)), 3), round(float(np.median(w.rel_stop)), 3))
rl, nl = yu2016.window_rate(r, lit, win)
rd, nd = yu2016.window_rate(r, dark, win)
starts = r["trials"]["start"].to_numpy()
sp = np.sort(r["spikes"])
per_trial = lambda m: np.array([np.searchsorted(sp, s + win[1]) - np.searchsorted(sp, s + win[0]) for s in starts[m]])
p = stats.mannwhitneyu(per_trial(lit), per_trial(dark)).pvalue if lit.sum() and dark.sum() else np.nan
return {"window": win, "lit_trials": int(lit.sum()), "dark_trials": int(dark.sum()), "rate_light": rl,
"rate_dark": rd, "spikes": nl + nd, "p": p}
pv = []
for r in l4:
if r["genotype"] != "PV-Cre":
continue
for site, info in r["sites"].items():
e = light_effect(r, site)
if e and e["spikes"] >= yu2016.MIN_SPIKES:
pv.append({"path": r["path"], "mode": r["mode"], "cls": cl.set_index("path").cls[r["path"]], **e})
pv = pd.DataFrame(pv)
pv["ratio"] = pv.rate_light / pv.rate_dark
pv["suppressed"] = (pv.ratio < 0.5) & (pv.p < 0.01)
pv["raised"] = (pv.ratio > 1) & (pv.p < 0.01)
print("PV-Cre cells with light at the vS1 site:", len(pv), "; light window (s after trial start):",
sorted({tuple(float(x) for x in np.round(w, 2)) for w in pv.window}))
print("Suppressed (rate ratio below 0.5, p < 0.01):", int(pv.suppressed.sum()), "; raised (p < 0.01):", int(pv.raised.sum()))
print(pv.groupby("mode").agg(cells=("ratio", "size"), suppressed=("suppressed", "sum"), raised=("raised", "sum"),
median_ratio=("ratio", "median")).to_string())
print("\nSuppressed cells:")
print(pv[pv.suppressed][["path", "mode", "rate_dark", "rate_light", "p"]].round(4).to_string(index=False))
PV-Cre cells with light at the vS1 site: 46 ; light window (s after trial start): [(0.73, 2.73), (0.82, 2.32), (0.82, 2.82), (0.82, 3.32), (0.92, 1.92), (0.92, 2.42), (0.92, 3.42), (0.92, 4.12), (1.02, 2.02), (1.02, 2.52), (1.07, 4.12)]
Suppressed (rate ratio below 0.5, p < 0.01): 3 ; raised (p < 0.01): 13
cells suppressed raised median_ratio
mode
juxtacellular 39 3 9 1.057043
whole-cell 7 0 4 1.577365
Suppressed cells:
path mode rate_dark rate_light p
sub-anm244024/sub-anm244024_ses-20141112_obj-vigb9a_behavior+icephys+ogen.nwb juxtacellular 17.5610 7.5385 0.0
sub-anm266945/sub-anm266945_ses-20141220_obj-7o69ev_behavior+icephys+ogen.nwb juxtacellular 41.3810 3.9649 0.0
sub-anm300169/sub-anm300169_ses-20150926_obj-rp0tw4_behavior+icephys+ogen.nwb juxtacellular 28.7903 1.4583 0.0
In PV-Cre mice the long light epoch at the vS1 site (594 nm in the metadata) cuts the firing of a few juxtacellular cells by more than half and raises that of others. This is what an inhibitory opsin in PV interneurons would do. The files do not name the opsin; the paper's supplement does (eNpHR in PV cells, Supp. Fig. 13, where 3 of 4 PV cells are inhibited), and the PV-Cre experiments are read below on that basis.
# Spike detection. Light artifacts: the fraction of a cell's spikes within 0.3 ms of a light pulse onset or offset,
# against the fraction expected if spikes were placed at random in the analysed span of each trial.
rows = []
for r in l4:
sp = np.sort(r["spikes"])
edges = np.sort(np.concatenate([np.r_[r["light"][s]["pulses"][:, 0], r["light"][s]["pulses"][:, 1]] for s in r["light"]] or [np.zeros(0)]))
if len(sp) < yu2016.MIN_SPIKES or len(edges) == 0:
continue
j = np.searchsorted(edges, sp)
d = np.minimum(np.abs(sp - edges[np.clip(j - 1, 0, len(edges) - 1)]), np.abs(edges[np.clip(j, 0, len(edges) - 1)] - sp))
chance = min(1.0, 2 * 0.0003 * len(edges) / (len(r["trials"]) * yu2016.ANALYSIS_END_S))
rows.append({"path": r["path"], "mode": r["mode"], "cls": cl.set_index("path").cls[r["path"]], "spikes": len(sp),
"light_edges": len(edges), "near_edge": float(np.mean(d < 0.0003)), "chance": chance})
art = pd.DataFrame(rows)
art["excess"] = art.near_edge / art.chance
print(f"Cells with light and at least {yu2016.MIN_SPIKES} spikes: {len(art)}. Fraction of spikes within 0.3 ms of a light edge, "
f"median {art.near_edge.median():.4f} (chance median {art.chance.median():.4f}); max {art.near_edge.max():.4f}")
print("Cells with more than 1% of spikes at a light edge and more than 3 times chance:")
print(art[(art.near_edge > 0.01) & (art.excess > 3)].round(4).to_string(index=False))
# Example traces: the first trial of one tagged juxtacellular cell and one excitatory whole-cell cell, with the
# detected spikes and the light.
ex_paths = [cl[(cl.cls == "FS") & (cl["mode"] == "juxtacellular")].sort_values("spikes").path.iloc[-1],
cl[cl.cls == "EXC"].sort_values("spikes").path.iloc[-1]]
fig, ax = plt.subplots(2, 1, figsize=(10, 4.5))
for a, pth in zip(ax, ex_paths):
r = next(x for x in l4 if x["path"] == pth)
s0, n = r["trials"].start.iloc[0], min(int(r["vm_trial_s"] * r["vm_hz"]), len(r["vm_head"]))
t = np.arange(n) / r["vm_hz"]
a.plot(t, r["vm_head"][:n], lw=0.4, color="0.3")
sp = r["spikes"][(r["spikes"] >= s0) & (r["spikes"] < s0 + t[-1])] - s0
a.plot(sp, np.interp(sp, t, r["vm_head"][:n]), "r.", ms=3)
for s in r["light"]:
for p0, p1 in r["light"][s]["pulses"]:
if s0 <= p0 < s0 + t[-1]:
a.axvspan(p0 - s0, p1 - s0, color="C0", alpha=0.3)
a.set_title(f"{pth.split('/')[-1]} ({r['mode']}, {cl.set_index('path').cls[pth]}): {len(sp)} spikes in trial 1", fontsize=8)
a.set_ylabel("stored value (mV)")
ax[1].set_xlabel("s after trial start"); plt.tight_layout(); plt.show()
print("Axes: each panel shows the first trial (up to the first 100,000 samples) and its full range.")
Cells with light and at least 20 spikes: 93. Fraction of spikes within 0.3 ms of a light edge, median 0.0005 (chance median 0.0003); max 0.0465
Cells with more than 1% of spikes at a light edge and more than 3 times chance:
path mode cls spikes light_edges near_edge chance excess
sub-anm220466/sub-anm220466_ses-20140403_behavior+icephys+ogen.nwb whole-cell FS 4221 1980 0.0102 0.0016 6.4313
sub-anm220470/sub-anm220470_ses-20140409_obj-bb09nm_behavior+icephys+ogen.nwb whole-cell FS 2040 3620 0.0123 0.0037 3.3007
sub-anm281589/sub-anm281589_ses-20150319_behavior+icephys+ogen.nwb juxtacellular FS 16794 3298 0.0111 0.0023 4.7575
Axes: each panel shows the first trial (up to the first 100,000 samples) and its full range.
Light artifacts are not counted as spikes to any extent that matters. Three cells have more than 1% of their spikes within 0.3 ms of a light onset or offset and more than 3 times chance; all three are tagged cells, whose light-evoked spikes come 1.6 to 4.6 ms after the onset of a 5 ms pulse, near its offset. The largest fraction in any cell (0.047) is below 3 times chance for that cell. The example traces show the detected spikes on the action potentials of a juxtacellular and a whole-cell recording. Spike detection was not checked against any ground truth.
Measures for every unit and cell¶
Each VPM unit and each L4 cell with a named principal whisker, over its trials without light: the touch response, the whisking modulation and the touch selectivity, as defined in the claims file and the helper module. A unit or cell enters a test when it has at least 20 spikes and at least 10 touches in those trials.
rows = []
for r in vpm:
dark = (r["trials"].photo_stim_mode == "").to_numpy()
for u in r["units"]:
if r["principal"] is None:
rows.append({"path": r["path"], "subject": r["subject"], "unit": u["unit"], "cls": "VPM"})
continue
rows.append({"path": r["path"], "subject": r["subject"], "unit": u["unit"], "cls": "VPM",
**yu2016.response_measures(u["spikes"], r, trial_mask=dark)})
for r in l4:
c = cl.set_index("path").loc[r["path"]]
if r["principal"] is None:
rows.append({"path": r["path"], "subject": r["subject"], "unit": 0, "cls": c.cls})
continue
rows.append({"path": r["path"], "subject": r["subject"], "unit": 0, "cls": c.cls,
**yu2016.response_measures(r["spikes"], r, trial_mask=~yu2016.light_trials(r))})
meas = pd.DataFrame(rows)
meas["included"] = (meas.n_spikes >= yu2016.MIN_SPIKES) & (meas.n_touches >= yu2016.MIN_TOUCHES)
meas["touch_resp"] = meas.touch_p < yu2016.ALPHA_CELL
meas["whisk_mod"] = meas.whisk_p < yu2016.ALPHA_CELL
meas["whisk_mod_rayleigh"] = meas.rayleigh_p < yu2016.ALPHA_CELL
print("Included and dropped, by class (dropped: no principal whisker, or too few spikes or touches):")
print(meas.groupby("cls").agg(total=("included", "size"), included=("included", "sum"),
no_principal=("n_spikes", lambda x: int(x.isna().sum()))).to_string())
meas["whisk_fit"] = meas.whisk_r2 > 0.25
inc = meas[meas.included]
summary = inc.groupby("cls").agg(
n=("included", "size"), touch_responsive=("touch_resp", "mean"), whisk_modulated=("whisk_mod", "mean"),
whisk_mod_rayleigh=("whisk_mod_rayleigh", "mean"),
touch_resp_hz=("touch_resp_hz", "median"), whisk_mod_hz=("whisk_mod_hz", "median"),
rate_whisk=("rate_whisk", "median"), rate_quiet=("rate_quiet", "median"),
touch_selectivity=("touch_selectivity", "median"), touches=("n_touches", "median"))
print("\nMedians (fractions for the two significance columns):")
print(summary.round(3).to_string())
print("\nRanges over included cells and units:")
print(inc.groupby("cls")[["touch_resp_hz", "whisk_mod_hz", "rate_whisk", "touch_selectivity"]].agg(["min", "max"]).round(2).to_string())
# Null for the L4 cells: the same measures with spikes moved to the next trial
nrows = []
for r in l4:
if r["principal"] is None:
continue
m = yu2016.response_measures(yu2016.shift_spikes(r["spikes"], r["trials"]), r, trial_mask=~yu2016.light_trials(r))
nrows.append({"path": r["path"], "null_touch": m["touch_p"] < yu2016.ALPHA_CELL, "null_whisk": m["whisk_p"] < yu2016.ALPHA_CELL,
"null_fit": m["whisk_r2"] > 0.25, "null_ok": m["n_spikes"] >= yu2016.MIN_SPIKES and m["n_touches"] >= yu2016.MIN_TOUCHES})
nl = inc.merge(pd.DataFrame(nrows), on="path")
nl = nl[nl.null_ok]
print("\nL4 cells, null with spikes moved to the next trial (fractions; cells with enough spikes and touches after the move):")
print(nl.groupby("cls").agg(n=("path", "size"), touch_responsive=("null_touch", "mean"), whisk_modulated=("null_whisk", "mean"),
r2_above_025=("null_fit", "mean")).round(2).to_string())
print("\nThe paper's phase measure (r2 of a sinusoid fit above 0.25), fraction of included cells:",
inc.groupby("cls").whisk_fit.mean().round(2).to_dict())
Included and dropped, by class (dropped: no principal whisker, or too few spikes or touches):
total included no_principal
cls
EXC 22 18 0
FS 23 20 3
VPM 29 24 0
other 85 53 5
Medians (fractions for the two significance columns):
n touch_responsive whisk_modulated whisk_mod_rayleigh touch_resp_hz whisk_mod_hz rate_whisk rate_quiet touch_selectivity touches
cls
EXC 18 0.556 0.278 0.167 2.074 0.027 0.232 0.108 0.980 370.0
FS 20 0.650 0.800 0.750 34.905 2.636 13.571 5.323 0.916 336.0
VPM 24 0.833 0.875 0.917 7.520 1.300 12.781 3.781 0.883 943.5
other 53 0.623 0.604 0.679 11.124 1.880 5.883 1.386 0.836 180.0
Ranges over included cells and units:
touch_resp_hz whisk_mod_hz rate_whisk touch_selectivity
min max min max min max min max
cls
EXC -3.10 24.18 0.0 2.96 0.00 2.91 0.0 1.0
FS -2.94 112.73 0.0 7.65 1.95 40.21 0.0 1.0
VPM -0.83 34.81 0.0 7.22 0.30 36.57 0.0 1.0
other -8.31 90.78 0.0 29.76 0.36 61.31 0.0 1.0
L4 cells, null with spikes moved to the next trial (fractions; cells with enough spikes and touches after the move):
n touch_responsive whisk_modulated r2_above_025
cls
EXC 18 0.00 0.06 0.33
FS 20 0.00 0.00 0.40
other 53 0.04 0.00 0.34
The paper's phase measure (r2 of a sinusoid fit above 0.25), fraction of included cells: {'EXC': 0.56, 'FS': 0.8, 'VPM': 0.96, 'other': 0.7}
With the spikes moved to the next trial, no FS cell and 0.06 of excitatory cells pass the whisking test, and no FS or excitatory cell passes the touch test (0.04 of the other cells). The paper's phase measure, the r² of a sinusoid fit above 0.25, marks 0.33 to 0.40 of L4 cells with the spikes moved to the next trial: with 12 phase bins it does not separate modulation from chance here, so it is shown only as a variant.
# Touch input per mouse. ION transection removes the input from the whiskers; Supplementary Table 2 gives 9 such
# VGAT-ChR2 mice (9 whole-cell excitatory and 10 cell-attached FS cells), but no file says which. A cell has a
# touch input when its spikes respond to touch (p < 0.01) or, in whole-cell, its membrane potential rises after
# touch (median at least 0.5 mV, p < 0.01). Mice where no tested cell has one are flagged.
vmt = pd.DataFrame([{"path": r["path"], "vm_touch_n": r["vm_touch_n"], "vm_touch_mv": r["vm_touch_mv"],
"vm_touch_p": r["vm_touch_p"]} for r in l4])
cells = meas[meas.cls != "VPM"].merge(vmt, on="path").merge(meta[["path", "mode", "line"]], on="path")
cells["spike_touch"] = cells.included & cells.touch_resp
cells["vm_touch"] = (cells.vm_touch_mv >= 0.5) & (cells.vm_touch_p < 0.01)
cells["vm_tested"] = cells.vm_touch_n >= 10
cells["tested"] = cells.included | cells.vm_tested
wcv = cells[cells.vm_tested]
print(f"Whole-cell cells with at least 10 touches: {len(wcv)}; touch-evoked change median {wcv.vm_touch_mv.median():.2f} mV "
f"(range {wcv.vm_touch_mv.min():.2f} to {wcv.vm_touch_mv.max():.2f}); with a touch input {int(wcv.vm_touch.sum())}")
mice = cells.groupby("subject").agg(line=("line", "first"), cells=("path", "size"), tested=("tested", "sum"),
spike_touch=("spike_touch", "sum"), vm_tested=("vm_tested", "sum"), vm_touch=("vm_touch", "sum"),
median_vm_touch_mv=("vm_touch_mv", "median"))
mice["no_touch_input"] = (mice.tested > 0) & (mice.spike_touch + mice.vm_touch == 0)
print("Mice with no tested cell:", list(mice[mice.tested == 0].index))
print("\nMice where no cell has a touch input, and mice where the membrane potential of every tested whole-cell cell shows none:")
print(mice[mice.no_touch_input | ((mice.vm_tested > 0) & (mice.vm_touch == 0))].round(2).to_string())
no_input = set(mice[mice.no_touch_input].index)
fl_cells = cells[cells.subject.isin(no_input)]
print(f"\nFlagged mice: {len(no_input)} (lines {mice.loc[sorted(no_input)].line.value_counts().to_dict()}); their cells by mode and class:")
print(pd.crosstab(fl_cells["mode"], fl_cells.cls).to_string())
print("For comparison, Supplementary Table 2: 9 ION-transected VGAT-ChR2 mice, 9 whole-cell excitatory and 10 cell-attached FS cells.")
print("Whole-cell cells of the flagged mice:", fl_cells[fl_cells["mode"] == "whole-cell"][["subject", "cls", "vm_touch_n", "vm_touch_mv", "vm_touch_p"]].round(3).to_dict("records"))
amb = mice[~mice.no_touch_input & (mice.vm_tested > 0) & (mice.vm_touch == 0)].index
print("Cells of the mice whose tested whole-cell cells all lack a touch input but that are not flagged:")
print(cells[cells.subject.isin(amb)][["subject", "mode", "cls", "included", "touch_resp_hz", "touch_p", "vm_touch_n", "vm_touch_mv", "vm_touch_p"]].round(4).to_string(index=False))
Whole-cell cells with at least 10 touches: 53; touch-evoked change median 2.82 mV (range -1.84 to 10.30); with a touch input 40
Mice with no tested cell: ['anm223517', 'anm324650', 'anm337496']
Mice where no cell has a touch input, and mice where the membrane potential of every tested whole-cell cell shows none:
line cells tested spike_touch vm_tested vm_touch median_vm_touch_mv no_touch_input
subject
anm273341 VGAT-ChR2-EYFP 2 2 0 0 0 NaN True
anm275231 VGAT-ChR2-EYFP 3 3 0 0 0 NaN True
anm281592 VGAT-ChR2-EYFP 3 3 1 1 0 -1.84 False
anm318530 VGAT-ChR2-EYFP 5 4 0 4 0 -0.00 True
anm318531 VGAT-ChR2-EYFP 2 2 1 1 0 0.09 False
anm318532 VGAT-ChR2-EYFP 5 3 0 3 0 0.09 True
Flagged mice: 4 (lines {'VGAT-ChR2-EYFP': 4}); their cells by mode and class:
cls EXC FS other
mode
juxtacellular 0 6 1
whole-cell 3 0 5
For comparison, Supplementary Table 2: 9 ION-transected VGAT-ChR2 mice, 9 whole-cell excitatory and 10 cell-attached FS cells.
Whole-cell cells of the flagged mice: [{'subject': 'anm318530', 'cls': 'EXC', 'vm_touch_n': 57, 'vm_touch_mv': 0.1, 'vm_touch_p': 0.626}, {'subject': 'anm318530', 'cls': 'other', 'vm_touch_n': 83, 'vm_touch_mv': -0.108, 'vm_touch_p': 0.4}, {'subject': 'anm318530', 'cls': 'other', 'vm_touch_n': 194, 'vm_touch_mv': 0.316, 'vm_touch_p': 0.001}, {'subject': 'anm318530', 'cls': 'EXC', 'vm_touch_n': 169, 'vm_touch_mv': -0.184, 'vm_touch_p': 0.882}, {'subject': 'anm318532', 'cls': 'EXC', 'vm_touch_n': 133, 'vm_touch_mv': 0.091, 'vm_touch_p': 0.3}, {'subject': 'anm318532', 'cls': 'other', 'vm_touch_n': 41, 'vm_touch_mv': 0.135, 'vm_touch_p': 0.526}, {'subject': 'anm318532', 'cls': 'other', 'vm_touch_n': 0, 'vm_touch_mv': nan, 'vm_touch_p': nan}, {'subject': 'anm318532', 'cls': 'other', 'vm_touch_n': 33, 'vm_touch_mv': -0.034, 'vm_touch_p': 0.844}]
Cells of the mice whose tested whole-cell cells all lack a touch input but that are not flagged:
subject mode cls included touch_resp_hz touch_p vm_touch_n vm_touch_mv vm_touch_p
anm281592 juxtacellular FS True 49.6970 0.0000 0 NaN NaN
anm281592 whole-cell EXC True -3.1016 0.9997 374 -1.8418 1.0000
anm281592 juxtacellular FS True -0.6349 0.8413 0 NaN NaN
anm318531 juxtacellular other True 11.7460 0.0063 0 NaN NaN
anm318531 whole-cell other False 0.0000 NaN 69 0.0912 0.3791
Almost every whole-cell cell depolarizes after touch (median 2.82 mV; 40 of 53 tested cells have a touch input by this rule). In 4 VGAT-ChR2 mice no tested cell has one: anm318530 and anm318532 (8 whole-cell cells, change -0.18 to 0.32 mV, and one tagged cell each, without a named principal whisker) and anm273341 and anm275231 (juxtacellular cells only, none responding to touch). anm318531 is ambiguous: its one tested whole-cell cell has no touch input, but a juxtacellular cell responds to touch (p = 0.006, at the edge of the rule). With it, these three mice hold 9 whole-cell cells, the number of ION-cut whole-cell cells in Supplementary Table 2; 8 of them were tested and lack a touch input, and one (in anm318532) has too few touches to be tested. These mice are consistent with the ION-transected mice of Figure 7, which the files do not mark. The table lists 9 such mice, so others may be among the mice with too few tested cells. The 0.5 mV threshold was chosen after seeing these values. Because the flag is drawn from touch responses, leaving these mice out favours any claim about touch, so it is used only in variants.
vpm-touch¶
Neurons in the ventral posteromedial nucleus (VPM) fire in response to touch in mice locating objects with their whiskers.
v = inc[inc.cls == "VPM"]
frac = v.touch_resp.mean()
print(f"VPM units included: {len(v)} of {int((meas.cls == 'VPM').sum())}; touch-responsive (p < 0.01): "
f"{int(v.touch_resp.sum())} ({frac:.2f}). Criterion: >= 0.5 reproduced, 0.25-0.5 partially.")
print(f"Touch response (Hz): median {v.touch_resp_hz.median():.1f}, range {v.touch_resp_hz.min():.1f} to "
f"{v.touch_resp_hz.max():.1f}; rate 0-25 ms after touch median {v.touch_post_hz.median():.1f} Hz, "
f"before {v.touch_pre_hz.median():.1f} Hz")
print("Variant, p < 0.05:", round((v.touch_p < 0.05).mean(), 2))
print("\nBy mouse:")
print(v.groupby("subject").agg(units=("touch_resp", "size"), responsive=("touch_resp", "sum"),
median_touch_resp_hz=("touch_resp_hz", "median")).round(1).to_string())
print("\nNull: the same test with spikes moved to the next trial gives a touch-responsive fraction of", end=" ")
shifted = []
for r in vpm:
if r["principal"] is None:
continue
dark = (r["trials"].photo_stim_mode == "").to_numpy()
for u in r["units"]:
m = yu2016.response_measures(yu2016.shift_spikes(u["spikes"], r["trials"]), r, trial_mask=dark)
if m["n_spikes"] >= yu2016.MIN_SPIKES and m["n_touches"] >= yu2016.MIN_TOUCHES:
shifted.append(m["touch_p"] < yu2016.ALPHA_CELL)
print(round(np.mean(shifted), 2), f"({len(shifted)} units)")
VPM units included: 24 of 29; touch-responsive (p < 0.01): 20 (0.83). Criterion: >= 0.5 reproduced, 0.25-0.5 partially.
Touch response (Hz): median 7.5, range -0.8 to 34.8; rate 0-25 ms after touch median 25.0 Hz, before 14.4 Hz
Variant, p < 0.05: 0.88
By mouse:
units responsive median_touch_resp_hz
subject
anm184389 7 5 4.9
anm186997 2 2 22.3
anm199549 5 5 25.8
anm199551 1 1 4.8
anm199552 7 6 7.3
anm203464 2 1 24.1
Null: the same test with spikes moved to the next trial gives a touch-responsive fraction of
0.0 (24 units)
Result: reproduced. 20 of the 24 included VPM units (0.83) fire more in the 25 ms after a touch than in the 25 ms before it (p < 0.01), against 0 of 24 when the spikes are moved to the next trial, and at least one unit responds in each of the 6 mice. The median touch response is 7.5 Hz.
vpm-whisking¶
VPM neurons fire in response to whisker movement.
frac = v.whisk_mod.mean()
print(f"VPM units included: {len(v)}; modulated by whisking phase (trial-shift null, p < 0.01): {int(v.whisk_mod.sum())} "
f"({frac:.2f}). Criterion: >= 0.5 reproduced, 0.25-0.5 partially.")
print(f"Whisking modulation above the null (Hz): median {v.whisk_mod_hz.median():.2f}, range {v.whisk_mod_hz.min():.2f} to "
f"{v.whisk_mod_hz.max():.2f}; null median per unit, median {v.whisk_null_median_hz.median():.2f} Hz; null draws per unit "
f"{int(v.n_null.min())} to {int(v.n_null.max())}; spikes during non-touch whisking per unit median {int(v.n_whisk_spikes.median())}")
print("Variant, p < 0.05:", round((v.whisk_p < 0.05).mean(), 2))
print("First version (Rayleigh test of spike phases, p < 0.01):", round(v.whisk_mod_rayleigh.mean(), 2))
rq = v.dropna(subset=["rate_whisk", "rate_quiet"])
print(f"Variant, rate during non-touch whisking against quiet (amplitude < {yu2016.QUIET_AMP_DEG} deg): "
f"median {rq.rate_whisk.median():.2f} against {rq.rate_quiet.median():.2f} Hz, higher in "
f"{(rq.rate_whisk > rq.rate_quiet).sum()} of {len(rq)} (Wilcoxon p = {stats.wilcoxon(rq.rate_whisk, rq.rate_quiet).pvalue:.2g})")
alt = []
for r in vpm:
if r["principal"] is None:
continue
dark = (r["trials"].photo_stim_mode == "").to_numpy()
for u in r["units"]:
old = yu2016.WHISK_AMP_DEG
yu2016.WHISK_AMP_DEG = 5.0
m = yu2016.response_measures(u["spikes"], r, trial_mask=dark)
yu2016.WHISK_AMP_DEG = old
if m["n_spikes"] >= yu2016.MIN_SPIKES and m["n_touches"] >= yu2016.MIN_TOUCHES:
alt.append(m["whisk_p"] < yu2016.ALPHA_CELL)
print("Variant, whisking threshold 5 degrees:", round(np.mean(alt), 2), f"({len(alt)} units)")
print("\nBy mouse:")
print(v.groupby("subject").agg(units=("whisk_mod", "size"), modulated=("whisk_mod", "sum"),
median_whisk_mod_hz=("whisk_mod_hz", "median")).round(2).to_string())
print("Null (spikes moved to the next trial): fraction modulated by the trial-shift test, and by the Rayleigh test,", end=" ")
null, null_r = [], []
for r in vpm:
if r["principal"] is None:
continue
dark = (r["trials"].photo_stim_mode == "").to_numpy()
for u in r["units"]:
m = yu2016.response_measures(yu2016.shift_spikes(u["spikes"], r["trials"]), r, trial_mask=dark)
if m["n_spikes"] >= yu2016.MIN_SPIKES and m["n_touches"] >= yu2016.MIN_TOUCHES:
null.append(m["whisk_p"] < yu2016.ALPHA_CELL)
null_r.append(m["rayleigh_p"] < yu2016.ALPHA_CELL)
print(round(np.mean(null), 2), "and", round(np.mean(null_r), 2), f"({len(null)} units)")
VPM units included: 24; modulated by whisking phase (trial-shift null, p < 0.01): 21 (0.88). Criterion: >= 0.5 reproduced, 0.25-0.5 partially. Whisking modulation above the null (Hz): median 1.30, range 0.00 to 7.22; null median per unit, median 0.20 Hz; null draws per unit 399 to 399; spikes during non-touch whisking per unit median 4658 Variant, p < 0.05: 0.92 First version (Rayleigh test of spike phases, p < 0.01): 0.92 Variant, rate during non-touch whisking against quiet (amplitude < 1.0 deg): median 12.78 against 3.78 Hz, higher in 23 of 24 (Wilcoxon p = 1.2e-06)
Variant, whisking threshold 5 degrees: 0.92 (24 units)
By mouse:
units modulated median_whisk_mod_hz
subject
anm184389 7 7 1.00
anm186997 2 1 0.62
anm199549 5 5 1.68
anm199551 1 1 0.58
anm199552 7 7 2.93
anm203464 2 0 0.08
Null (spikes moved to the next trial): fraction modulated by the trial-shift test, and by the Rayleigh test,
0.12 and 0.88 (24 units)
Result: reproduced. 21 of the 24 included VPM units (0.88) are modulated by whisking phase away from touch under the corrected test, against 0.12 with the spikes moved to the next trial; 23 of 24 also fire more during whisking than at rest (median 12.78 against 3.78 Hz). The first version of the test (Rayleigh) gives 0.92 but also 0.88 with the wrong alignment, so it is not used. One mouse (2 units) has no modulated unit.
fs-touch-and-whisking¶
L4 fast-spiking (FS) interneurons inherit the responses of VPM, firing in response to both touch and whisker movement.
fs = inc[inc.cls == "FS"]
print(f"FS (tagged) cells included: {len(fs)} of {int((meas.cls == 'FS').sum())} "
f"(dropped: {int(meas[(meas.cls == 'FS')].n_spikes.isna().sum())} without a named principal whisker)")
print(f"Touch-responsive: {int(fs.touch_resp.sum())} ({fs.touch_resp.mean():.2f}); whisking-modulated: "
f"{int(fs.whisk_mod.sum())} ({fs.whisk_mod.mean():.2f}); both: {int((fs.touch_resp & fs.whisk_mod).sum())}. "
"Criterion: both fractions >= 0.5 reproduced; one of them partially.")
print(f"Touch response (Hz) median {fs.touch_resp_hz.median():.1f} (range {fs.touch_resp_hz.min():.1f} to "
f"{fs.touch_resp_hz.max():.1f}); whisking modulation (Hz) median {fs.whisk_mod_hz.median():.2f} "
f"(range {fs.whisk_mod_hz.min():.2f} to {fs.whisk_mod_hz.max():.2f})")
print("Variant p < 0.05: touch", round((fs.touch_p < 0.05).mean(), 2), ", whisking", round((fs.whisk_p < 0.05).mean(), 2))
print("First version (Rayleigh test, p < 0.01): whisking", round(fs.whisk_mod_rayleigh.mean(), 2))
print("Null draws per cell:", int(fs.n_null.min()), "to", int(fs.n_null.max()),
"; cells with fewer than 99 (smallest possible p above 0.01):", int((fs.n_null < 99).sum()))
for d in (0.1, 0.3):
alt_cls = pd.DataFrame([classify(r, d) for r in l4]).set_index("path").cls
a = inc[inc.path.map(alt_cls) == "FS"]
print(f"Variant, tag threshold {d}: {len(a)} cells, touch {a.touch_resp.mean():.2f}, whisking {a.whisk_mod.mean():.2f}")
print("For comparison, VPM: touch", round(v.touch_resp.mean(), 2), ", whisking", round(v.whisk_mod.mean(), 2),
"; whisking modulation (Hz) FS against VPM, Mann-Whitney p =",
round(stats.mannwhitneyu(fs.whisk_mod_hz, v.whisk_mod_hz).pvalue, 3))
print("\nBy mouse:")
print(fs.groupby("subject").agg(cells=("touch_resp", "size"), touch=("touch_resp", "sum"), whisking=("whisk_mod", "sum"),
median_touch_hz=("touch_resp_hz", "median"), median_whisk_hz=("whisk_mod_hz", "median")).round(2).to_string())
a = fs[~fs.subject.isin(no_input)]
print(f"\nVariant, without the mice flagged as lacking touch input ({len(fs) - len(a)} cells left out): {len(a)} cells, "
f"touch {a.touch_resp.mean():.2f}, whisking {a.whisk_mod.mean():.2f}")
print("Variant, the paper's phase measure (r2 > 0.25):", round(fs.whisk_fit.mean(), 2), "; without flagged mice:", round(a.whisk_fit.mean(), 2))
FS (tagged) cells included: 20 of 23 (dropped: 3 without a named principal whisker)
Touch-responsive: 13 (0.65); whisking-modulated: 16 (0.80); both: 11. Criterion: both fractions >= 0.5 reproduced; one of them partially.
Touch response (Hz) median 34.9 (range -2.9 to 112.7); whisking modulation (Hz) median 2.64 (range 0.00 to 7.65)
Variant p < 0.05: touch 0.65 , whisking 0.85
First version (Rayleigh test, p < 0.01): whisking 0.75
Null draws per cell: 31 to 399 ; cells with fewer than 99 (smallest possible p above 0.01): 2
Variant, tag threshold 0.1: 20 cells, touch 0.65, whisking 0.80
Variant, tag threshold 0.3: 20 cells, touch 0.65, whisking 0.80
For comparison, VPM: touch 0.83 , whisking 0.88 ; whisking modulation (Hz) FS against VPM, Mann-Whitney p = 0.085
By mouse:
cells touch whisking median_touch_hz median_whisk_hz
subject
anm220466 1 0 1 3.35 3.07
anm220470 1 0 1 -2.94 1.44
anm260070 6 6 6 55.29 4.03
anm273341 1 0 1 1.37 1.05
anm273348 3 3 3 74.33 2.24
anm275231 3 0 2 3.01 1.19
anm281589 1 1 1 17.77 2.38
anm281592 2 1 1 24.53 1.45
anm305599 1 1 0 40.75 0.09
anm307766 1 1 0 112.73 7.65
Variant, without the mice flagged as lacking touch input (4 cells left out): 16 cells, touch 0.81, whisking 0.81
Variant, the paper's phase measure (r2 > 0.25): 0.8 ; without flagged mice: 0.75
Result: reproduced. Of the 20 included tagged cells, 13 (0.65) are touch-responsive and 16 (0.80) are modulated by whisking phase, and 11 are both. Their median touch response (34.9 Hz) is larger than that of VPM units (7.5 Hz). The tagged cells are GABAergic cells identified here by light pulses in VGAT-ChR2 mice, not FS cells identified by waveform as the paper did, and 3 tagged cells without a named principal whisker are left out. 4 of the 20 come from 2 mice flagged as lacking touch input (probably ION-transected), and none of the 4 responds to touch; without them, 0.81 of 16 are touch-responsive and 0.81 are modulated by whisking.
exc-mainly-touch¶
In contrast with VPM neurons and L4 FS interneurons, L4 excitatory neurons respond mainly to touch.
ex = inc[inc.cls == "EXC"]
print(f"Excitatory cells included: {len(ex)} of {int((meas.cls == 'EXC').sum())}")
groups = {"EXC": ex, "FS": fs, "VPM": v}
for k, g in groups.items():
s = g.touch_selectivity.dropna()
print(f"{k}: n = {len(g)}, selectivity defined for {len(s)}, median {s.median():.3f} "
f"(IQR {s.quantile(0.25):.3f} to {s.quantile(0.75):.3f}); touch-responsive {g.touch_resp.mean():.2f}; "
f"whisking-modulated {g.whisk_mod.mean():.2f} (cells with under 99 null draws: {int((g.n_null < 99).sum())}); touch response median {g.touch_resp_hz.median():.2f} Hz; "
f"whisking modulation median {g.whisk_mod_hz.median():.2f} Hz; rate during whisking median {g.rate_whisk.median():.2f} Hz")
p_fs = stats.mannwhitneyu(ex.touch_selectivity.dropna(), fs.touch_selectivity.dropna(), alternative="greater").pvalue
p_vpm = stats.mannwhitneyu(ex.touch_selectivity.dropna(), v.touch_selectivity.dropna(), alternative="greater").pvalue
print(f"\nMain test, one-sided Mann-Whitney: EXC more touch selective than FS p = {p_fs:.3f}; than VPM p = {p_vpm:.3f}. "
"Criterion: both < 0.05 reproduced, one partially.")
print("Cells with selectivity 0 (no touch increase):", {k: int((g.touch_selectivity == 0).sum()) for k, g in groups.items()},
"; selectivity 1 (no whisking modulation):", {k: int((g.touch_selectivity == 1).sum()) for k, g in groups.items()})
# Variants (do not decide the outcome)
def fisher(a, b, col):
t = [[int(a[col].sum()), int((~a[col]).sum())], [int(b[col].sum()), int((~b[col]).sum())]]
return stats.fisher_exact(t, alternative="less").pvalue
print(f"\nVariant: fraction whisking-modulated EXC {ex.whisk_mod.mean():.2f} against FS {fs.whisk_mod.mean():.2f} "
f"(Fisher one-sided p = {fisher(ex, fs, 'whisk_mod'):.2g}) and VPM {v.whisk_mod.mean():.2f} (p = {fisher(ex, v, 'whisk_mod'):.2g})")
for k, g in groups.items():
g = g.assign(rel=g.whisk_mod_hz / g.rate_whisk.where(g.rate_whisk > 0))
groups[k] = g
print("Variant: whisking modulation above the null relative to the whisking rate, medians",
{k: round(float(g.rel.median()), 3) for k, g in groups.items()},
f"; EXC below FS p = {stats.mannwhitneyu(groups['EXC'].rel.dropna(), groups['FS'].rel.dropna(), alternative='less').pvalue:.3f}"
f", below VPM p = {stats.mannwhitneyu(groups['EXC'].rel.dropna(), groups['VPM'].rel.dropna(), alternative='less').pvalue:.3f}")
def rate_sel(g):
tr = g.touch_resp_hz.clip(lower=0)
wr = (g.rate_whisk - g.rate_quiet).clip(lower=0)
return (tr / (tr + wr)).where((tr + wr) > 0)
rs = {k: rate_sel(g).dropna() for k, g in groups.items()}
print("Variant: selectivity with the whisking-minus-quiet rate in place of the phase modulation, medians",
{k: round(float(x.median()), 3) for k, x in rs.items()},
f"; EXC above FS p = {stats.mannwhitneyu(rs['EXC'], rs['FS'], alternative='greater').pvalue:.3f}, above VPM p = "
f"{stats.mannwhitneyu(rs['EXC'], rs['VPM'], alternative='greater').pvalue:.3f}")
r0 = {k: g.touch_selectivity_raw.dropna() for k, g in groups.items()}
print("First version (selectivity with 2 x vector strength x rate, uncorrected), medians",
{k: round(float(x.median()), 3) for k, x in r0.items()},
f"; EXC above FS p = {stats.mannwhitneyu(r0['EXC'], r0['FS'], alternative='greater').pvalue:.3f}, above VPM p = "
f"{stats.mannwhitneyu(r0['EXC'], r0['VPM'], alternative='greater').pvalue:.3f}")
for f in (0.5, 0.7):
alt_cls = pd.DataFrame([classify(r, yu2016.TAG_MIN_DIFF, f) for r in l4]).set_index("path").cls
a = inc[inc.path.map(alt_cls) == "EXC"].touch_selectivity.dropna()
print(f"Variant: width threshold {f} ms, {len(a)} EXC, median selectivity {a.median():.3f}, above FS p = "
f"{stats.mannwhitneyu(a, fs.touch_selectivity.dropna(), alternative='greater').pvalue:.3f}, above VPM p = "
f"{stats.mannwhitneyu(a, v.touch_selectivity.dropna(), alternative='greater').pvalue:.3f}")
ug = inc[inc.path.isin(cl[(cl.genotype == "VGAT-ChR2-EYFP") & cl.tag_tested & ~cl.tagged].path)]
print(f"Variant: untagged VGAT-ChR2 cells of any mode as excitatory ({len(ug)}), median selectivity "
f"{ug.touch_selectivity.median():.3f}, whisking-modulated {ug.whisk_mod.mean():.2f}")
print("By mouse (cells, median selectivity):", ex.groupby("subject").touch_selectivity.agg(["size", "median"]).round(3).to_dict("index"))
print("\nExcitatory cells, one row each:")
print(ex[["subject", "n_spikes", "n_touches", "touch_resp_hz", "touch_p", "whisk_amp_raw_hz", "whisk_null_median_hz",
"whisk_mod_hz", "whisk_p", "rate_whisk", "touch_selectivity"]].round(3).to_string(index=False))
fig, ax = plt.subplots(1, 2, figsize=(10, 3.2))
for i, (k, g) in enumerate(groups.items()):
ax[0].scatter(np.full(len(g), i) + np.linspace(-0.15, 0.15, len(g)), g.touch_selectivity, s=12)
ax[0].set_xticks(range(3)); ax[0].set_xticklabels(list(groups)); ax[0].set_ylabel("touch selectivity"); ax[0].set_ylim(-0.05, 1.05)
for k, g in groups.items():
ax[1].scatter(g.touch_resp_hz.clip(lower=0.01), g.whisk_mod_hz.clip(lower=0.01), s=12, label=k)
ax[1].set_xscale("log"); ax[1].set_yscale("log"); ax[1].set_xlabel("touch response (Hz)"); ax[1].set_ylabel("whisking modulation (Hz)")
ax[1].legend(fontsize=7); plt.tight_layout(); plt.show()
allg = pd.concat(groups.values())
print("Right panel: values at or below 0.01 Hz are drawn at 0.01:", int((allg.touch_resp_hz <= 0.01).sum()), "touch,",
int((allg.whisk_mod_hz <= 0.01).sum()), "whisking; selectivity is within 0 to 1.")
# Variants on the data that bear on the selectivity test
print("\nSpikes in the 25 ms after touches, per excitatory cell: median", int(ex.touch_spikes_post.median()), ", range",
int(ex.touch_spikes_post.min()), "to", int(ex.touch_spikes_post.max()), "; cells with selectivity 0 or 1:",
int(ex.touch_selectivity.isin([0, 1]).sum()), "of", len(ex))
def sel_test(e, label):
pf = stats.mannwhitneyu(e.touch_selectivity.dropna(), fs.touch_selectivity.dropna(), alternative="greater").pvalue
pv_ = stats.mannwhitneyu(e.touch_selectivity.dropna(), v.touch_selectivity.dropna(), alternative="greater").pvalue
print(f"Variant, {label}: {len(e)} EXC, median selectivity {e.touch_selectivity.median():.3f}, above FS p = {pf:.3f}, above VPM p = {pv_:.3f}")
sel_test(ex[(ex.touch_spikes_post + ex.touch_spikes_pre) >= 20], "EXC with at least 20 spikes in the touch windows")
sel_test(ex[~ex.subject.isin(no_input)], "EXC without the mice flagged as lacking touch input")
fs_k, v_k = fs[~fs.subject.isin(no_input)], v
pf = stats.mannwhitneyu(ex[~ex.subject.isin(no_input)].touch_selectivity.dropna(), fs_k.touch_selectivity.dropna(), alternative="greater").pvalue
print(f"Variant, flagged mice left out of both EXC and FS: above FS p = {pf:.3f}")
iso = []
for r in recs:
if r["principal"] is None:
continue
if r["kind"] == "VPM":
dark = (r["trials"].photo_stim_mode == "").to_numpy()
cand = [(u["unit"], u["spikes"]) for u in r["units"]]
else:
dark = ~yu2016.light_trials(r)
cand = [(0, r["spikes"])]
for unit, sp in cand:
row = inc[(inc.path == r["path"]) & (inc.unit == unit)]
if len(row) == 0 or row.cls.iloc[0] not in groups:
continue
m = yu2016.response_measures(sp, r, trial_mask=dark, isolated_touch_s=yu2016.TOUCH_WIN_S)
iso.append({"cls": row.cls.iloc[0], "sel": m["touch_selectivity"], "touch_p": m["touch_p"]})
iso = pd.DataFrame(iso)
ie, ifs, iv = (iso[iso.cls == k].sel.dropna() for k in ("EXC", "FS", "VPM"))
print(f"Variant, only touches with no touch onset in the 25 ms before: medians EXC {ie.median():.3f}, FS {ifs.median():.3f}, "
f"VPM {iv.median():.3f}; above FS p = {stats.mannwhitneyu(ie, ifs, alternative='greater').pvalue:.3f}, above VPM p = "
f"{stats.mannwhitneyu(ie, iv, alternative='greater').pvalue:.3f}")
print("Variant, the paper's phase measure (r2 > 0.25): fraction EXC", round(ex.whisk_fit.mean(), 2), ", FS", round(fs.whisk_fit.mean(), 2),
", VPM", round(v.whisk_fit.mean(), 2), f"; EXC below FS Fisher p = {fisher(ex, fs, 'whisk_fit'):.2g}, below VPM p = {fisher(ex, v, 'whisk_fit'):.2g}")
# Inclusion: untagged whole-cell cells that are not in the EXC group
wcu = cl[(cl["mode"] == "whole-cell") & ~cl.tagged].merge(meas[["path", "n_spikes", "included"]], on="path", how="left")
print("\nUntagged whole-cell cells:", len(wcu), "; in the EXC group and included:", int(((wcu.cls == "EXC") & wcu.included.fillna(False).astype(bool)).sum()),
"; without a width (under 5 spikes):", int(wcu.fwhm_ms.isna().sum()), "; with a width but under 20 spikes or 10 touches in trials without light:",
int((wcu.fwhm_ms.notna() & ~wcu.included.fillna(False).astype(bool)).sum()),
"; median spikes in trials without light of those left out:", wcu[~wcu.included.fillna(False).astype(bool)].n_spikes.median())
Excitatory cells included: 18 of 22
EXC: n = 18, selectivity defined for 16, median 0.980 (IQR 0.027 to 1.000); touch-responsive 0.56; whisking-modulated 0.28 (cells with under 99 null draws: 3); touch response median 2.07 Hz; whisking modulation median 0.03 Hz; rate during whisking median 0.23 Hz
FS: n = 20, selectivity defined for 19, median 0.916 (IQR 0.716 to 0.948); touch-responsive 0.65; whisking-modulated 0.80 (cells with under 99 null draws: 2); touch response median 34.90 Hz; whisking modulation median 2.64 Hz; rate during whisking median 13.57 Hz
VPM: n = 24, selectivity defined for 24, median 0.883 (IQR 0.769 to 0.938); touch-responsive 0.83; whisking-modulated 0.88 (cells with under 99 null draws: 0); touch response median 7.52 Hz; whisking modulation median 1.30 Hz; rate during whisking median 12.78 Hz
Main test, one-sided Mann-Whitney: EXC more touch selective than FS p = 0.074; than VPM p = 0.090. Criterion: both < 0.05 reproduced, one partially.
Cells with selectivity 0 (no touch increase): {'EXC': 4, 'FS': 1, 'VPM': 2} ; selectivity 1 (no whisking modulation): {'EXC': 4, 'FS': 0, 'VPM': 1}
Variant: fraction whisking-modulated EXC 0.28 against FS 0.80 (Fisher one-sided p = 0.0016) and VPM 0.88 (p = 0.00011)
Variant: whisking modulation above the null relative to the whisking rate, medians {'EXC': 0.272, 'FS': 0.191, 'VPM': 0.15} ; EXC below FS p = 0.836, below VPM p = 0.861
Variant: selectivity with the whisking-minus-quiet rate in place of the phase modulation, medians {'EXC': 0.992, 'FS': 0.754, 'VPM': 0.568} ; EXC above FS p = 0.047, above VPM p = 0.032
First version (selectivity with 2 x vector strength x rate, uncorrected), medians {'EXC': 0.923, 'FS': 0.858, 'VPM': 0.785} ; EXC above FS p = 0.176, above VPM p = 0.258
Variant: width threshold 0.5 ms, 16 EXC, median selectivity 0.980, above FS p = 0.074, above VPM p = 0.090
Variant: width threshold 0.7 ms, 15 EXC, median selectivity 0.980, above FS p = 0.112, above VPM p = 0.126
Variant: untagged VGAT-ChR2 cells of any mode as excitatory (10), median selectivity 0.699, whisking-modulated 0.80
By mouse (cells, median selectivity): {'anm196837': {'size': 1, 'median': nan}, 'anm214848': {'size': 2, 'median': 0.972}, 'anm220470': {'size': 1, 'median': 0.998}, 'anm234411': {'size': 1, 'median': 0.98}, 'anm236459': {'size': 1, 'median': 1.0}, 'anm236462': {'size': 1, 'median': 1.0}, 'anm244028': {'size': 1, 'median': 0.0}, 'anm266945': {'size': 4, 'median': 0.036}, 'anm266951': {'size': 1, 'median': 1.0}, 'anm281592': {'size': 1, 'median': 0.0}, 'anm300172': {'size': 1, 'median': 1.0}, 'anm309008': {'size': 1, 'median': 1.0}, 'anm318530': {'size': 1, 'median': 0.0}, 'anm318532': {'size': 1, 'median': 0.902}}
Excitatory cells, one row each:
subject n_spikes n_touches touch_resp_hz touch_p whisk_amp_raw_hz whisk_null_median_hz whisk_mod_hz whisk_p rate_whisk touch_selectivity
anm196837 53.0 375.0 1.920 0.001 NaN NaN NaN NaN 0.000 NaN
anm214848 245.0 366.0 16.831 0.000 0.452 0.096 0.356 0.005 0.270 0.979
anm214848 34.0 913.0 0.219 0.083 0.021 0.013 0.008 0.343 0.012 0.965
anm220470 137.0 110.0 10.182 0.000 0.073 0.056 0.017 0.312 0.099 0.998
anm234411 162.0 495.0 3.636 0.000 0.137 0.063 0.074 0.067 0.164 0.980
anm236459 991.0 518.0 5.405 0.000 0.075 0.154 0.000 0.876 2.065 1.000
anm236462 626.0 337.0 2.730 0.003 0.130 0.167 0.000 0.649 0.454 1.000
anm244028 500.0 617.0 -0.324 0.751 0.252 0.152 0.100 0.138 0.970 0.000
anm266945 56.0 352.0 -0.341 0.958 0.044 0.057 0.000 0.631 0.067 NaN
anm266945 31.0 644.0 0.000 0.500 0.045 0.018 0.027 0.042 0.034 0.000
anm266945 70.0 91.0 24.176 0.000 0.548 0.195 0.353 0.045 0.468 0.986
anm266945 669.0 366.0 0.109 0.353 3.326 0.366 2.961 0.004 2.914 0.036
anm266951 120.0 149.0 4.295 0.004 0.259 0.416 0.000 0.733 0.182 1.000
anm281592 434.0 374.0 -3.102 1.000 0.603 0.092 0.511 0.004 0.789 0.000
anm300172 280.0 449.0 2.227 0.002 0.053 0.104 0.000 0.845 0.353 1.000
anm309008 88.0 408.0 5.784 0.000 0.017 0.016 0.002 0.363 0.008 1.000
anm318530 21.0 169.0 0.000 NaN 0.203 0.061 0.142 0.006 0.193 0.000
anm318532 132.0 133.0 1.504 0.066 0.240 0.077 0.163 0.003 0.599 0.902
Right panel: values at or below 0.01 Hz are drawn at 0.01: 9 touch, 9 whisking; selectivity is within 0 to 1. Spikes in the 25 ms after touches, per excitatory cell: median 23 , range 0 to 172 ; cells with selectivity 0 or 1: 8 of 18 Variant, EXC with at least 20 spikes in the touch windows: 12 EXC, median selectivity 0.998, above FS p = 0.003, above VPM p = 0.005 Variant, EXC without the mice flagged as lacking touch input: 16 EXC, median selectivity 0.983, above FS p = 0.024, above VPM p = 0.038 Variant, flagged mice left out of both EXC and FS: above FS p = 0.033
Variant, only touches with no touch onset in the 25 ms before: medians EXC 0.980, FS 0.917, VPM 0.882; above FS p = 0.084, above VPM p = 0.131 Variant, the paper's phase measure (r2 > 0.25): fraction EXC 0.56 , FS 0.8 , VPM 0.96 ; EXC below FS Fisher p = 0.1, below VPM p = 0.0025 Untagged whole-cell cells: 53 ; in the EXC group and included: 18 ; without a width (under 5 spikes): 26 ; with a width but under 20 spikes or 10 touches in trials without light: 5 ; median spikes in trials without light of those left out: 1.0
Result: not reproduced. By the criterion's test the excitatory cells are not significantly more touch selective than FS cells (p = 0.074) or VPM units (p = 0.090), though their median selectivity is the highest (0.980 against 0.916 and 0.883). The test is fragile. Selectivity rests on few spikes in many excitatory cells (median 23 in the 25 ms after touches; 8 of 18 cells sit at exactly 0 or 1), and the groups mix in cells from mice flagged as lacking touch input. Without those mice the excitatory cells are more touch selective than both groups (p = 0.024 and 0.038), and so are the 12 excitatory cells with at least 20 spikes in the touch windows (p = 0.003 and 0.005). These variants do not decide the outcome: the flag is inferred from touch responses, and both selections favour the claim. Counting only isolated touches gives p = 0.084 and 0.131. The other measures point the way the claim does: 0.28 of excitatory cells are modulated by whisking against 0.80 of FS cells (p = 0.0016) and 0.88 of VPM units (p = 0.00011), and a selectivity built from the whisking-minus-rest rate is higher than in both (p = 0.047 and 0.032). The excitatory group holds the 18 broad-spiking whole-cell cells with enough spikes. Of the 53 untagged whole-cell cells, 26 have fewer than 5 spikes and 5 more have too few spikes or touches; the cells left out fire a median of 1 spike.
fs-suppress-movement-spiking¶
FS interneurons reduce movement-related spiking in L4 excitatory neurons; silencing them increases the spiking of excitatory neurons during whisker movement.
def light_measures(r, site):
w = yu2016.light_windows(r, site)
lit = np.zeros(len(r["trials"]), bool)
lit[w.trial.to_numpy(int)] = True
dark = ~yu2016.light_trials(r)
win = [(float(np.median(w.rel_start)), float(np.median(w.rel_stop)))] * len(r["trials"])
a = yu2016.response_measures(r["spikes"], r, trial_mask=lit, time_window=win)
b = yu2016.response_measures(r["spikes"], r, trial_mask=dark, time_window=win)
return a, b, win[0], int(lit.sum()), int(dark.sum())
rows = []
for r in l4:
if r["genotype"] != "PV-Cre" or r["principal"] is None:
continue
for site in r["sites"]:
if len(yu2016.light_windows(r, site)) == 0:
continue
a, b, win, nlit, ndark = light_measures(r, site)
rows.append({"path": r["path"], "subject": r["subject"], "mode": r["mode"],
"cls": cl.set_index("path").cls[r["path"]], "window": tuple(float(x) for x in np.round(win, 2)),
"lit_trials": nlit, "dark_trials": ndark,
"spikes": a["n_spikes"] + b["n_spikes"], "touches": a["n_touches"] + b["n_touches"],
"touches_light": a["n_touches"], "touches_dark": b["n_touches"],
"whisk_spk_light": a["n_whisk_spikes"], "whisk_spk_dark": b["n_whisk_spikes"],
"touch_spk_light": a["touch_spikes_post"], "touch_spk_dark": b["touch_spikes_post"],
"whisk_s_light": a["whisk_s"], "whisk_s_dark": b["whisk_s"],
"rate_whisk_light": a["rate_whisk"], "rate_whisk_dark": b["rate_whisk"],
"rate_light": a["rate"], "rate_dark": b["rate"],
"sel_light": a["touch_selectivity"], "sel_dark": b["touch_selectivity"],
"sel_raw_light": a["touch_selectivity_raw"], "sel_raw_dark": b["touch_selectivity_raw"],
"touch_light": a["touch_resp_hz"], "touch_dark": b["touch_resp_hz"],
"wmod_light": a["whisk_mod_hz"], "wmod_dark": b["whisk_mod_hz"]})
pvm = pd.DataFrame(rows)
pvm["included"] = (pvm.spikes >= yu2016.MIN_SPIKES) & (pvm.touches >= yu2016.MIN_TOUCHES)
pvx = pvm[(pvm.cls == "EXC") & pvm.included]
print(f"PV-Cre excitatory cells with light: {int((pvm.cls == 'EXC').sum())}, included {len(pvx)}")
d = pvx.rate_whisk_light - pvx.rate_whisk_dark
p = stats.wilcoxon(pvx.rate_whisk_light, pvx.rate_whisk_dark, alternative="greater").pvalue
print(f"Rate during non-touch whisking inside the light window: rises in {(d > 0).sum()} of {len(pvx)}; median "
f"{pvx.rate_whisk_dark.median():.2f} Hz without light, {pvx.rate_whisk_light.median():.2f} Hz with; median change "
f"{d.median():.2f} Hz; one-sided Wilcoxon p = {p:.3f}. Criterion: rises in most and p < 0.05 reproduced.")
print(pvx[["subject", "window", "lit_trials", "dark_trials", "whisk_s_dark", "whisk_s_light", "whisk_spk_dark", "whisk_spk_light",
"rate_whisk_dark", "rate_whisk_light", "rate_dark", "rate_light", "wmod_dark", "wmod_light"]].round(2).to_string(index=False))
print("\nBehaviour inside the light window, per trial (median over cells), without and with light: whisking time (s)",
round((pvx.whisk_s_dark / pvx.dark_trials).median(), 3), "and", round((pvx.whisk_s_light / pvx.lit_trials).median(), 3),
"; touches", round((pvx.touches_dark / pvx.dark_trials).median(), 2), "and", round((pvx.touches_light / pvx.lit_trials).median(), 2))
print("\nVariant, rate over the whole light window:", (pvx.rate_light > pvx.rate_dark).sum(), "of", len(pvx), "rise, p =",
round(stats.wilcoxon(pvx.rate_light, pvx.rate_dark, alternative="greater").pvalue, 3))
dd = (pvx.wmod_light - pvx.wmod_dark).dropna()
print("Variant, whisking modulation (Hz):", (dd > 0).sum(), "of", len(dd), "rise, median change", round(dd.median(), 2),
", p =", round(stats.wilcoxon(dd, alternative="greater").pvalue, 3))
rest = pvx[pvx.subject != pvx.subject.value_counts().index[0]]
dr = rest.rate_whisk_light - rest.rate_whisk_dark
print(f"Variant, without the mouse with most cells ({pvx.subject.value_counts().index[0]}): rises in {(dr > 0).sum()} of {len(rest)}, "
f"p = {stats.wilcoxon(rest.rate_whisk_light, rest.rate_whisk_dark, alternative='greater').pvalue:.3f}")
print("By mouse (rise / cells):", pvx.assign(rise=d > 0).groupby("subject").rise.agg(["sum", "size"]).to_dict("index"))
jx = pvm[(pvm["mode"] == "juxtacellular") & pvm.included]
jx = jx[~jx.path.isin(pv[pv.suppressed].path)]
dj = (jx.rate_whisk_light - jx.rate_whisk_dark).dropna()
print(f"\nVariant: PV-Cre juxtacellular cells not suppressed by light, {len(dj)} from {jx.subject.nunique()} mice: "
f"rise in {(dj > 0).sum()}, median {jx.rate_whisk_dark.median():.2f} Hz without light and {jx.rate_whisk_light.median():.2f} Hz with, "
f"median change {dj.median():.2f} Hz, p = {stats.wilcoxon(dj, alternative='greater').pvalue:.3g}; spikes during whisking per cell "
f"without light median {int(jx.whisk_spk_dark.median())}")
PV-Cre excitatory cells with light: 7, included 7
Rate during non-touch whisking inside the light window: rises in 6 of 7; median 0.05 Hz without light, 1.07 Hz with; median change 0.56 Hz; one-sided Wilcoxon p = 0.016. Criterion: rises in most and p < 0.05 reproduced.
subject window lit_trials dark_trials whisk_s_dark whisk_s_light whisk_spk_dark whisk_spk_light rate_whisk_dark rate_whisk_light rate_dark rate_light wmod_dark wmod_light
anm244028 (0.82, 3.32) 58 112 51.68 32.73 58 55 1.12 1.68 0.98 1.54 0.24 0.19
anm266945 (0.82, 2.32) 35 79 20.25 8.38 1 9 0.05 1.07 0.18 1.28 0.01 0.00
anm266945 (0.82, 2.32) 73 122 41.27 25.41 1 10 0.02 0.39 0.04 0.71 0.05 0.05
anm266945 (0.82, 2.32) 5 11 2.88 1.15 0 0 0.00 0.00 2.48 1.07 NaN 0.00
anm266945 (0.82, 2.32) 33 56 10.79 5.15 30 27 2.78 5.24 3.37 2.81 2.01 4.55
anm266951 (0.92, 1.92) 5 15 4.83 2.10 0 1 0.00 0.48 1.67 2.20 0.00 1.09
anm300172 (0.73, 2.73) 61 84 24.20 21.18 6 46 0.25 2.17 0.88 2.58 0.00 0.81
Behaviour inside the light window, per trial (median over cells), without and with light: whisking time (s) 0.288 and 0.347 ; touches 3.59 and 3.4
Variant, rate over the whole light window: 5 of 7 rise, p = 0.188
Variant, whisking modulation (Hz): 3 of 6 rise, median change 0.4 , p = 0.219
Variant, without the mouse with most cells (anm266945): rises in 3 of 3, p = 0.125
By mouse (rise / cells): {'anm244028': {'sum': 1, 'size': 1}, 'anm266945': {'sum': 3, 'size': 4}, 'anm266951': {'sum': 1, 'size': 1}, 'anm300172': {'sum': 1, 'size': 1}}
Variant: PV-Cre juxtacellular cells not suppressed by light, 32 from 8 mice: rise in 24, median 5.27 Hz without light and 6.61 Hz with, median change 1.74 Hz, p = 0.000419; spikes during whisking per cell without light median 61
Result: reproduced. In PV-Cre mice, with the vS1 light on, the spike rate of excitatory cells during non-touch whisking rises in 6 of 7 cells (median 0.05 to 1.07 Hz, p = 0.016), from 4 mice. The evidence is thin: 4 cells come from anm266945, and without that mouse the rate rises in 3 of 3 (p = 0.125, the smallest possible with 3 cells). Several rates rest on 0 to 10 spikes during whisking. Over the whole light window the rise is not significant (5 of 7, p = 0.188), nor is the rise of the phase modulation (3 of 6, p = 0.219). The supplement confirms that this light drives eNpHR in PV cells; it cuts the firing of 3 PV-Cre cells by more than half (data check), as in Supp. Fig. 13a (3 of 4 PV cells). The mice whisk and touch about as much with the light as without (0.35 against 0.29 s of whisking and 3.4 against 3.6 touches per trial). The PV-Cre juxtacellular cells not suppressed by light, which Supplementary Table 2 suggests are mostly excitatory (38 of 42 cell-attached PV-Cre cells), show the same rise in 24 of 32 (p = 0.00042). As the claims file says, that selection is biased toward a rise, so it does not decide the outcome.
fs-enhance-touch-selectivity¶
By suppressing movement-related spiking, FS interneurons enhance the selectivity of L4 excitatory neurons for touch.
s = pvx.dropna(subset=["sel_light", "sel_dark"])
ds = s.sel_light - s.sel_dark
p = stats.wilcoxon(s.sel_light, s.sel_dark, alternative="less").pvalue if (ds != 0).any() else np.nan
print(f"Excitatory cells with selectivity defined in both conditions: {len(s)} of {len(pvx)}")
print(f"Touch selectivity lower with light in {(ds < 0).sum()} of {len(s)}; median {s.sel_dark.median():.3f} without light, "
f"{s.sel_light.median():.3f} with; one-sided Wilcoxon p = {p:.3f}. Criterion: lower in most and p < 0.05 reproduced.")
print(s[["subject", "touch_dark", "touch_light", "wmod_dark", "wmod_light", "sel_dark", "sel_light"]].round(3).to_string(index=False))
t = pvx.dropna(subset=["touch_light", "touch_dark"])
s0 = pvx.dropna(subset=["sel_raw_light", "sel_raw_dark"])
print("First version (uncorrected selectivity): lower with light in", (s0.sel_raw_light < s0.sel_raw_dark).sum(), "of", len(s0),
", median", round(s0.sel_raw_dark.median(), 3), "->", round(s0.sel_raw_light.median(), 3))
print("\nVariant, touch response (Hz) with and without light: rises in", (t.touch_light > t.touch_dark).sum(), "of", len(t),
", median", round(t.touch_dark.median(), 2), "->", round(t.touch_light.median(), 2))
print("\nSpikes in the 25 ms after touches inside the light window, per cell (without light, with):",
list(zip(pvx.touch_spk_dark, pvx.touch_spk_light)), "; touches:", list(zip(pvx.touches_dark, pvx.touches_light)))
sj = jx.dropna(subset=["sel_light", "sel_dark"])
dsj = sj.sel_light - sj.sel_dark
print(f"Variant: PV-Cre juxtacellular cells not suppressed by light ({len(sj)}): selectivity lower with light in {(dsj < 0).sum()}, "
f"median {sj.sel_dark.median():.3f} without light and {sj.sel_light.median():.3f} with, one-sided p = "
f"{stats.wilcoxon(sj.sel_light, sj.sel_dark, alternative='less').pvalue:.3g}; touch response rises in "
f"{(sj.touch_light > sj.touch_dark).sum()}, whisking modulation rises in {(sj.wmod_light > sj.wmod_dark).sum()}")
Excitatory cells with selectivity defined in both conditions: 6 of 7 Touch selectivity lower with light in 2 of 6; median 0.083 without light, 0.949 with; one-sided Wilcoxon p = 0.953. Criterion: lower in most and p < 0.05 reproduced. subject touch_dark touch_light wmod_dark wmod_light sel_dark sel_light anm244028 -0.344 4.642 0.241 0.188 0.000 0.961 anm266945 -0.574 8.615 0.006 0.000 0.000 1.000 anm266945 -0.105 2.258 0.049 0.047 0.000 0.980 anm266945 0.398 15.556 2.012 4.547 0.165 0.774 anm266951 19.394 16.000 0.000 1.089 1.000 0.936 anm300172 2.046 4.238 0.000 0.808 1.000 0.840 First version (uncorrected selectivity): lower with light in 1 of 5 , median 0.0 -> 0.87 Variant, touch response (Hz) with and without light: rises in 5 of 7 , median 0.4 -> 8.62 Spikes in the 25 ms after touches inside the light window, per cell (without light, with): [(13, 37), (1, 29), (0, 20), (40, 5), (4, 25), (18, 8), (34, 45)] ; touches: [(582, 293), (209, 130), (380, 248), (52, 15), (201, 54), (33, 15), (391, 302)] Variant: PV-Cre juxtacellular cells not suppressed by light (30): selectivity lower with light in 14, median 0.885 without light and 0.881 with, one-sided p = 0.613; touch response rises in 20, whisking modulation rises in 18
Result: not reproduced. With the vS1 light on, touch selectivity is lower in 2 of 6 excitatory cells (median 0.083 without light, 0.949 with, p = 0.953). The touch response rises with the light (median 0.4 to 8.62 Hz) more than the whisking modulation does; the uncorrected version gives the same (lower in 1 of 5). Without light, three cells sit at selectivity 0 because their touch response inside the window is below zero, on 0 to 13 spikes after touches, so the measure is near its floor there. In the larger juxtacellular PV-Cre population not suppressed by light, selectivity is lower with light in 14 of 30 (median 0.885 against 0.881, p = 0.613), so no effect in the claim's direction is seen there either.
thalamus-drives-fs¶
Thalamic activity drives L4 FS interneurons.
rows = []
for r in l4:
for site, info in r["sites"].items():
if info["region"] != "VPM":
continue
e = light_effect(r, site)
if e is None:
continue
w = yu2016.light_windows(r, site)
row = {"path": r["path"], "subject": r["subject"], "genotype": r["genotype"], "mode": r["mode"],
"cls": cl.set_index("path").cls[r["path"]], "pulses_per_trial": w.n_pulses.median(), **e}
if r["principal"] is not None:
a, b, _, nlit, ndark = light_measures(r, site)
row.update(rate_whisk_light=a["rate_whisk"], rate_whisk_dark=b["rate_whisk"],
whisk_s_light=a["whisk_s"] / nlit, whisk_s_dark=b["whisk_s"] / ndark,
touches_light=a["n_touches"] / nlit, touches_dark=b["n_touches"] / ndark)
rows.append(row)
th = pd.DataFrame(rows)
th["included"] = th.spikes >= yu2016.MIN_SPIKES
f = th[(th.cls == "FS") & th.included]
d = f.rate_light - f.rate_dark
p = stats.wilcoxon(f.rate_light, f.rate_dark, alternative="less").pvalue
print(f"Cells with light at the VPM site: {len(th)} (genotypes {th.genotype.replace('', '(none)').value_counts().to_dict()}); "
f"tagged FS among them: {int((th.cls == 'FS').sum())}, included {len(f)}")
print(f"FS rate inside the light window: falls in {(d < 0).sum()} of {len(f)}; median {f.rate_dark.median():.1f} Hz without light, "
f"{f.rate_light.median():.1f} Hz with (median ratio {(f.rate_light / f.rate_dark).median():.2f}); one-sided Wilcoxon p = {p:.3g}. "
"Criterion: falls in most and p < 0.05 reproduced.")
print(f[["subject", "window", "pulses_per_trial", "lit_trials", "dark_trials", "rate_dark", "rate_light", "p"]].round(3).to_string(index=False))
fw = f.dropna(subset=["rate_whisk_light", "rate_whisk_dark"])
print(f"\nVariant, rate during non-touch whisking: falls in {(fw.rate_whisk_light < fw.rate_whisk_dark).sum()} of {len(fw)}, "
f"p = {stats.wilcoxon(fw.rate_whisk_light, fw.rate_whisk_dark, alternative='less').pvalue:.3g}")
o = th[th.included]
print(f"Variant, every cell with VPM light and at least 20 spikes ({len(o)}): falls in {(o.rate_light < o.rate_dark).sum()}, "
f"median ratio {(o.rate_light / o.rate_dark).median():.2f}; by class:",
o.assign(fall=o.rate_light < o.rate_dark).groupby("cls").fall.agg(["sum", "size"]).to_dict("index"))
print("By mouse (FS falls / cells):", f.assign(fall=d < 0).groupby("subject").fall.agg(["sum", "size"]).to_dict("index"))
print("Behaviour inside the light window, per trial (median over FS cells), without and with light: whisking time (s)",
round(fw.whisk_s_dark.median(), 3), "and", round(fw.whisk_s_light.median(), 3), "; touches",
round(fw.touches_dark.median(), 2), "and", round(fw.touches_light.median(), 2))
Cells with light at the VPM site: 33 (genotypes {'VGAT-ChR2-EYFP': 31, '(none)': 2}); tagged FS among them: 9, included 9
FS rate inside the light window: falls in 9 of 9; median 17.9 Hz without light, 7.7 Hz with (median ratio 0.40); one-sided Wilcoxon p = 0.00195. Criterion: falls in most and p < 0.05 reproduced.
subject window pulses_per_trial lit_trials dark_trials rate_dark rate_light p
anm260070 (0.324, 2.818) 250.0 31 71 24.476 7.657 0.000
anm260070 (0.324, 2.818) 250.0 35 84 19.633 7.928 0.000
anm260070 (0.324, 2.818) 250.0 38 110 9.824 6.637 0.055
anm260070 (0.323, 2.818) 250.0 23 69 17.949 5.106 0.000
anm260070 (0.323, 2.818) 250.0 27 64 15.813 7.853 0.000
anm260070 (0.324, 2.819) 250.0 24 57 34.687 7.699 0.000
anm273348 (0.324, 2.818) 250.0 20 57 12.296 3.348 0.001
anm273348 (0.324, 2.818) 250.0 23 54 14.613 9.379 0.002
anm273348 (0.323, 2.818) 250.0 26 67 21.751 10.128 0.000
Variant, rate during non-touch whisking: falls in 9 of 9, p = 0.00195
Variant, every cell with VPM light and at least 20 spikes (21): falls in 18, median ratio 0.59; by class: {'EXC': {'sum': 1, 'size': 1}, 'FS': {'sum': 9, 'size': 9}, 'other': {'sum': 8, 'size': 11}}
By mouse (FS falls / cells): {'anm260070': {'sum': 6, 'size': 6}, 'anm273348': {'sum': 3, 'size': 3}}
Behaviour inside the light window, per trial (median over FS cells), without and with light: whisking time (s) 0.992 and 0.595 ; touches 4.45 and 5.13
Result: reproduced. With light at the VPM site, the firing of all 9 tagged FS cells falls (median 17.9 to 7.7 Hz, median ratio 0.40, p = 0.00195), and their rate during non-touch whisking falls in 9 of 9. The light is 250 pulses per trial (100 Hz, as in Supp. Fig. 9i). The mice whisk less with the light (0.60 against 0.99 s per trial in the window), so the fall of the whisking-period rate is the stronger evidence. The cells come from 2 mice, and the drop is not specific to FS cells: 18 of 21 cells recorded with this light fire less.
sensory-input-drives-fs¶
The movement-related activity of L4 FS interneurons is driven by sensory input (reafference through the thalamus), not by a corollary discharge from motor areas.
Not tested. The supplement shows the experiment: Figure 7 compares the spike-phase relationship of FS cells in control mice and in mice whose infraorbital nerve was transected (Supp. Figs. 11, 12). Supplementary Table 2 says 9 VGAT-ChR2 mice were transected, and their cells are in the dandiset, but no file says which mice they are (data check). Blocked: metadata insufficient. The cell below is not a test: it compares the tagged cells of the mice flagged in the data check as lacking touch input with the others.
# Not a test of the claim (blocked): tagged FS cells in mice flagged as lacking touch input, against the others.
fsx = inc[inc.cls == "FS"].assign(flagged=lambda d: d.subject.isin(no_input))
print(fsx.groupby("flagged").agg(cells=("path", "size"), mice=("subject", "nunique"), touch_responsive=("touch_resp", "mean"),
whisk_modulated=("whisk_mod", "mean"), whisk_mod_hz=("whisk_mod_hz", "median"),
r2=("whisk_r2", "median"), r2_above_025=("whisk_fit", "mean"), rate_whisk=("rate_whisk", "median")).round(3).to_string())
a_, b_ = fsx[fsx.flagged], fsx[~fsx.flagged]
if len(a_) and len(b_):
print(f"Whisking modulation (Hz) lower in flagged mice: Mann-Whitney p = {stats.mannwhitneyu(a_.whisk_mod_hz, b_.whisk_mod_hz, alternative='less').pvalue:.3f}; "
f"r2 lower: p = {stats.mannwhitneyu(a_.whisk_r2, b_.whisk_r2, alternative='less').pvalue:.3f}")
print(fsx[fsx.flagged][["subject", "n_spikes", "touch_resp_hz", "touch_p", "whisk_mod_hz", "whisk_p", "whisk_r2"]].round(4).to_string(index=False))
cells mice touch_responsive whisk_modulated whisk_mod_hz r2 r2_above_025 rate_whisk flagged False 16 8 0.812 0.812 2.898 0.534 0.75 14.880 True 4 2 0.000 0.750 1.121 0.497 1.00 10.795 Whisking modulation (Hz) lower in flagged mice: Mann-Whitney p = 0.061; r2 lower: p = 0.625 subject n_spikes touch_resp_hz touch_p whisk_mod_hz whisk_p whisk_r2 anm273341 1604.0 1.3699 0.2696 1.0527 0.0066 0.3318 anm275231 1618.0 4.0000 0.1392 2.9756 0.0081 0.6663 anm275231 3075.0 1.6970 0.1273 1.1892 0.0057 0.6619 anm275231 12710.0 3.0126 0.2033 0.4982 0.2443 0.3138
In the 2 flagged mice that have included tagged cells, none of the 4 cells responds to touch, 3 are still modulated by whisking phase, and their median modulation is lower (1.12 against 2.90 Hz in 16 cells of 8 other mice, p = 0.061); the r² of the sinusoid fit is not lower (0.50 against 0.53). With 4 cells and an inferred label this says little either way.
Summary¶
| Claim | Outcome | Main number |
|---|---|---|
vpm-touch |
Reproduced | 20 of 24 VPM units touch-responsive (shifted null 0 of 24) |
vpm-whisking |
Reproduced | 21 of 24 units modulated by whisking phase (shifted null 0.12) |
fs-touch-and-whisking |
Reproduced | tagged FS cells: touch 0.65, whisking 0.80 of 20 |
exc-mainly-touch |
Not reproduced | selectivity above FS p = 0.074, above VPM p = 0.090 |
fs-suppress-movement-spiking |
Reproduced | whisking rate rises in 6 of 7 excitatory cells, p = 0.016 |
fs-enhance-touch-selectivity |
Not reproduced | selectivity lower with light in 2 of 6, p = 0.953 |
thalamus-drives-fs |
Reproduced | FS rate falls with VPM light in 9 of 9, p = 0.00195 |
sensory-input-drives-fs |
Blocked (metadata insufficient) | ION-transected mice not marked in the files |
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 full text was not available; the claims are those of
the abstract and figure titles, and the supplement was read only after the first run. No number of
the paper's main text is tested. Every measure is a choice made here: the touch window (25 ms), the
whisking band and threshold, the phase modulation and its null (the paper's sinusoid r² is shown as
a variant), the touch selectivity index and the inclusion rules. FS cells are light-tagged GABAergic
cells in VGAT-ChR2 mice, not cells inside the paper's waveform boundary, and excitatory cells are
broad-spiking untagged whole-cell cells; the paper's excitatory groups also hold cell-attached cells.
Cells from ION-transected mice, which the paper keeps apart, may be in both groups because the files
do not mark them; the 4 mice inferred here to be transected contribute 3 excitatory and 4 included FS
cells. Leaving them out is shown as a variant. Under it the selectivity test of exc-mainly-touch
passes (p = 0.024 and 0.038), but the outcome stays not reproduced, because the flag is drawn from
touch responses and favours the claim. The whisking test of the claims file (Rayleigh) was replaced
after the first run, when its null control failed; both are shown. Membrane potential, the subject of
Figures 2 to 4, is not analysed except to find cells with a touch input.
What was found about the dandiset.
- No file names the cell type of its L4 neuron, the opsin, or what each light condition does;
photo_stim_modehas no description, its labels do not map to one kind of light, andphoto_stim_poweris empty in every L4 file. - No file marks the ION-transected mice of Figure 7 (9 VGAT-ChR2 mice in Supplementary Table 2); 4 mice whose cells have no touch input are probably among them.
- The genotype is missing for the 6 VPM mice and 3 L4 mice, and SOM-IRES-Cre x Ai32 mice are
labelled
Ai32. - Every L4 file gives layer 4, though the paper's waveform analysis includes L5 cells.
- The session description gives the wrong recording mode for 24 of 130 L4 files, all from PV-Cre mice (23 described as whole-cell hold juxtacellular traces); by the trace, PV-Cre has 9 whole-cell cells where Supplementary Table 2 has 5.
- The L4 trace is sampled at 10 kHz in files up to March 2015 and at 20 kHz from late August 2015, which only the timestamps show.
- In the L4 files
first_touch_timeis the trial start plus the true latency divided by 1000; in the VPM files it is correct. - 8 files name no principal whisker.
- No recording is deposited twice: every trace is distinct, and files of equal length differ.