Reproducing Yu et al. (2016) from dandiset 000005¶

Paper: Yu J, Gutnisky DA, Hires SA, Svoboda K (2016). Layer 4 fast-spiking interneurons filter thalamocortical signals during active somatosensation. Nature Neuroscience 19:1647-1657, doi:10.1038/nn.4412.

Dandiset: DANDI:000005, version 0.220126.1853. The paper cites no dandiset version (it predates the archive). 0.220126.1853 is the only published version, so it is pinned; the draft (last modified 2026-02-12) lists the same 148 asset paths and sizes (printed below). The version is the same as that of the earlier attempt, 000005-yu-2016-01.

Claims: claims/000005/10.1038-nn.4412.yaml, written by a reader from the version of record with its Online Methods and the supplement (Supplementary Figures 1 to 14, Supplementary Tables 1 and 2), and checked by a second reader on 2026-10-20. It replaces the claims file of the earlier attempt, which was written from the abstract. The ids of eight earlier claims are kept; their criteria are new. The paper's data statement points to CRCNS ssc-7; no analysis code was found.

This is a repeat attempt. The earlier attempt's findings are checked again in the data check.

Outside input: Supplementary Table 1 of the paper (the mouse line of each mouse), read at run time from the supplement PDF on the publisher's site. It is used to name the mouse lines that the files leave blank and to compare the cells per line with Supplementary Table 2.

Claim Tested
behavior-touches yes
vpm-touch yes
vpm-whisking yes
vpm-whisking-phase yes
exc-vm-touch-whisking yes
exc-threshold-touch-vs-whisking yes
exc-effective-reversal yes
exc-inhibition-timing blocked: data missing
exc-inhibition-whisking blocked: data missing
fs-touch-response yes
fs-vm-touch-whisking yes
fs-whisking-rate yes
fs-whisking-phase yes
fs-touch-and-whisking yes
exc-mainly-touch yes
vpm-photoinhibition blocked: data missing
exc-thalamus-photoinhibition yes
thalamus-drives-fs run; blocked, too few cells
rabies-vpm-input blocked: data missing
ion-cut-exc-no-touch, ion-cut-exc-whisking, ion-cut-exc-phase, ion-cut-fs-no-touch, sensory-input-drives-fs, ion-cut-fs-phase, ion-cut-behavior blocked: metadata insufficient
pv-enphr-suppression yes
fs-suppress-movement-spiking yes
fs-enhance-touch-selectivity yes
exc-touch-spikes-enphr yes
auc-weak-strong yes
auc-groups yes
behavior-false-alarms yes

Readings. The claims file names readings for details the paper leaves open (O1 filter direction, O2 samples of the rank-sum test, O3 threshold rule, O4 resting Vm, O5 light windows, O6 fraction of control, O7 laser powers, O8 derivative, O9 time of a crossing bin, O10 whisking-onset windows, O11 touches bounding whisking epochs). Each is run where it applies.

Answers. A reader answered this attempt's seven questions on 2026-10-20 (questions/000005.yaml), and the claims file was updated: dVm/dt is taken at the recording's sampling rate, not in 1 ms bins (O8); the PSTH latency is run with the bin's left edge and its centre (O9); fs-touch-response runs four FS readings (driven at p < 0.001 or p < 0.01, each with and without the waveform boundary); behavior-false-alarms runs the chi-squared test with and without Yates's correction; the paired tests of thalamus-drives-fs are two-sided; behavior-touches runs every trial and trials without light; exc-vm-touch-whisking runs the whisking-onset and touch readings O10 and O11. The sections of the claims these bear on say what each answer changed. Cell groups hold the cells of every mouse ("all"), since the files do not mark the ION-cut mice; a variant without the mice flagged as lacking touch input ("no_flagged") is shown and does not decide an outcome.

Revision after the first verification round. The tag test now counts every 5 ms pulse of the 5 and 10 Hz trains (the earlier rule kept only pulses with no other pulse within 100 ms); the touch readings O11 are run for every claim on whisking epochs, and O10 and O11 for every claim on whisking onsets; trials of a whole-cell cell held near 0 mV are left out at rest; and the data check adds spike detection quality, the tagging coverage of every VGAT-ChR2 cell, the injected current of every file, the pole times and the ION-cut candidates. New outputs show the cause of the near-zero Vm latencies and the stability of the AUC claims over 20 seeds.

In [1]:
import re
import time
import warnings

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy import stats

import yu2016

pd.set_option("display.width", 220)
pd.set_option("display.max_columns", 40)
pd.set_option("display.max_rows", 300)
warnings.filterwarnings("ignore", category=RuntimeWarning)

started = time.time()
recs = yu2016.load_all()
vpm = [r for r in recs if r["kind"] == "VPM"]
l4 = [r for r in recs if r["kind"] == "L4"]
by_path = {r["path"]: r for r in l4}
print(f"loaded {len(recs)} files from dandiset {yu2016.DANDISET_ID} version {yu2016.DANDISET_VERSION} "
      f"in {time.time() - started:.0f} s")
loaded 148 files from dandiset 000005 version 0.220126.1853 in 355 s

Data check¶

No random numbers are drawn in the data check. Each check of the metadata against the values is stated under its output.

In [2]:
vc = yu2016.versions_check()
print("Versions (identifier, last modified):", vc["versions"])
print("Assets in the pinned version:", vc["pinned_assets"], "; in the draft:", vc["draft_assets"],
      "; same paths and sizes:", vc["draft_same_paths_and_sizes"])
files = pd.DataFrame([{"kind": r["kind"], "subject": r["subject"], "genotype": r["genotype"] or "(none)",
                       "path": r["path"], "trials": len(r["trials"])} for r in recs])
print("Files by kind:", files.kind.value_counts().to_dict(), "; subjects:", files.subject.nunique(),
      "(VPM", files[files.kind == "VPM"].subject.nunique(), ", L4", files[files.kind == "L4"].subject.nunique(),
      ", in both", len(set(files[files.kind == "VPM"].subject) & set(files[files.kind == "L4"].subject)), ")")
print("Trials: VPM", int(files[files.kind == "VPM"].trials.sum()), ", L4", int(files[files.kind == "L4"].trials.sum()))
lines = yu2016.supplement_mouse_lines()
line_of = dict(zip(lines.subject, lines.line))
files["line"] = files.subject.map(line_of)
print(f"\nSupplementary Table 1 lists {len(lines)} mice; {files.subject.drop_duplicates().isin(lines.subject).sum()} "
      f"of the {files.subject.nunique()} mice of the dandiset are in it.")
print("Files by genotype in the files (rows) against the mouse line in Supplementary Table 1 (columns):")
print(pd.crosstab([files.kind, files.genotype], files.line.fillna("(not listed)")).to_string())
print("Mice without a genotype in their files, or given as Ai32:",
      files[files.genotype.isin(["(none)", "Ai32"])].drop_duplicates("subject").groupby(["genotype", "line"]).subject.apply(list).to_dict())

meta = pd.DataFrame([{"path": r["path"], "subject": r["subject"], "general_fields": ",".join(r["general_fields"]),
                      "subject_fields": ",".join(r["subject_fields"]),
                      "region": r["ic_location"].split(";")[0].replace("brain_region:", "").strip(),
                      "layer": r["ic_location"].split("cortical_layer:")[1].split(";")[0].strip(),
                      "dv_mm": float(r["ic_location"].split("coordinate_dv:")[1].split(";")[0])} for r in l4])
print("\nFields under general/ in the L4 files:", meta.general_fields.value_counts().to_dict())
print("Fields under general/subject:", meta.subject_fields.value_counts().to_dict())
print("Electrode location of the L4 files: region", meta.region.value_counts().to_dict(), "; cortical layer",
      meta.layer.value_counts().to_dict(), f"; depth (coordinate_dv) {meta.dv_mm.min():.2f} to {meta.dv_mm.max():.2f} mm")
print("VPM electrode groups:", pd.Series([loc.split(";")[1].strip() for r in vpm for loc in r["location"]]).value_counts().to_dict())
terms = {"ION": r"\bION\b", "infraorbital": r"(?i)infraorbital", "nerve": r"(?i)\bnerves?\b", "transect": r"(?i)transect",
         "lesion": r"(?i)lesion", "cut": r"(?i)\bcut\b", "surgery": r"(?i)surg", "sham": r"(?i)\bsham\b",
         "control": r"(?i)\bcontrol\b", "rabies": r"(?i)rabies", "histology": r"(?i)histolog", "eNpHR": r"(?i)NpHR|halorhodopsin"}
print("L4 files whose metadata text (every string under general/ and the session description) contains each term:",
      {k: sum(bool(re.search(v, r["metadata_text"])) for r in l4) for k, v in terms.items()})
print("Units in the VPM files:", sum(len(r["units"]) for r in vpm), "in", len(vpm), "files; unit descriptions start with",
      pd.Series([u["desc"].split(".")[0] for r in vpm for u in r["units"]]).value_counts().to_dict(),
      "; VPM files with a photostimulation series:", sum(r["has_light"] for r in vpm))
print("L4 files with an electrode outside barrel cortex:", int((meta.region != "barrel cortex").sum()))
Versions (identifier, last modified): [('draft', '2026-02-12 14:38:18'), ('0.220126.1853', '2022-01-26 18:53:27')]
Assets in the pinned version: 148 ; in the draft: 148 ; same paths and sizes: True
Files by kind: {'L4': 130, 'VPM': 18} ; subjects: 55 (VPM 6 , L4 49 , in both 0 )
Trials: VPM 7656 , L4 11211
Supplementary Table 1 lists 55 mice; 55 of the 55 mice of the dandiset are in it.
Files by genotype in the files (rows) against the mouse line in Supplementary Table 1 (columns):
line                 C57BL/6  PV-ChR2  PV-IRES-Cre  PV-IRES-Cre x Ai32  SOM-IRES-Cre x Ai32  VGAT-ChR2-EYFP
kind genotype                                                                                              
L4   (none)                0        1            0                   1                    0               1
     Ai32                  0        0            0                   0                    5               0
     C57BL6                8        0            0                   0                    0               0
     PV-Cre                0        0           48                   0                    0               0
     VGAT-ChR2-EYFP        0        0            0                   0                    0              66
VPM  (none)                0        0            0                   0                    0              18
Mice without a genotype in their files, or given as Ai32: {('(none)', 'PV-ChR2'): ['anm250509'], ('(none)', 'PV-IRES-Cre x Ai32'): ['anm223517'], ('(none)', 'VGAT-ChR2-EYFP'): ['anm184389', 'anm186997', 'anm199549', 'anm199551', 'anm199552', 'anm203464', 'anm260074'], ('Ai32', 'SOM-IRES-Cre x Ai32'): ['anm308247', 'anm324650', 'anm337495', 'anm337496', 'anm342684']}

Fields under general/ in the L4 files: {'devices,experiment_description,experimenter,institution,intracellular_ephys,keywords,optogenetics,related_publications,subject': 121, 'devices,experiment_description,experimenter,institution,intracellular_ephys,keywords,related_publications,subject': 9}
Fields under general/subject: {'genotype,sex,species,subject_id': 130}
Electrode location of the L4 files: region {'barrel cortex': 130} ; cortical layer {'4': 130} ; depth (coordinate_dv) 0.42 to 0.62 mm
VPM electrode groups: {'brain_subregion: VPM': 18}
L4 files whose metadata text (every string under general/ and the session description) contains each term: {'ION': 0, 'infraorbital': 0, 'nerve': 0, 'transect': 0, 'lesion': 0, 'cut': 0, 'surgery': 0, 'sham': 0, 'control': 0, 'rabies': 0, 'histology': 0, 'eNpHR': 0}
Units in the VPM files: 29 in 18 files; unit descriptions start with {'thalamic': 29} ; VPM files with a photostimulation series: 0
L4 files with an electrode outside barrel cortex: 0

0.220126.1853 is the only published version, and the draft holds the same asset paths and sizes. The 18 VPM files come from 6 mice and the 130 L4 files from 49 others; all 55 mice are in Supplementary Table 1. Where a file gives a genotype it agrees with the table's mouse line, but 9 mice have none (the 6 VPM mice and 3 L4 mice) and the 5 SOM-IRES-Cre x Ai32 mice are given as Ai32 (earlier finding 3, still present). The files hold no surgery, lesion, opsin or notes field, and no text names ION, infraorbital, nerve, transection, cut, sham or eNpHR (earlier findings 2 and 5, still present). Every L4 file gives barrel cortex, layer 4, at 0.42 to 0.62 mm (earlier finding 4, still present). The VPM files hold no photostimulation series, and no L4 file is outside barrel cortex, so the thalamic multiunit recordings of Fig. 6b,c are not in the dandiset.

In [3]:
mode = pd.DataFrame([{"path": r["path"], "subject": r["subject"], "line": line_of[r["subject"]], "label": r["label_mode"],
                      "from_trace": r["mode"], "vm_median": r["vm_median"], "lowfreq_sd": r["lowfreq_sd_mv"], "vm_hz": r["vm_hz"],
                      "trial_s": r["vm_trial_s"], "spikes": len(r["spikes"]),
                      "cur_conv": r.get("current_conversion", np.nan), "cur_unit": r.get("current_unit", ""),
                      "cur_test": r.get("current_end_of_trial_median", np.nan), "cur_min": r.get("current_trial_min", np.nan),
                      "trials_near_0mV": int(np.sum(r["trial_vm_median"] > -20)) if r["mode"] == "whole-cell" else 0,
                      "cur_trial_median": r.get("current_trial_median", np.nan),
                      "trials_current_above_0.5": int(np.nansum(r.get("trial_current_median", np.zeros(1)) > 0.5)),
                      "trial_vm_median_min": float(np.nanmin(r["trial_vm_median"])), "trial_vm_median_max": float(np.nanmax(r["trial_vm_median"]))}
                     for r in l4])
print("Recording mode in the session description (rows) against the trace (columns; whole-cell when the median trace "
      "is below -20 mV):")
print(pd.crosstab([mode.line, mode.label], mode.from_trace).to_string())
print("\nMedian of the trace and s.d. of its 1 to 100 Hz band (stored values; unit 'volts', conversion 0.001, so mV):")
print(mode.groupby("from_trace")[["vm_median", "lowfreq_sd"]].describe().round(2).T.to_string())
print("Trace sampling rate (Hz) and trial length (s), files:", mode.groupby(["vm_hz", "trial_s"]).size().to_dict())
print("First file at 20 kHz:", min(p.split("_ses-")[1][:8] for p in mode[mode.vm_hz == 20000].path),
      "; last at 10 kHz:", max(p.split("_ses-")[1][:8] for p in mode[mode.vm_hz == 10000].path))
ts = pd.DataFrame([{"outside": r["vm_outside_trials"], "first_ms": r["vm_block_first_rel"] * 1000} for r in l4])
print("Trace samples outside every trial:", int(ts.outside.sum()), "; largest offset of a trial's first sample from the "
      "trial start (ms):", round(ts.first_ms.max(), 2))
print("\nInjected current (CurrentClampStimulus): unit", mode.cur_unit.unique().tolist(), ", conversion",
      mode.cur_conv.unique().tolist(), "; stored value of the end-of-trial test pulse in whole-cell files: median",
      round(mode[mode.from_trace == "whole-cell"].cur_min.median(), 3), "(files at about -0.1:",
      int(np.isclose(mode[mode.from_trace == "whole-cell"].cur_min, -0.1, atol=0.01).sum()), "of",
      int((mode.from_trace == "whole-cell").sum()), "); the Methods give a -0.1 nA test pulse")
print("\nWhole-cell files with trials whose median Vm is above -20 mV (a cell held near 0 mV by current):")
print(mode[mode.trials_near_0mV > 0][["path", "line", "trials_near_0mV", "vm_median"]].to_string(index=False))
print("Held trials (median Vm above -20 mV) are left out of every whole-cell measure at rest:", int(sum(r["held_trials"].sum() for r in l4)),
      "trials in", sum(bool(r["held_trials"].any()) for r in l4), "file")
# Fig. 3 recordings: depolarizing current of about 1 nA (stored in nA, see above) in any trial of any file
print("\nMedian injected current per trial (stored values, nA), over files, by mode from the trace:")
print(mode.groupby("from_trace").cur_trial_median.describe().round(3).to_string())
print("Files with any trial whose median injected current is above 0.5 nA:",
      mode[mode["trials_current_above_0.5"] > 0][["path", "from_trace", "line", "trials_current_above_0.5", "cur_trial_median", "vm_median", "lowfreq_sd"]]
      .assign(path=lambda d: d.path.str.split("/").str[1].str[:40]).round(3).to_dict("records"))
cur_ = mode[mode["trials_current_above_0.5"] > 0]
print("These files:", len(cur_), "; modes", cur_.from_trace.value_counts().to_dict(), "; lines", cur_.line.value_counts().to_dict(), "; 1 to 100 Hz s.d. at most",
      round(cur_.lowfreq_sd.max(), 2), "mV (whole-cell files: median", round(mode[mode.from_trace == "whole-cell"].lowfreq_sd.median(), 2), "mV)")
ca_ = mode[mode.from_trace == "cell-attached"]
print("Cell-attached files (by the trace): trial-median Vm from", round(ca_.trial_vm_median_min.min(), 2), "to", round(ca_.trial_vm_median_max.max(), 2),
      "mV; files with any trial-median Vm below -20 mV:", int((ca_.trial_vm_median_min < -20).sum()))
print("Cell-attached files (by the trace) with a 1 to 100 Hz s.d. above 0.5 mV:",
      mode[(mode.from_trace == "cell-attached") & (mode.lowfreq_sd > 0.5)][["path", "vm_median", "lowfreq_sd"]].round(2).to_dict("records"))
Recording mode in the session description (rows) against the trace (columns; whole-cell when the median trace is below -20 mV):
from_trace                         cell-attached  whole-cell
line                label                                   
C57BL/6             whole-cell                 0           8
PV-ChR2             whole-cell                 0           1
PV-IRES-Cre         juxtacellular             16           1
                    whole-cell                23           8
PV-IRES-Cre x Ai32  whole-cell                 0           1
SOM-IRES-Cre x Ai32 juxtacellular              4           0
                    whole-cell                 0           1
VGAT-ChR2-EYFP      juxtacellular             30           0
                    whole-cell                 0          37

Median of the trace and s.d. of its 1 to 100 Hz band (stored values; unit 'volts', conversion 0.001, so mV):
from_trace        cell-attached  whole-cell
vm_median  count          73.00       57.00
           mean            1.76      -60.82
           std             5.19        9.09
           min           -16.89      -76.81
           25%             0.09      -67.37
           50%             2.26      -62.51
           75%             4.44      -56.02
           max            12.97      -39.21
lowfreq_sd count          73.00       57.00
           mean            0.19        2.43
           std             0.18        1.27
           min             0.08        0.14
           25%             0.12        1.58
           50%             0.15        1.95
           75%             0.18        3.21
           max             1.30        5.85
Trace sampling rate (Hz) and trial length (s), files: {(10000.0, 5.2): 29, (10000.0, 5.4999): 1, (10000.0, 5.5): 62, (20000.0, 5.9): 38}
First file at 20 kHz: 20150827 ; last at 10 kHz: 20150320
Trace samples outside every trial: 0 ; largest offset of a trial's first sample from the trial start (ms): 4.76

Injected current (CurrentClampStimulus): unit ['amperes'] , conversion [1e-06] ; stored value of the end-of-trial test pulse in whole-cell files: median 0.0 (files at about -0.1: 14 of 57 ); the Methods give a -0.1 nA test pulse

Whole-cell files with trials whose median Vm is above -20 mV (a cell held near 0 mV by current):
                                                              path    line  trials_near_0mV  vm_median
sub-anm250509/sub-anm250509_ses-20140817_behavior+icephys+ogen.nwb PV-ChR2               13 -39.208553
Held trials (median Vm above -20 mV) are left out of every whole-cell measure at rest: 13 trials in 1 file

Median injected current per trial (stored values, nA), over files, by mode from the trace:
               count   mean    std  min  25%  50%  75%    max
from_trace                                                   
cell-attached   73.0  0.442  0.807 -0.0  0.0  0.0  0.0  1.897
whole-cell      57.0  0.004  0.010 -0.0  0.0  0.0  0.0  0.043
Files with any trial whose median injected current is above 0.5 nA: [{'path': 'sub-anm260070_ses-20150217_obj-1p0sabo_b', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 110, 'cur_trial_median': 1.897, 'vm_median': -2.274, 'lowfreq_sd': 0.168}, {'path': 'sub-anm260070_ses-20150217_obj-2vta88_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 126, 'cur_trial_median': 1.896, 'vm_median': 2.783, 'lowfreq_sd': 0.101}, {'path': 'sub-anm260070_ses-20150218_obj-vgzohl_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 154, 'cur_trial_median': 1.897, 'vm_median': 2.883, 'lowfreq_sd': 0.145}, {'path': 'sub-anm260070_ses-20150219_obj-1dodaf3_b', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 122, 'cur_trial_median': 1.897, 'vm_median': 6.47, 'lowfreq_sd': 0.331}, {'path': 'sub-anm260070_ses-20150219_obj-trbrrk_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 96, 'cur_trial_median': 1.896, 'vm_median': 4.445, 'lowfreq_sd': 0.149}, {'path': 'sub-anm260070_ses-20150223_obj-t5wgzh_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 92, 'cur_trial_median': 1.896, 'vm_median': 4.578, 'lowfreq_sd': 0.134}, {'path': 'sub-anm260070_ses-20150223_obj-vy6mbx_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 83, 'cur_trial_median': 1.897, 'vm_median': 3.636, 'lowfreq_sd': 0.147}, {'path': 'sub-anm260077_ses-20150301_obj-1ox41vu_b', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 50, 'cur_trial_median': 1.897, 'vm_median': 0.79, 'lowfreq_sd': 0.18}, {'path': 'sub-anm260077_ses-20150301_obj-fmhxwt_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 38, 'cur_trial_median': 1.896, 'vm_median': 1.533, 'lowfreq_sd': 0.182}, {'path': 'sub-anm260077_ses-20150304_behavior+icep', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 42, 'cur_trial_median': 1.897, 'vm_median': 6.996, 'lowfreq_sd': 0.137}, {'path': 'sub-anm273347_ses-20150224_obj-1h07z6p_b', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 45, 'cur_trial_median': 1.896, 'vm_median': 6.515, 'lowfreq_sd': 0.226}, {'path': 'sub-anm273347_ses-20150224_obj-i6vwao_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 50, 'cur_trial_median': 1.897, 'vm_median': 5.431, 'lowfreq_sd': 0.124}, {'path': 'sub-anm273348_ses-20150226_obj-1cv3c91_b', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 78, 'cur_trial_median': 1.897, 'vm_median': 2.491, 'lowfreq_sd': 0.111}, {'path': 'sub-anm273348_ses-20150226_obj-1m5xcbf_b', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 81, 'cur_trial_median': 1.896, 'vm_median': 2.799, 'lowfreq_sd': 0.168}, {'path': 'sub-anm273348_ses-20150226_obj-efumcd_be', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 95, 'cur_trial_median': 1.897, 'vm_median': 4.86, 'lowfreq_sd': 0.153}, {'path': 'sub-anm273348_ses-20150227_behavior+icep', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 58, 'cur_trial_median': 1.897, 'vm_median': 3.309, 'lowfreq_sd': 0.148}, {'path': 'sub-anm281589_ses-20150319_behavior+icep', 'from_trace': 'cell-attached', 'line': 'VGAT-ChR2-EYFP', 'trials_current_above_0.5': 168, 'cur_trial_median': 1.896, 'vm_median': 5.36, 'lowfreq_sd': 0.141}]
These files: 17 ; modes {'cell-attached': 17} ; lines {'VGAT-ChR2-EYFP': 17} ; 1 to 100 Hz s.d. at most 0.33 mV (whole-cell files: median 1.95 mV)
Cell-attached files (by the trace): trial-median Vm from -21.85 to 20.72 mV; files with any trial-median Vm below -20 mV: 1
Cell-attached files (by the trace) with a 1 to 100 Hz s.d. above 0.5 mV: [{'path': 'sub-anm244025/sub-anm244025_ses-20141026_obj-1npw0ls_behavior+icephys+ogen.nwb', 'vm_median': -8.17, 'lowfreq_sd': 1.09}, {'path': 'sub-anm266945/sub-anm266945_ses-20141220_obj-7o69ev_behavior+icephys+ogen.nwb', 'vm_median': -16.89, 'lowfreq_sd': 1.3}]

The recording mode in the session description disagrees with the trace for 24 files, all from PV-IRES-Cre mice: 23 described as whole-cell sit near 0 mV with a 1 to 100 Hz s.d. of 0.08 to 0.3 mV and small extracellular spikes, as cell-attached recordings do, and one described as juxtacellular sits near -65 mV (earlier finding 6, still present). The mode read from the trace is used below; the stored trace is in mV (conversion 0.001 to volts), which the whole-cell resting potentials confirm. The trace is sampled at 10 kHz up to March 2015 and at 20 kHz from late August 2015, which only the timestamps state (earlier finding 8, still present); timestamps start at the trial start and no sample lies outside a trial.

New: the injected current (CurrentClampStimulus) is stored with unit amperes and conversion 1e-6, so a stored -0.1 would be -0.1 uA. The end-of-trial test pulse, which the Methods give as -0.1 nA, is stored as about -0.1 in 14 of the 57 whole-cell files, so the stored values are in nA and the conversion is wrong by a factor of 1,000. Only one whole-cell file has trials held near 0 mV (13 trials of the PV-ChR2 cell); these trials are now left out of every measure at rest. 17 cell-attached files of VGAT-ChR2 mice carry a stored current of about 1.9 in every trial, but their traces sit near 0 mV with a 1 to 100 Hz s.d. of at most 0.33 mV (whole-cell files: median 1.95 mV), so they are not whole-cell cells held near 0 mV; no trial of any other file has a median current above 0.5. The trial-median trace of the cell-attached files spans -21.85 to 20.72 mV, and only one file has a trial below -20 mV. So the recordings of L4 excitatory neurons depolarized to reveal inhibition (Fig. 3, 6 neurons) are not in the dandiset. Two cell-attached files have a larger slow component (1.1 and 1.3 mV), still far below the whole-cell values; both are from PV-IRES-Cre mice.

In [4]:
wh = []
for r in recs:
    b = r["beh"]
    row = {"kind": r["kind"], "path": r["path"], "principal": b["principal"] is not None, "whiskers": len(b["whisker_names"])}
    if b["principal"] is not None:
        row.update(dt_ms=b["dt_median"] * 1000, angle_min=b["angle_range"][0], angle_max=b["angle_range"][1],
                   angle_nan=b["angle_nan"], touches=len(b["touch_on"]), licks=len(b["licks"]),
                   samples_per_trial=float(np.median(np.bincount(b["trial"][b["trial"] >= 0])[np.bincount(b["trial"][b["trial"] >= 0]) > 0])))
    wh.append(row)
wh = pd.DataFrame(wh)
p = wh[wh.principal]
print("Files with a whisker named principal:", len(p), "of", len(wh), "; without:", [x.split("/")[1] for x in wh[~wh.principal].path])
print("Whisker sampling interval (ms):", sorted(p.dt_ms.round(3).unique().tolist()), "; samples per trial, files by value:",
      p.samples_per_trial.value_counts().sort_index().to_dict())
print("Principal whisker angle (deg): min", round(p.angle_min.min(), 1), ", max", round(p.angle_max.max(), 1),
      "; median fraction of NaN samples", round(p.angle_nan.median(), 4))
print("Touch onsets of the principal whisker per file: median", int(p.touches.median()), ", range", int(p.touches.min()),
      "to", int(p.touches.max()), "; lick onsets per file: median", int(p.licks.median()))
ep = pd.DataFrame([{"kind": r["kind"], **{f"{f}:{k}": r["beh"][f][k].sum() / 1000 for f in yu2016.FILTERS for k in ("whisk", "nonwhisk")},
                    **{f"{f}:onsets": len(r["beh"][f]["onsets"]) for f in yu2016.FILTERS}} for r in recs if r["beh"]["principal"]])
print("\nWhisking epochs and nonwhisking periods (s per file) and whisking onsets per file, medians, by filter reading:")
print(ep.groupby("kind").median().round(1).T.to_string())

rows = []
for r in recs:
    b = r["beh"]
    if b["principal"] is None:
        continue
    on_t = b["t"][b["touch_on"]]
    tr = r["trials"]
    for s, e, ft in zip(tr.start, tr.stop, tr.first_touch_time):
        x = on_t[(on_t >= s) & (on_t < e)]
        if len(x) and np.isfinite(ft):
            rows.append({"kind": r["kind"], "table_rel": ft - s, "series_rel": x[0] - s})
ft = pd.DataFrame(rows)
for kind, g in ft.groupby("kind"):
    print(f"\n{kind}: {len(g)} trials with a touch. first_touch_time minus trial start, median {g.table_rel.median():.4f} s; "
          f"first touch onset of the series, median {g.series_rel.median():.3f} s; table equals series within 2 ms: "
          f"{np.mean(np.abs(g.table_rel - g.series_rel) < 0.002):.3f}; table x 1000 within 2 ms of the series: "
          f"{np.mean(np.abs(g.table_rel * 1000 - g.series_rel) < 0.002):.3f}")

# pole times in the trial table, relative to the trial start (AUC reading a uses them; first_touch_time is scaled wrongly)
pt = pd.DataFrame([{"kind": r["kind"], "pole_in": x0 - s, "pole_out": x1 - s} for r in recs
                   for s, x0, x1 in zip(r["trials"].start, r["trials"].pole_in_time, r["trials"].pole_out_time)])
print("\npole_in_time and pole_out_time minus trial start (s), by kind:")
print(pt.groupby("kind").describe(percentiles=[0.05, 0.5, 0.95]).round(3).T.to_string())
print("L4 trials with pole_in after the trial start and pole_out within the 5 s video, 0.5 s or more apart:",
      round(float(((pt.kind == "L4") & (pt.pole_in >= 0) & (pt.pole_out <= 5) & (pt.pole_out - pt.pole_in >= 0.5)).sum() / (pt.kind == "L4").sum()), 3))
# median first touch of the touch series against pole in, L4
ftp = []
for r in l4:
    b = r["beh"]
    if b["principal"] is None:
        continue
    on_t = b["t"][b["touch_on"]]
    for s, e, x0 in zip(r["trials"].start, r["trials"].stop, r["trials"].pole_in_time):
        x = on_t[(on_t >= s) & (on_t < e)]
        if len(x):
            ftp.append(x[0] - x0)
print("L4 trials with a touch: first touch onset minus pole_in_time (s), median", round(float(np.median(ftp)), 3), ", 5th percentile",
      round(float(np.percentile(ftp, 5)), 3), ", fraction negative", round(float(np.mean(np.array(ftp) < 0)), 4))

# VPM: spikes per touch (VPM definition) of each unit for the touches of each tracked whisker
rows = []
for r in vpm:
    b = r["beh"]
    allm = np.ones(len(r["trials"]), bool)
    for u in r["units"]:
        c = yu2016.counts_1k(u["spikes"], b)
        res = {}
        for name, w in b["whisker_touches"].items():
            bb = yu2016.with_whisker(b, name)
            s = yu2016.spikes_per_touch(c, b["trial"], yu2016.touches(bb, allm, 0.025), "vpm")
            res[name] = s["spt"] if s["n_touches"] >= 20 else np.nan
        pw = b["principal"].split("_")[-1].upper()
        best = max(res, key=lambda k: -np.inf if np.isnan(res[k]) else res[k])
        rows.append({"path": r["path"], "unit": u["unit"], "file_pw": pw, "best": best, "spt_pw": res[pw], "spt_best": res[best],
                     "whiskers": len(res)})
pwu = pd.DataFrame(rows)
multi = pwu[pwu.whiskers > 1]
print(f"\nVPM units in files with more than one tracked whisker: {len(multi)}; units whose largest response (spikes per touch, "
      f"at least 20 touches) is to a whisker other than the file's principal one: {int((multi.best != multi.file_pw).sum())}")
print(multi[multi.best != multi.file_pw].assign(path=lambda d: d.path.str.split("/").str[1].str[:30]).round(2).to_string(index=False))
Files with a whisker named principal: 140 of 148 ; without: ['sub-anm230103_ses-20140131_behavior+icephys+ogen.nwb', 'sub-anm266945_ses-20141222_obj-ianjb9_behavior+icephys+ogen.nwb', 'sub-anm281589_ses-20150320_behavior+icephys+ogen.nwb', 'sub-anm300172_ses-20150908_obj-n5zk5z_behavior+icephys+ogen.nwb', 'sub-anm318530_ses-20151128_behavior+icephys+ogen.nwb', 'sub-anm318532_ses-20151125_obj-1kqnryo_behavior+icephys+ogen.nwb', 'sub-anm324650_ses-20160422_behavior+icephys+ogen.nwb', 'sub-anm337496_ses-20160524_behavior+icephys+ogen.nwb']
Whisker sampling interval (ms): [1.0] ; samples per trial, files by value: {5000.0: 140}
Principal whisker angle (deg): min -98.4 , max 94.4 ; median fraction of NaN samples 0.0
Touch onsets of the principal whisker per file: median 354 , range 0 to 4265 ; lick onsets per file: median 842

Whisking epochs and nonwhisking periods (s per file) and whisking onsets per file, medians, by filter reading:
kind                  L4     VPM
filtfilt:whisk      77.4   402.3
filtfilt:nonwhisk  132.1  1164.3
lfilter:whisk       79.3   425.5
lfilter:nonwhisk   124.9  1117.0
filtfilt:onsets     69.0   410.0
lfilter:onsets      69.5   402.5
L4: 5627 trials with a touch. first_touch_time minus trial start, median 0.0014 s; first touch onset of the series, median 1.373 s; table equals series within 2 ms: 0.000; table x 1000 within 2 ms of the series: 0.956

VPM: 3154 trials with a touch. first_touch_time minus trial start, median 1.3240 s; first touch onset of the series, median 1.324 s; table equals series within 2 ms: 1.000; table x 1000 within 2 ms of the series: 0.000

pole_in_time and pole_out_time minus trial start (s), by kind:
kind                   L4       VPM
pole_in  count  11211.000  7656.000
         mean       0.840     1.012
         std        0.043     0.198
         min        0.832     0.000
         5%         0.832     0.428
         50%        0.832     1.060
         95%        0.833     1.077
         max        1.084     1.296
pole_out count  11211.000  7656.000
         mean       3.485     3.026
         std        1.783     0.642
         min        2.433     0.000
         5%         2.803     2.171
         50%        2.835     3.376
         95%        6.524     3.558
         max       44.315     3.752
L4 trials with pole_in after the trial start and pole_out within the 5 s video, 0.5 s or more apart: 0.899
L4 trials with a touch: first touch onset minus pole_in_time (s), median 0.528 , 5th percentile 0.265 , fraction negative 0.0
VPM units in files with more than one tracked whisker: 20; units whose largest response (spikes per touch, at least 20 touches) is to a whisker other than the file's principal one: 11
                          path  unit file_pw best  spt_pw  spt_best  whiskers
sub-anm184389_ses-20130207_beh     0      B2   B1    0.12      0.24         3
sub-anm184389_ses-20130207_beh     1      B2 BETA    0.63      0.96         3
sub-anm184389_ses-20130211_beh     1    BETA   B1   -0.01      1.02         3
sub-anm184389_ses-20130212_beh     0      B2   B1    0.08      0.31         3
sub-anm184389_ses-20130212_beh     1      B2   B1    0.81      0.91         3
sub-anm184389_ses-20130213_beh     0      B1   B2     NaN      0.17         3
sub-anm184389_ses-20130213_beh     1      B1   B2     NaN      0.13         3
sub-anm184389_ses-20130213_beh     2      B1 BETA     NaN      1.07         3
sub-anm184389_ses-20130213_beh     3      B1   B2     NaN      0.03         3
sub-anm184389_ses-20130213_beh     4      B1   B2     NaN      0.03         3
sub-anm203464_ses-20130702_beh     0      C2   C3     NaN      1.24         3

The whisker variables are at 1 kHz, 5,000 samples (5 s) per trial, in degrees with a plausible range. 8 files name no principal whisker (earlier finding 9, still present); they are left out of every measure that needs touch or whisking. In the L4 files first_touch_time matches the touch series only when multiplied by 1000 (0.956 of trials), so the latency was divided by 1000 once too often; in the VPM files it is correct (earlier finding 7, still present). The analysis uses the touch series. The pole times are not scaled like first_touch_time: in the L4 files pole_in_time lies 0.832 s after the trial start in the median trial (0.832 to 1.084 s), pole_out_time 2.835 s in the median, and the first touch follows pole in by 0.528 s in the median and never precedes it, so the no-touch windows of AUC reading (a), drawn between them, lie where the pole is present (0.899 of L4 trials have pole in and out 0.5 s or more apart within the 5 s video). In the VPM files each file names one principal whisker, though the session description says each unit has its own: 11 of the 20 units in files with more than one tracked whisker respond most to another whisker, some strongly (earlier finding 9, now shown per unit).

In [5]:
light = []
for r in l4:
    tr = r["trials"]
    for site, L in r["light"].items():
        w = yu2016.light_windows(r["light"], site, tr)
        modes = tr.photo_stim_mode.to_numpy()[w.trial.to_numpy(int)] if len(w) else []
        dur = np.diff(L["pulses"], axis=1).ravel() if len(L["pulses"]) else np.zeros(0)
        light.append({"line": line_of[r["subject"]], "site_region": L["region"], "lambda_nm": L["lambda_nm"],
                      "power_max": L["power_max"], "lit_trials": len(w), "short_pulses": int((dur < 0.0075).sum()),
                      "long_epochs": int((dur > 0.1).sum()), "isolated_pulses": len(yu2016.isolated_pulses(r["light"], site)),
                      "tag_pulses": len(yu2016.tag_pulses(r["light"], site)),
                      "pulses_per_lit_trial": w.n_pulses.median() if len(w) else 0, "modes": ",".join(sorted(set(modes)))})
light = pd.DataFrame(light)
print("Photostimulation sites by mouse line, labelled region and wavelength:")
print(light.groupby(["line", "site_region", "lambda_nm"]).agg(
    files=("lit_trials", "size"), with_light=("lit_trials", lambda x: int((x > 0).sum())), lit_trials=("lit_trials", "sum"),
    short_pulses=("short_pulses", "sum"), isolated=("isolated_pulses", "sum"), tag=("tag_pulses", "sum"), long_epochs=("long_epochs", "sum"),
    pulses_per_trial=("pulses_per_lit_trial", "median"), power_max=("power_max", "max")).to_string())
print("\nphoto_stim_mode values of the lit trials, by site region:")
print(light[light.lit_trials > 0].groupby("site_region").modes.value_counts().to_string())
desc = pd.DataFrame([r["trials"].attrs.get("descriptions", {}) for r in recs])
print("Descriptions of trial-table columns (distinct values over files):",
      {c: sorted(desc[c].dropna().unique().tolist()) for c in ["type", "response", "photo_stim_mode", "photo_stim_power", "stim_present"] if c in desc})
print("Trial table photo_stim_power is NaN in", sum(int(r["trials"].photo_stim_power.isna().all()) for r in l4), "of", len(l4), "L4 files")
# the pulse rate of the trains at the site labelled VPM
rates = []
for r in l4:
    for site, L in r["light"].items():
        if L["region"] == "VPM" and len(L["pulses"]) > 10:
            d = np.diff(L["pulses"][:, 0])
            rates.append(1 / np.median(d[d < 0.05]) if np.any(d < 0.05) else np.nan)
print("Pulse rate within trains at the site labelled VPM (Hz), files:", pd.Series(np.round(rates)).value_counts().to_dict())
Photostimulation sites by mouse line, labelled region and wavelength:
                                           files  with_light  lit_trials  short_pulses  isolated   tag  long_epochs  pulses_per_trial  power_max
line                site_region lambda_nm                                                                                                       
PV-ChR2             VPM         594.0          1           1          58          6660         0    41           17             150.0       15.0
PV-IRES-Cre         vS1         594.0         47          47        1490             0         0     0         1832               1.0        3.0
PV-IRES-Cre x Ai32  vS1         594.0          1           0           0             0         0     0            0               0.0        0.0
SOM-IRES-Cre x Ai32 vM1         473.0          3           3          54          5400         0    54            0             100.0       15.0
                    vS1         594.0          5           3          16           100       100   100            0               5.0       15.0
VGAT-ChR2-EYFP      VPM         594.0         34          32         760        165357        32   785            1             250.0       15.0
                    VPM and vS1 473.0         24          21         119         11383       689   760            5              12.0       15.0
                    vM1         473.0         25          23         360         40149        25   406            0             100.0       15.0
                    vS1         594.0         33          29         431         15160      1449  1707            0              10.0       15.0

photo_stim_mode values of the lit trials, by site region:
site_region  modes                            
VPM          shift                                21
             5ms_pulse                            10
             5ms_pulse,constant                    1
             constant_shift,shift                  1
VPM and vS1  5ms_pulse                            19
             5ms_pulse,constant,constant_shift     1
             5ms_pulse,constant                    1
vM1          5ms_pulse                            18
             shift                                 7
             5ms_grid,5ms_pulse                    1
vS1          constant                             47
             shift                                17
             5ms_pulse                            14
             shift,shiftgrid                       1
Descriptions of trial-table columns (distinct values over files): {'type': [''], 'response': [''], 'photo_stim_mode': [''], 'photo_stim_power': ['(mW) stimulation power'], 'stim_present': ['is this a stim or no-stim trial']}
Trial table photo_stim_power is NaN in 130 of 130 L4 files
Pulse rate within trains at the site labelled VPM (Hz), files: {100.0: 21, 50.0: 12}

photo_stim_mode and type have empty descriptions, photo_stim_power is NaN in every L4 file, and the mode labels do not map to one kind of light (earlier finding 5, still present). The light epochs are read from the photostimulation series. In VGAT-ChR2 mice the site labelled VPM is given as 594 nm, and pulses at the site labelled vS1 (594 nm) drive VGAT-ChR2 cells at short latency (below); ChR2 is driven by blue light, so these wavelengths look wrong (earlier finding 10, still present). New: the trains at the site labelled VPM run at 100 Hz in 21 files and at 50 Hz in 12, while the Methods give 100 Hz. Trial types are spelled both Nogo and NoGo (new); they are read as one label.

In [6]:
# Alignment of spikes and behaviour: each cell's touch response (spikes per touch, VPM or Fig. 5 definition) and
# phase-coupling r2 with its spikes in their own trial, and moved to the next trial at the same time after its start.
rows = []
for r in recs:
    b = r["beh"]
    if b["principal"] is None:
        continue
    allm = np.ones(len(r["trials"]), bool) if r["kind"] == "VPM" else ~yu2016.lit_trials(r["light"], r["trials"])
    cells = [(u["unit"], u["spikes"]) for u in r["units"]] if r["kind"] == "VPM" else [(0, r["spikes"])]
    for unit, sp in cells:
        if len(sp) < 50:
            continue
        out = {"kind": r["kind"], "path": r["path"], "unit": unit}
        for tag, s in (("true", sp), ("shift", yu2016.shift_spikes(sp, r["trials"]))):
            c = yu2016.counts_1k(s, b)
            c = np.where(allm[np.clip(b["trial"], 0, None)], c, 0)
            out[f"spt_{tag}"] = yu2016.spikes_per_touch(c, b["trial"], yu2016.touches(b, allm), "vpm" if r["kind"] == "VPM" else "cortex5")["spt"]
            vals, _ = yu2016.phase_binned(b, "filtfilt", count=c)
            out[f"r2_{tag}"] = yu2016.phase_fit(vals)["r2"]
        rows.append(out)
align = pd.DataFrame(rows)
for kind, g in align.groupby("kind"):
    g = g.dropna()
    print(f"{kind}: {len(g)} units or cells with at least 50 spikes. Spikes per touch, median own trial {g.spt_true.median():.3f}, "
          f"next trial {g.spt_shift.median():.3f}, larger in own trial for {(g.spt_true > g.spt_shift).sum()} "
          f"(signed-rank p = {stats.wilcoxon(g.spt_true, g.spt_shift).pvalue:.2g}); phase r2, median own {g.r2_true.median():.2f}, "
          f"next {g.r2_shift.median():.2f} (p = {stats.wilcoxon(g.r2_true, g.r2_shift).pvalue:.2g}); r2 > 0.25 own "
          f"{(g.r2_true > 0.25).mean():.2f}, next {(g.r2_shift > 0.25).mean():.2f}")
bad = align[(align.spt_true <= align.spt_shift) & (align.r2_true <= align.r2_shift)]
print("Units or cells where neither measure is larger in their own trial:", len(bad), "of", len(align))

# Duplicates: identical traces, identical spike trains, or identical trial tables across files
hashes = pd.Series([r["vm_hash"] for r in l4])
print("\nL4 files sharing a trace hash with another file:", int(hashes.duplicated(keep=False).sum()))
sk = {}
for r in vpm:
    for u in r["units"]:
        sk.setdefault((len(u["spikes"]), round(float(u["spikes"][:5].sum()), 4)), []).append((r["path"], u["unit"]))
print("VPM units with identical spike count and first spike times:", [v for v in sk.values() if len(v) > 1])
tt = pd.Series([(r["subject"], len(r["trials"]), round(float(r["trials"].start.iloc[-1]), 3)) for r in recs])
print("Files sharing subject, trial count and last trial start with another file:", int(tt.duplicated(keep=False).sum()))
L4: 90 units or cells with at least 50 spikes. Spikes per touch, median own trial 0.346, next trial 0.004, larger in own trial for 73 (signed-rank p = 7e-11); phase r2, median own 0.37, next 0.19 (p = 5.6e-06); r2 > 0.25 own 0.63, next 0.41
VPM: 29 units or cells with at least 50 spikes. Spikes per touch, median own trial 0.268, next trial 0.000, larger in own trial for 24 (signed-rank p = 1.8e-05); phase r2, median own 0.58, next 0.30 (p = 0.0015); r2 > 0.25 own 0.86, next 0.55
Units or cells where neither measure is larger in their own trial: 9 of 121

L4 files sharing a trace hash with another file: 0
VPM units with identical spike count and first spike times: []
Files sharing subject, trial count and last trial start with another file: 0

Spikes and behaviour share a clock in the data as a whole: for VPM units and L4 cells, the touch response and the phase coupling are larger with spikes in their own trial than moved to the next trial (signed-rank p printed above). This cannot be shown for a cell that responds to neither; 9 of 121 such cells are consistent with the clock, not shown to share it. No two files share a trace, a VPM spike train or a trial table, so no recording is deposited twice. Chronic VPM units recorded on several days may be the same neurons; the files give no identifiers that link them, and each unit is counted once, as in the paper's 29.

In [7]:
# Cell classes, by the rules of the claims file's header (see the helper's docstring for the tests):
# FS: in VGAT-ChR2-EYFP mice, driven by 5 ms light pulses at the cortical site within 5 ms of onset (isolated pulses,
#     the pulses of 5 and 10 Hz trains, one-sided binomial test against the same 5 ms 50 ms earlier; the paper gives no test, so the claims file names
#     p < 0.001 and p < 0.01 as readings) and inside the Supp. Fig. 6 waveform boundary (cell-attached cells only;
#     with and without the boundary are readings of fs-touch-response); in PV-IRES-Cre mice, PV cells inactivated by the eNpHR light (rate inside
#     the light lower than in the same span of trials without light, Mann-Whitney over trials, p < 0.05); in lines
#     without a tag, whole-cell cells with spikes no wider than 0.35 ms at half height (the largest FS width the Methods
#     give). Every other L4 cell is excitatory (E).
def light_effect(r):
    """Spike rate inside the light (median span of the lit trials) against the same span of trials without light."""
    out = None
    ctl = ~yu2016.lit_trials(r["light"], r["trials"])
    for site, L in r["light"].items():
        w = yu2016.light_windows(r["light"], site, r["trials"])
        if len(w) == 0 or not (np.diff(L["pulses"], axis=1) > 0.1).any():
            continue
        lit = np.zeros(len(r["trials"]), bool)
        lit[w.trial.to_numpy(int)] = True
        win = (float(np.median(w.rel_start)), float(np.median(w.rel_stop)))
        sp, st = np.sort(r["spikes"]), r["trials"].start.to_numpy()
        per = lambda m: (np.searchsorted(sp, st[m] + win[1]) - np.searchsorted(sp, st[m] + win[0])) / (win[1] - win[0])
        a, b = per(lit), per(ctl)
        p = stats.mannwhitneyu(a, b, alternative="less").pvalue if len(a) and len(b) and (a.sum() + b.sum()) > 0 else np.nan
        out = {"light_site": L["region"], "light_win": win, "lit_trials": int(lit.sum()), "ctl_trials": int(ctl.sum()),
               "rate_ctl": b.mean(), "rate_light": a.mean(), "light_p_lower": p}
    return out

rows = []
for r in l4:
    row = {"path": r["path"], "subject": r["subject"], "line": line_of[r["subject"]], "mode": r["mode"], "spikes": len(r["spikes"]),
           "fwhm_ms": r["fwhm_ms"], "ca_duration_ms": r["ca_duration_ms"], "ca_ratio": r["ca_ratio"], "pw": r["beh"]["principal"] is not None}
    best = None
    for site, L in r["light"].items():
        if "vS1" not in L["region"]:
            continue
        lr = yu2016.light_pulse_response(r["light"], r["spikes"], site)
        if lr["n_pulses"] >= yu2016.TAG_MIN_PULSES and (best is None or lr["p_evoked"] - lr["p_base"] > best["p_evoked"] - best["p_base"]):
            on_ = yu2016.tag_pulses(r["light"], site)
            ipi = np.diff(L["pulses"][:, 0])
            best = {**lr, "tag_site": L["region"], "ipi_ms": float(np.median(ipi[ipi < 1]) * 1000) if np.any(ipi < 1) else np.nan,
                    **{f"{k}_{rule}": yu2016.light_pulse_response(r["light"], r["spikes"], site, rule).get(k, np.nan)
                       for rule in ("first", "isolated") for k in ("n_pulses", "p_value")}}
    if best:
        row.update(tag_site=best["tag_site"], pulses=best["n_pulses"], p_evoked=best["p_evoked"], p_base=best["p_base"],
                   tag_p=best["p_value"], tag_latency_ms=best["median_latency_ms"], ipi_ms=best["ipi_ms"],
                   pulses_first=best["n_pulses_first"], tag_p_first=best["p_value_first"],
                   pulses_isolated=best["n_pulses_isolated"], tag_p_isolated=best["p_value_isolated"])
    le = light_effect(r) if row["line"] == "PV-IRES-Cre" else None
    if le:
        row.update(le)
    rows.append(row)
cells = pd.DataFrame(rows)
cells["tag_tested"] = cells.get("pulses", pd.Series(np.nan, index=cells.index)).notna()
cells["driven"] = cells.tag_p < yu2016.TAG_MAX_P
cells["driven_01"] = cells.tag_p < 0.01
cells["in_boundary"] = (cells.ca_duration_ms < yu2016.FS_MAX_DURATION_MS) & (cells.ca_ratio < yu2016.FS_MAX_RATIO)
cells["pv_inactivated"] = (cells.line == "PV-IRES-Cre") & (cells.light_p_lower < 0.05) & (cells.rate_light < cells.rate_ctl)
untagged_lines = ~cells.line.isin(["VGAT-ChR2-EYFP", "PV-IRES-Cre"])
cells["thin_wc"] = untagged_lines & (cells["mode"] == "whole-cell") & (cells.fwhm_ms <= 0.35)

def classify(driven_col="driven", boundary=True):
    driven = (cells.line == "VGAT-ChR2-EYFP") & cells[driven_col]
    fs = driven & ((cells["mode"] == "whole-cell") | cells.in_boundary) if boundary else driven
    cls = np.where(fs | cells.pv_inactivated | cells.thin_wc, "FS", "E")
    # driven (GABAergic) cells outside the boundary are neither FS nor excitatory (Supp. Fig. 6)
    return np.where(driven & ~fs, "other GABAergic", cls)

cells["cls"] = classify()                        # the claims file's rule: driven (p < 0.001) and inside the boundary
cells["cls_01"] = classify("driven_01")          # reading: driven at p < 0.01
cells["cls_tag"] = classify(boundary=False)      # driven at p < 0.001, any waveform (reading of fs-touch-response only)
cells["cls_01_tag"] = classify("driven_01", boundary=False)   # driven at p < 0.01, any waveform (same)
print("L4 cells by mouse line, mode and class (driven at p < 0.001 and, cell-attached, inside the waveform boundary):")
print(pd.crosstab([cells.line, cells["mode"]], cells.cls).to_string())
print("Cells whose class changes with the tag at p < 0.01:", cells[cells.cls != cells.cls_01].path.str.split("/").str[1].str[:40].tolist())
print("Cells that are FS without the waveform boundary (driven, outside it):",
      cells[cells.cls != cells.cls_tag][["path", "ca_duration_ms", "ca_ratio"]].assign(path=lambda x: x.path.str.split("/").str[1].str[:36]).round(2).values.tolist())
vg = cells[(cells.line == "VGAT-ChR2-EYFP")]
print(f"\nVGAT-ChR2 cells: {len(vg)}; tested with at least 10 tag pulses at a cortical site {int(vg.tag_tested.sum())}; driven (p < 0.001) "
      f"{int(vg.driven.sum())}, (p < 0.01) {int(vg.driven_01.sum())}")
d = vg[vg.driven]
print("Driven cells: evoked spike probability median", round(d.p_evoked.median(), 2), "(baseline", round(d.p_base.median(), 2),
      "); first-spike latency median", round(d.tag_latency_ms.median(), 2), "ms, range", round(d.tag_latency_ms.min(), 2), "to",
      round(d.tag_latency_ms.max(), 2))
print("VGAT-ChR2 cells without at least 10 tag pulses at a cortical site:", vg[~vg.tag_tested][["path", "mode", "spikes", "fwhm_ms"]]
      .assign(path=lambda x: x.path.str.split("/").str[1].str[:40]).round(2).to_dict("records"))
print("\nTagging coverage, every VGAT-ChR2 cell with a cortical site: tag pulses (5 and 10 Hz trains, the rule used), median interval "
      "between pulses (ms), and the tag p under the rule used, the first pulse of each train, and isolated pulses (the earlier rule):")
print(vg[vg.tag_tested][["path", "mode", "spikes", "pulses", "ipi_ms", "tag_p", "pulses_first", "tag_p_first", "pulses_isolated", "tag_p_isolated",
                         "tag_latency_ms"]].assign(path=lambda x: x.path.str.split("/").str[1].str[:34]).round(4).to_string(index=False))
print("Driven at p < 0.001 under each pulse rule: trains (used)", int((vg.tag_p < 0.001).sum()), "; first pulse of each train",
      int((vg.tag_p_first < 0.001).sum()), "; isolated pulses", int((vg.tag_p_isolated < 0.001).sum()))
print("Cells driven under the rule used and not under the first-pulse rule:",
      vg[(vg.tag_p < 0.001) & ~(vg.tag_p_first < 0.001)].path.str.split("/").str[1].str[:34].tolist())
ca = vg[vg["mode"] == "cell-attached"]
print("\nCell-attached VGAT-ChR2 cells, Supp. Fig. 6 measures (peak-to-trough duration ms, peak/trough ratio), by tag:")
print(ca.groupby("driven")[["ca_duration_ms", "ca_ratio"]].describe().round(2).T.to_string())
print("Driven cell-attached cells inside the boundary (duration < 0.27 ms and ratio < 2.8):", int((ca.driven & ca.in_boundary).sum()),
      "of", int(ca.driven.sum()), "; undriven inside it:", int((~ca.driven & ca.in_boundary).sum()), "of", int((~ca.driven).sum()))
pv = cells[cells.line == "PV-IRES-Cre"].dropna(subset=["rate_ctl"])
print(f"\nPV-IRES-Cre cells with the eNpHR light: {len(pv)} (light span {sorted(set(tuple(np.round(w, 2)) for w in pv.light_win))[:3]} ...); "
      f"rate lower with light (p < 0.05): {int(pv.pv_inactivated.sum())}; ratio light/control below 0.5 among them: "
      f"{int((pv.pv_inactivated & (pv.rate_light < 0.5 * pv.rate_ctl)).sum())}")
print(pv[pv.pv_inactivated | (pv.rate_light < 0.8 * pv.rate_ctl)][["path", "mode", "spikes", "rate_ctl", "rate_light", "light_p_lower", "ca_duration_ms", "ca_ratio"]]
      .assign(path=lambda x: x.path.str.split("/").str[1].str[:40]).round(3).to_string(index=False))
print("\nThin-spiking whole-cell cells of lines without a tag:", cells[cells.thin_wc][["path", "line", "fwhm_ms", "spikes"]]
      .assign(path=lambda x: x.path.str.split("/").str[1].str[:40]).round(2).to_dict("records"))
print("Whole-cell spike width at half height (ms) of the lines without a tag:",
      cells[untagged_lines & (cells["mode"] == "whole-cell")][["line", "fwhm_ms"]].round(2).values.tolist())
# Against Supplementary Table 2 (cells per line, mode and type; control and ION-cut cells together)
paper = pd.DataFrame([("VGAT-ChR2-EYFP", "whole-cell", 32, 5), ("VGAT-ChR2-EYFP", "cell-attached", 6, 24),
                      ("PV-IRES-Cre", "whole-cell", 5, 0), ("PV-IRES-Cre", "cell-attached", 38, 4),
                      ("PV-IRES-Cre x Ai32", "whole-cell", 1, 0), ("C57BL/6", "whole-cell", 7, 1), ("PV-ChR2", "whole-cell", 0, 1),
                      ("SOM-IRES-Cre x Ai32", "whole-cell", 1, 0), ("SOM-IRES-Cre x Ai32", "cell-attached", 4, 0)],
                     columns=["line", "mode", "paper_E", "paper_FS"])
ours = cells.groupby(["line", "mode"]).cls.value_counts().unstack(fill_value=0).reindex(columns=["E", "FS", "other GABAergic"], fill_value=0)
cmp_ = paper.merge(ours.reset_index().rename(columns={"E": "files_E", "FS": "files_FS", "other GABAergic": "files_other"}),
                   on=["line", "mode"], how="outer").fillna(0)
print("\nCells per line, mode and type: Supplementary Table 2 (E includes 9 whole-cell ION-cut cells, FS 10 cell-attached ION-cut cells) "
      "against the files:")
print(cmp_.to_string(index=False))
print("Totals: paper E", int(cmp_.paper_E.sum()), "FS", int(cmp_.paper_FS.sum()), "; files E", int(cmp_.files_E.sum()), "FS",
      int(cmp_.files_FS.sum()), "other GABAergic", int(cmp_.files_other.sum()))
L4 cells by mouse line, mode and class (driven at p < 0.001 and, cell-attached, inside the waveform boundary):
cls                                 E  FS  other GABAergic
line                mode                                  
C57BL/6             whole-cell      7   1                0
PV-ChR2             whole-cell      1   0                0
PV-IRES-Cre         cell-attached  36   3                0
                    whole-cell      9   0                0
PV-IRES-Cre x Ai32  whole-cell      1   0                0
SOM-IRES-Cre x Ai32 cell-attached   4   0                0
                    whole-cell      1   0                0
VGAT-ChR2-EYFP      cell-attached  10  12                8
                    whole-cell     33   4                0
Cells whose class changes with the tag at p < 0.01: ['sub-anm273347_ses-20150224_obj-i6vwao_be', 'sub-anm273348_ses-20150227_behavior+icep', 'sub-anm318531_ses-20151230_obj-1jbfh1x_b']
Cells that are FS without the waveform boundary (driven, outside it): [['sub-anm260070_ses-20150217_obj-2vta8', 0.7, 13.99], ['sub-anm273348_ses-20150226_obj-1cv3c', 0.3, 2.88], ['sub-anm273348_ses-20150226_obj-1m5xc', 0.7, 12.2], ['sub-anm273348_ses-20150226_obj-efumc', 0.9, 12.31], ['sub-anm275231_ses-20150303_obj-10ju1', 0.3, 2.31], ['sub-anm275231_ses-20150303_obj-g3yot', 0.8, 19.23], ['sub-anm275231_ses-20150304_behavior+', 0.3, 2.26], ['sub-anm281589_ses-20150319_behavior+', 0.6, 2.56]]

VGAT-ChR2 cells: 67; tested with at least 10 tag pulses at a cortical site 41; driven (p < 0.001) 24, (p < 0.01) 27
Driven cells: evoked spike probability median 1.0 (baseline 0.05 ); first-spike latency median 2.3 ms, range 1.6 to 4.55
VGAT-ChR2 cells without at least 10 tag pulses at a cortical site: [{'path': 'sub-anm190964_ses-20130504_behavior+icep', 'mode': 'whole-cell', 'spikes': 35, 'fwhm_ms': 0.66}, {'path': 'sub-anm190964_ses-20130507_behavior+icep', 'mode': 'whole-cell', 'spikes': 1, 'fwhm_ms': nan}, {'path': 'sub-anm230103_ses-20140130_behavior+icep', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm230103_ses-20140131_behavior+icep', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm231235_ses-20140129_obj-1swt3sc_b', 'mode': 'whole-cell', 'spikes': 1, 'fwhm_ms': nan}, {'path': 'sub-anm231235_ses-20140129_obj-z4ez73_be', 'mode': 'whole-cell', 'spikes': 3, 'fwhm_ms': nan}, {'path': 'sub-anm231235_ses-20140131_behavior+icep', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm234411_ses-20140216_behavior+icep', 'mode': 'whole-cell', 'spikes': 198, 'fwhm_ms': 0.89}, {'path': 'sub-anm234412_ses-20140204_behavior+icep', 'mode': 'whole-cell', 'spikes': 1, 'fwhm_ms': nan}, {'path': 'sub-anm234412_ses-20140205_behavior+icep', 'mode': 'whole-cell', 'spikes': 2, 'fwhm_ms': nan}, {'path': 'sub-anm253096_ses-20140817_behavior+icep', 'mode': 'whole-cell', 'spikes': 1, 'fwhm_ms': nan}, {'path': 'sub-anm253096_ses-20140818_behavior+icep', 'mode': 'whole-cell', 'spikes': 3, 'fwhm_ms': nan}, {'path': 'sub-anm260070_ses-20150219_obj-1dodaf3_b', 'mode': 'cell-attached', 'spikes': 16350, 'fwhm_ms': nan}, {'path': 'sub-anm260073_ses-20141008_behavior+icep', 'mode': 'whole-cell', 'spikes': 2, 'fwhm_ms': nan}, {'path': 'sub-anm260073_ses-20141009_behavior+icep', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm260074_ses-20141016_behavior+icep', 'mode': 'whole-cell', 'spikes': 11087, 'fwhm_ms': 0.37}, {'path': 'sub-anm260076_ses-20141015_behavior+icep', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm260077_ses-20150301_obj-fmhxwt_be', 'mode': 'cell-attached', 'spikes': 926, 'fwhm_ms': nan}, {'path': 'sub-anm273347_ses-20150224_obj-1h07z6p_b', 'mode': 'cell-attached', 'spikes': 774, 'fwhm_ms': nan}, {'path': 'sub-anm281592_ses-20150308_obj-pqe4n1_be', 'mode': 'whole-cell', 'spikes': 434, 'fwhm_ms': 0.84}, {'path': 'sub-anm309008_ses-20150924_behavior+icep', 'mode': 'whole-cell', 'spikes': 88, 'fwhm_ms': 0.83}, {'path': 'sub-anm318530_ses-20151127_obj-1qoeopw_b', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm318530_ses-20151127_obj-1sp1rkm_b', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}, {'path': 'sub-anm318530_ses-20151127_obj-h9a8vz_be', 'mode': 'whole-cell', 'spikes': 39, 'fwhm_ms': 1.57}, {'path': 'sub-anm318532_ses-20151125_obj-1nndczg_b', 'mode': 'whole-cell', 'spikes': 5, 'fwhm_ms': 0.35}, {'path': 'sub-anm318532_ses-20151125_obj-ombqp3_be', 'mode': 'whole-cell', 'spikes': 0, 'fwhm_ms': nan}]

Tagging coverage, every VGAT-ChR2 cell with a cortical site: tag pulses (5 and 10 Hz trains, the rule used), median interval between pulses (ms), and the tag p under the rule used, the first pulse of each train, and isolated pulses (the earlier rule):
                              path          mode  spikes  pulses  ipi_ms  tag_p  pulses_first  tag_p_first  pulses_isolated  tag_p_isolated  tag_latency_ms
sub-anm190963_ses-20130408_behavio    whole-cell    1157    26.0   10.00 1.0000          26.0       1.0000              0.0             NaN             NaN
sub-anm220466_ses-20140402_behavio    whole-cell       0    12.0   20.00 1.0000          12.0       1.0000              0.0             NaN             NaN
sub-anm220466_ses-20140403_behavio    whole-cell    4221   196.0   20.00 0.0000          25.0       0.0000            190.0          0.0000           3.600
sub-anm220470_ses-20140409_obj-1hu    whole-cell     156    27.0   20.00 1.0000           9.0       1.0000             20.0          1.0000             NaN
sub-anm220470_ses-20140409_obj-bb0    whole-cell    2040   213.0   20.00 0.0000          24.0       0.0039            210.0          0.0000           4.550
sub-anm230107_ses-20130922_behavio    whole-cell       2    11.0   20.00 1.0000          11.0       1.0000              0.0             NaN             NaN
sub-anm260070_ses-20150217_obj-1p0 cell-attached   10110    38.0   10.00 0.0000          38.0       0.0000             35.0          0.0000           1.600
sub-anm260070_ses-20150217_obj-2vt cell-attached    9966    53.0   10.00 0.0000           9.0       0.0078             48.0          0.0000           2.100
sub-anm260070_ses-20150218_obj-h5o cell-attached    1054    36.0  200.00 1.0000           3.0       1.0000             36.0          1.0000             NaN
sub-anm260070_ses-20150218_obj-vgz cell-attached    7678    74.0   20.00 0.0000           8.0       0.0039             72.0          0.0000           2.600
sub-anm260070_ses-20150219_obj-trb cell-attached    6575    72.0  200.00 0.0000           6.0       0.0312             72.0          0.0000           2.500
sub-anm260070_ses-20150222_behavio cell-attached    1397    24.0  200.00 0.2500           2.0       0.5000             24.0          0.2500           4.750
sub-anm260070_ses-20150223_obj-t5w cell-attached    5774    25.0   10.00 0.0002           3.0       0.1250             24.0          0.0005           2.350
sub-anm260070_ses-20150223_obj-vy6 cell-attached   10209    25.0   10.00 0.0002           3.0       0.2500             24.0          0.0005           2.400
sub-anm260077_ses-20150301_obj-1ox cell-attached    1554    36.0  200.00 0.0312           3.0       0.5000             36.0          0.0312           2.900
sub-anm260077_ses-20150304_behavio cell-attached    7435    39.0   10.00 0.3633           4.0       0.7500             18.0          0.7500           2.500
sub-anm273341_ses-20150307_behavio cell-attached    2036   147.0  100.00 0.0000           9.0       0.0020             76.0          0.0000           2.300
sub-anm273341_ses-20150308_behavio cell-attached    7328   210.0   20.00 0.0003          65.0       0.0007            181.0          0.0007           2.500
sub-anm273347_ses-20150224_obj-i6v cell-attached    1026    36.0  200.00 0.0017           3.0       0.2500             36.0          0.0017           2.250
sub-anm273348_ses-20150226_obj-1cv cell-attached    3565    24.0  200.00 0.0000           2.0       0.2500             24.0          0.0000           2.100
sub-anm273348_ses-20150226_obj-1m5 cell-attached    4765    49.0   10.00 0.0000           5.0       0.0312             48.0          0.0000           2.500
sub-anm273348_ses-20150226_obj-efu cell-attached    8884    37.0   10.00 0.0000           4.0       0.0625             36.0          0.0000           1.900
sub-anm273348_ses-20150227_behavio cell-attached    5312    12.0  200.00 0.0020           1.0       0.5000             12.0          0.0020           1.600
sub-anm275231_ses-20150303_obj-10j cell-attached    2579    63.0   20.00 0.0000           8.0       0.0039             60.0          0.0000           1.900
sub-anm275231_ses-20150303_obj-g3y cell-attached    4281    13.0   20.00 0.0001           2.0       0.2500             12.0          0.0002           1.900
sub-anm275231_ses-20150304_behavio cell-attached   18665   113.0   20.00 0.0000           8.0       0.0078             41.0          0.0000           2.100
sub-anm281589_ses-20150319_behavio cell-attached   16794    37.0   20.00 0.0000          15.0       0.0000             24.0          0.0000           2.500
sub-anm281589_ses-20150320_behavio cell-attached    9055   182.0   20.00 0.0000          17.0       0.0000            180.0          0.0000           2.200
sub-anm281592_ses-20150308_obj-10s cell-attached    4148   122.0   20.00 0.0000          12.0       0.0005            120.0          0.0000           2.300
sub-anm281592_ses-20150309_behavio cell-attached     286    49.0   10.00 0.0000           5.0       0.0312             48.0          0.0000           2.300
sub-anm305599_ses-20150915_behavio    whole-cell    5806    58.0     NaN 0.0000          58.0       0.0000             58.0          0.0000           2.175
sub-anm307766_ses-20150909_behavio    whole-cell    6411   108.0     NaN 0.0000         108.0       0.0000            108.0          0.0000           2.150
sub-anm307766_ses-20150910_behavio    whole-cell     281    37.0     NaN 0.7500          37.0       0.7500             37.0          0.7500           0.300
sub-anm309008_ses-20150925_behavio    whole-cell       0    68.0  200.00 1.0000          32.0       1.0000             68.0          1.0000             NaN
sub-anm318530_ses-20151127_obj-1fc    whole-cell       6    10.0  200.00 1.0000           1.0       1.0000             10.0          1.0000             NaN
sub-anm318530_ses-20151128_behavio cell-attached   10401    20.0  200.00 0.0001           2.0       0.2500             20.0          0.0001           2.500
sub-anm318531_ses-20151230_obj-1jb cell-attached    7047    10.0  207.25 0.0020           1.0       0.5000             10.0          0.0020           1.650
sub-anm318531_ses-20151230_obj-meu    whole-cell       2    10.0  207.25 1.0000           1.0       1.0000             10.0          1.0000             NaN
sub-anm318532_ses-20151125_obj-1jh    whole-cell     177    92.0   20.00 1.0000          11.0       1.0000             90.0          1.0000          12.350
sub-anm318532_ses-20151125_obj-1kq cell-attached     166    10.0  200.00 0.0010           1.0       0.5000             10.0          0.0010           2.900
sub-anm318532_ses-20151125_obj-1ny    whole-cell       0    10.0  200.00 1.0000           1.0       1.0000             10.0          1.0000             NaN
Driven at p < 0.001 under each pulse rule: trains (used) 24 ; first pulse of each train 8 ; isolated pulses 24
Cells driven under the rule used and not under the first-pulse rule: ['sub-anm220470_ses-20140409_obj-bb0', 'sub-anm260070_ses-20150217_obj-2vt', 'sub-anm260070_ses-20150218_obj-vgz', 'sub-anm260070_ses-20150219_obj-trb', 'sub-anm260070_ses-20150223_obj-t5w', 'sub-anm260070_ses-20150223_obj-vy6', 'sub-anm273341_ses-20150307_behavio', 'sub-anm273348_ses-20150226_obj-1cv', 'sub-anm273348_ses-20150226_obj-1m5', 'sub-anm273348_ses-20150226_obj-efu', 'sub-anm275231_ses-20150303_obj-10j', 'sub-anm275231_ses-20150303_obj-g3y', 'sub-anm275231_ses-20150304_behavio', 'sub-anm281592_ses-20150309_behavio', 'sub-anm318530_ses-20151128_behavio', 'sub-anm318532_ses-20151125_obj-1kq']

Cell-attached VGAT-ChR2 cells, Supp. Fig. 6 measures (peak-to-trough duration ms, peak/trough ratio), by tag:
driven                False  True 
ca_duration_ms count  10.00  20.00
               mean    0.55   0.35
               std     0.24   0.24
               min     0.20   0.15
               25%     0.32   0.20
               50%     0.60   0.20
               75%     0.70   0.38
               max     0.90   0.90
ca_ratio       count  10.00  20.00
               mean   12.07   4.34
               std    11.02   5.37
               min     1.30   1.18
               25%     2.62   1.36
               50%     9.85   2.28
               75%    16.88   2.66
               max    30.56  19.23
Driven cell-attached cells inside the boundary (duration < 0.27 ms and ratio < 2.8): 12 of 20 ; undriven inside it: 1 of 10

PV-IRES-Cre cells with the eNpHR light: 47 (light span [(np.float64(0.73), np.float64(2.73)), (np.float64(0.82), np.float64(2.32)), (np.float64(0.82), np.float64(2.82))] ...); rate lower with light (p < 0.05): 3; ratio light/control below 0.5 among them: 3
                                    path          mode  spikes  rate_ctl  rate_light  light_p_lower  ca_duration_ms  ca_ratio
sub-anm244024_ses-20141112_obj-oqgb91_be cell-attached     865     5.808       4.434          0.079             0.6     9.993
sub-anm244024_ses-20141112_obj-vigb9a_be cell-attached    5123    17.561       7.539          0.000             0.2     1.429
sub-anm266945_ses-20141220_obj-7o69ev_be cell-attached   10811    41.400       3.965          0.000             0.7     9.114
sub-anm266945_ses-20141221_obj-1vgsemf_b    whole-cell      84     2.485       1.067          0.203             NaN       NaN
sub-anm300169_ses-20150926_obj-rp0tw4_be cell-attached    8966    28.767       1.458          0.000             0.3     3.767

Thin-spiking whole-cell cells of lines without a tag: [{'path': 'sub-anm236461_ses-20140212_behavior+icep', 'line': 'C57BL/6', 'fwhm_ms': 0.25, 'spikes': 3186}]
Whole-cell spike width at half height (ms) of the lines without a tag: [['C57BL/6', nan], ['C57BL/6', 0.66], ['C57BL/6', 0.74], ['PV-IRES-Cre x Ai32', 1.03], ['C57BL/6', 0.73], ['C57BL/6', nan], ['C57BL/6', 0.25], ['C57BL/6', nan], ['C57BL/6', 0.95], ['PV-ChR2', 0.44], ['SOM-IRES-Cre x Ai32', nan]]

Cells per line, mode and type: Supplementary Table 2 (E includes 9 whole-cell ION-cut cells, FS 10 cell-attached ION-cut cells) against the files:
               line          mode  paper_E  paper_FS  files_E  files_FS  files_other
            C57BL/6    whole-cell        7         1        7         1            0
            PV-ChR2    whole-cell        0         1        1         0            0
        PV-IRES-Cre cell-attached       38         4       36         3            0
        PV-IRES-Cre    whole-cell        5         0        9         0            0
 PV-IRES-Cre x Ai32    whole-cell        1         0        1         0            0
SOM-IRES-Cre x Ai32 cell-attached        4         0        4         0            0
SOM-IRES-Cre x Ai32    whole-cell        1         0        1         0            0
     VGAT-ChR2-EYFP cell-attached        6        24       10        12            8
     VGAT-ChR2-EYFP    whole-cell       32         5       33         4            0
Totals: paper E 94 FS 35 ; files E 102 FS 20 other GABAergic 8

No file names the cell type of its L4 neuron (earlier finding 1, still present), so the classes are built from the data by the claims file's rules. The tag test now counts every pulse of the 5 and 10 Hz trains. In VGAT-ChR2 mice, 41 cells have at least 10 tag pulses at a cortical site and 24 of them are driven within 5 ms (latency 1.6 to 4.6 ms), the same 24 as under the earlier rule of isolated pulses; the first pulse of each train alone drives 8, since many cells had few trains. Supplementary Table 2 counts 29 FS cells there (19 control and 10 ION-cut). The other 26 VGAT-ChR2 cells have fewer than 10 short pulses at any cortical site (the tagging table lists every cell tested; the list above names the others, among them the cells with 16,350 and 11,087 spikes), so no pulse rule can test them and they count as excitatory. The waveform boundary of Supplementary Fig. 6, as read in the claims file, keeps 12 of the 20 driven cell-attached cells; the other 8 are GABAergic but outside it and are left out of both classes. Their measures (duration 0.3 to 0.9 ms, ratio 2.26 to 19.23) follow the figure's inset (peak, the trough after it and the time between them), but 4 of these driven cells have ratios of 12.2 to 19.2, beyond the figure's axis (8), where the figure has no cell, so the measure here does not place every cell as the figure does. The tag at p < 0.01 adds 3 cells, all outside the boundary, so it changes no FS group. In PV-IRES-Cre mice 3 cells are inhibited by the light (the paper has 4 PV cells); in the lines without a tag one whole-cell cell has spikes 0.25 ms wide. The PV-ChR2 cell, FS in Supplementary Table 2, has spikes 0.44 ms wide here and counts as excitatory. The result: 20 FS cells against the table's 35 (25 control and 10 ION-cut), and 102 excitatory cells against 94.

In [8]:
# Spike detection (standard choices, helper docstring) checked against what a spike train allows: the fraction of interspike
# intervals under 1.5 ms (refractory violations), and the size of the detected spikes (whole-cell, median prominence in mV,
# detection needs 20; cell-attached, median peak in robust s.d. of the high-passed trace, detection needs 6). Rates by mode and
# class are set against the paper's.
sq = []
for r in l4:
    sp = np.sort(r["spikes"])
    isi = np.diff(sp)
    c = cells.set_index("path").loc[r["path"]]
    sq.append({"path": r["path"], "subject": r["subject"], "line": c.line, "mode": r["mode"], "cls": c.cls, "spikes": len(sp),
               "isi_viol": float(np.mean(isi < 0.0015)) if len(isi) else np.nan, "spike_size": r["spike_size"],
               "rate_all": len(sp) / (len(r["trials"]) * yu2016.ANALYSIS_END_S)})
spikeq = pd.DataFrame(sq)
print("L4 cells, by mode and class: fraction of interspike intervals under 1.5 ms, spike size, and spikes/s over the first 5 s of every trial")
print(spikeq.groupby(["mode", "cls"]).agg(cells=("path", "size"), isi_viol_median=("isi_viol", "median"), isi_viol_max=("isi_viol", "max"),
                                          above_1pct=("isi_viol", lambda x: int((x > 0.01).sum())), size_median=("spike_size", "median"),
                                          size_min=("spike_size", "min"), rate_median=("rate_all", "median"), rate_mean=("rate_all", "mean")).round(4).to_string())
print("Cells with more than 1% of intervals under 1.5 ms:", spikeq[spikeq.isi_viol > 0.01][["path", "mode", "cls", "spikes", "isi_viol", "spike_size", "rate_all"]]
      .assign(path=lambda d: d.path.str.split("/").str[1].str[:34]).round(3).to_dict("records"))
vq = [float(np.mean(np.diff(np.sort(u["spikes"])) < 0.0015)) for r in vpm for u in r["units"] if len(u["spikes"]) > 1]
print("VPM units (sorted in the files): fraction under 1.5 ms, median", round(float(np.median(vq)), 4), ", max", round(float(np.max(vq)), 4))
e_ = spikeq[spikeq.cls == "E"]
print("E cells, spikes/s over all time, by mode: whole-cell mean", round(e_[e_["mode"] == "whole-cell"].rate_all.mean(), 3), ", cell-attached mean",
      round(e_[e_["mode"] == "cell-attached"].rate_all.mean(), 3), "; the paper's E rates are 0.4 (nonwhisking) and 0.6 (whisking) spikes/s over 85 cells")
def dead_time(sp, dead=0.002):
    """Spike times with every spike less than `dead` s after the last kept spike removed."""
    keep, last = [], -np.inf
    for t in np.sort(sp):
        if t - last >= dead:
            keep.append(t)
            last = t
    return np.array(keep)
ca_e = [r for r in l4 if r["mode"] == "cell-attached" and cells.set_index("path").cls[r["path"]] == "E"]
r0 = np.array([len(r["spikes"]) / (len(r["trials"]) * yu2016.ANALYSIS_END_S) for r in ca_e])
r1 = np.array([len(dead_time(r["spikes"])) / (len(r["trials"]) * yu2016.ANALYSIS_END_S) for r in ca_e])
print(f"Cell-attached E cells: with a 2 ms dead time imposed on the detected spikes the mean rate goes from {r0.mean():.2f} to {r1.mean():.2f} "
      f"spikes/s (median {np.median(r0):.2f} to {np.median(r1):.2f}), so double detection removes {1 - r1.sum() / r0.sum():.3f} of the spikes")
for thr in (6, 8, 10):
    big = e_[(e_["mode"] == "cell-attached")]
    print(f"Cell-attached E cells whose median spike is at least {thr} robust s.d.: {int((big.spike_size >= thr).sum())} of {len(big)}")
L4 cells, by mode and class: fraction of interspike intervals under 1.5 ms, spike size, and spikes/s over the first 5 s of every trial
                               cells  isi_viol_median  isi_viol_max  above_1pct  size_median  size_min  rate_median  rate_mean
mode          cls                                                                                                             
cell-attached E                   50           0.0518        0.2378          40      20.9195    7.6509       4.2550     6.2016
              FS                  15           0.0146        0.0914           9      46.3597   21.1511      13.4184    14.3382
              other GABAergic      8           0.0443        0.3122           6      40.7092   12.1231      13.6473    15.0751
whole-cell    E                   52           0.0000        0.0001           0      42.7639   20.2737       0.0326     0.8798
              FS                   5           0.0000        0.0019           0      30.0857   22.9691      10.9590    10.5278
Cells with more than 1% of intervals under 1.5 ms: [{'path': 'sub-anm244024_ses-20141111_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 662, 'isi_viol': 0.098, 'spike_size': 25.454, 'rate_all': 1.351}, {'path': 'sub-anm244024_ses-20141112_obj-b6y', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 3113, 'isi_viol': 0.05, 'spike_size': 44.203, 'rate_all': 2.707}, {'path': 'sub-anm244024_ses-20141112_obj-ccd', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1301, 'isi_viol': 0.076, 'spike_size': 8.776, 'rate_all': 4.909}, {'path': 'sub-anm244024_ses-20141112_obj-oqg', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 865, 'isi_viol': 0.093, 'spike_size': 15.41, 'rate_all': 3.089}, {'path': 'sub-anm244024_ses-20141112_obj-vig', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 5123, 'isi_viol': 0.036, 'spike_size': 63.168, 'rate_all': 15.293}, {'path': 'sub-anm244024_ses-20141130_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 413, 'isi_viol': 0.022, 'spike_size': 41.588, 'rate_all': 1.53}, {'path': 'sub-anm244024_ses-20141201_obj-11k', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1536, 'isi_viol': 0.085, 'spike_size': 10.669, 'rate_all': 5.585}, {'path': 'sub-anm244024_ses-20141201_obj-wv1', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 2068, 'isi_viol': 0.053, 'spike_size': 13.839, 'rate_all': 9.4}, {'path': 'sub-anm244024_ses-20141203_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1078, 'isi_viol': 0.135, 'spike_size': 18.534, 'rate_all': 1.978}, {'path': 'sub-anm244025_ses-20141024_obj-1x7', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1030, 'isi_viol': 0.037, 'spike_size': 41.636, 'rate_all': 5.282}, {'path': 'sub-anm244025_ses-20141026_obj-13g', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1662, 'isi_viol': 0.063, 'spike_size': 29.284, 'rate_all': 3.227}, {'path': 'sub-anm244025_ses-20141026_obj-1np', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 516, 'isi_viol': 0.035, 'spike_size': 28.218, 'rate_all': 0.759}, {'path': 'sub-anm244028_ses-20141021_obj-12p', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1450, 'isi_viol': 0.057, 'spike_size': 44.657, 'rate_all': 4.143}, {'path': 'sub-anm244028_ses-20141021_obj-mgz', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1733, 'isi_viol': 0.091, 'spike_size': 20.075, 'rate_all': 4.03}, {'path': 'sub-anm244028_ses-20141021_obj-xh7', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 453, 'isi_viol': 0.06, 'spike_size': 39.912, 'rate_all': 1.888}, {'path': 'sub-anm244028_ses-20141022_obj-1fh', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1445, 'isi_viol': 0.06, 'spike_size': 10.241, 'rate_all': 3.568}, {'path': 'sub-anm260070_ses-20150217_obj-1p0', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 10110, 'isi_viol': 0.018, 'spike_size': 69.608, 'rate_all': 18.054}, {'path': 'sub-anm260070_ses-20150217_obj-2vt', 'mode': 'cell-attached', 'cls': 'other GABAergic', 'spikes': 9966, 'isi_viol': 0.047, 'spike_size': 35.512, 'rate_all': 15.572}, {'path': 'sub-anm260070_ses-20150218_obj-h5o', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1054, 'isi_viol': 0.234, 'spike_size': 23.847, 'rate_all': 4.302}, {'path': 'sub-anm260070_ses-20150222_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1397, 'isi_viol': 0.027, 'spike_size': 12.268, 'rate_all': 3.287}, {'path': 'sub-anm260070_ses-20150223_obj-t5w', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 5774, 'isi_viol': 0.015, 'spike_size': 49.136, 'rate_all': 12.285}, {'path': 'sub-anm260070_ses-20150223_obj-vy6', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 10209, 'isi_viol': 0.015, 'spike_size': 55.861, 'rate_all': 24.307}, {'path': 'sub-anm260077_ses-20150301_obj-1ox', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1554, 'isi_viol': 0.055, 'spike_size': 26.485, 'rate_all': 6.094}, {'path': 'sub-anm260077_ses-20150301_obj-fmh', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 926, 'isi_viol': 0.037, 'spike_size': 18.818, 'rate_all': 4.749}, {'path': 'sub-anm266945_ses-20141220_obj-1g7', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 532, 'isi_viol': 0.147, 'spike_size': 23.143, 'rate_all': 0.838}, {'path': 'sub-anm266945_ses-20141221_obj-164', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 2105, 'isi_viol': 0.071, 'spike_size': 33.41, 'rate_all': 4.626}, {'path': 'sub-anm266945_ses-20141221_obj-1du', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 6001, 'isi_viol': 0.238, 'spike_size': 12.144, 'rate_all': 11.652}, {'path': 'sub-anm266945_ses-20141222_obj-3ih', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1171, 'isi_viol': 0.072, 'spike_size': 17.589, 'rate_all': 3.903}, {'path': 'sub-anm266945_ses-20141222_obj-ian', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 4575, 'isi_viol': 0.112, 'spike_size': 18.346, 'rate_all': 6.06}, {'path': 'sub-anm273341_ses-20150307_behavio', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 2036, 'isi_viol': 0.011, 'spike_size': 21.151, 'rate_all': 8.664}, {'path': 'sub-anm273341_ses-20150308_behavio', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 7328, 'isi_viol': 0.016, 'spike_size': 46.36, 'rate_all': 11.725}, {'path': 'sub-anm273347_ses-20150224_obj-1h0', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 774, 'isi_viol': 0.021, 'spike_size': 10.127, 'rate_all': 3.365}, {'path': 'sub-anm273348_ses-20150226_obj-1m5', 'mode': 'cell-attached', 'cls': 'other GABAergic', 'spikes': 4765, 'isi_viol': 0.046, 'spike_size': 49.352, 'rate_all': 11.622}, {'path': 'sub-anm273348_ses-20150226_obj-efu', 'mode': 'cell-attached', 'cls': 'other GABAergic', 'spikes': 8884, 'isi_viol': 0.012, 'spike_size': 42.801, 'rate_all': 18.318}, {'path': 'sub-anm273348_ses-20150227_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 5312, 'isi_viol': 0.023, 'spike_size': 30.963, 'rate_all': 18.007}, {'path': 'sub-anm275231_ses-20150303_obj-10j', 'mode': 'cell-attached', 'cls': 'other GABAergic', 'spikes': 2579, 'isi_viol': 0.115, 'spike_size': 78.181, 'rate_all': 11.723}, {'path': 'sub-anm275231_ses-20150304_behavio', 'mode': 'cell-attached', 'cls': 'other GABAergic', 'spikes': 18665, 'isi_viol': 0.043, 'spike_size': 82.01, 'rate_all': 27.449}, {'path': 'sub-anm281589_ses-20150319_behavio', 'mode': 'cell-attached', 'cls': 'other GABAergic', 'spikes': 16794, 'isi_viol': 0.312, 'spike_size': 12.123, 'rate_all': 19.758}, {'path': 'sub-anm281589_ses-20150320_behavio', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 9055, 'isi_viol': 0.057, 'spike_size': 30.22, 'rate_all': 17.085}, {'path': 'sub-anm300169_ses-20150926_obj-rp0', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 8966, 'isi_viol': 0.045, 'spike_size': 47.947, 'rate_all': 20.851}, {'path': 'sub-anm300170_ses-20150917_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 5628, 'isi_viol': 0.102, 'spike_size': 19.031, 'rate_all': 8.658}, {'path': 'sub-anm300170_ses-20150918_obj-1c4', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 2365, 'isi_viol': 0.06, 'spike_size': 21.271, 'rate_all': 5.5}, {'path': 'sub-anm300170_ses-20150918_obj-1dj', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 951, 'isi_viol': 0.148, 'spike_size': 20.569, 'rate_all': 5.944}, {'path': 'sub-anm300171_ses-20150827_obj-dcd', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 2794, 'isi_viol': 0.027, 'spike_size': 7.83, 'rate_all': 8.87}, {'path': 'sub-anm300171_ses-20150827_obj-qlj', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1904, 'isi_viol': 0.036, 'spike_size': 11.916, 'rate_all': 5.77}, {'path': 'sub-anm300171_ses-20150828_obj-1yy', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 928, 'isi_viol': 0.028, 'spike_size': 9.016, 'rate_all': 3.437}, {'path': 'sub-anm300172_ses-20150904_obj-66q', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1258, 'isi_viol': 0.033, 'spike_size': 10.618, 'rate_all': 3.268}, {'path': 'sub-anm300172_ses-20150904_obj-i50', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1002, 'isi_viol': 0.025, 'spike_size': 18.998, 'rate_all': 4.175}, {'path': 'sub-anm300172_ses-20150908_obj-6sz', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 4868, 'isi_viol': 0.118, 'spike_size': 36.661, 'rate_all': 13.337}, {'path': 'sub-anm300172_ses-20150908_obj-n5z', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 975, 'isi_viol': 0.098, 'spike_size': 17.905, 'rate_all': 3.75}, {'path': 'sub-anm318530_ses-20151128_behavio', 'mode': 'cell-attached', 'cls': 'FS', 'spikes': 10401, 'isi_viol': 0.091, 'spike_size': 69.479, 'rate_all': 17.192}, {'path': 'sub-anm318531_ses-20151230_obj-1jb', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 7047, 'isi_viol': 0.051, 'spike_size': 52.299, 'rate_all': 14.236}, {'path': 'sub-anm324650_ses-20160422_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1291, 'isi_viol': 0.06, 'spike_size': 33.662, 'rate_all': 2.152}, {'path': 'sub-anm337495_ses-20160601_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 1343, 'isi_viol': 0.073, 'spike_size': 14.378, 'rate_all': 3.4}, {'path': 'sub-anm342684_ses-20160609_behavio', 'mode': 'cell-attached', 'cls': 'E', 'spikes': 2589, 'isi_viol': 0.017, 'spike_size': 7.651, 'rate_all': 5.284}]
VPM units (sorted in the files): fraction under 1.5 ms, median 0.0097 , max 0.0983
E cells, spikes/s over all time, by mode: whole-cell mean 0.88 , cell-attached mean 6.202 ; the paper's E rates are 0.4 (nonwhisking) and 0.6 (whisking) spikes/s over 85 cells
Cell-attached E cells: with a 2 ms dead time imposed on the detected spikes the mean rate goes from 6.20 to 5.70 spikes/s (median 4.25 to 4.08), so double detection removes 0.081 of the spikes
Cell-attached E cells whose median spike is at least 6 robust s.d.: 50 of 50
Cell-attached E cells whose median spike is at least 8 robust s.d.: 48 of 50
Cell-attached E cells whose median spike is at least 10 robust s.d.: 45 of 50

Spike detection. Whole-cell spikes are clean: no whole-cell E cell has more than 0.01% of its interspike intervals under 1.5 ms, and the median spike is 42.8 mV high. In cell-attached cells the median spike is 20.9 robust s.d. (E) and 46.4 (FS) above the noise, far above the 6 s.d. threshold (45 of 50 cell-attached E cells at 10 s.d. or more), but the median E cell has 5.2% of its intervals under 1.5 ms (40 of 50 above 1%), so some spikes are detected twice or a neighbouring unit is picked up. Imposing a 2 ms dead time removes 8.1% of the cell-attached E spikes and lowers their mean rate only from 6.20 to 5.70 spikes/s. Over all time, cell-attached E cells fire 6.20 spikes/s on average and whole-cell E cells 0.88, against the paper's 0.4 and 0.6 spikes/s for its 85 E cells. Double detection does not explain the high cell-attached rates, which drive the rate values of exc-mainly-touch, fs-suppress-movement-spiking and auc-groups. Whether these cells fire this fast or the recordings hold more than one unit cannot be told from the files. The sorted VPM units have a median of 0.97% of intervals under 1.5 ms.

Measures for every unit and cell¶

Each VPM unit and L4 cell with a named principal whisker, over its trials without light, by the definitions of the helper module.

In [9]:
# Every measure of every VPM unit and L4 cell over its trials without light (claims, Measures; helper docstring).
def ctl_mask(r):
    """Trials without light, and (whole-cell) not held near 0 mV by current."""
    if r["kind"] == "VPM":
        return np.ones(len(r["trials"]), bool)
    return ~yu2016.lit_trials(r["light"], r["trials"]) & ~r["held_trials"]

rows, vrows = [], []
for r in recs:
    b = r["beh"]
    if b["principal"] is None:
        continue
    m = ctl_mask(r)
    if r["kind"] == "VPM":
        for u in r["units"]:
            rows.append({"path": r["path"], "subject": r["subject"], "unit": u["unit"], "cls": "VPM", "mode": "probe",
                         **yu2016.spike_measures(u["spikes"], b, m, "vpm", np.random.default_rng(u["unit"]))})
        continue
    c = cells.set_index("path").loc[r["path"]]
    rows.append({"path": r["path"], "subject": r["subject"], "unit": 0, "cls": c.cls, "mode": r["mode"],
                 **yu2016.spike_measures(r["spikes"], b, m, "cortex5", np.random.default_rng(0), fs_fit=True)})
    if r["mode"] == "whole-cell":
        for filt in yu2016.FILTERS:
            for reading in ("a", "b"):
                v = yu2016.vm_measures(r, m, filt, reading)
                vrows.append({"path": r["path"], "subject": r["subject"], "filt": filt, "O3": reading, "touch_avg_n": r["touch_avg_n"],
                              **{k: x for k, x in v.items() if np.ndim(x) == 0}, "touch_pre": v["touch_pre"], "touch_dvm": v["touch_dvm"],
                              "whisk_pre": v["whisk_pre"], "whisk_dvm": v["whisk_dvm"]})
meas = pd.DataFrame(rows)
vmm = pd.DataFrame(vrows)
for col in ["cls_01", "cls_tag", "cls_01_tag"]:
    meas[col] = meas.path.map(cells.set_index("path")[col]).fillna("VPM")
print("Units and cells measured (named principal whisker):", meas.cls.value_counts().to_dict(), "; whole-cell cells with Vm measures:",
      vmm.path.nunique())
print("Dropped for lack of a named principal whisker:", cells[~cells.pw].cls.value_counts().to_dict())

# Whisking-epoch measures under the touch readings O11 (claims header; answer of 2026-10-20): every tracked whisker (a) or the
# principal whisker (b) bounds the epochs, from 10 ms before a touch onset to 100 ms after its offset (i) or onset (ii). a-i is
# the main reading above.
orows = []
for r in recs:
    b = r["beh"]
    if b["principal"] is None:
        continue
    m = ctl_mask(r)
    units = [(u["unit"], u["spikes"]) for u in r["units"]] if r["kind"] == "VPM" else [(0, r["spikes"])]
    for o11 in yu2016.O11:
        bo = yu2016.beh_o11(b, o11)
        for unit, sp in units:
            orows.append({"path": r["path"], "unit": unit, "O11": o11,
                          **yu2016.spike_measures(sp, bo, m, None, None, fs_fit=r["kind"] != "VPM", touch=False)})
meas_o11 = pd.DataFrame(orows)
RATE_COLS = [c for c in meas_o11.columns if ":" in c]
NUM_COLS = [c for c in RATE_COLS if meas_o11[c].dtype.kind in "fi"]
chk = meas.set_index(["path", "unit"])[NUM_COLS].sub(meas_o11[meas_o11.O11 == "a-i"].set_index(["path", "unit"])[NUM_COLS]).abs()
print("O11 a-i against the main epochs, largest difference of any numeric epoch column:", float(chk.max().max()))
Units and cells measured (named principal whisker): {'E': 97, 'VPM': 29, 'FS': 17, 'other GABAergic': 8} ; whole-cell cells with Vm measures: 56
Dropped for lack of a named principal whisker: {'E': 5, 'FS': 3}
O11 a-i against the main epochs, largest difference of any numeric epoch column: 0.0
In [10]:
# Mice without touch input. ION transection removes the input from the whiskers (Fig. 7); the files do not mark it.
# A cell has a touch input when its spikes per touch (Fig. 5 definition) has a 95% bootstrap interval above zero, or,
# whole-cell, its average touch PSP is at least 1 mV. Mice where no cell has one are flagged.
vm0 = vmm[(vmm.filt == "filtfilt") & (vmm.O3 == "a")].set_index("path")
cm = meas[meas.cls != "VPM"].copy()
cm["line"] = cm.subject.map(line_of)
cm["psp_mv"] = cm.path.map(vm0.amp_mv)
cm["spike_input"] = cm.spt_lo > 0
cm["vm_input"] = cm.psp_mv >= 1.0
perf = {r["path"]: r["trials"].response.isin(["Hit", "CR"]).mean() for r in l4}
cm["perf"] = cm.path.map(perf)
mice = cm.groupby("subject").agg(line=("line", "first"), cells=("path", "size"), whole_cell=("mode", lambda x: int((x == "whole-cell").sum())),
                                 fs=("cls", lambda x: int((x == "FS").sum())), spike_input=("spike_input", "sum"),
                                 vm_input=("vm_input", "sum"), max_psp_mv=("psp_mv", "max"), performance=("perf", "mean"))
mice["no_touch_input"] = (mice.spike_input + mice.vm_input) == 0
# ION-cut mice are VGAT-ChR2-EYFP mice (Supplementary Table 2) and performed at chance (Results); the variant leaves out the
# VGAT-ChR2 mice without touch input or with performance below 0.5. Both rules were chosen after seeing these values.
vgm = mice.line == "VGAT-ChR2-EYFP"
flagged = sorted(mice[vgm & (mice.no_touch_input | (mice.performance < 0.5))].index)
print("Mice without a cell with touch input, and VGAT-ChR2 mice with performance below 0.56:")
print(mice[mice.no_touch_input | (vgm & (mice.performance < 0.56))].round(2).to_string())
fc = cells[cells.subject.isin(flagged)]
print(f"\nFlagged mice (VGAT-ChR2, no touch input or performance below 0.5): {flagged}; their cells by mode and class:", pd.crosstab(fc["mode"], fc.cls).to_dict())
print("Supplementary Table 2: 9 ION-cut VGAT-ChR2 mice with 9 whole-cell E and 10 cell-attached FS cells.")
# ION-cut candidates: Supplementary Table 2 suggests the ION-cut mice had no fibre in VPM. VGAT-ChR2 mice by whether any of their
# files has a light site labelled VPM, against the flag.
vsite = pd.Series({r["subject"]: any(L["region"] == "VPM" for L in r["light"].values()) for r in l4})
vgat_m = mice[vgm].assign(vpm_site=lambda d: d.index.map(vsite), flagged=lambda d: d.index.isin(flagged))
print("VGAT-ChR2 mice by a light site labelled VPM (rows) and the flag (columns):")
print(pd.crosstab(vgat_m.vpm_site, vgat_m.flagged).to_string())
print("VGAT-ChR2 mice without a VPM site:", vgat_m[~vgat_m.vpm_site][["cells", "whole_cell", "spike_input", "vm_input", "max_psp_mv", "performance", "flagged"]]
      .round(2).to_string())
print("Performance (proportion correct per recording), median: flagged mice", round(cm[cm.subject.isin(flagged)].perf.median(), 2),
      "; other mice", round(cm[~cm.subject.isin(flagged)].perf.median(), 2))
Mice without a cell with touch input, and VGAT-ChR2 mice with performance below 0.56:
                         line  cells  whole_cell  fs  spike_input  vm_input  max_psp_mv  performance  no_touch_input
subject                                                                                                             
anm223517  PV-IRES-Cre x Ai32      1           1   0            0         0         NaN         0.78            True
anm260074      VGAT-ChR2-EYFP      1           1   0            1         1        5.34         0.51           False
anm273341      VGAT-ChR2-EYFP      2           0   2            0         0         NaN         0.47            True
anm275231      VGAT-ChR2-EYFP      3           0   0            1         0         NaN         0.46           False
anm281589      VGAT-ChR2-EYFP      1           0   0            1         0         NaN         0.55           False
anm318530      VGAT-ChR2-EYFP      4           4   0            0         0        0.75         0.49            True
anm318531      VGAT-ChR2-EYFP      2           1   0            1         0        0.04         0.53           False
anm318532      VGAT-ChR2-EYFP      4           4   0            0         0        0.30         0.55            True

Flagged mice (VGAT-ChR2, no touch input or performance below 0.5): ['anm273341', 'anm275231', 'anm318530', 'anm318532']; their cells by mode and class: {'E': {'cell-attached': 0, 'whole-cell': 8}, 'FS': {'cell-attached': 4, 'whole-cell': 0}, 'other GABAergic': {'cell-attached': 3, 'whole-cell': 0}}
Supplementary Table 2: 9 ION-cut VGAT-ChR2 mice with 9 whole-cell E and 10 cell-attached FS cells.
VGAT-ChR2 mice by a light site labelled VPM (rows) and the flag (columns):
flagged   False  True 
vpm_site              
False        11      4
True         12      0
VGAT-ChR2 mice without a VPM site:            cells  whole_cell  spike_input  vm_input  max_psp_mv  performance  flagged
subject                                                                              
anm190963      1           1            1         1        6.74         0.76    False
anm190964      2           2            0         2        6.07         0.79    False
anm220466      2           2            0         2        3.85         0.82    False
anm220470      2           2            1         2       10.84         0.68    False
anm230107      1           1            0         1        3.99         0.76    False
anm273341      2           0            0         0         NaN         0.47     True
anm275231      3           0            1         0         NaN         0.46     True
anm281589      1           0            1         0         NaN         0.55    False
anm281592      3           1            1         0        0.13         0.63    False
anm305599      1           1            1         1        4.90         0.70    False
anm307766      2           2            1         1        8.38         0.58    False
anm309008      2           2            1         2        8.13         0.59    False
anm318530      4           4            0         0        0.75         0.49     True
anm318531      2           1            1         0        0.04         0.53    False
anm318532      4           4            0         0        0.30         0.55     True
Performance (proportion correct per recording), median: flagged mice 0.48 ; other mice 0.69

The files do not mark the 9 ION-cut mice. In 3 VGAT-ChR2 mice no cell has a touch input (the 8 whole-cell excitatory cells of anm318530 and anm318532 have average touch PSPs of 0.75 mV or less), and in anm275231 and anm273341 the mice performed below 0.5 correct. These 4 mice are consistent with the ION-cut mice of Fig. 7 and are left out in the variant "no_flagged". The rule was chosen after seeing the values, and it selects by touch responses, so it favours claims about touch; it never decides an outcome. It finds 8 of the table's 9 whole-cell ION-cut cells and 7 of its 10 cell-attached ones (3 of which are outside the waveform boundary here). Supplementary Table 2 suggests the ION-cut mice had no fibre in VPM: of the 27 VGAT-ChR2 mice with L4 cells, 12 have a light site labelled VPM and none of them is flagged, and the 4 flagged mice are among the 15 without one. The other 11 mice without a VPM site include 8 whose cells have touch PSPs of 3.85 to 10.84 mV, so they are not ION-cut. The remaining 3 (anm281589, anm281592 and anm318531) have no cell with a touch PSP of 1 mV or more (anm281589 has no whole-cell cell), but each has a cell whose spikes per touch has a 95% interval above zero, so by this attempt's rule they have touch input and are weak candidates at most. That makes at most 7 candidates for the paper's 9 ION-cut mice, so either some ION-cut mice had a VPM fibre or the hint does not hold, and the files cannot confirm which mice were cut.

In [11]:
# The findings of the earlier attempt (000005-yu-2016-01, version 0.220126.1853), each checked again on this version.
pc_ = pd.crosstab([mode.line, mode.label], mode.from_trace)
mislabel = int(((mode.label == "whole-cell") & (mode.from_trace == "cell-attached")).sum() + ((mode.label == "juxtacellular") & (mode.from_trace == "whole-cell")).sum())
status = [
    ("1 No L4 file names the cell type of its neuron", "still present",
     f"fields under general/subject: {meta.subject_fields.unique().tolist()}; no cell-type field or text in any L4 file"),
    ("2 No file marks the ION-transected mice", "still present",
     f"files whose metadata text names ION, infraorbital, nerve, transection, lesion, cut, surgery or sham: "
     f"{sum(any(re.search(terms[k], r['metadata_text']) for k in ['ION', 'infraorbital', 'nerve', 'transect', 'lesion', 'cut', 'surgery', 'sham']) for r in l4)}"),
    ("3 Genotype missing for the 6 VPM mice and 3 L4 mice; SOM-IRES-Cre x Ai32 given as Ai32", "still present",
     f"mice without a genotype: {files[files.genotype == '(none)'].subject.nunique()}; given as Ai32: {files[files.genotype == 'Ai32'].subject.nunique()}"),
    ("4 Every L4 file gives cortical layer 4", "still present", f"layers given: {meta.layer.unique().tolist()}, depth {meta.dv_mm.min():.2f} to {meta.dv_mm.max():.2f} mm"),
    ("5 Optogenetic conditions not described (photo_stim_mode, power NaN, no opsin)", "still present",
     f"photo_stim_power NaN in {sum(int(r['trials'].photo_stim_power.isna().all()) for r in l4)} of {len(l4)} L4 files; files naming eNpHR: "
     f"{sum(bool(re.search(terms['eNpHR'], r['metadata_text'])) for r in l4)}"),
    ("6 Session description gives the wrong recording mode for 24 of 130 L4 files", "still present",
     f"files whose described mode disagrees with the trace: {mislabel}"),
    ("7 L4 first_touch_time is the trial start plus the latency divided by 1000", "still present",
     f"L4 trials where the table value x 1000 matches the touch series: {np.mean(np.abs(ft[ft.kind == 'L4'].table_rel * 1000 - ft[ft.kind == 'L4'].series_rel) < 0.002):.3f}"),
    ("8 L4 trace at 10 or 20 kHz, stated only by the timestamps", "still present", f"files by rate: {mode.vm_hz.value_counts().to_dict()}"),
    ("9 8 files name no principal whisker; VPM files give one per file though units have their own", "still present",
     f"files without one: {int((~wh.principal).sum())}; VPM units responding most to another whisker: {int((multi.best != multi.file_pw).sum())} of {len(multi)}"),
    ("10 VPM-site light given as 594 nm in VGAT-ChR2 mice; vS1 594 nm pulses drive VGAT-ChR2 cells", "still present",
     f"VGAT-ChR2 cells driven by pulses at sites labelled vS1 594 nm: {int(((cells.tag_site == 'vS1') & cells.driven).sum())}"),
    ("11 No analysis code linked by the paper or the dandiset", "still present", "the dandiset links only the paper (related_publications); the claims file found no code"),
]
print(pd.DataFrame(status, columns=["earlier finding", "status", "evidence here"]).to_string(index=False))
                                                                             earlier finding        status                                                                                                evidence here
                                              1 No L4 file names the cell type of its neuron still present fields under general/subject: ['genotype,sex,species,subject_id']; no cell-type field or text in any L4 file
                                                     2 No file marks the ION-transected mice still present       files whose metadata text names ION, infraorbital, nerve, transection, lesion, cut, surgery or sham: 0
      3 Genotype missing for the 6 VPM mice and 3 L4 mice; SOM-IRES-Cre x Ai32 given as Ai32 still present                                                                 mice without a genotype: 9; given as Ai32: 5
                                                      4 Every L4 file gives cortical layer 4 still present                                                                   layers given: ['4'], depth 0.42 to 0.62 mm
               5 Optogenetic conditions not described (photo_stim_mode, power NaN, no opsin) still present                                           photo_stim_power NaN in 130 of 130 L4 files; files naming eNpHR: 0
                 6 Session description gives the wrong recording mode for 24 of 130 L4 files still present                                                      files whose described mode disagrees with the trace: 24
                   7 L4 first_touch_time is the trial start plus the latency divided by 1000 still present                                       L4 trials where the table value x 1000 matches the touch series: 0.956
                                   8 L4 trace at 10 or 20 kHz, stated only by the timestamps still present                                                                    files by rate: {10000.0: 92, 20000.0: 38}
9 8 files name no principal whisker; VPM files give one per file though units have their own still present                                 files without one: 8; VPM units responding most to another whisker: 11 of 20
10 VPM-site light given as 594 nm in VGAT-ChR2 mice; vS1 594 nm pulses drive VGAT-ChR2 cells still present                                            VGAT-ChR2 cells driven by pulses at sites labelled vS1 594 nm: 13
                                     11 No analysis code linked by the paper or the dandiset still present                      the dandiset links only the paper (related_publications); the claims file found no code

Every finding of the earlier attempt is still present on this version, which is the same version as before: the dandiset has not changed since 2022 (draft and published assets are identical). The new findings of this attempt are the current conversion, the missing recordings (Fig. 3 depolarized cells, Fig. 6b,c thalamic multiunits, Supplementary Fig. 10 histology), the 50 Hz trains, and the two spellings of the no-lick trial type.

In [12]:
# Groups used below. The files do not mark ION-cut mice, so every group holds the cells of all mice ("all"); the
# variant leaves out the VGAT-ChR2 mice flagged as lacking touch input or performing below 0.5 ("no_flagged").
FS_READINGS = {"tag p<0.001, boundary": "cls", "tag p<0.01, boundary": "cls_01"}
FS_VARIANT = {"tag p<0.001, any waveform (variant)": "cls_tag"}
# fs-touch-response names four readings of the FS group (answer of 2026-10-20)
FS_READINGS_TOUCH = {"(a)(i) p<0.001, boundary": "cls", "(a)(ii) p<0.001, no boundary": "cls_tag",
                     "(b)(i) p<0.01, boundary": "cls_01", "(b)(ii) p<0.01, no boundary": "cls_01_tag"}

def group(cls, mode=None, sel="all", col="cls"):
    g = meas[meas[col] == cls] if cls != "VPM" else meas[meas.cls == "VPM"]
    if mode:
        g = g[g["mode"] == mode]
    if sel == "no_flagged":
        g = g[~g.subject.isin(flagged)]
    return g

def o11_group(g, o11):
    """`g` with its whisking-epoch columns taken under touch reading O11 `o11`."""
    x = meas_o11[meas_o11.O11 == o11].set_index(["path", "unit"])[RATE_COLS]
    return g.drop(columns=RATE_COLS).join(x, on=["path", "unit"])

def rate_summary(g):
    """Per O11 reading, filter (O1) and rank-sum samples (O2): increases, decreases, rates, the signed-rank test of whisking
    against nonwhisking rate, the cells with r2 > 0.25, and the cells that multiplex (spikes per touch above zero and a
    significant whisking change)."""
    rows = []
    for o11 in yu2016.O11:
        go = o11_group(g, o11)
        for f in yu2016.FILTERS:
            for o2 in ("O2a", "O2b"):
                rows.append({"O11": o11, "filter": f, "samples": o2, "n": len(go), "increase": int((go[f"{f}:{o2}"] == 1).sum()),
                             "decrease": int((go[f"{f}:{o2}"] == -1).sum()), "nonwhisk_mean": go[f"{f}:rate_nonwhisk"].mean(),
                             "nonwhisk_median": go[f"{f}:rate_nonwhisk"].median(), "whisk_mean": go[f"{f}:rate_whisk"].mean(),
                             "signed_rank_p": wilcox(go[f"{f}:rate_nonwhisk"], go[f"{f}:rate_whisk"]),
                             "whisk_higher": int((go[f"{f}:rate_whisk"] > go[f"{f}:rate_nonwhisk"]).sum()),
                             "r2_above_0.25": int((go[f"{f}:r2"] > 0.25).sum()), "multiplex": int(((go.spt_lo > 0) & (go[f"{f}:{o2}"] != 0)).sum())})
    return pd.DataFrame(rows)

def ranges(t, cols):
    """Range of each column over the rows of a reading table."""
    return {c: (float(f"{t[c].min():.3g}"), float(f"{t[c].max():.3g}")) for c in cols}

def fmt_p(t):
    """The p-value columns of a table as text with two significant digits."""
    return t.assign(**{c: t[c].map(lambda x: "%.2g" % x) for c in t.columns if c.startswith("p_") or c.endswith("_p")})

def wilcox(x, y, alternative="two-sided"):
    x, y = np.asarray(x, float), np.asarray(y, float)
    ok = np.isfinite(x) & np.isfinite(y)
    if ok.sum() < 2 or np.all(x[ok] == y[ok]):
        return np.nan
    return float(stats.wilcoxon(x[ok], y[ok], alternative=alternative).pvalue)

def within(value, expected, tol):
    return bool(np.isfinite(value) and abs(value - expected) <= tol)

behavior-touches¶

Mice touched the pole often when it was within reach (lick trials, 5.7 +- 5.3 touches per trial over 4,147 trials) and rarely when it was distal (0.9 +- 3.4 over 4,173 trials); performance was 0.72 +- 0.11 in cortical and 0.71 +- 0.07 in VPM recordings.

In [13]:
def touch_counts(r, whiskers="pw", ctl_only=True):
    b, tr = r["beh"], r["trials"]
    if b["principal"] is None:
        return None
    m = ctl_mask(r) if ctl_only else np.ones(len(tr), bool)
    if whiskers == "pw":
        on_t = b["t"][b["touch_on"]]
    else:
        on_t = np.sort(np.concatenate([b["t"][w["on"]] for w in b["whisker_touches"].values()]))
    k = yu2016.per_trial_index(on_t, tr)
    n = np.bincount(k[k >= 0], minlength=len(tr))
    return pd.DataFrame({"type": tr.type.str.capitalize().to_numpy()[m], "touches": n[m], "correct": tr.response.isin(["Hit", "CR"]).to_numpy()[m]})

# reading (a): the 111 control cortical cells cannot be identified (no ION-cut marks), so every cortical cell counts, as in every
# other group; the cells of the flagged mice left out are a variant that does not decide
ctl_cortex = meas[meas.cls != "VPM"].path.unique()
nf_cortex = meas[(meas.cls != "VPM") & ~meas.subject.isin(flagged)].path.unique()
res = []
for rec_set, paths in (("a: control cortical recordings", set(ctl_cortex)), ("b: all sessions", {r["path"] for r in recs})):
    for wk in ("pw", "any"):
        for ctl_only in (True, False):
            t = [touch_counts(r, wk, ctl_only) for r in recs if r["path"] in paths]
            t = pd.concat([x for x in t if x is not None])
            go, nogo = t[t.type == "Go"].touches, t[t.type == "Nogo"].touches
            res.append({"recordings": rec_set, "whiskers": wk, "light": "y: without light" if ctl_only else "x: every trial",
                        "go_trials": len(go), "go_mean": go.mean(), "go_sd": go.std(), "nogo_trials": len(nogo), "nogo_mean": nogo.mean(),
                        "nogo_sd": nogo.std()})
res = pd.DataFrame(res)
print("Trial types in the files (as spelled):", pd.Series([x for r in recs for x in r["trials"].type]).value_counts().to_dict(),
      "; responses:", pd.Series([x for r in recs for x in r["trials"].response]).value_counts().to_dict())
print("Touches per trial (paper: Go 5.7 +- 5.3 over 4,147 trials, tolerance 0.57; Nogo 0.9 +- 3.4 over 4,173, tolerance 0.09);")
print("readings: recordings (a), (b); whiskers (i) principal 'pw', (ii) any tracked 'any'; light (x) every trial, (y) trials without light:")
print(res.round(2).to_string(index=False))
# performance per recording averaged over cells (cortex) and over units (VPM)
perf_rows = []
for ctl_only in (True, False):
    pc = [(touch_counts(by_path[p], "pw", ctl_only).correct.mean()) for p in ctl_cortex]
    pv_ = [touch_counts(r, "pw", ctl_only).correct.mean() for r in vpm for u in r["units"]]
    perf_rows.append({"light": "y: without light" if ctl_only else "x: every trial", "cortex_cells": len(pc), "cortex_mean": np.mean(pc), "cortex_sd": np.std(pc, ddof=1),
                      "vpm_units": len(pv_), "vpm_mean": np.mean(pv_), "vpm_sd": np.std(pv_, ddof=1)})
perf_df = pd.DataFrame(perf_rows)
print("\nPerformance (proportion correct per recording), averaged over cortical cells "
      "and over VPM units (reading a: every cortical cell; paper: 0.72 +- 0.11 over 111 cells, 0.71 +- 0.07 over 29 units; tolerances 0.072 and 0.071):")
print(perf_df.round(3).to_string(index=False))
for ctl_only in (True, False):
    nfc = [touch_counts(by_path[p], "pw", ctl_only).correct.mean() for p in nf_cortex]
    print(f"Variant, flagged mice left out ({'y' if ctl_only else 'x'}):", len(nfc), "cells, mean", round(np.mean(nfc), 3))
ok_touch = res[(res.go_mean.sub(5.7).abs() <= 0.57) & (res.nogo_mean.sub(0.9).abs() <= 0.09)]
print("Readings with both touch counts within tolerance:", ok_touch[["recordings", "whiskers", "light"]].values.tolist())
print("Readings with the lick-trial count within tolerance:", res[res.go_mean.sub(5.7).abs() <= 0.57][["recordings", "whiskers", "light"]].values.tolist())
print("Readings with the no-lick count within tolerance:", res[res.nogo_mean.sub(0.9).abs() <= 0.09][["recordings", "whiskers", "light"]].values.tolist())
Trial types in the files (as spelled): {'Go': 10214, 'Nogo': 5579, 'NoGo': 3074} ; responses: {'Hit': 6000, 'CR': 5868, 'Miss': 4214, 'FA': 2785}
Touches per trial (paper: Go 5.7 +- 5.3 over 4,147 trials, tolerance 0.57; Nogo 0.9 +- 3.4 over 4,173, tolerance 0.09);
readings: recordings (a), (b); whiskers (i) principal 'pw', (ii) any tracked 'any'; light (x) every trial, (y) trials without light:
                    recordings whiskers            light  go_trials  go_mean  go_sd  nogo_trials  nogo_mean  nogo_sd
a: control cortical recordings       pw y: without light       3637     7.49   6.82         3639       1.17     3.02
a: control cortical recordings       pw   x: every trial       5212     7.53   6.79         5149       1.14     2.99
a: control cortical recordings      any y: without light       3637    10.05   7.74         3639       1.66     4.12
a: control cortical recordings      any   x: every trial       5212    10.00   7.82         5149       1.61     4.06
               b: all sessions       pw y: without light       8219     5.74   7.39         6713       0.80     2.48
               b: all sessions       pw   x: every trial       9794     6.04   7.32         8223       0.85     2.57
               b: all sessions      any y: without light       8219     8.14   8.82         6713       1.23     3.58
               b: all sessions      any   x: every trial       9794     8.42   8.72         8223       1.28     3.64
Performance (proportion correct per recording), averaged over cortical cells and over VPM units (reading a: every cortical cell; paper: 0.72 +- 0.11 over 111 cells, 0.71 +- 0.07 over 29 units; tolerances 0.072 and 0.071):
           light  cortex_cells  cortex_mean  cortex_sd  vpm_units  vpm_mean  vpm_sd
y: without light           122        0.694      0.139         29     0.585   0.101
  x: every trial           122        0.681      0.120         29     0.585   0.101
Variant, flagged mice left out (y): 109 cells, mean 0.718
Variant, flagged mice left out (x): 109 cells, mean 0.703
Readings with both touch counts within tolerance: [['b: all sessions', 'pw', 'x: every trial']]
Readings with the lick-trial count within tolerance: [['b: all sessions', 'pw', 'y: without light'], ['b: all sessions', 'pw', 'x: every trial']]
Readings with the no-lick count within tolerance: [['b: all sessions', 'pw', 'x: every trial']]

Result: partially reproduced. Under light reading (x), every trial, with all sessions (b) and the principal whisker (i), mice touched the pole 6.04 times per lick trial (9,794 trials) and 0.85 times per no-lick trial (8,223 trials), both within tolerance of 5.7 and 0.9; no other reading puts both counts within tolerance. Under light reading (y), trials without light only, the same recordings give 5.74 and 0.80, so the no-lick count is outside tolerance and that reading does not match. The trial counts are more than twice the paper's 4,147 and 4,173, so the matching reading is not the paper's set of trials, and no reading gives the paper's trial counts (the nearest, 5,212 and 5,149, come from reading (a) with every trial). Performance during the cortical recordings, now over all 122 cortical cells as in every other group, is 0.681 under (x) and 0.694 under (y), within tolerance of 0.72 (0.703 and 0.718 without the flagged mice, a variant), but during the VPM recordings, which have no light trials, it is 0.585 over 29 units, outside 0.71 +- 0.071.

vpm-touch¶

VPM neurons respond to active touch with a sharp, short-latency increase in spike rate (0.6 +- 0.5 spikes per touch, median 0.4; latency 3.2 +- 1.0 ms, median 2.5 ms).

In [14]:
v = group("VPM")
print(f"VPM units: {len(v)} (touches with ITI >= 25 ms per unit: median {int(v.n_touches.median())}, range {int(v.n_touches.min())} to {int(v.n_touches.max())})")
print(f"Spikes per touch (VPM definition): mean {v.spt.mean():.3f} (expected 0.6 +- 0.093), median {v.spt.median():.3f} "
      f"(expected 0.4 +- 0.04), s.d. {v.spt.std():.3f}, range {v.spt.min():.2f} to {v.spt.max():.2f}; interval above 0 in {(v.spt_lo > 0).sum()}")
lat = v.dropna(subset=["lat_centre"])
print(f"Latency, units passing the exclusion: {len(lat)} (left out: {v.lat_excluded[v.lat_centre.isna()].value_counts().to_dict()})")
for name in ("lat_edge", "lat_centre"):
    print(f"  {'O9 a, left edge' if name == 'lat_edge' else 'O9 b, centre'} ({name}): mean {lat[name].mean():.2f} ms (expected 3.2 +- 0.32), median {lat[name].median():.2f} ms (expected 2.5 +- 0.25), "
          f"s.d. {lat[name].std():.2f}, range {lat[name].min():.1f} to {lat[name].max():.1f}")
print("Latencies above 10 ms:", lat[lat.lat_centre > 10][["path", "unit", "lat_centre", "spt"]].assign(path=lambda d: d.path.str[-40:-20]).to_dict("records"))
print("\nBy mouse:")
print(v.groupby("subject").agg(units=("spt", "size"), spt_mean=("spt", "mean"), lat_median=("lat_centre", "median")).round(2).to_string())
bw = pwu.set_index(["path", "unit"])
vv = v.set_index(["path", "unit"]).join(bw[["best", "spt_best"]])
print(f"\nVariant (not the criterion; biased upward): each unit's most responsive tracked whisker, spikes per touch mean "
      f"{vv.spt_best.mean():.3f}, median {vv.spt_best.median():.3f}")
fig, ax = plt.subplots(1, 2, figsize=(8, 2.8))
ax[0].hist(v.spt, bins=np.arange(-0.2, 1.6, 0.1), color="0.5"); ax[0].set_xlabel("spikes per touch"); ax[0].set_ylabel("VPM units")
ax[1].hist(lat.lat_centre, bins=np.arange(0, 41, 1), color="0.5"); ax[1].set_xlabel("latency (ms, bin centre)")
plt.tight_layout(); plt.show()
print("Axes hold every value: spikes per touch", round(v.spt.min(), 2), "to", round(v.spt.max(), 2), "; latency", lat.lat_centre.min(), "to", lat.lat_centre.max())
VPM units: 29 (touches with ITI >= 25 ms per unit: median 640, range 3 to 1415)
Spikes per touch (VPM definition): mean 0.403 (expected 0.6 +- 0.093), median 0.268 (expected 0.4 +- 0.04), s.d. 0.392, range -0.01 to 1.25; interval above 0 in 23
Latency, units passing the exclusion: 25 (left out: {'max rate below 10 spikes/s': 3, 'no crossing': 1})
  O9 a, left edge (lat_edge): mean 5.64 ms (expected 3.2 +- 0.32), median 2.00 ms (expected 2.5 +- 0.25), s.d. 13.86, range 1.0 to 71.0
  O9 b, centre (lat_centre): mean 6.14 ms (expected 3.2 +- 0.32), median 2.50 ms (expected 2.5 +- 0.25), s.d. 13.86, range 1.5 to 71.5
Latencies above 10 ms: [{'path': '184389_ses-20130213_', 'unit': 1, 'lat_centre': 71.5, 'spt': 0.0}, {'path': '203464_ses-20130702_', 'unit': 0, 'lat_centre': 14.5, 'spt': 1.1111111111111112}]

By mouse:
           units  spt_mean  lat_median
subject                               
anm184389     12      0.25         4.0
anm186997      2      0.65         3.0
anm199549      5      0.64         1.5
anm199551      1      0.14         3.5
anm199552      7      0.24         2.5
anm203464      2      1.18         8.5

Variant (not the criterion; biased upward): each unit's most responsive tracked whisker, spikes per touch mean 0.485, median 0.285
No description has been provided for this image
Axes hold every value: spikes per touch -0.01 to 1.25 ; latency 1.5 to 71.5

Result: not reproduced. Over the 29 VPM units, spikes per touch average 0.403 (median 0.268), outside the tolerances of 0.6 and 0.4. The latency median is 2.5 ms with the bin centre (O9 b) and 2.0 ms with its left edge (O9 a) against 2.5 ms, but the mean is 6.14 or 5.64 ms against 3.2, pulled up by one unit at 71.5 ms; 25 units pass the latency exclusion. The files name one principal whisker per file, and 11 of 20 units in files with several whiskers respond most to another one; taking each unit's most responsive whisker, a choice biased upward that the criterion does not make, raises the mean to 0.485, still outside the tolerance.

vpm-whisking¶

VPM neurons fire at 5.1 +- 5.5 spikes/s without whisker movement or touch; most (25 of 29) increase their rate during whisking without touch, and the mean rate rises from 5.1 to 13.5 spikes/s. Three of 29 do not increase with whisking but still respond to touch.

In [15]:
rows = []
for f in yu2016.FILTERS:
    for o2 in ("O2a", "O2b"):
        rows.append({"filter": f, "samples": o2, "increase": int((v[f"{f}:{o2}"] == 1).sum()), "decrease": int((v[f"{f}:{o2}"] == -1).sum()),
                     "nonwhisk_mean": v[f"{f}:rate_nonwhisk"].mean(), "nonwhisk_median": v[f"{f}:rate_nonwhisk"].median(),
                     "nonwhisk_sd": v[f"{f}:rate_nonwhisk"].std(), "whisk_mean": v[f"{f}:rate_whisk"].mean(),
                     "whisk_sd": v[f"{f}:rate_whisk"].std(), "signed_rank_p": "%.2g" % wilcox(v[f"{f}:rate_nonwhisk"], v[f"{f}:rate_whisk"]),
                     "whisk_higher": int((v[f"{f}:rate_whisk"] > v[f"{f}:rate_nonwhisk"]).sum())})
vw = pd.DataFrame(rows)
print("VPM units:", len(v), "; expected: increases 25 +- 2.5 of 29, nonwhisking mean 5.1 +- 1.02, median 3.3 +- 0.33, whisking mean 13.5 +- 2.36 spikes/s")
print(vw.round(4).to_string(index=False))
print("Seconds of nonwhisking and whisking per unit (filtfilt): median", round(v["filtfilt:time_nonwhisk"].median(), 1), "and",
      round(v["filtfilt:time_whisk"].median(), 1))
vs = rate_summary(v)
print("\nUnder every O11 reading (touches bounding the epochs), filter and sample reading:")
print(fmt_p(vs.drop(columns=["r2_above_0.25", "multiplex"])).round(4).to_string(index=False))
print("Ranges over the 16 readings:", ranges(vs, ["increase", "nonwhisk_mean", "nonwhisk_median", "whisk_mean", "signed_rank_p"]))
print("Units without a significant increase (filtfilt, O2a): spikes per touch",
      v[v["filtfilt:O2a"] != 1][["spt", "spt_lo"]].round(2).values.tolist())
VPM units: 29 ; expected: increases 25 +- 2.5 of 29, nonwhisking mean 5.1 +- 1.02, median 3.3 +- 0.33, whisking mean 13.5 +- 2.36 spikes/s
  filter samples  increase  decrease  nonwhisk_mean  nonwhisk_median  nonwhisk_sd  whisk_mean  whisk_sd signed_rank_p  whisk_higher
filtfilt     O2a        24         1         3.7609           1.7079       4.0008     11.5391   10.9661       5.2e-08            28
filtfilt     O2b        25         1         3.7609           1.7079       4.0008     11.5391   10.9661       5.2e-08            28
 lfilter     O2a        21         1         3.9250           1.7213       4.0878     11.0064   10.6188       7.1e-08            28
 lfilter     O2b        24         1         3.9250           1.7213       4.0878     11.0064   10.6188       7.1e-08            28
Seconds of nonwhisking and whisking per unit (filtfilt): median 1004.6 and 488.9

Under every O11 reading (touches bounding the epochs), filter and sample reading:
 O11   filter samples  n  increase  decrease  nonwhisk_mean  nonwhisk_median  whisk_mean signed_rank_p  whisk_higher
 a-i filtfilt     O2a 29        24         1         3.7609           1.7079     11.5391       5.2e-08            28
 a-i filtfilt     O2b 29        25         1         3.7609           1.7079     11.5391       5.2e-08            28
 a-i  lfilter     O2a 29        21         1         3.9250           1.7213     11.0064       7.1e-08            28
 a-i  lfilter     O2b 29        24         1         3.9250           1.7213     11.0064       7.1e-08            28
a-ii filtfilt     O2a 29        24         1         3.7352           1.8357     11.6062       5.2e-08            28
a-ii filtfilt     O2b 29        25         1         3.7352           1.8357     11.6062       5.2e-08            28
a-ii  lfilter     O2a 29        21         1         3.9029           1.8559     11.0658       7.1e-08            28
a-ii  lfilter     O2b 29        24         1         3.9029           1.8559     11.0658       7.1e-08            28
 b-i filtfilt     O2a 29        24         1         3.7972           1.7079     12.0716       5.2e-08            28
 b-i filtfilt     O2b 29        25         1         3.7972           1.7079     12.0716       5.2e-08            28
 b-i  lfilter     O2a 29        22         1         3.9643           1.7213     11.5216       7.1e-08            28
 b-i  lfilter     O2b 29        24         1         3.9643           1.7213     11.5216       7.1e-08            28
b-ii filtfilt     O2a 29        24         1         3.7735           1.8357     12.0908       5.2e-08            28
b-ii filtfilt     O2b 29        25         1         3.7735           1.8357     12.0908       5.2e-08            28
b-ii  lfilter     O2a 29        22         1         3.9410           1.8559     11.5426       7.1e-08            28
b-ii  lfilter     O2b 29        24         1         3.9410           1.8559     11.5426       7.1e-08            28
Ranges over the 16 readings: {'increase': (21.0, 25.0), 'nonwhisk_mean': (3.74, 3.96), 'nonwhisk_median': (1.71, 1.86), 'whisk_mean': (11.0, 12.1), 'signed_rank_p': (5.22e-08, 7.08e-08)}
Units without a significant increase (filtfilt, O2a): spikes per touch [[0.0, 0.0], [0.0, 0.0], [0.79, 0.69], [0.08, 0.06], [1.25, 1.14]]

Result: partially reproduced. The whisking rate is above the nonwhisking rate over the 29 units under every reading of O1, O2 and O11 (signed-rank p = 5.2e-08 to 7.1e-08 for the main epochs; higher in 28). Over the 16 readings 21 to 25 units increase significantly, so the count is outside 25 +- 2.5 under some; the mean whisking rate is 11.01 to 12.09 spikes/s, within 13.5 +- 2.36 under some readings only, and the nonwhisking rate is below the paper's under every reading (mean 3.74 to 3.96 against 5.1 +- 1.02; median 1.71 to 1.86 against 3.3 +- 0.33).

vpm-whisking-phase¶

The spike rate of VPM neurons is modulated by the phase of whisker movement.

In [16]:
for f in yu2016.FILTERS:
    print(f"{f}: VPM units with r2 > 0.25: {int((v[f'{f}:r2'] > 0.25).sum())} of {len(v)}; r2 median {v[f'{f}:r2'].median():.2f}; "
          f"coupling strength of coupled units median {v[v[f'{f}:r2'] > 0.25][f'{f}:strength'].median():.2f} spikes/s "
          f"(range {v[v[f'{f}:r2'] > 0.25][f'{f}:strength'].min():.2f} to {v[v[f'{f}:r2'] > 0.25][f'{f}:strength'].max():.2f})")
print("Units with r2 > 0.25 under each O11 reading and filter:", vs.drop_duplicates(["O11", "filter"]).set_index(["O11", "filter"])["r2_above_0.25"].to_dict())
nv = align[align.kind == "VPM"]
print(f"Reference (not part of the criterion): with spikes moved to the next trial, {int((nv.r2_shift > 0.25).sum())} of {len(nv)} units "
      "have r2 > 0.25 (filtfilt)")
print("By mouse (filtfilt):", v.assign(c=v["filtfilt:r2"] > 0.25).groupby("subject").c.agg(["sum", "size"]).to_dict("index"))
filtfilt: VPM units with r2 > 0.25: 25 of 29; r2 median 0.58; coupling strength of coupled units median 1.41 spikes/s (range 0.12 to 7.18)
lfilter: VPM units with r2 > 0.25: 27 of 29; r2 median 0.65; coupling strength of coupled units median 0.91 spikes/s (range 0.10 to 3.28)
Units with r2 > 0.25 under each O11 reading and filter: {('a-i', 'filtfilt'): 25, ('a-i', 'lfilter'): 27, ('a-ii', 'filtfilt'): 26, ('a-ii', 'lfilter'): 26, ('b-i', 'filtfilt'): 25, ('b-i', 'lfilter'): 26, ('b-ii', 'filtfilt'): 26, ('b-ii', 'lfilter'): 26}
Reference (not part of the criterion): with spikes moved to the next trial, 16 of 29 units have r2 > 0.25 (filtfilt)
By mouse (filtfilt): {'anm184389': {'sum': 10, 'size': 12}, 'anm186997': {'sum': 1, 'size': 2}, 'anm199549': {'sum': 5, 'size': 5}, 'anm199551': {'sum': 1, 'size': 1}, 'anm199552': {'sum': 7, 'size': 7}, 'anm203464': {'sum': 1, 'size': 2}}

Result: reproduced. 25 to 27 of the 29 VPM units have a sinusoid fit of rate against phase with r2 > 0.25 under both filters and every O11 reading, more than half. The measure is weak as a test: with spikes moved to the next trial, 16 of 29 units also pass, which the criterion does not ask for; the units' own spikes pass far more often (25 against 16, and r2 is higher in their own trial, p = 0.0015 in the data check).

exc-vm-touch-whisking¶

In L4 excitatory neurons (37 whole-cell), resting Vm spans -76 to -45 mV (-63 +- 8 mV); every neuron depolarizes after touch (latency 4.8 +- 0.8 ms, amplitude 6.0 +- 2.9 mV), while whisking causes only a small depolarization or hyperpolarization (1.2 +- 2.1 mV).

In [17]:
def vm_table(cls, sel, col="cls", filt="filtfilt", o3="a"):
    paths = group(cls, "whole-cell", sel, col).path
    return vmm[vmm.path.isin(paths) & (vmm.filt == filt) & (vmm.O3 == o3)].set_index("path")

for sel in ("all", "no_flagged"):
    e = vm_table("E", sel)
    pos = e[e.amp_mv > 0]
    print(f"[{sel}] whole-cell E cells: {len(e)} (paper 37); average touch PSP from a median of {int(e.touch_avg_n.median())} touches "
          f"(range {int(e.touch_avg_n.min())} to {int(e.touch_avg_n.max())}); amplitude > 0 in {len(pos)}, latency found in "
          f"{int(pos.latency_ms.notna().sum())} of them")
    print(f"   touch latency (expected mean 4.8 +- 0.48 ms), dVm/dt at the sampling rate (the criterion): mean {e.latency_ms.mean():.2f} "
          f"(s.d. {e.latency_ms.std():.2f}, median {e.latency_ms.median():.2f}, range {e.latency_ms.min():.2f} to {e.latency_ms.max():.2f}); "
          f"for comparison only, 1 ms bins (no longer a reading): left edge mean {e.latency_1ms_edge.mean():.2f}, centre mean "
          f"{e.latency_1ms_centre.mean():.2f}; amplitude mean {e.amp_mv.mean():.2f} mV (expected 6.0 +- 0.6; s.d. {e.amp_mv.std():.2f}, "
          f"median {e.amp_mv.median():.2f}, range {e.amp_mv.min():.2f} to {e.amp_mv.max():.2f})")
    print(f"   resting Vm O4a mean {e.rest_mean.mean():.2f} mV (s.d. {e.rest_mean.std():.2f}, range {e.rest_mean.min():.1f} to "
          f"{e.rest_mean.max():.1f}); O4b (median per cell) mean {e.rest_median.mean():.2f} (expected -63 +- 6.3)")
    for filt in yu2016.FILTERS:
        ef = vm_table("E", sel, filt=filt)
        print(f"   {filt}, earlier rule of this attempt (epochs O11 a-i, 250 ms rule on principal-whisker touches): whisking change (a) onset windows mean {ef.whisk_onset_change.mean():.2f} mV (s.d. {ef.whisk_onset_change.std():.2f}; "
              f"cells with onsets {int(ef.whisk_onset_change.notna().sum())}, onsets per cell median {int(ef.n_onsets.median())}); "
              f"(b) epochs mean {ef.whisk_epoch_change.mean():.2f} mV (s.d. {ef.whisk_epoch_change.std():.2f}); expected 1.2 +- 0.35")
e = vm_table("E", "all")
print("Cells without a positive touch PSP or a latency:", e[(e.amp_mv <= 0) | e.latency_ms.isna()][["subject", "amp_mv", "latency_ms", "touch_avg_n"]]
      .round(2).reset_index(drop=True).to_dict("records"))
print("By mouse line:", e.assign(line=e.subject.map(line_of)).groupby("line")[["amp_mv", "latency_ms", "rest_mean"]].mean().round(2).to_dict("index"))

# Whisking onsets and epochs under readings O10 (windows of the 2 degree rise: a, Supp. Fig. 4 windows; b, 0 to 250 ms
# after against the whole nonwhisking period) and O11 (touches bounding epochs and onsets: a, every tracked whisker;
# b, the principal whisker; i, 10 ms before onset to 100 ms after offset; ii, to 100 ms after onset). Answer of 2026-10-20.
o_rows = []
for cls_, p in [("E", p) for p in group("E", "whole-cell").path] + [("FS", p) for p in group("FS", "whole-cell").path]:
    r = by_path[p]
    m = ctl_mask(r)
    vm = r["vm1k"].astype(float)
    tri = r["beh"]["trial"]
    use = (tri >= 0) & m[np.clip(tri, 0, None)] & np.isfinite(vm)
    for filt in yu2016.FILTERS:
        for (o10, o11) in r["beh"][filt]["readings"]:
            e_ = yu2016.beh_reading(r["beh"], filt, o10, o11)[filt]
            res_, pre_, d_ = yu2016.vm_epoch_measures(vm, use, e_)
            o_rows.append({"cls": cls_, "path": p, "subject": r["subject"], "filter": filt, "O10": o10, "O11": o11, **res_,
                           "whisk_pre": np.array(pre_), "whisk_dvm": np.array(d_)})
ovm_all = pd.DataFrame(o_rows)
ovm = ovm_all[ovm_all.cls == "E"]
rows = []
for sel in ("all", "no_flagged"):
    g0 = ovm if sel == "all" else ovm[~ovm.subject.isin(flagged)]
    for (filt, o10, o11), g in g0.groupby(["filter", "O10", "O11"]):
        rows.append({"cells": sel, "filter": filt, "O10": o10, "O11": o11, "n": len(g), "rest_O4a": g.rest_mean.mean(),
                     "rest_O4b": g.rest_median.mean(), "onset_cells": int(g.whisk_onset_change.notna().sum()),
                     "onsets_median": g.n_onsets.median(), "whisk_change_a_onsets": g.whisk_onset_change.mean(),
                     "whisk_change_a_sd": g.whisk_onset_change.std(), "whisk_change_b_epochs": g.whisk_epoch_change.mean(),
                     "a_within": within(g.whisk_onset_change.mean(), 1.2, 0.35), "b_within": within(g.whisk_epoch_change.mean(), 1.2, 0.35),
                     "rest_within": within(g.rest_mean.mean(), -63, 6.3)})
oread = pd.DataFrame(rows)
print("\nWhisking change and resting Vm of the whole-cell E cells under readings O10 and O11 (expected whisking change 1.2 +- 0.35 mV, "
      "resting Vm -63 +- 6.3 mV; the touch values above do not depend on these readings):")
print(oread.round(2).to_string(index=False))
oa = oread[oread.cells == "all"]
print("Readings (all cells) with the whisking change within tolerance: measure (a)", int(oa.a_within.sum()), "of", len(oa),
      "; measure (b)", int(oa.b_within.sum()), "of", len(oa), "; resting Vm within tolerance in", int(oa.rest_within.sum()), "of", len(oa))
print("Range over readings (all cells): whisking change (a)", round(oa.whisk_change_a_onsets.min(), 2), "to", round(oa.whisk_change_a_onsets.max(), 2),
      "mV, (b)", round(oa.whisk_change_b_epochs.min(), 2), "to", round(oa.whisk_change_b_epochs.max(), 2), "mV; resting Vm (O4 a)",
      round(oa.rest_O4a.min(), 2), "to", round(oa.rest_O4a.max(), 2), "mV")
[all] whole-cell E cells: 51 (paper 37); average touch PSP from a median of 205 touches (range 0 to 662); amplitude > 0 in 48, latency found in 48 of them
   touch latency (expected mean 4.8 +- 0.48 ms), dVm/dt at the sampling rate (the criterion): mean 7.68 (s.d. 14.58, median 4.90, range 0.00 to 101.15); for comparison only, 1 ms bins (no longer a reading): left edge mean 14.89, centre mean 15.39; amplitude mean 4.78 mV (expected 6.0 +- 0.6; s.d. 3.67, median 4.69, range 0.01 to 15.64)
   resting Vm O4a mean -61.69 mV (s.d. 8.81, range -76.0 to -38.8); O4b (median per cell) mean -61.89 (expected -63 +- 6.3)
   filtfilt, earlier rule of this attempt (epochs O11 a-i, 250 ms rule on principal-whisker touches): whisking change (a) onset windows mean 0.86 mV (s.d. 1.73; cells with onsets 51, onsets per cell median 61); (b) epochs mean 1.10 mV (s.d. 1.64); expected 1.2 +- 0.35
   lfilter, earlier rule of this attempt (epochs O11 a-i, 250 ms rule on principal-whisker touches): whisking change (a) onset windows mean 0.75 mV (s.d. 1.60; cells with onsets 51, onsets per cell median 62); (b) epochs mean 1.08 mV (s.d. 1.69); expected 1.2 +- 0.35
[no_flagged] whole-cell E cells: 43 (paper 37); average touch PSP from a median of 253 touches (range 3 to 662); amplitude > 0 in 41, latency found in 41 of them
   touch latency (expected mean 4.8 +- 0.48 ms), dVm/dt at the sampling rate (the criterion): mean 4.97 (s.d. 3.08, median 4.90, range 0.00 to 17.90); for comparison only, 1 ms bins (no longer a reading): left edge mean 5.83, centre mean 6.33; amplitude mean 5.54 mV (expected 6.0 +- 0.6; s.d. 3.44, median 5.45, range 0.01 to 15.64)
   resting Vm O4a mean -60.53 mV (s.d. 9.05, range -76.0 to -38.8); O4b (median per cell) mean -60.73 (expected -63 +- 6.3)
   filtfilt, earlier rule of this attempt (epochs O11 a-i, 250 ms rule on principal-whisker touches): whisking change (a) onset windows mean 0.92 mV (s.d. 1.88; cells with onsets 43, onsets per cell median 63); (b) epochs mean 1.24 mV (s.d. 1.74); expected 1.2 +- 0.35
   lfilter, earlier rule of this attempt (epochs O11 a-i, 250 ms rule on principal-whisker touches): whisking change (a) onset windows mean 0.80 mV (s.d. 1.73; cells with onsets 43, onsets per cell median 65); (b) epochs mean 1.21 mV (s.d. 1.80); expected 1.2 +- 0.35
Cells without a positive touch PSP or a latency: [{'subject': 'anm223517', 'amp_mv': nan, 'latency_ms': nan, 'touch_avg_n': 3}, {'subject': 'anm307766', 'amp_mv': nan, 'latency_ms': nan, 'touch_avg_n': 4}, {'subject': 'anm318532', 'amp_mv': nan, 'latency_ms': nan, 'touch_avg_n': 0}]
By mouse line: {'C57BL/6': {'amp_mv': 6.5, 'latency_ms': 5.1, 'rest_mean': -62.05}, 'PV-ChR2': {'amp_mv': 8.36, 'latency_ms': 4.5, 'rest_mean': -38.81}, 'PV-IRES-Cre': {'amp_mv': 5.53, 'latency_ms': 4.62, 'rest_mean': -54.17}, 'PV-IRES-Cre x Ai32': {'amp_mv': nan, 'latency_ms': nan, 'rest_mean': -57.76}, 'SOM-IRES-Cre x Ai32': {'amp_mv': 8.35, 'latency_ms': 2.6, 'rest_mean': -71.76}, 'VGAT-ChR2-EYFP': {'amp_mv': 3.91, 'latency_ms': 9.48, 'rest_mean': -64.25}}
Whisking change and resting Vm of the whole-cell E cells under readings O10 and O11 (expected whisking change 1.2 +- 0.35 mV, resting Vm -63 +- 6.3 mV; the touch values above do not depend on these readings):
     cells   filter O10  O11  n  rest_O4a  rest_O4b  onset_cells  onsets_median  whisk_change_a_onsets  whisk_change_a_sd  whisk_change_b_epochs  a_within  b_within  rest_within
       all filtfilt   a  a-i 51    -61.69    -61.89           51           57.0                   0.83               1.72                   1.10     False      True         True
       all filtfilt   a a-ii 51    -61.67    -61.88           51           59.0                   0.82               1.73                   1.10     False      True         True
       all filtfilt   a  b-i 51    -61.69    -61.89           51           61.0                   0.87               1.73                   1.13      True      True         True
       all filtfilt   a b-ii 51    -61.67    -61.88           51           61.0                   0.86               1.74                   1.14      True      True         True
       all filtfilt   b  a-i 51    -61.69    -61.89           51           58.0                   0.78               1.73                   1.10     False      True         True
       all filtfilt   b a-ii 51    -61.67    -61.88           51           58.0                   0.78               1.73                   1.10     False      True         True
       all filtfilt   b  b-i 51    -61.69    -61.89           51           58.0                   0.82               1.75                   1.13     False      True         True
       all filtfilt   b b-ii 51    -61.67    -61.88           51           58.0                   0.81               1.75                   1.14     False      True         True
       all  lfilter   a  a-i 51    -61.68    -61.88           51           57.0                   0.73               1.59                   1.08     False      True         True
       all  lfilter   a a-ii 51    -61.66    -61.87           51           57.0                   0.72               1.59                   1.08     False      True         True
       all  lfilter   a  b-i 51    -61.68    -61.88           51           62.0                   0.76               1.60                   1.11     False      True         True
       all  lfilter   a b-ii 51    -61.66    -61.87           51           62.0                   0.74               1.60                   1.11     False      True         True
       all  lfilter   b  a-i 51    -61.68    -61.88           51           55.0                   0.65               1.56                   1.08     False      True         True
       all  lfilter   b a-ii 51    -61.66    -61.87           51           56.0                   0.65               1.56                   1.08     False      True         True
       all  lfilter   b  b-i 51    -61.68    -61.88           51           61.0                   0.68               1.57                   1.11     False      True         True
       all  lfilter   b b-ii 51    -61.66    -61.87           51           61.0                   0.67               1.57                   1.11     False      True         True
no_flagged filtfilt   a  a-i 43    -60.53    -60.73           43           61.0                   0.88               1.87                   1.24      True      True         True
no_flagged filtfilt   a a-ii 43    -60.52    -60.72           43           61.0                   0.87               1.87                   1.24      True      True         True
no_flagged filtfilt   a  b-i 43    -60.53    -60.73           43           63.0                   0.92               1.88                   1.27      True      True         True
no_flagged filtfilt   a b-ii 43    -60.52    -60.72           43           65.0                   0.91               1.89                   1.28      True      True         True
no_flagged filtfilt   b  a-i 43    -60.53    -60.73           43           61.0                   0.82               1.87                   1.24     False      True         True
no_flagged filtfilt   b a-ii 43    -60.52    -60.72           43           61.0                   0.82               1.88                   1.24     False      True         True
no_flagged filtfilt   b  b-i 43    -60.53    -60.73           43           64.0                   0.86               1.90                   1.27      True      True         True
no_flagged filtfilt   b b-ii 43    -60.52    -60.72           43           64.0                   0.86               1.90                   1.28      True      True         True
no_flagged  lfilter   a  a-i 43    -60.52    -60.71           43           62.0                   0.78               1.73                   1.21     False      True         True
no_flagged  lfilter   a a-ii 43    -60.51    -60.70           43           62.0                   0.77               1.73                   1.21     False      True         True
no_flagged  lfilter   a  b-i 43    -60.52    -60.71           43           65.0                   0.81               1.74                   1.24     False      True         True
no_flagged  lfilter   a b-ii 43    -60.51    -60.70           43           65.0                   0.79               1.74                   1.24     False      True         True
no_flagged  lfilter   b  a-i 43    -60.52    -60.71           43           59.0                   0.67               1.69                   1.21     False      True         True
no_flagged  lfilter   b a-ii 43    -60.51    -60.70           43           59.0                   0.67               1.69                   1.21     False      True         True
no_flagged  lfilter   b  b-i 43    -60.52    -60.71           43           67.0                   0.70               1.71                   1.24     False      True         True
no_flagged  lfilter   b b-ii 43    -60.51    -60.70           43           67.0                   0.70               1.71                   1.24     False      True         True
Readings (all cells) with the whisking change within tolerance: measure (a) 2 of 16 ; measure (b) 16 of 16 ; resting Vm within tolerance in 16 of 16
Range over readings (all cells): whisking change (a) 0.65 to 0.87 mV, (b) 1.08 to 1.14 mV; resting Vm (O4 a) -61.69 to -61.66 mV

Result: partially reproduced. Every one of the 48 whole-cell excitatory cells with enough touches depolarizes after touch and has a latency (3 cells have fewer than 5 touches). Resting Vm (mean -61.69 mV, or -61.89 with the median per cell) is within tolerance under every reading, but the touch amplitude (4.78 mV against 6.0 +- 0.6) and the latency (mean 7.68 ms with dVm/dt at the sampling rate, median 4.90, against 4.8 +- 0.48) are not, and neither depends on the whisking readings. The latency diagnostic below shows that the criterion's rule picks single-sample excursions of dVm/dt in 5 cells (under 1 ms) and is pulled up by late crossings in others (up to 101 ms). Under the 16 combinations of readings O10, O11 and the two filters, the whisking change with the onset windows ranges from 0.65 to 0.87 mV and is within 1.2 +- 0.35 in 2 (zero-phase filter, O10 a, O11 b-i and b-ii, 0.87 and 0.86 mV); with the epochs it is 1.08 to 1.14 mV and within tolerance in all 16. The group holds 51 cells against the paper's 37 and includes the 8 cells of the flagged mice; without them the amplitude is 5.54 mV and the latency 4.97 ms, both within tolerance, but that variant does not decide. Amplitudes go down to 0.01 mV: the peak of the average minus the pre-touch mean is positive for nearly any average, so "every neuron depolarizes" is a weak test.

exc-threshold-touch-vs-whisking¶

Touch moves the membrane potential of L4 excitatory neurons much closer to spike threshold than whisking does (shift 6.1 +- 3.2 mV against 1.1 +- 2.0 mV).

In [18]:
rows = []
for sel in ("all", "no_flagged"):
    for filt in yu2016.FILTERS:
        for o3 in ("a", "b"):
            e = vm_table("E", sel, filt=filt, o3=o3).dropna(subset=["shift_whisk", "shift_touch"])
            rows.append({"cells": sel, "filter": filt, "O3": o3, "n": len(e), "shift_whisk_mean": e.shift_whisk.mean(),
                         "shift_whisk_sd": e.shift_whisk.std(), "shift_touch_mean": e.shift_touch.mean(), "shift_touch_sd": e.shift_touch.std(),
                         "touch_larger": int((e.shift_touch > e.shift_whisk).sum()),
                         "p_touch_gt_whisk": "%.2g" % wilcox(e.shift_touch, e.shift_whisk, "greater"),
                         "threshold_median_mv": e.threshold_median.median(), "silent_cells": int((e.threshold_median == -42).sum())})
print("Shift toward threshold (10th percentile of distance to threshold, nonwhisking minus whisking or touch epochs); "
      "expected whisking 1.1 +- 0.33 mV, touch 6.1 +- 0.61 mV, touch > whisking by signed-rank p < 0.05")
print(pd.DataFrame(rows).round(2).to_string(index=False))
ta = vmm[vmm.O3 == "a"].set_index(["path", "filt"]).threshold_median
tb = vmm[vmm.O3 == "b"].set_index(["path", "filt"]).threshold_median
print("O3 a against b: largest difference of a cell's median threshold", round(float((ta - tb).abs().max()), 4), "mV; spikes whose threshold differs:",
      sum(int(np.sum(~np.isclose(r["spike_thr"]["a"], r["spike_thr"]["b"], equal_nan=True))) for r in l4 if r["mode"] == "whole-cell"), "of",
      sum(len(r["spike_thr"]["a"]) for r in l4 if r["mode"] == "whole-cell"),
      "(reading a adds the trial's mean dVm/dt to the crossing level of reading b)")
trows = []
for p in group("E", "whole-cell").path:
    r = by_path[p]
    m = ctl_mask(r)
    for o11 in yu2016.O11:
        rr = {**r, "beh": yu2016.beh_o11(r["beh"], o11)}
        for filt in yu2016.FILTERS:
            x = yu2016.vm_measures(rr, m, filt, "a")
            trows.append({"O11": o11, "filter": filt, "shift_whisk": x["shift_whisk"], "shift_touch": x["shift_touch"]})
tr11 = pd.DataFrame(trows).dropna()
t11 = tr11.groupby(["O11", "filter"]).apply(lambda g: pd.Series({"n": len(g), "shift_whisk_mean": g.shift_whisk.mean(), "shift_touch_mean": g.shift_touch.mean(),
                                                                 "touch_larger": int((g.shift_touch > g.shift_whisk).sum()),
                                                                 "p_touch_gt_whisk": wilcox(g.shift_touch, g.shift_whisk, "greater")})).reset_index()
print("\nUnder each O11 reading (threshold O3 a):")
print(fmt_p(t11).round(3).to_string(index=False))
Shift toward threshold (10th percentile of distance to threshold, nonwhisking minus whisking or touch epochs); expected whisking 1.1 +- 0.33 mV, touch 6.1 +- 0.61 mV, touch > whisking by signed-rank p < 0.05
     cells   filter O3  n  shift_whisk_mean  shift_whisk_sd  shift_touch_mean  shift_touch_sd  touch_larger p_touch_gt_whisk  threshold_median_mv  silent_cells
       all filtfilt  a 50              0.84            2.75              4.62            3.50            44            1e-10                -42.0            17
       all filtfilt  b 50              0.84            2.75              4.62            3.50            44            1e-10                -42.0            17
       all  lfilter  a 50              1.10            2.01              4.67            3.54            43            9e-11                -42.0            17
       all  lfilter  b 50              1.10            2.01              4.67            3.54            43            9e-11                -42.0            17
no_flagged filtfilt  a 43              0.76            2.78              5.24            3.16            40            8e-12                -42.0            14
no_flagged filtfilt  b 43              0.76            2.78              5.24            3.16            40            8e-12                -42.0            14
no_flagged  lfilter  a 43              1.03            1.85              5.29            3.20            40          3.8e-12                -42.0            14
no_flagged  lfilter  b 43              1.03            1.85              5.29            3.20            40          3.8e-12                -42.0            14
O3 a against b: largest difference of a cell's median threshold 0.0089 mV; spikes whose threshold differs: 8 of 41620 (reading a adds the trial's mean dVm/dt to the crossing level of reading b)
Under each O11 reading (threshold O3 a):
 O11   filter    n  shift_whisk_mean  shift_touch_mean  touch_larger p_touch_gt_whisk
 a-i filtfilt 50.0             0.845             4.621          44.0            1e-10
 a-i  lfilter 50.0             1.096             4.672          43.0            9e-11
a-ii filtfilt 50.0             1.063             4.598          43.0          1.1e-10
a-ii  lfilter 50.0             1.102             4.649          43.0            9e-11
 b-i filtfilt 50.0             0.898             4.617          44.0          1.1e-10
 b-i  lfilter 50.0             1.145             4.667          43.0          1.1e-10
b-ii filtfilt 50.0             1.123             4.597          43.0          1.3e-10
b-ii  lfilter 50.0             1.156             4.646          43.0          1.1e-10

Result: partially reproduced. Touch moves the 50 excitatory cells closer to threshold than whisking does in 43 or 44 of them under every filter and O11 reading (signed-rank p = 9e-11 to 1.3e-10). The touch shift averages 4.60 to 4.67 mV, outside the tolerance of 6.1 +- 0.61, and the whisking shift 0.84 to 1.16 mV, within 1.1 +- 0.33 under every reading. The two threshold readings O3 give the same values: a cell's median threshold differs by at most 0.0089 mV and 8 of 41,620 spikes change threshold, since a trial's mean dVm/dt is close to zero. 17 of the 50 cells fire no spike, so their threshold is the default -42 mV.

exc-effective-reversal¶

The effective reversal potential of the touch response of L4 excitatory neurons is more depolarized than resting Vm, while that of whisking is close to rest and more hyperpolarized than that of touch.

In [19]:
rows = []
for sel in ("all", "no_flagged"):
    for filt in yu2016.FILTERS:
        e = vm_table("E", sel, filt=filt)
        rt = e.apply(lambda x: yu2016.reversal(x.touch_pre, x.touch_dvm)[0], axis=1)
        rw = e.apply(lambda x: yu2016.reversal(x.whisk_pre, x.whisk_dvm)[0], axis=1)
        ok = rt.notna() & rw.notna()
        pt = yu2016.reversal(np.concatenate(e.touch_pre.to_list()), np.concatenate(e.touch_dvm.to_list()))
        pw_ = yu2016.reversal(np.concatenate(e.whisk_pre.to_list()), np.concatenate(e.whisk_dvm.to_list()))
        rows.append({"cells": sel, "filter": filt, "n_both": int(ok.sum()), "touch_rev_median": rt[ok].median(), "whisk_rev_median": rw[ok].median(),
                     "touch_above_whisk": int((rt[ok] > rw[ok]).sum()), "rest_median": e.rest_mean.median(),
                     "pooled_touch_rev": pt[0], "pooled_touch_slope": pt[1], "pooled_whisk_rev": pw_[0], "pooled_whisk_slope": pw_[1]})
print("Effective reversal potentials (mV): (a) per neuron, medians over neurons with both; (b) one pooled fit per event type")
print(pd.DataFrame(rows).round(2).to_string(index=False))
e = vm_table("E", "all")
rt_ = e.apply(lambda x: yu2016.reversal(x.touch_pre, x.touch_dvm)[0], axis=1).dropna().sort_values()
print("Per-neuron touch reversals (mV), the middle six of", len(rt_), ":", rt_.iloc[len(rt_) // 2 - 3:len(rt_) // 2 + 3].round(3).tolist(),
      "; every value:", rt_.round(1).tolist())
rrows = []
for (filt, o10, o11), g in ovm.groupby(["filter", "O10", "O11"]):
    rw = g.set_index("path").apply(lambda x: yu2016.reversal(x.whisk_pre, x.whisk_dvm)[0], axis=1)
    both = rw.dropna().index.intersection(rt_.index)
    rrows.append({"filter": filt, "O10": o10, "O11": o11, "n_both": len(both), "touch_rev_median": rt_[both].median(),
                  "whisk_rev_median": rw[both].median(), "touch_above_whisk": int((rt_[both] > rw[both]).sum()),
                  "pooled_whisk_rev": yu2016.reversal(np.concatenate(g.whisk_pre.to_list()), np.concatenate(g.whisk_dvm.to_list()))[0]})
print("\nWhisking reversal under each onset reading O10 and O11 (touch reversals do not depend on them):")
print(pd.DataFrame(rrows).round(2).to_string(index=False))
print("Per-neuron touch events median", int(e.touch_dvm.map(len).median()), ", whisking onsets median", int(e.whisk_dvm.map(len).median()))
Effective reversal potentials (mV): (a) per neuron, medians over neurons with both; (b) one pooled fit per event type
     cells   filter  n_both  touch_rev_median  whisk_rev_median  touch_above_whisk  rest_median  pooled_touch_rev  pooled_touch_slope  pooled_whisk_rev  pooled_whisk_slope
       all filtfilt      48            -50.00            -58.49                 39       -63.90            -31.90               -0.19            -55.30               -0.18
       all  lfilter      48            -50.00            -59.78                 38       -63.91            -31.90               -0.19            -56.27               -0.19
no_flagged filtfilt      41            -49.09            -57.25                 37       -62.36            -34.45               -0.22            -54.91               -0.20
no_flagged  lfilter      41            -49.09            -58.21                 36       -62.32            -34.45               -0.22            -55.85               -0.20
Per-neuron touch reversals (mV), the middle six of 48 : [-51.525, -50.965, -50.231, -49.769, -49.595, -49.467] ; every value: [-215.7, -70.2, -70.0, -67.2, -65.7, -64.4, -63.1, -62.9, -62.1, -60.4, -60.2, -59.5, -56.0, -55.8, -55.5, -54.8, -53.4, -52.7, -52.4, -52.0, -51.8, -51.5, -51.0, -50.2, -49.8, -49.6, -49.5, -49.1, -48.8, -46.0, -45.7, -45.3, -45.0, -44.8, -39.5, -39.4, -38.8, -38.4, -36.7, -36.6, -33.7, -29.0, -27.0, -19.2, -16.4, -6.7, 16.6, 18.3]
Whisking reversal under each onset reading O10 and O11 (touch reversals do not depend on them):
  filter O10  O11  n_both  touch_rev_median  whisk_rev_median  touch_above_whisk  pooled_whisk_rev
filtfilt   a  a-i      48            -50.00            -57.43                 38            -55.41
filtfilt   a a-ii      48            -50.00            -57.43                 39            -55.49
filtfilt   a  b-i      48            -50.00            -58.50                 39            -55.27
filtfilt   a b-ii      48            -50.00            -58.50                 40            -55.34
filtfilt   b  a-i      48            -50.00            -58.35                 38            -51.62
filtfilt   b a-ii      48            -50.00            -58.35                 38            -51.67
filtfilt   b  b-i      48            -50.00            -58.64                 40            -51.59
filtfilt   b b-ii      48            -50.00            -58.64                 40            -51.63
 lfilter   a  a-i      48            -50.00            -60.28                 38            -56.24
 lfilter   a a-ii      48            -50.00            -60.28                 38            -56.30
 lfilter   a  b-i      48            -50.00            -59.78                 38            -56.22
 lfilter   a b-ii      48            -50.00            -59.78                 38            -56.29
 lfilter   b  a-i      47            -49.77            -60.91                 39            -53.32
 lfilter   b a-ii      47            -49.77            -60.91                 39            -53.27
 lfilter   b  b-i      47            -49.77            -60.91                 39            -53.20
 lfilter   b b-ii      47            -49.77            -60.91                 39            -53.19
Per-neuron touch events median 205 , whisking onsets median 61

Result: reproduced. Fitted per neuron, the touch reversal (median -50.0 mV) lies above the whisking reversal (median -57.4 to -60.9 mV over the filters and the O10 and O11 readings) in 38 to 40 of 47 or 48 neurons and above the median resting Vm (-63.9 mV); one pooled fit per event type gives -31.90 mV for touch and -51.6 to -56.3 mV for whisking, both above rest as well. The median of exactly -50.00 mV is the mean of the two middle neurons, -50.231 and -49.769 mV.

fs-touch-response¶

L4 FS interneurons fire at 9.3 +- 8.5 spikes/s without whisker movement and fire 1.9 +- 1.0 spikes per touch, with a spike latency of 5.3 +- 2.1 ms.

In [20]:
# FS readings of the criterion (answer of 2026-10-20): driven at (a) p < 0.001 or (b) p < 0.01, each (i) with or
# (ii) without the approximate waveform boundary. The paper's counts are the check: 19 ChR2-tagged control FS cells
# (5 whole-cell, 14 cell-attached), and 14 control plus 10 ION-cut cell-attached FS cells in VGAT-ChR2-EYFP mice.
cnt = []
for rname, col in FS_READINGS_TOUCH.items():
    vgat = cells[(cells.line == "VGAT-ChR2-EYFP") & (cells[col] == "FS")]
    cnt.append({"FS reading": rname, "VGAT_FS_whole_cell": int((vgat["mode"] == "whole-cell").sum()),
                "VGAT_FS_cell_attached": int((vgat["mode"] == "cell-attached").sum()),
                "VGAT_FS_cell_attached_no_flagged": int(((vgat["mode"] == "cell-attached") & ~vgat.subject.isin(flagged)).sum()),
                "FS_all_lines": int((cells[col] == "FS").sum()), "FS_with_principal_whisker": int((meas[col] == "FS").sum())})
print("FS cells per reading; Supplementary Table 2: VGAT-ChR2 whole-cell 5 (control), cell-attached 24 (14 control + 10 ION-cut); "
      "control ChR2-tagged 19; control FS of all lines 26 (text) or 25 (Supp. Fig. 7)")
print(pd.DataFrame(cnt).to_string(index=False))
rows = []
for rname, col in FS_READINGS_TOUCH.items():
    for sel in ("all", "no_flagged"):
        g = group("FS", None, sel, col)
        lat = g.dropna(subset=["lat_centre"])
        row = {"FS reading": rname, "cells": sel, "n": len(g), "nonwhisk_mean": g["filtfilt:rate_nonwhisk"].mean(),
               "nonwhisk_mean_lfilter": g["lfilter:rate_nonwhisk"].mean(), "nonwhisk_sd": g["filtfilt:rate_nonwhisk"].std(),
               "spt_mean": g.spt.mean(), "spt_sd": g.spt.std(), "spt_median": g.spt.median(), "lat_n": len(lat),
               "lat_O9a_edge_mean": lat.lat_edge.mean(), "lat_O9b_centre_mean": lat.lat_centre.mean(), "lat_centre_median": lat.lat_centre.median(),
               "lat_sd": lat.lat_centre.std(), "lat_max": lat.lat_centre.max()}
        row["within_of_3"] = (within(row["nonwhisk_mean"], 9.3, 1.67) + within(row["spt_mean"], 1.9, 0.2)
                              + max(within(row["lat_O9a_edge_mean"], 5.3, 0.53), within(row["lat_O9b_centre_mean"], 5.3, 0.53)))
        rows.append(row)
fsr = pd.DataFrame(rows)
print("\nFS cells, trials without light; expected nonwhisking 9.3 +- 1.67 spikes/s, spikes per touch 1.9 +- 0.2, latency 5.3 +- 0.53 ms (24 cells); "
      "within_of_3 counts the values within tolerance (latency under either O9 reading; nonwhisking with the zero-phase filter)")
print(fsr.round(2).to_string(index=False))
nw11 = []
for rname, col in FS_READINGS_TOUCH.items():
    t = rate_summary(group("FS", None, "all", col))
    nw11.append({"FS reading": rname, "nonwhisk_mean_min": t.nonwhisk_mean.min(), "nonwhisk_mean_max": t.nonwhisk_mean.max(),
                 "within_under_all": bool(t.nonwhisk_mean.sub(9.3).abs().le(1.67).all())})
print("\nNonwhisking rate over the O11 readings and both filters (expected 9.3 +- 1.67):")
print(pd.DataFrame(nw11).round(2).to_string(index=False))
g = group("FS")
print("Main group (a)(i) by line and mode:", g.assign(line=g.subject.map(line_of)).groupby(["line", "mode"]).size().to_dict())
print("FS cells left out of the latency, (a)(i):", g[g.lat_centre.isna()].lat_excluded.value_counts().to_dict())
FS cells per reading; Supplementary Table 2: VGAT-ChR2 whole-cell 5 (control), cell-attached 24 (14 control + 10 ION-cut); control ChR2-tagged 19; control FS of all lines 26 (text) or 25 (Supp. Fig. 7)
                  FS reading  VGAT_FS_whole_cell  VGAT_FS_cell_attached  VGAT_FS_cell_attached_no_flagged  FS_all_lines  FS_with_principal_whisker
    (a)(i) p<0.001, boundary                   4                     12                                 8            20                         17
(a)(ii) p<0.001, no boundary                   4                     20                                13            28                         25
     (b)(i) p<0.01, boundary                   4                     12                                 8            20                         17
 (b)(ii) p<0.01, no boundary                   4                     23                                16            31                         28

FS cells, trials without light; expected nonwhisking 9.3 +- 1.67 spikes/s, spikes per touch 1.9 +- 0.2, latency 5.3 +- 0.53 ms (24 cells); within_of_3 counts the values within tolerance (latency under either O9 reading; nonwhisking with the zero-phase filter)
                  FS reading      cells  n  nonwhisk_mean  nonwhisk_mean_lfilter  nonwhisk_sd  spt_mean  spt_sd  spt_median  lat_n  lat_O9a_edge_mean  lat_O9b_centre_mean  lat_centre_median  lat_sd  lat_max  within_of_3
    (a)(i) p<0.001, boundary        all 17           8.53                   8.64         8.59      1.50    1.07        1.93     13               5.23                 5.73                4.5    3.54     15.5            2
    (a)(i) p<0.001, boundary no_flagged 15           8.55                   8.70         9.07      1.69    0.99        1.98     13               5.23                 5.73                4.5    3.54     15.5            2
(a)(ii) p<0.001, no boundary        all 25           8.73                   8.81         8.49      1.48    1.23        1.67     19               6.16                 6.66                4.5    5.23     24.5            1
(a)(ii) p<0.001, no boundary no_flagged 20           8.04                   8.18         8.13      1.82    1.15        1.96     18               5.17                 5.67                4.5    3.03     15.5            3
     (b)(i) p<0.01, boundary        all 17           8.53                   8.64         8.59      1.50    1.07        1.93     13               5.23                 5.73                4.5    3.54     15.5            2
     (b)(i) p<0.01, boundary no_flagged 15           8.55                   8.70         9.07      1.69    0.99        1.98     13               5.23                 5.73                4.5    3.54     15.5            2
 (b)(ii) p<0.01, no boundary        all 28           8.71                   8.82         8.29      1.39    1.21        1.28     22              10.64                11.14                5.5   16.67     78.5            1
 (b)(ii) p<0.01, no boundary no_flagged 23           8.12                   8.27         7.92      1.66    1.16        1.78     21              10.00                10.50                5.5   16.80     78.5            1
Nonwhisking rate over the O11 readings and both filters (expected 9.3 +- 1.67):
                  FS reading  nonwhisk_mean_min  nonwhisk_mean_max  within_under_all
    (a)(i) p<0.001, boundary               8.52               8.72              True
(a)(ii) p<0.001, no boundary               8.71               8.88              True
     (b)(i) p<0.01, boundary               8.52               8.72              True
 (b)(ii) p<0.01, no boundary               8.70               8.88              True
Main group (a)(i) by line and mode: {('C57BL/6', 'whole-cell'): 1, ('PV-IRES-Cre', 'cell-attached'): 3, ('VGAT-ChR2-EYFP', 'cell-attached'): 9, ('VGAT-ChR2-EYFP', 'whole-cell'): 4}
FS cells left out of the latency, (a)(i): {'no crossing': 4}

Result: blocked (methods underspecified). The FS readings of the criterion give different outcomes. With the boundary, (a)(i) and (b)(i) give the same 17 cells with a principal whisker (the paper has 26): the nonwhisking rate (8.53 spikes/s; 8.52 to 8.72 over the O11 readings and filters) and the latency (5.23 ms with the bin edge, 5.73 ms with the centre, over 13 cells) are within tolerance and spikes per touch (1.50) is not, so two of three values match (partially reproduced). Without the boundary, (a)(ii) has 25 cells and (b)(ii) 28, with 1.48 and 1.39 spikes per touch and latencies of 6.16 to 6.66 and 10.64 to 11.14 ms, so only the nonwhisking rate is within tolerance (not reproduced). Against the paper's counts, the dandiset's VGAT-ChR2 mice give 4 whole-cell FS cells (paper 5) under every reading, and 12, 20, 12 and 23 cell-attached FS cells under (a)(i), (a)(ii), (b)(i) and (b)(ii), against 24 (14 control and 10 ION-cut), so the readings without the boundary come closer to the paper's count. Spikes per touch is 1.39 to 1.50 under every reading, below 1.9 +- 0.2.

fs-vm-touch-whisking¶

In whole-cell L4 FS interneurons (n = 7), touch depolarizes the membrane by 7.0 +- 5.2 mV with onset 4.0 +- 0.6 ms, and whisking depolarizes it by 2.7 +- 2.1 mV toward threshold.

In [21]:
for sel in ("all", "no_flagged"):
    for filt in yu2016.FILTERS:
        f_ = vm_table("FS", sel, filt=filt)
        print(f"[{sel}, {filt}] whole-cell FS cells: {len(f_)} (paper 7): touch amplitude mean {f_.amp_mv.mean():.2f} mV (expected 7.0 +- 1.97), "
              f"latency mean (expected 4.0 +- 0.4 ms) at the sampling rate {f_.latency_ms.mean():.2f} (median {f_.latency_ms.median():.2f}); for "
              f"comparison only (no longer a reading), 1 ms edge {f_.latency_1ms_edge.mean():.2f}, centre {f_.latency_1ms_centre.mean():.2f}; whisking (a) onset change mean {f_.whisk_onset_change.mean():.2f} mV, "
              f"(b) threshold-distance shift mean {f_.shift_whisk.mean():.2f} mV (expected 2.7 +- 0.79)")
print(vm_table("FS", "all")[["subject", "amp_mv", "latency_ms", "latency_1ms_centre", "whisk_onset_change", "shift_whisk", "n_onsets", "touch_avg_n"]]
      .assign(line=lambda d: d.subject.map(line_of)).round(2).reset_index(drop=True).to_string())
fo = ovm_all[ovm_all.cls == "FS"].groupby(["filter", "O10", "O11"]).whisk_onset_change.agg(["size", "mean"]).reset_index()
print("Whisking change (a), onset windows, of the whole-cell FS cells under each O10 and O11 reading: range", round(fo["mean"].min(), 2), "to",
      round(fo["mean"].max(), 2), "mV; within 2.7 +- 0.79 in", int(fo["mean"].sub(2.7).abs().le(0.79).sum()), "of", len(fo))
print("Variant (not a reading of the claims file): the PV-ChR2 cell, FS in Supplementary Table 2, added:",
      vmm[(vmm.subject == "anm250509") & (vmm.filt == "filtfilt") & (vmm.O3 == "a")][["amp_mv", "latency_ms", "latency_1ms_centre", "whisk_onset_change", "shift_whisk"]].round(2).to_dict("records"))
[all, filtfilt] whole-cell FS cells: 5 (paper 7): touch amplitude mean 5.56 mV (expected 7.0 +- 1.97), latency mean (expected 4.0 +- 0.4 ms) at the sampling rate 1.88 (median 1.35); for comparison only (no longer a reading), 1 ms edge 3.67, centre 4.17; whisking (a) onset change mean 2.54 mV, (b) threshold-distance shift mean 1.01 mV (expected 2.7 +- 0.79)
[all, lfilter] whole-cell FS cells: 5 (paper 7): touch amplitude mean 5.56 mV (expected 7.0 +- 1.97), latency mean (expected 4.0 +- 0.4 ms) at the sampling rate 1.88 (median 1.35); for comparison only (no longer a reading), 1 ms edge 3.67, centre 4.17; whisking (a) onset change mean 2.24 mV, (b) threshold-distance shift mean 0.82 mV (expected 2.7 +- 0.79)
[no_flagged, filtfilt] whole-cell FS cells: 5 (paper 7): touch amplitude mean 5.56 mV (expected 7.0 +- 1.97), latency mean (expected 4.0 +- 0.4 ms) at the sampling rate 1.88 (median 1.35); for comparison only (no longer a reading), 1 ms edge 3.67, centre 4.17; whisking (a) onset change mean 2.54 mV, (b) threshold-distance shift mean 1.01 mV (expected 2.7 +- 0.79)
[no_flagged, lfilter] whole-cell FS cells: 5 (paper 7): touch amplitude mean 5.56 mV (expected 7.0 +- 1.97), latency mean (expected 4.0 +- 0.4 ms) at the sampling rate 1.88 (median 1.35); for comparison only (no longer a reading), 1 ms edge 3.67, centre 4.17; whisking (a) onset change mean 2.24 mV, (b) threshold-distance shift mean 0.82 mV (expected 2.7 +- 0.79)
     subject  amp_mv  latency_ms  latency_1ms_centre  whisk_onset_change  shift_whisk  n_onsets  touch_avg_n            line
0  anm220466    2.90        0.00                 NaN                2.06         1.32       175          134  VGAT-ChR2-EYFP
1  anm220470    1.20        0.30                 NaN                0.76         1.60       147           58  VGAT-ChR2-EYFP
2  anm236461   10.44        5.10                 5.5                7.32         4.02        18          131         C57BL/6
3  anm305599    4.90        2.65                 4.5                2.31         1.39        18           88  VGAT-ChR2-EYFP
4  anm307766    8.38        1.35                 2.5                0.25        -3.29         7           11  VGAT-ChR2-EYFP
Whisking change (a), onset windows, of the whole-cell FS cells under each O10 and O11 reading: range 1.47 to 2.55 mV; within 2.7 +- 0.79 in 12 of 16
Variant (not a reading of the claims file): the PV-ChR2 cell, FS in Supplementary Table 2, added: [{'amp_mv': 8.36, 'latency_ms': 4.5, 'latency_1ms_centre': 3.5, 'whisk_onset_change': 1.7, 'shift_whisk': 2.05}]
In [22]:
# Cause of the near-zero Vm latencies (the verifier's request). For each whole-cell E and FS cell, the touch-aligned average
# (full rate) and its first difference dVm/dt, with the criterion's threshold (3 s.d. of dVm/dt 0 to 25 ms before onset). Also,
# as a diagnostic that decides nothing, the latency with the baseline's mean dVm/dt subtracted before the comparison.
def lat_diag(rec):
    fs = rec["vm_hz"]
    a = rec["touch_avg"].astype(float)
    i0 = int(yu2016.EVENT_PRE_S * fs)
    d = np.diff(a) * fs / 1000.0
    base = d[i0 - int(0.025 * fs):i0]
    thr = 3 * base.std()
    hit = np.flatnonzero(d[i0:] > thr)
    hit_m = np.flatnonzero(d[i0:] - base.mean() > thr)
    pre = a[i0 - int(0.005 * fs):i0].mean()
    return {"fs": fs, "base_mean": base.mean(), "base_sd": base.std(), "thr": thr, "d_first_0.5ms_max": d[i0:i0 + int(0.0005 * fs)].max(),
            "latency_ms": hit[0] / fs * 1000 if len(hit) else np.nan, "latency_mean_subtracted_ms": hit_m[0] / fs * 1000 if len(hit_m) else np.nan,
            "vm_0_to_1ms_minus_pre": a[i0:i0 + int(0.001 * fs)].mean() - pre, "d": d, "a": a - pre, "i0": i0}
ld = []
for cls_ in ("E", "FS"):
    for p in group(cls_, "whole-cell").path:
        r = by_path[p]
        if r.get("touch_avg") is None or r["touch_avg_n"] < 5:
            continue
        x = lat_diag(r)
        ld.append({"cls": cls_, "path": p, "subject": r["subject"], "touches": r["touch_avg_n"], **{k: v for k, v in x.items() if np.ndim(v) == 0}})
ld = pd.DataFrame(ld)
print("Cells with the criterion's latency under 2 ms:")
print(ld[ld.latency_ms < 2].drop(columns="path").round(3).to_string(index=False))
print("All cells: dVm/dt baseline mean against its s.d., median ratio", round(float((ld.base_mean / ld.base_sd).median()), 3))
for cls_, g in ld.groupby("cls"):
    print(f"{cls_}: {len(g)} cells; latency mean {g.latency_ms.mean():.2f} ms (median {g.latency_ms.median():.2f}); under 1 ms {int((g.latency_ms < 1).sum())}; "
          f"diagnostic with the baseline mean subtracted, mean {g.latency_mean_subtracted_ms.mean():.2f} ms")
fig, ax = plt.subplots(2, max(1, int((ld.latency_ms < 2).sum())), figsize=(2.4 * max(1, int((ld.latency_ms < 2).sum())), 4), squeeze=False)
for j, p in enumerate(ld[ld.latency_ms < 2].path):
    x = lat_diag(by_path[p])
    fs, i0 = x["fs"], x["i0"]
    sl = slice(i0 - int(0.002 * fs), i0 + int(0.002 * fs))
    tms = (np.arange(len(x["a"]))[sl] - i0) / fs * 1000
    ax[0, j].plot(tms, x["a"][sl], color="k", lw=0.8); ax[0, j].axvline(0, color="C0", lw=0.5)
    ax[0, j].set_title(by_path[p]["subject"], fontsize=7)
    ax[1, j].plot(tms, x["d"][sl], color="k", lw=0.8); ax[1, j].axhline(x["thr"], color="C3", lw=0.6); ax[1, j].axvline(0, color="C0", lw=0.5)
    ax[1, j].set_xlabel("ms from touch onset", fontsize=7)
    print(f"{by_path[p]['subject']} ({ld.set_index('path').cls[p]}): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms:",
          np.round(x["a"][sl][::max(1, int(fs // 5000))], 3).tolist())
    print("   dVm/dt (mV/ms), same samples:", np.round(x["d"][sl][::max(1, int(fs // 5000))], 2).tolist(), "; threshold", round(x["thr"], 2))
ax[0, 0].set_ylabel("Vm - pre (mV)"); ax[1, 0].set_ylabel("dVm/dt (mV/ms)")
plt.tight_layout(); plt.show()
print("Each panel shows -2 to +2 ms; every plotted value lies within the axes.")
Cells with the criterion's latency under 2 ms:
cls   subject  touches      fs  base_mean  base_sd   thr  d_first_0.5ms_max  latency_ms  latency_mean_subtracted_ms  vm_0_to_1ms_minus_pre   i0
  E anm190964       59 10000.0     -0.008    0.117 0.351              0.316        1.40                        1.40                  0.171  500
  E anm220466       76 10000.0     -0.018    0.142 0.426              0.562        0.00                        0.00                 -0.064  500
  E anm231235      253 10000.0     -0.012    0.089 0.268              0.276        0.30                        0.30                 -0.083  500
  E anm234412      301 10000.0     -0.019    0.136 0.407              0.217        1.00                        1.00                 -0.045  500
  E anm253096      239 10000.0     -0.075    0.105 0.314              0.043        1.80                        1.20                  0.016  500
  E anm266945      303 10000.0     -0.003    0.124 0.372              0.210        0.90                        0.90                 -0.035  500
  E anm318532      116 20000.0      0.020    0.290 0.870              2.143        0.45                        0.45                  0.098 1000
  E anm318532       40 20000.0     -0.001    0.451 1.352              8.400        0.35                        0.35                  0.081 1000
 FS anm220466      134 10000.0     -0.007    0.237 0.711              0.958        0.00                        0.00                  0.592  500
 FS anm220470       58 10000.0      0.016    0.616 1.847              3.053        0.30                        0.30                  0.597  500
 FS anm307766       11 20000.0     -0.028    0.413 1.240              1.069        1.35                        1.35                  0.429 1000
All cells: dVm/dt baseline mean against its s.d., median ratio -0.243
E: 48 cells; latency mean 7.68 ms (median 4.90); under 1 ms 5; diagnostic with the baseline mean subtracted, mean 7.48 ms
FS: 5 cells; latency mean 1.88 ms (median 1.35); under 1 ms 2; diagnostic with the baseline mean subtracted, mean 1.76 ms
anm190964 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [-0.02, -0.012, -0.023, -0.014, -0.009, -0.003, 0.014, 0.024, 0.041, 0.056, 0.117, 0.147, 0.168, 0.211, 0.189, 0.194, 0.211, 0.264, 0.364, 0.476]
   dVm/dt (mV/ms), same samples: [0.07, -0.06, 0.05, 0.04, 0.01, 0.07, 0.08, 0.1, 0.01, 0.3, 0.2, 0.05, 0.32, -0.13, -0.05, -0.12, 0.26, 0.42, 0.62, 0.38] ; threshold 0.35
anm220466 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [-0.017, -0.044, -0.073, -0.082, -0.089, -0.049, -0.004, -0.039, -0.077, -0.114, -0.112, -0.003, -0.005, -0.054, -0.13, -0.165, -0.177, -0.169, -0.142, -0.142]
   dVm/dt (mV/ms), same samples: [-0.03, -0.27, -0.09, -0.09, 0.11, 0.62, -0.38, -0.24, -0.24, -0.1, 0.56, 0.08, -0.26, -0.48, -0.24, -0.02, -0.08, 0.09, -0.02, 0.14] ; threshold 0.43
anm231235 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [0.025, 0.023, 0.014, 0.022, -0.002, -0.002, -0.016, -0.04, -0.05, -0.07, -0.068, -0.074, -0.08, -0.09, -0.095, -0.09, -0.103, -0.113, -0.125, -0.106]
   dVm/dt (mV/ms), same samples: [0.09, -0.04, 0.05, -0.16, -0.06, 0.08, -0.18, -0.06, -0.13, -0.06, 0.07, -0.34, 0.04, 0.01, 0.02, 0.1, -0.06, 0.04, 0.2, 0.01] ; threshold 0.27
anm234412 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [-0.023, -0.039, -0.033, -0.018, -0.023, -0.012, -0.019, -0.025, -0.021, -0.061, -0.051, -0.044, -0.053, -0.039, -0.043, -0.087, -0.037, -0.054, -0.041, -0.042]
   dVm/dt (mV/ms), same samples: [0.16, -0.16, 0.11, -0.06, 0.01, 0.09, -0.06, 0.01, -0.43, -0.31, 0.22, -0.17, 0.05, -0.04, 0.02, 0.5, -0.18, 0.09, -0.01, 0.02] ; threshold 0.41
anm253096 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [0.011, 0.003, 0.006, -0.001, -0.009, -0.007, -0.012, -0.011, -0.015, -0.0, 0.009, 0.011, 0.016, 0.018, 0.019, 0.028, 0.002, 0.036, 0.034, 0.0]
   dVm/dt (mV/ms), same samples: [-0.08, 0.02, -0.03, -0.03, 0.01, -0.02, 0.0, -0.04, 0.08, 0.02, 0.03, 0.04, 0.04, 0.02, 0.03, 0.04, 0.3, -0.01, 0.03, 0.34] ; threshold 0.31
anm266945 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [0.006, -0.009, -0.015, -0.019, -0.024, -0.025, -0.049, -0.025, -0.019, -0.028, -0.029, -0.045, -0.021, -0.043, -0.037, -0.047, -0.056, -0.054, -0.07, -0.056]
   dVm/dt (mV/ms), same samples: [0.0, -0.08, -0.03, -0.13, -0.16, -0.1, -0.04, 0.07, -0.22, 0.08, 0.21, 0.18, -0.05, 0.1, -0.49, -0.35, -0.2, 0.09, -0.01, -0.07] ; threshold 0.37
anm318532 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [-0.002, 0.015, 0.05, 0.153, 0.189, 0.19, 0.163, 0.152, 0.136, 0.167, 0.124, 0.088, 0.072, 0.139, 0.097, 0.054, -0.002, -0.06, -0.114, -0.114]
   dVm/dt (mV/ms), same samples: [0.18, 0.55, 0.13, 0.16, -0.26, -0.12, 0.08, 0.03, 0.83, -0.39, -0.09, 0.25, -1.19, -0.34, -0.36, -0.35, -0.12, -0.29, -1.19, 1.6] ; threshold 0.87
anm318532 (E): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [0.061, 0.058, 0.057, 0.069, 0.02, 0.004, 0.027, 0.029, 0.042, 0.023, 0.028, 0.036, 0.176, 0.152, 0.115, 0.068, 0.063, 0.067, 0.082, 0.073]
   dVm/dt (mV/ms), same samples: [-0.1, 0.3, 0.12, -0.6, -0.18, 0.63, 0.44, 0.19, -0.29, -0.26, 0.23, 0.65, 0.32, -0.37, -0.12, 0.04, 0.25, -0.19, -0.26, -0.39] ; threshold 1.35
anm220466 (FS): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [0.289, 0.383, 0.394, 0.379, 0.36, 0.382, 0.354, 0.33, 0.332, 0.35, 0.462, 0.625, 0.657, 0.592, 0.568, 0.634, 0.544, 0.45, 0.396, 0.381]
   dVm/dt (mV/ms), same samples: [0.66, 0.14, -0.12, -0.06, 0.03, 0.03, -0.27, -0.0, 0.13, 0.4, 0.96, 0.16, -0.24, -0.26, 0.5, -0.32, -0.57, -0.4, -0.08, 0.16] ; threshold 0.71
anm220470 (FS): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [-0.083, 0.088, 0.295, 0.212, 0.246, 0.278, 0.421, 0.45, 0.352, 0.255, 0.257, 0.281, 0.77, 0.797, 0.76, 0.733, 0.542, 0.756, 0.655, 0.671]
   dVm/dt (mV/ms), same samples: [1.23, 1.1, -0.15, -0.41, 0.46, 0.74, 0.25, -0.45, -0.66, -0.39, 0.05, 1.84, 0.99, -1.04, 0.53, -1.09, 0.46, 0.1, -0.61, 0.12] ; threshold 1.85
anm307766 (FS): Vm minus pre (mV) -2 to +2 ms, every 0.2 ms: [-0.278, -0.267, -0.297, -0.288, -0.226, -0.105, 0.004, 0.097, 0.13, 0.132, 0.215, 0.281, 0.382, 0.516, 0.594, 0.637, 0.719, 0.93, 1.152, 1.325]
   dVm/dt (mV/ms), same samples: [0.23, -0.32, 0.33, 0.25, 0.51, 0.61, 0.86, -0.28, -0.07, 0.63, 0.43, 0.25, 0.35, 0.69, 0.1, 0.7, 1.09, 1.33, 1.24, 0.5] ; threshold 1.24
No description has been provided for this image
Each panel shows -2 to +2 ms; every plotted value lies within the axes.

The latencies under 2 ms in E and FS cells come from single samples of dVm/dt, at 10 or 20 kHz, that just cross 3 s.d. of the baseline before any sustained rise; the Vm of the E cells is within 0.17 mV of its pre-touch mean in the first millisecond (the values above). A 3 s.d. threshold on an unsmoothed derivative is crossed by single samples of noise; in the laser-onset averages of exc-thalamus-photoinhibition 1 and 3 of the 250 baseline samples already cross it. This is the rule the criterion fixes (O8, answer of 2026-10-20), so the latencies stand as measured; the diagnostic decides nothing.

Result: not reproduced. The dandiset holds 5 whole-cell FS cells by the claims file's rules (the paper has 7). Their touch amplitude averages 5.56 mV (within 7.0 +- 1.97) and the whisking depolarization 2.54 mV with the onset windows (within 2.7 +- 0.79; 1.47 to 2.55 mV over the O10 and O11 readings, within tolerance in 12 of 16; 1.01 mV with the threshold distance), but the touch latency, with dVm/dt at the sampling rate as the criterion says, averages 1.88 ms (median 1.35) against 4.0 +- 0.4. The latency diagnostic above (after exc-vm-touch-whisking) shows why: in anm220466 (0.00 ms) and anm220470 (0.30 ms) the first crossing is a single sample of dVm/dt just above the threshold (0.96 against 0.71 and 3.05 against 1.85 mV/ms), in averages whose Vm already lies 0.26 to 0.46 mV above its pre-touch mean at onset. Subtracting the baseline's mean dVm/dt, a diagnostic that decides nothing, does not move these crossings (mean 1.76 ms), so the rule as the criterion gives it, not an offset, yields these latencies.

fs-whisking-rate¶

Most L4 FS interneurons (23 of 26) increase their spike rate during whisking without touch and 3 of 26 decrease it; over the population the rate roughly doubles (to 21.1 +- 15.8 spikes/s).

In [23]:
def fs_rate_table(groups):
    rows = []
    for rname, col in groups.items():
        for sel in ("all", "no_flagged"):
            g = group("FS", None, sel, col)
            for f in yu2016.FILTERS:
                for o2 in ("O2a", "O2b"):
                    rows.append({"FS reading": rname, "cells": sel, "filter": f, "samples": o2, "n": len(g),
                                 "increase": int((g[f"{f}:{o2}"] == 1).sum()), "decrease": int((g[f"{f}:{o2}"] == -1).sum()),
                                 "nonwhisk_mean": g[f"{f}:rate_nonwhisk"].mean(), "whisk_mean": g[f"{f}:rate_whisk"].mean(),
                                 "whisk_sd": g[f"{f}:rate_whisk"].std(), "whisk_median": g[f"{f}:rate_whisk"].median(),
                                 "signed_rank_p": "%.2g" % wilcox(g[f"{f}:rate_nonwhisk"], g[f"{f}:rate_whisk"])})
    return pd.DataFrame(rows)
print("FS cells, trials without light; expected increases 23 +- 2.3 and decreases 3 +- 0.3 of 26, whisking mean 21.1 +- 3.1 spikes/s")
print(fs_rate_table({**FS_READINGS, **FS_VARIANT}).round(2).to_string(index=False))
fs11 = rate_summary(group("FS"))
print("\nMain FS group under every O11 reading, filter and sample reading:")
print(fmt_p(fs11.drop(columns=["r2_above_0.25", "multiplex"])).round(4).to_string(index=False))
print("Ranges over the 16 readings:", ranges(fs11, ["increase", "decrease", "whisk_mean", "signed_rank_p"]))
FS cells, trials without light; expected increases 23 +- 2.3 and decreases 3 +- 0.3 of 26, whisking mean 21.1 +- 3.1 spikes/s
                         FS reading      cells   filter samples  n  increase  decrease  nonwhisk_mean  whisk_mean  whisk_sd  whisk_median signed_rank_p
              tag p<0.001, boundary        all filtfilt     O2a 17        13         2           8.53       19.02     14.34         16.20       0.00029
              tag p<0.001, boundary        all filtfilt     O2b 17        13         1           8.53       19.02     14.34         16.20       0.00029
              tag p<0.001, boundary        all  lfilter     O2a 17        13         1           8.64       17.77     12.95         14.90        0.0005
              tag p<0.001, boundary        all  lfilter     O2b 17        13         1           8.64       17.77     12.95         14.90        0.0005
              tag p<0.001, boundary no_flagged filtfilt     O2a 15        11         2           8.55       20.22     14.84         17.28       0.00061
              tag p<0.001, boundary no_flagged filtfilt     O2b 15        13         1           8.55       20.22     14.84         17.28       0.00061
              tag p<0.001, boundary no_flagged  lfilter     O2a 15        11         1           8.70       18.81     13.41         16.29        0.0012
              tag p<0.001, boundary no_flagged  lfilter     O2b 15        13         1           8.70       18.81     13.41         16.29        0.0012
               tag p<0.01, boundary        all filtfilt     O2a 17        13         2           8.53       19.02     14.34         16.20       0.00029
               tag p<0.01, boundary        all filtfilt     O2b 17        13         1           8.53       19.02     14.34         16.20       0.00029
               tag p<0.01, boundary        all  lfilter     O2a 17        13         1           8.64       17.77     12.95         14.90        0.0005
               tag p<0.01, boundary        all  lfilter     O2b 17        13         1           8.64       17.77     12.95         14.90        0.0005
               tag p<0.01, boundary no_flagged filtfilt     O2a 15        11         2           8.55       20.22     14.84         17.28       0.00061
               tag p<0.01, boundary no_flagged filtfilt     O2b 15        13         1           8.55       20.22     14.84         17.28       0.00061
               tag p<0.01, boundary no_flagged  lfilter     O2a 15        11         1           8.70       18.81     13.41         16.29        0.0012
               tag p<0.01, boundary no_flagged  lfilter     O2b 15        13         1           8.70       18.81     13.41         16.29        0.0012
tag p<0.001, any waveform (variant)        all filtfilt     O2a 25        17         4           8.73       16.53     12.73         13.47       0.00019
tag p<0.001, any waveform (variant)        all filtfilt     O2b 25        15         3           8.73       16.53     12.73         13.47       0.00019
tag p<0.001, any waveform (variant)        all  lfilter     O2a 25        17         3           8.81       15.63     11.53         12.61       0.00029
tag p<0.001, any waveform (variant)        all  lfilter     O2b 25        15         3           8.81       15.63     11.53         12.61       0.00029
tag p<0.001, any waveform (variant) no_flagged filtfilt     O2a 20        14         3           8.04       17.78     13.54         13.96       0.00013
tag p<0.001, any waveform (variant) no_flagged filtfilt     O2b 20        15         2           8.04       17.78     13.54         13.96       0.00013
tag p<0.001, any waveform (variant) no_flagged  lfilter     O2a 20        14         2           8.18       16.58     12.24         13.19       0.00039
tag p<0.001, any waveform (variant) no_flagged  lfilter     O2b 20        15         2           8.18       16.58     12.24         13.19       0.00039

Main FS group under every O11 reading, filter and sample reading:
 O11   filter samples  n  increase  decrease  nonwhisk_mean  nonwhisk_median  whisk_mean signed_rank_p  whisk_higher
 a-i filtfilt     O2a 17        13         2         8.5279           4.5529     19.0164       0.00029            16
 a-i filtfilt     O2b 17        13         1         8.5279           4.5529     19.0164       0.00029            16
 a-i  lfilter     O2a 17        13         1         8.6430           4.4015     17.7652        0.0005            16
 a-i  lfilter     O2b 17        13         1         8.6430           4.4015     17.7652        0.0005            16
a-ii filtfilt     O2a 17        13         2         8.5849           4.5903     19.0370       0.00029            16
a-ii filtfilt     O2b 17        13         1         8.5849           4.5903     19.0370       0.00029            16
a-ii  lfilter     O2a 17        13         1         8.7128           4.4859     17.7978       0.00038            16
a-ii  lfilter     O2b 17        13         1         8.7128           4.4859     17.7978       0.00038            16
 b-i filtfilt     O2a 17        14         2         8.5205           4.5648     19.1368       0.00021            16
 b-i filtfilt     O2b 17        13         1         8.5205           4.5648     19.1368       0.00021            16
 b-i  lfilter     O2a 17        13         1         8.6355           4.4180     17.8717        0.0005            16
 b-i  lfilter     O2b 17        13         1         8.6355           4.4180     17.8717        0.0005            16
b-ii filtfilt     O2a 17        13         3         8.5909           4.6185     19.1680       0.00021            16
b-ii filtfilt     O2b 17        13         1         8.5909           4.6185     19.1680       0.00021            16
b-ii  lfilter     O2a 17        13         1         8.7225           4.5005     17.9122        0.0005            15
b-ii  lfilter     O2b 17        13         1         8.7225           4.5005     17.9122        0.0005            15
Ranges over the 16 readings: {'increase': (13.0, 14.0), 'decrease': (1.0, 3.0), 'whisk_mean': (17.8, 19.2), 'signed_rank_p': (0.000214, 0.000504)}

Result: partially reproduced. The 17 FS cells fire more during whisking under every filter, sample and O11 reading (signed-rank p = 0.0002 to 0.0005), with a mean whisking rate of 17.77 to 19.17 spikes/s (within 21.1 +- 3.1 with the zero-phase filter; outside with the forward one). 13 or 14 cells increase and 1 to 3 decrease significantly against the paper's 23 and 3 of 26, so the increase count is outside its tolerance under every reading; the group is smaller than the paper's.

fs-whisking-phase¶

Spikes of L4 FS interneurons are coupled to whisking phase.

In [24]:
rows = []
for rname, col in {**FS_READINGS, **FS_VARIANT}.items():
    for sel in ("all", "no_flagged"):
        g = group("FS", None, sel, col)
        for f in yu2016.FILTERS:
            rows.append({"FS reading": rname, "cells": sel, "filter": f, "n": len(g), "coupled": int((g[f"{f}:r2"] > 0.25).sum()),
                         "double_fits": int((g[f"{f}:fit"] == "double").sum()), "r2_median": g[f"{f}:r2"].median(),
                         "strength_median": g[f"{f}:strength"].median(), "whisk_spikes_median": g[f"{f}:whisk_spikes"].median()})
print("FS cells coupled to whisking phase (r2 > 0.25; sinusoid, or with the second harmonic when its r2 is 1.5 times higher):")
print(pd.DataFrame(rows).round(2).to_string(index=False))
print("Main FS group, cells with r2 > 0.25 under each O11 reading and filter:",
      fs11.drop_duplicates(["O11", "filter"]).set_index(["O11", "filter"])["r2_above_0.25"].to_dict(), "of", len(group("FS")))
na = align[align.path.isin(group("FS").path)]
print(f"Reference (not part of the criterion): with spikes moved to the next trial (single sinusoid), {int((na.r2_shift > 0.25).sum())} "
      f"of {len(na)} FS cells have r2 > 0.25")
FS cells coupled to whisking phase (r2 > 0.25; sinusoid, or with the second harmonic when its r2 is 1.5 times higher):
                         FS reading      cells   filter  n  coupled  double_fits  r2_median  strength_median  whisk_spikes_median
              tag p<0.001, boundary        all filtfilt 17       16            5       0.73             5.37                769.0
              tag p<0.001, boundary        all  lfilter 17       16            3       0.61             3.01                709.0
              tag p<0.001, boundary no_flagged filtfilt 15       15            5       0.74             5.43                769.0
              tag p<0.001, boundary no_flagged  lfilter 15       14            3       0.61             3.33                709.0
               tag p<0.01, boundary        all filtfilt 17       16            5       0.73             5.37                769.0
               tag p<0.01, boundary        all  lfilter 17       16            3       0.61             3.01                709.0
               tag p<0.01, boundary no_flagged filtfilt 15       15            5       0.74             5.43                769.0
               tag p<0.01, boundary no_flagged  lfilter 15       14            3       0.61             3.33                709.0
tag p<0.001, any waveform (variant)        all filtfilt 25       22            7       0.66             2.63                750.0
tag p<0.001, any waveform (variant)        all  lfilter 25       23            7       0.58             2.48                698.0
tag p<0.001, any waveform (variant) no_flagged filtfilt 20       19            6       0.74             4.33                759.5
tag p<0.001, any waveform (variant) no_flagged  lfilter 20       18            5       0.61             2.55                703.5
Main FS group, cells with r2 > 0.25 under each O11 reading and filter: {('a-i', 'filtfilt'): 16, ('a-i', 'lfilter'): 16, ('a-ii', 'filtfilt'): 16, ('a-ii', 'lfilter'): 15, ('b-i', 'filtfilt'): 16, ('b-i', 'lfilter'): 15, ('b-ii', 'filtfilt'): 16, ('b-ii', 'lfilter'): 15} of 17
Reference (not part of the criterion): with spikes moved to the next trial (single sinusoid), 9 of 17 FS cells have r2 > 0.25

Result: reproduced. 15 or 16 of 17 FS cells are coupled to whisking phase (r2 > 0.25) under both filters and every O11 reading. As for VPM, 9 of the 17 also pass with spikes moved to the next trial, which the criterion does not ask for.

fs-touch-and-whisking¶

Touch- and whisking-evoked spikes are typically multiplexed in individual L4 FS neurons, as in VPM neurons.

In [25]:
rows = []
for rname, col in {**FS_READINGS, **FS_VARIANT, "VPM units": None}.items():
    g = group("VPM") if col is None else group("FS", None, "all", col)
    for f in yu2016.FILTERS:
        for o2 in ("O2a", "O2b"):
            mult = (g.spt_lo > 0) & (g[f"{f}:{o2}"] != 0)
            rows.append({"group": rname, "filter": f, "samples": o2, "n": len(g), "touch": int((g.spt_lo > 0).sum()),
                         "whisking": int((g[f"{f}:{o2}"] != 0).sum()), "both": int(mult.sum()), "fraction": mult.mean()})
print("Cells whose spikes per touch has a 95% interval above zero and whose whisking rate differs from the nonwhisking rate "
      "(rank-sum p < 0.05); more than half needed under every reading")
print(pd.DataFrame(rows).round(2).to_string(index=False))
print("Main FS group, cells that multiplex under each O11, filter and sample reading: range", int(fs11.multiplex.min()), "to",
      int(fs11.multiplex.max()), "of", len(group("FS")), "; VPM units:", int(vs.multiplex.min()), "to", int(vs.multiplex.max()), "of", len(v))
for sel in ("no_flagged",):
    g = group("FS", None, sel)
    print(f"[{sel}] FS: both in", [int(((g.spt_lo > 0) & (g[f'{f}:{o}'] != 0)).sum()) for f in yu2016.FILTERS for o in ("O2a", "O2b")], "of", len(g))
Cells whose spikes per touch has a 95% interval above zero and whose whisking rate differs from the nonwhisking rate (rank-sum p < 0.05); more than half needed under every reading
                              group   filter samples  n  touch  whisking  both  fraction
              tag p<0.001, boundary filtfilt     O2a 17     12        15    11      0.65
              tag p<0.001, boundary filtfilt     O2b 17     12        14    12      0.71
              tag p<0.001, boundary  lfilter     O2a 17     12        14    11      0.65
              tag p<0.001, boundary  lfilter     O2b 17     12        14    12      0.71
               tag p<0.01, boundary filtfilt     O2a 17     12        15    11      0.65
               tag p<0.01, boundary filtfilt     O2b 17     12        14    12      0.71
               tag p<0.01, boundary  lfilter     O2a 17     12        14    11      0.65
               tag p<0.01, boundary  lfilter     O2b 17     12        14    12      0.71
tag p<0.001, any waveform (variant) filtfilt     O2a 25     18        21    16      0.64
tag p<0.001, any waveform (variant) filtfilt     O2b 25     18        18    16      0.64
tag p<0.001, any waveform (variant)  lfilter     O2a 25     18        20    16      0.64
tag p<0.001, any waveform (variant)  lfilter     O2b 25     18        18    16      0.64
                          VPM units filtfilt     O2a 29     23        25    21      0.72
                          VPM units filtfilt     O2b 29     23        26    22      0.76
                          VPM units  lfilter     O2a 29     23        22    18      0.62
                          VPM units  lfilter     O2b 29     23        25    21      0.72
Main FS group, cells that multiplex under each O11, filter and sample reading: range 11 to 12 of 17 ; VPM units: 18 to 22 of 29
[no_flagged] FS: both in [11, 12, 11, 12] of 15

Result: reproduced. 11 or 12 of the 17 FS cells (0.65 to 0.71) both respond to touch and change their rate with whisking under every filter, sample and O11 reading, more than half; the VPM units give 18 to 22 of 29 (0.62 to 0.76).

exc-mainly-touch¶

L4 excitatory neurons respond mostly to touch (0.3 +- 0.4 spikes per touch, n = 85) and only weakly to whisker movement; their rate does not change significantly between nonwhisking and whisking (0.4 to 0.6 spikes/s, P = 0.20).

In [26]:
rows = []
for sel in ("all", "no_flagged"):
    g = group("E", None, sel)
    for f in yu2016.FILTERS:
        rows.append({"cells": sel, "filter": f, "n": len(g), "whole_cell": int((g["mode"] == "whole-cell").sum()), "spt_mean": g.spt.mean(),
                     "spt_sd": g.spt.std(), "spt_median": g.spt.median(), "nonwhisk_mean": g[f"{f}:rate_nonwhisk"].mean(),
                     "nonwhisk_median": g[f"{f}:rate_nonwhisk"].median(), "whisk_mean": g[f"{f}:rate_whisk"].mean(),
                     "whisk_median": g[f"{f}:rate_whisk"].median(), "whisk_minus_nonwhisk": (g[f"{f}:rate_whisk"] - g[f"{f}:rate_nonwhisk"]).mean(),
                     "signed_rank_p": "%.2g" % wilcox(g[f"{f}:rate_nonwhisk"], g[f"{f}:rate_whisk"]),
                     "whisk_higher": int((g[f"{f}:rate_whisk"] > g[f"{f}:rate_nonwhisk"]).sum())})
print("L4 E cells, trials without light; expected spikes per touch 0.3 +- 0.043, nonwhisking 0.4 +- 0.065, whisking 0.6 +- 0.098 spikes/s; "
      "absence judged by the signed-rank test at 0.05")
print(pd.DataFrame(rows).round(3).to_string(index=False))
e11 = rate_summary(group("E")).drop_duplicates(["O11", "filter"])
print("\nUnder every O11 reading and filter:")
print(fmt_p(e11[["O11", "filter", "n", "nonwhisk_mean", "whisk_mean", "signed_rank_p", "whisk_higher"]]).round(4).to_string(index=False))
g = group("E")
print("Spike detection by mode (see the data check): fraction of interspike intervals under 1.5 ms, median over cells",
      g.path.map(spikeq.set_index("path").isi_viol).groupby(g["mode"]).median().round(4).to_dict())
print("E cells by line and mode:", g.assign(line=g.subject.map(line_of)).groupby(["line", "mode"]).size().to_dict())
print("Rates by mode (filtfilt), means:", g.groupby("mode")[["spt", "filtfilt:rate_nonwhisk", "filtfilt:rate_whisk"]].mean().round(3).to_dict("index"))
L4 E cells, trials without light; expected spikes per touch 0.3 +- 0.043, nonwhisking 0.4 +- 0.065, whisking 0.6 +- 0.098 spikes/s; absence judged by the signed-rank test at 0.05
     cells   filter  n  whole_cell  spt_mean  spt_sd  spt_median  nonwhisk_mean  nonwhisk_median  whisk_mean  whisk_median  whisk_minus_nonwhisk signed_rank_p  whisk_higher
       all filtfilt 97          51     0.320   0.643       0.048          1.529            0.237       3.551         0.589                 2.022       9.1e-07            55
       all  lfilter 97          51     0.320   0.643       0.048          1.567            0.257       3.382         0.593                 1.815       8.7e-06            51
no_flagged filtfilt 89          43     0.344   0.662       0.075          1.666            0.399       3.866         0.707                 2.200       1.5e-06            53
no_flagged  lfilter 89          43     0.344   0.662       0.075          1.708            0.430       3.681         0.675                 1.973       1.5e-05            49
Under every O11 reading and filter:
 O11   filter  n  nonwhisk_mean  whisk_mean signed_rank_p  whisk_higher
 a-i filtfilt 97         1.5290      3.5510       9.1e-07            55
 a-i  lfilter 97         1.5670      3.3821       8.7e-06            51
a-ii filtfilt 97         1.5495      3.5983       3.8e-07            56
a-ii  lfilter 97         1.5905      3.4265       8.5e-06            50
 b-i filtfilt 97         1.5348      3.5962       4.6e-07            57
 b-i  lfilter 97         1.5736      3.4304       4.2e-06            53
b-ii filtfilt 97         1.5512      3.6437       1.8e-07            58
b-ii  lfilter 97         1.5925      3.4768       3.5e-06            53
Spike detection by mode (see the data check): fraction of interspike intervals under 1.5 ms, median over cells {'cell-attached': 0.0504, 'whole-cell': 0.0}
E cells by line and mode: {('C57BL/6', 'whole-cell'): 7, ('PV-ChR2', 'whole-cell'): 1, ('PV-IRES-Cre', 'cell-attached'): 34, ('PV-IRES-Cre', 'whole-cell'): 9, ('PV-IRES-Cre x Ai32', 'whole-cell'): 1, ('SOM-IRES-Cre x Ai32', 'cell-attached'): 2, ('SOM-IRES-Cre x Ai32', 'whole-cell'): 1, ('VGAT-ChR2-EYFP', 'cell-attached'): 10, ('VGAT-ChR2-EYFP', 'whole-cell'): 32}
Rates by mode (filtfilt), means: {'cell-attached': {'spt': 0.534, 'filtfilt:rate_nonwhisk': 2.451, 'filtfilt:rate_whisk': 6.193}, 'whole-cell': {'spt': 0.122, 'filtfilt:rate_nonwhisk': 0.698, 'filtfilt:rate_whisk': 1.168}}

Result: not reproduced. The 97 excitatory cells fire more during whisking than without it under every filter and O11 reading (signed-rank p = 9.1e-07 for the main epochs, at most 8.7e-06 for the forward filter), so the paper's absence of a change is not found. Spikes per touch (0.320) is within tolerance of 0.3, but the nonwhisking (1.53 to 1.59 spikes/s) and whisking (3.38 to 3.64) rates are well above 0.4 and 0.6; the 46 cell-attached cells fire more than the 51 whole-cell ones (2.45 against 0.70 spikes/s without whisking). The data check shows that double detection removes only 8.1% of the cell-attached spikes, so it does not explain the high rates.

Light experiments¶

The functions below select the light trials and the windows used by the thalamic photoinhibition and PV suppression claims (windows O5, fractions O6, laser powers O7).

In [27]:
# Light designs. Thalamic photoinhibition: trials lit at the site labelled VPM and at no other site (VGAT-ChR2 mice).
# PV suppression: trials lit at the vS1 site of PV-IRES-Cre mice (constant light). Control trials have no light at
# any site. Windows O5: (a) control samples inside the median light span of the lit trials, (b) anywhere.
def light_design(r, region):
    tr = r["trials"]
    sites = [s for s, L in r["light"].items() if L["region"] == region and len(L["pulses"])]
    if not sites:
        return None
    s = sites[0]
    w = yu2016.light_windows(r["light"], s, tr)
    other = set()
    for s2 in r["light"]:
        if s2 != s:
            other |= set(yu2016.light_windows(r["light"], s2, tr).trial.astype(int))
    w = w[~w.trial.isin(other)]
    if len(w) == 0:
        return None
    P = r["light"][s]["pulses"]
    pk = yu2016.per_trial_index(P[:, 0], tr)
    pw = r["light"][s].get("pulse_power", np.ones(len(P)))
    power = {int(t): float(pw[pk == t].max()) for t in w.trial}
    return {"site": s, "windows": {int(t): (a, b) for t, a, b in zip(w.trial, w.rel_start, w.rel_stop)},
            "span": (float(w.rel_start.median()), float(w.rel_stop.median())), "power": power,
            "ctl": ~yu2016.lit_trials(r["light"], tr)}

def masks(r, d, reading, top_power=False):
    b, tr = r["beh"], r["trials"]
    tri = b["trial"]
    rel = b["t"] - tr.start.to_numpy()[np.clip(tri, 0, None)]
    lo, hi = np.full(len(tr), np.inf), np.full(len(tr), -np.inf)
    pmax = max(d["power"].values())
    for t, (a, c) in d["windows"].items():
        if not top_power or d["power"][t] == pmax:
            lo[t], hi[t] = a, c
    k = np.clip(tri, 0, None)
    use_l = (tri >= 0) & (rel >= lo[k]) & (rel < hi[k])
    use_c = (tri >= 0) & d["ctl"][k]
    if reading == "a":
        use_c &= (rel >= d["span"][0]) & (rel < d["span"][1])
    return use_l, use_c

def light_compare(r, spikes, d, filt="filtfilt", reading="a", definition="cortex5", min_iti=0.025, top_power=False, o11="a-i"):
    b = r["beh"] if o11 == "a-i" else yu2016.beh_o11(r["beh"], o11)
    use_l, use_c = masks(r, d, reading, top_power)
    count = yu2016.counts_1k(spikes, b)
    out = {}
    on = yu2016.touches(b, None, min_iti)
    for tag, u in (("light", use_l), ("ctl", use_c)):
        er = yu2016.epoch_rates(np.where(u, count, 0), b, filt, u)
        out[f"{tag}_nonwhisk"], out[f"{tag}_whisk"] = er["rate_nonwhisk"], er["rate_whisk"]
        out[f"{tag}_t_nonwhisk"], out[f"{tag}_t_whisk"] = er["time_nonwhisk"], er["time_whisk"]
        s = yu2016.spikes_per_touch(count, b["trial"], on[u[on]], definition)
        out[f"{tag}_spt"], out[f"{tag}_touches"] = s["spt"], s["n_touches"]
        out[f"{tag}_rate"] = count[u].sum() / max(u.sum(), 1) * 1000
    return out

def frac(light, ctl, how):
    light, ctl = np.asarray(light, float), np.asarray(ctl, float)
    ok = np.isfinite(light) & np.isfinite(ctl)
    if how == "a":
        return light[ok].mean() / ctl[ok].mean()
    ok &= ctl > 0
    return float(np.median(light[ok] / ctl[ok]))

thalamus-drives-fs¶

Photoinhibiting the thalamus reduces the spike rates of L4 FS interneurons during nonwhisking (to 4% of control), whisking (to 13%) and touch (to 32%), so FS interneurons are driven by thalamic input.

In [28]:
def thal_rows(col):
    out = []
    for r in l4:
        c = cells.set_index("path").loc[r["path"]]
        if c[col] != "FS" or c.line != "VGAT-ChR2-EYFP" or r["beh"]["principal"] is None:
            continue
        d = light_design(r, "VPM")
        if d is None:
            continue
        for filt in yu2016.FILTERS:
            for reading in ("a", "b"):
                for top in (False, True):
                    for o11 in yu2016.O11:
                        out.append({"path": r["path"], "subject": r["subject"], "filter": filt, "O5": reading, "O7": "b" if top else "a",
                                    "O11": o11, "lit_trials": len(d["windows"]), "powers": sorted(set(d["power"].values())),
                                    **light_compare(r, r["spikes"], d, filt, reading, top_power=top, o11=o11)})
    return pd.DataFrame(out)

def thal_summary(t):
    rows = []
    for (filt, o5, o7, o11), g in t.groupby(["filter", "O5", "O7", "O11"]):
        row = {"filter": filt, "O5": o5, "O7": o7, "O11": o11, "cells": len(g)}
        for m in ("nonwhisk", "whisk", "spt"):
            gg = g.dropna(subset=[f"light_{m}", f"ctl_{m}"])
            row.update({f"{m}_n": len(gg), f"{m}_O6a": frac(gg[f"light_{m}"], gg[f"ctl_{m}"], "a"),
                        f"{m}_O6b": frac(gg[f"light_{m}"], gg[f"ctl_{m}"], "b"),
                        f"{m}_lower": int((gg[f"light_{m}"] < gg[f"ctl_{m}"]).sum()), f"{m}_p": "%.2g" % wilcox(gg[f"light_{m}"], gg[f"ctl_{m}"]),
                        f"{m}_p_one_sided": "%.2g" % wilcox(gg[f"light_{m}"], gg[f"ctl_{m}"], "less")})
        rows.append(row)
    return pd.DataFrame(rows)

th = thal_rows("cls")
print("FS cells with thalamic light:", th.path.nunique(), "from mice", sorted(th.subject.unique()), "; lit trials per cell",
      th.drop_duplicates("path").lit_trials.tolist(), "; laser powers (stored values)", th.drop_duplicates("path").powers.tolist())
print("Expected fractions of control: nonwhisking 0.04 +- 0.004, whisking 0.13 +- 0.013, spikes per touch 0.32 +- 0.032; each two-sided "
      "signed-rank test p < 0.05 with light lower (answer of 2026-10-20); the one-sided p (light lower) is a variant that does not decide")
n5 = th.path.nunique()
print(f"Smallest exact signed-rank p with {n5} cells: two-sided {2 * 0.5 ** n5:.4f}, one-sided {0.5 ** n5:.4f}")
ths = thal_summary(th)
print(ths[ths.O11 == "a-i"].round(3).to_string(index=False))
print("Over the O11 readings (32 combinations): fractions nonwhisking", ranges(ths, ["nonwhisk_O6a", "nonwhisk_O6b", "whisk_O6a", "whisk_O6b"]),
      "; two-sided p", {m: (ths[f"{m}_p"].astype(float).min(), ths[f"{m}_p"].astype(float).max()) for m in ("nonwhisk", "whisk")})
tv = thal_rows("cls_tag")
print(f"\nVariant (tag only, any waveform): {tv.path.nunique()} cells")
print(thal_summary(tv)[lambda d: (d.O7 == "a") & (d.O11 == "a-i")].round(3).to_string(index=False))
print("Per cell (filtfilt, O5 a, O7 a):")
print(th[(th["filter"] == "filtfilt") & (th.O5 == "a") & (th.O7 == "a") & (th.O11 == "a-i")][["subject", "ctl_nonwhisk", "light_nonwhisk", "ctl_whisk", "light_whisk",
      "ctl_spt", "light_spt", "light_touches", "light_t_nonwhisk", "light_t_whisk"]].round(3).to_string(index=False))
FS cells with thalamic light: 5 from mice ['anm260070'] ; lit trials per cell [31, 38, 23, 27, 24] ; laser powers (stored values) [[15.0], [3.75, 15.0], [3.75, 15.0], [3.75, 15.0], [3.75, 15.0]]
Expected fractions of control: nonwhisking 0.04 +- 0.004, whisking 0.13 +- 0.013, spikes per touch 0.32 +- 0.032; each two-sided signed-rank test p < 0.05 with light lower (answer of 2026-10-20); the one-sided p (light lower) is a variant that does not decide
Smallest exact signed-rank p with 5 cells: two-sided 0.0625, one-sided 0.0312
  filter O5 O7 O11  cells  nonwhisk_n  nonwhisk_O6a  nonwhisk_O6b  nonwhisk_lower nonwhisk_p nonwhisk_p_one_sided  whisk_n  whisk_O6a  whisk_O6b  whisk_lower whisk_p whisk_p_one_sided  spt_n  spt_O6a  spt_O6b  spt_lower spt_p spt_p_one_sided
filtfilt  a  a a-i      5           5         0.204         0.200               5      0.062                0.031        5      0.273      0.231            4    0.12             0.062      5    0.410    0.359          5 0.062           0.031
filtfilt  a  b a-i      5           5         0.022         0.000               5      0.062                0.031        5      0.203      0.163            4    0.12             0.062      5    0.400    0.381          5 0.062           0.031
filtfilt  b  a a-i      5           5         0.266         0.194               5      0.062                0.031        5      0.287      0.240            4    0.12             0.062      5    0.401    0.369          5 0.062           0.031
filtfilt  b  b a-i      5           5         0.028         0.000               5      0.062                0.031        5      0.214      0.177            4    0.12             0.062      5    0.391    0.392          5 0.062           0.031
 lfilter  a  a a-i      5           5         0.241         0.238               5      0.062                0.031        5      0.262      0.235            4    0.12             0.062      5    0.410    0.359          5 0.062           0.031
 lfilter  a  b a-i      5           5         0.030         0.012               5      0.062                0.031        5      0.208      0.168            4    0.12             0.062      5    0.400    0.381          5 0.062           0.031
 lfilter  b  a a-i      5           5         0.322         0.233               4       0.12                0.062        5      0.279      0.244            4    0.12             0.062      5    0.401    0.369          5 0.062           0.031
 lfilter  b  b a-i      5           5         0.040         0.017               5      0.062                0.031        5      0.221      0.179            4    0.12             0.062      5    0.391    0.392          5 0.062           0.031
Over the O11 readings (32 combinations): fractions nonwhisking {'nonwhisk_O6a': (0.0217, 0.324), 'nonwhisk_O6b': (0.0, 0.238), 'whisk_O6a': (0.197, 0.287), 'whisk_O6b': (0.146, 0.244)} ; two-sided p {'nonwhisk': (np.float64(0.062), np.float64(0.12)), 'whisk': (np.float64(0.12), np.float64(0.12))}
Variant (tag only, any waveform): 9 cells
  filter O5 O7 O11  cells  nonwhisk_n  nonwhisk_O6a  nonwhisk_O6b  nonwhisk_lower nonwhisk_p nonwhisk_p_one_sided  whisk_n  whisk_O6a  whisk_O6b  whisk_lower whisk_p whisk_p_one_sided  spt_n  spt_O6a  spt_O6b  spt_lower  spt_p spt_p_one_sided
filtfilt  a  a a-i      9           9         0.199         0.109               8      0.012               0.0059        9      0.292      0.231            8  0.0078            0.0039      9    0.462    0.420          9 0.0039           0.002
filtfilt  b  a a-i      9           9         0.229         0.139               8       0.02               0.0098        9      0.302      0.240            8  0.0078            0.0039      9    0.453    0.396          9 0.0039           0.002
 lfilter  a  a a-i      9           9         0.222         0.127               8      0.012               0.0059        9      0.277      0.235            8  0.0078            0.0039      9    0.462    0.420          9 0.0039           0.002
 lfilter  b  a a-i      9           9         0.264         0.179               7      0.027                0.014        9      0.290      0.244            8  0.0078            0.0039      9    0.453    0.396          9 0.0039           0.002
Per cell (filtfilt, O5 a, O7 a):
  subject  ctl_nonwhisk  light_nonwhisk  ctl_whisk  light_whisk  ctl_spt  light_spt  light_touches  light_t_nonwhisk  light_t_whisk
anm260070         3.907           0.000     26.133        4.268    2.170      0.910            145             4.877         13.589
anm260070         3.985           0.797      8.353        9.715    2.660      0.887            177            32.620         12.970
anm260070         5.222           2.850     18.536        4.275    2.295      0.824             34            17.895          7.251
anm260070         4.805           2.085     13.847        3.401    1.448      1.089            112             2.398         11.762
anm260070        20.115           2.039     43.661        8.544    2.201      0.706            126            19.621          7.725

Result: blocked (too few cells). Only 5 FS cells by the claims file's rule were recorded with thalamic light, all from one mouse (anm260070), and with 5 cells a two-sided signed-rank test, which the criterion names, cannot reach p < 0.05 (smallest p 0.0625; 0.062 to 0.12 over every reading here). The rates fall with the light in all 5 cells without whisking and in 4 of 5 during whisking, to 0.20 and 0.27 of control (population means, all powers, main epochs), and spikes per touch to 0.41; at the highest power only the nonwhisking rate falls to 0.022 of control. Over the 32 combinations of filter, window, power and O11 reading the fractions are 0 to 0.32 without whisking and 0.15 to 0.29 with it, above the paper's 0.13 for whisking under every reading. The one-sided variant, which does not decide, gives p = 0.031 for the nonwhisking rate and spikes per touch and 0.062 for the whisking rate. The variant without the waveform boundary (9 cells) passes all three two-sided tests (p = 0.012, 0.0078 and 0.0039) with fractions of 0.20, 0.29 and 0.46, outside tolerance, so the best it could give is partially reproduced.

exc-thalamus-photoinhibition¶

Thalamic photoinhibition hyperpolarizes L4 excitatory neurons and reduces their Vm fluctuations, with an onset latency of 4.2 ms, longer than the 2.8 ms latency of direct cortical GABAergic activation.

In [29]:
from scipy import signal as sg
bh, ah = sg.butter(2, 10 / 500, "high")
def vm_light(r, d, reading):
    b = r["beh"]
    use_l, use_c = masks(r, d, reading)
    vm = r["vm1k"].astype(float)
    hp = np.full(len(vm), np.nan)
    for k in np.unique(b["trial"][b["trial"] >= 0]):
        i = np.flatnonzero(b["trial"] == k)
        x = vm[i]
        g = np.isfinite(x)
        if g.sum() > 100:
            hp[i] = sg.filtfilt(bh, ah, np.interp(np.arange(len(x)), np.flatnonzero(g), x[g]))
            hp[i[~g]] = np.nan
    keep = ~b["excl"] & np.isfinite(vm)
    return {f"{t}_{q}": f(x[u & keep]) for t, u in (("light", use_l), ("ctl", use_c)) for q, x, f in (("vm", vm, np.mean), ("var", hp, np.nanvar))}

def resample(avg, fs):
    """20 kHz averages to 10 kHz by the mean of each pair of samples (an anti-alias step before decimation)."""
    k = int(fs // 10000)
    return avg if k == 1 else avg[: len(avg) // k * k].reshape(-1, k).mean(axis=1)

rows, avgs = [], []
for r in l4:
    c = cells.set_index("path").loc[r["path"]]
    if c.cls != "E" or r["mode"] != "whole-cell" or c.line != "VGAT-ChR2-EYFP" or "vm1k" not in r:
        continue
    d = light_design(r, "VPM")
    if d is None:
        continue
    for reading in ("a", "b"):
        rows.append({"path": r["path"], "subject": r["subject"], "O5": reading, "lit_trials": len(d["windows"]), **vm_light(r, d, reading)})
    la = r["light_avg"].get(d["site"], {}).get("first", (None, 0))
    if la[0] is not None:
        avgs.append(resample(la[0], r["vm_hz"]) - resample(la[0], r["vm_hz"])[:int(0.05 * 10000)].mean())
tp = pd.DataFrame(rows)
print("Whole-cell E cells with thalamic light:", tp.path.nunique(), "(paper 12), from", tp.subject.nunique(), "mice; lit trials per cell",
      tp.drop_duplicates("path").lit_trials.tolist())
for reading, g in tp.groupby("O5"):
    print(f"O5 {reading}: Vm lower with light in {(g.light_vm < g.ctl_vm).sum()} of {len(g)} (median change {np.median(g.light_vm - g.ctl_vm):.2f} mV, "
          f"signed-rank p = {wilcox(g.light_vm, g.ctl_vm):.2g}); variance (>10 Hz) lower in {(g.light_var < g.ctl_var).sum()} "
          f"(median ratio {np.median(g.light_var / g.ctl_var):.2f}, p = {wilcox(g.light_var, g.ctl_var):.2g})")
ga = np.mean(avgs, axis=0) if avgs else None
lat_thal = yu2016.neg_onset_latency(ga, len(avgs), 10000)
lat_thal_1ms = yu2016.neg_onset_latency(ga, len(avgs), 10000, binned=True)
print(f"Grand average aligned to thalamic laser onset ({len(avgs)} cells): onset latency of the hyperpolarization {lat_thal:.2f} ms at the sampling rate "
      f"(the criterion; expected 4.2 +- 0.42); for comparison only, {lat_thal_1ms:.1f} ms in 1 ms bins (no longer a reading); "
      f"mean change 10 to 50 ms after onset {ga[int(0.06 * 1e4):int(0.1 * 1e4)].mean():.2f} mV")
cav = []
for r in l4:
    c = cells.set_index("path").loc[r["path"]]
    if c.cls != "E" or r["mode"] != "whole-cell" or c.line != "VGAT-ChR2-EYFP" or "vm1k" not in r:
        continue
    for s, L in r["light"].items():
        if "vS1" in L["region"]:
            a, n = r["light_avg"].get(s, {}).get("isolated", (None, 0))
            if a is not None and n >= 5:
                x = resample(a, r["vm_hz"])
                cav.append(x - x[:int(0.05 * 1e4)].mean())
gc = np.mean(cav, axis=0) if cav else None
lat_ctx = yu2016.neg_onset_latency(gc, len(cav), 10000)
lat_ctx_1ms = yu2016.neg_onset_latency(gc, len(cav), 10000, binned=True)
print(f"Grand average aligned to cortical tag pulses ({len(cav)} whole-cell E cells in VGAT-ChR2 mice; paper 7): onset latency {lat_ctx:.2f} ms at the sampling rate "
      f"(expected 2.8 +- 0.28; 1 ms bins, comparison only, {lat_ctx_1ms:.1f} ms); mean change 5 to 20 ms after onset {gc[int(0.055 * 1e4):int(0.07 * 1e4)].mean():.2f} mV")
print("Grand averages 0 to 8 ms after onset, 0.5 ms steps (mV): thalamic", np.round(ga[500:581:5].astype(float), 2).tolist())
print("   cortical", np.round(gc[500:581:5].astype(float), 2).tolist())
fig, ax = plt.subplots(1, 2, figsize=(9, 2.8), sharey=True)
tt = (np.arange(len(ga)) / 1e4 - 0.05) * 1000
ax[0].plot(tt, ga, color="k", lw=0.8); ax[0].axvline(0, color="C0"); ax[0].set_title("thalamic light", fontsize=8)
ax[1].plot(tt, gc, color="k", lw=0.8); ax[1].axvline(0, color="C0"); ax[1].set_title("cortical tag pulse", fontsize=8)
for a in ax:
    a.set_xlim(-20, 60); a.set_xlabel("ms from laser onset")
ax[0].set_ylabel("Vm change (mV)"); plt.tight_layout(); plt.show()
print("Plotted range -20 to 60 ms; values (mV):", round(float(ga[300:1100].min()), 2), "to", round(float(ga[300:1100].max()), 2), "and",
      round(float(gc[300:1100].min()), 2), "to", round(float(gc[300:1100].max()), 2))
Whole-cell E cells with thalamic light: 13 (paper 12), from 8 mice; lit trials per cell [24, 25, 42, 41, 20, 30, 20, 22, 19, 9, 16, 18, 28]
O5 a: Vm lower with light in 12 of 13 (median change -4.02 mV, signed-rank p = 0.00049); variance (>10 Hz) lower in 10 (median ratio 0.55, p = 0.0046)
O5 b: Vm lower with light in 12 of 13 (median change -4.15 mV, signed-rank p = 0.00049); variance (>10 Hz) lower in 10 (median ratio 0.57, p = 0.0046)
Grand average aligned to thalamic laser onset (13 cells): onset latency of the hyperpolarization 0.10 ms at the sampling rate (the criterion; expected 4.2 +- 0.42); for comparison only, 5.0 ms in 1 ms bins (no longer a reading); mean change 10 to 50 ms after onset -3.70 mV
Grand average aligned to cortical tag pulses (14 whole-cell E cells in VGAT-ChR2 mice; paper 7): onset latency 0.10 ms at the sampling rate (expected 2.8 +- 0.28; 1 ms bins, comparison only, 3.0 ms); mean change 5 to 20 ms after onset -1.86 mV
Grand averages 0 to 8 ms after onset, 0.5 ms steps (mV): thalamic [-0.21, -0.22, -0.22, -0.21, -0.21, -0.21, -0.2, -0.21, -0.21, -0.23, -0.31, -0.4, -0.52, -0.65, -0.79, -0.91, -1.04]
   cortical [0.59, 0.59, 0.62, 0.63, 0.6, 0.6, 0.52, 0.36, 0.19, 0.04, -0.08, -0.23, -0.36, -0.53, -0.78, -0.99, -1.19]
No description has been provided for this image
Plotted range -20 to 60 ms; values (mV): -4.63 to 0.02 and -3.73 to 0.67
In [30]:
# The 0.1 ms laser latencies (the verifier's request): the grand averages -2 to +2 ms from laser onset, their first difference
# and the criterion's threshold (3 s.d. of the first difference 0 to 25 ms before onset, turned downward), at 10 kHz.
for name, avg_ in (("thalamic light", ga), ("cortical tag pulses", gc)):
    avg_ = np.asarray(avg_, float)
    i0 = int(0.05 * 1e4)
    d = np.diff(avg_)                    # d[i] is the change into sample i + 1
    base = d[i0 - 251:i0 - 1]
    thr = 3 * base.std()
    hit = np.flatnonzero(d[i0 - 1:] < -thr)
    print(f"{name}: Vm (mV) -2 to +2 ms in 0.1 ms steps:", np.round(avg_[i0 - 20:i0 + 21], 3).tolist())
    print("   first difference (mV per 0.1 ms) into each of those samples:", np.round(d[i0 - 21:i0 + 20], 4).tolist(), "; threshold", round(-thr, 4))
    print(f"   baseline samples beyond the threshold: {int(np.sum(base < -thr))} of {len(base)}; first sample beyond it after onset "
          f"{hit[0] / 10:.1f} ms, a change of {d[i0 - 1 + hit[0]]:.4f} mV; step across onset (mean 0 to 1 ms minus mean -1 to 0 ms) "
          f"{avg_[i0:i0 + 10].mean() - avg_[i0 - 10:i0].mean():.3f} mV")
    later = hit[hit >= 20]
    print(f"   first sample beyond it 2 ms or more after onset (diagnostic, decides nothing): {later[0] / 10 if len(later) else np.nan:.1f} ms")
thalamic light: Vm (mV) -2 to +2 ms in 0.1 ms steps: [-0.176, -0.179, -0.181, -0.179, -0.192, -0.188, -0.187, -0.19, -0.19, -0.191, -0.192, -0.197, -0.206, -0.194, -0.196, -0.199, -0.2, -0.202, -0.199, -0.212, -0.214, -0.275, -0.226, -0.221, -0.223, -0.22, -0.226, -0.229, -0.22, -0.218, -0.218, -0.212, -0.213, -0.207, -0.213, -0.213, -0.212, -0.217, -0.214, -0.22, -0.214]
   first difference (mV per 0.1 ms) into each of those samples: [-0.0039, -0.0031, -0.0017, 0.0015, -0.0128, 0.0039, 0.0011, -0.0025, -0.0008, -0.0005, -0.001, -0.0052, -0.0092, 0.0121, -0.0019, -0.0024, -0.0012, -0.0024, 0.0028, -0.0122, -0.0019, -0.0612, 0.0484, 0.0057, -0.0018, 0.0022, -0.0053, -0.0033, 0.0086, 0.0025, 0.0002, 0.0061, -0.0015, 0.006, -0.0056, -0.0008, 0.0019, -0.0053, 0.0024, -0.0052, 0.0057] ; threshold -0.0206
   baseline samples beyond the threshold: 1 of 250; first sample beyond it after onset 0.1 ms, a change of -0.0612 mV; step across onset (mean 0 to 1 ms minus mean -1 to 0 ms) -0.027 mV
   first sample beyond it 2 ms or more after onset (diagnostic, decides nothing): 4.9 ms
cortical tag pulses: Vm (mV) -2 to +2 ms in 0.1 ms steps: [0.667, 0.657, 0.669, 0.669, 0.658, 0.656, 0.654, 0.655, 0.654, 0.654, 0.654, 0.645, 0.628, 0.629, 0.615, 0.625, 0.617, 0.615, 0.605, 0.593, 0.589, 0.526, 0.596, 0.591, 0.585, 0.594, 0.59, 0.594, 0.606, 0.602, 0.616, 0.617, 0.625, 0.629, 0.627, 0.626, 0.613, 0.609, 0.603, 0.598, 0.603]
   first difference (mV per 0.1 ms) into each of those samples: [0.0121, -0.0095, 0.0114, 0.0001, -0.0107, -0.0017, -0.0027, 0.0018, -0.001, -0.0006, 0.0001, -0.0089, -0.0166, 0.0005, -0.0139, 0.0102, -0.0086, -0.0015, -0.0105, -0.0119, -0.0033, -0.0638, 0.0703, -0.0046, -0.0065, 0.0089, -0.0036, 0.0043, 0.0115, -0.0044, 0.0148, 0.0007, 0.0082, 0.0033, -0.0018, -0.0007, -0.0129, -0.0043, -0.0055, -0.0055, 0.0053] ; threshold -0.0557
   baseline samples beyond the threshold: 3 of 250; first sample beyond it after onset 0.1 ms, a change of -0.0638 mV; step across onset (mean 0 to 1 ms minus mean -1 to 0 ms) -0.035 mV
   first sample beyond it 2 ms or more after onset (diagnostic, decides nothing): 5.2 ms

Result: partially reproduced. Thalamic light hyperpolarizes 12 of 13 whole-cell excitatory cells (median -4.02 mV) and lowers their Vm variance above 10 Hz in 10 (median ratio 0.55), under both window readings. With dVm/dt at the sampling rate, as the criterion says, the onset latency is 0.10 ms for both the thalamic light (13 cells) and the cortical tag pulses (14 cells, now that every pulse of a train counts), against 4.2 and 2.8 ms, so the thalamic latency is not longer. The diagnostic above shows the cause: one sample 0.1 ms after light onset drops by 0.061 mV (thalamic) and 0.064 mV (cortical) and the next sample returns, a one-sample transient at light onset, likely an artifact, which crosses the threshold. The first crossing 2 ms or more after onset, a diagnostic that decides nothing, is at 4.9 ms (thalamic) and 5.2 ms (cortical), but the two thresholds differ (-0.0206 and -0.0557 mV per sample; 1 and 3 of the 250 baseline samples already cross them), so this does not compare the two latencies. The other outputs above put the thalamic onset later: 5.0 against 3.0 ms in 1 ms bins, and in 0.5 ms steps the thalamic average starts to fall at about 4.5 to 5 ms and the cortical one at about 3 ms. None of these is the criterion's rule, so they decide nothing; by the rule the thalamic latency is not longer. The 20 kHz averages are now brought to 10 kHz by averaging pairs of samples.

pv-enphr-suppression¶

Yellow light in PV-IRES-Cre mice expressing eNpHR3.0 strongly inhibits most L4 PV interneurons (3 of 4); their rate falls 3 ms after laser onset and L4 excitatory activity rises 2 ms later.

In [31]:
def boot_rate_ci(count, b, filt, use, rng):
    """95% bootstrap interval (1,000 draws over trials) of the rate in nonwhisking and whisking samples."""
    out = {}
    tri = b["trial"]
    for name in ("nonwhisk", "whisk"):
        m = b[filt][name] & use & (tri >= 0)
        ks = tri[m]
        n = tri.max() + 1
        c = np.bincount(ks, weights=count[m], minlength=n)
        t = np.bincount(ks, minlength=n) / 1000
        keep = t > 0
        c, t = c[keep], t[keep]
        if len(c) < 2:
            out[name] = (np.nan, np.nan)
            continue
        idx = rng.integers(0, len(c), (yu2016.N_BOOT, len(c)))
        bs = c[idx].sum(axis=1) / t[idx].sum(axis=1)
        out[name] = (np.percentile(bs, 2.5), np.percentile(bs, 97.5))
    return out

def onset_psth(r, d):
    """Rate (spikes/s) in 1 ms bins from 25 ms before to 100 ms after the first light sample of each lit trial."""
    sp = np.sort(r["spikes"])
    st = r["trials"].start.to_numpy()
    on = np.array([st[t] + w[0] for t, w in d["windows"].items()])
    edges = np.arange(-25, 101) / 1000
    h = np.zeros(len(edges) - 1)
    for o in on:
        h += np.histogram(sp[(sp >= o - 0.025) & (sp < o + 0.1)] - o, edges)[0]
    return h / len(on) * 1000

def psth_crossing(m, sign):
    lags = np.arange(-25, 100)
    dd = np.diff(m)
    lag_d = lags[1:]
    thr = 3 * dd[(lag_d >= -25) & (lag_d < 0)].std()
    hit = np.flatnonzero((lag_d >= 0) & (sign * dd > thr))
    return (float(lag_d[hit[0]]), float(lag_d[hit[0]] + 0.5)) if len(hit) else (np.nan, np.nan)

rng = np.random.default_rng(13)
rows, ps_pv, ps_e = [], [], []
for r in l4:
    c = cells.set_index("path").loc[r["path"]]
    if c.line != "PV-IRES-Cre" or r["beh"]["principal"] is None:
        continue
    d = light_design(r, "vS1")
    if d is None:
        continue
    ps = onset_psth(r, d)
    if c.cls == "FS":
        ps_pv.append(ps)
        b = r["beh"]
        use_l, use_c = masks(r, d, "a")
        count = yu2016.counts_1k(r["spikes"], b)
        ci = boot_rate_ci(count, b, "filtfilt", use_c, rng)
        lr = light_compare(r, r["spikes"], d, "filtfilt", "a")
        rows.append({"path": r["path"].split("/")[1][:40], "ctl_nonwhisk": lr["ctl_nonwhisk"], "ctl_nonwhisk_lo": ci["nonwhisk"][0],
                     "light_nonwhisk": lr["light_nonwhisk"], "ctl_whisk": lr["ctl_whisk"], "ctl_whisk_lo": ci["whisk"][0],
                     "light_whisk": lr["light_whisk"],
                     "strongly_inhibited": lr["light_nonwhisk"] < ci["nonwhisk"][0] and lr["light_whisk"] < ci["whisk"][0]})
    elif c.cls == "E":
        ps_e.append(ps)
pvr = pd.DataFrame(rows)
print("PV cells identified (PV-IRES-Cre, rate lower with the light):", len(pvr), "(paper 4); strongly inhibited (light rate below the lower "
      "bound of the control 95% interval in nonwhisking and whisking periods):", int(pvr.strongly_inhibited.sum()))
print(pvr.round(2).to_string(index=False))
gpv, ge = np.mean(ps_pv, axis=0), np.mean(ps_e, axis=0)
lpv, le_ = psth_crossing(gpv, -1), psth_crossing(ge, +1)
print(f"Grand average PSTH aligned to laser onset: PV cells ({len(ps_pv)}) rate falls at {lpv[0]:.1f} ms (O9 a, bin edge) / {lpv[1]:.1f} ms (O9 b, centre), "
      f"expected 3 +- 0.3; E cells ({len(ps_e)}) rate rises at {le_[0]:.1f} / {le_[1]:.1f} ms, a further delay of {le_[0] - lpv[0]:.1f} ms "
      f"(expected 2 +- 0.2)")
print("PV grand average, 0 to 10 ms (spikes/s):", np.round(gpv[25:36], 1).tolist(), "; 25 ms before: mean", round(gpv[:25].mean(), 1))
print("E grand average, 0 to 10 ms (spikes/s):", np.round(ge[25:36], 2).tolist(), "; 25 ms before: mean", round(ge[:25].mean(), 2))
PV cells identified (PV-IRES-Cre, rate lower with the light): 3 (paper 4); strongly inhibited (light rate below the lower bound of the control 95% interval in nonwhisking and whisking periods): 3
                                    path  ctl_nonwhisk  ctl_nonwhisk_lo  light_nonwhisk  ctl_whisk  ctl_whisk_lo  light_whisk  strongly_inhibited
sub-anm244024_ses-20141112_obj-vigb9a_be         13.06            11.01            3.30      21.32         18.28         6.33                True
sub-anm266945_ses-20141220_obj-7o69ev_be         41.73            31.96            2.00      56.58         48.57         4.28                True
sub-anm300169_ses-20150926_obj-rp0tw4_be         27.55            24.67            1.74      18.85         15.53         0.63                True
Grand average PSTH aligned to laser onset: PV cells (3) rate falls at nan ms (O9 a, bin edge) / nan ms (O9 b, centre), expected 3 +- 0.3; E cells (42) rate rises at 48.0 / 48.5 ms, a further delay of nan ms (expected 2 +- 0.2)
PV grand average, 0 to 10 ms (spikes/s): [26.7, 66.5, 49.0, 17.5, 0.0, 0.0, 0.0, 0.0, 0.0, 12.8, 0.0] ; 25 ms before: mean 37.1
E grand average, 0 to 10 ms (spikes/s): [7.76, 2.02, 3.24, 3.18, 2.32, 6.61, 5.08, 2.17, 7.85, 4.02, 7.89] ; 25 ms before: mean 3.1

Result: partially reproduced. 3 PV cells are identified by their inactivation, and all 3 are strongly inhibited (their light rate below the control interval in both periods); the paper's fourth PV cell cannot be found. Since the cells are found by their inactivation, the count cannot fail, and a fourth PV cell that the light did not inhibit would be classed excitatory. The parts that could fail fail: the dPSTH/dt rule finds no fall in the grand average of the 3 PV cells, and the excitatory cells' rise is found at 48.0 ms with the bin edge (O9 a) and 48.5 ms with its centre (O9 b), so neither latency is within tolerance under either bin position.

fs-suppress-movement-spiking¶

Partially suppressing PV (FS) interneurons increases the spike rate of L4 excitatory neurons during nonwhisking (0.6 to 1.6 spikes/s) and whisking (0.8 to 2.7 spikes/s).

In [32]:
def pv_rows(min_iti=0.025, definition="cortex5", o11s=yu2016.O11):
    out = []
    for r in l4:
        c = cells.set_index("path").loc[r["path"]]
        if c.line != "PV-IRES-Cre" or c.cls != "E" or r["beh"]["principal"] is None:
            continue
        d = light_design(r, "vS1")
        if d is None:
            continue
        for filt in yu2016.FILTERS:
            for reading in ("a", "b"):
                for o11 in o11s:
                    out.append({"path": r["path"], "subject": r["subject"], "mode": r["mode"], "filter": filt, "O5": reading, "O11": o11,
                                "lit_trials": len(d["windows"]), **light_compare(r, r["spikes"], d, filt, reading, definition, min_iti, o11=o11)})
    return pd.DataFrame(out)

pvE = pv_rows()
pvE8 = pv_rows(0.010, "cortex8", ("a-i",))
print("E cells in PV-IRES-Cre mice with the eNpHR light:", pvE.path.nunique(), "(paper 40 of 43), from", pvE.subject.nunique(), "mice;",
      pvE.drop_duplicates("path")["mode"].value_counts().to_dict())
rows = []
for (filt, o5, o11), g in pvE.groupby(["filter", "O5", "O11"]):
    g = g[(g.light_t_nonwhisk > 0) & (g.light_t_whisk > 0) & (g.ctl_t_nonwhisk > 0) & (g.ctl_t_whisk > 0)]
    kw = stats.kruskal(g.ctl_nonwhisk, g.ctl_whisk, g.light_nonwhisk, g.light_whisk).pvalue
    rows.append({"filter": filt, "O5": o5, "O11": o11, "n": len(g), "ctl_nonwhisk": g.ctl_nonwhisk.mean(), "light_nonwhisk": g.light_nonwhisk.mean(),
                 "ctl_whisk": g.ctl_whisk.mean(), "light_whisk": g.light_whisk.mean(),
                 "sem_light_nonwhisk": g.light_nonwhisk.sem(), "sem_light_whisk": g.light_whisk.sem(),
                 "nonwhisk_higher": int((g.light_nonwhisk > g.ctl_nonwhisk).sum()), "p_nonwhisk": "%.2g" % wilcox(g.light_nonwhisk, g.ctl_nonwhisk),
                 "whisk_higher": int((g.light_whisk > g.ctl_whisk).sum()), "p_whisk": "%.2g" % wilcox(g.light_whisk, g.ctl_whisk),
                 "kruskal_wallis_p": "%.2g" % kw})
print("Expected means: nonwhisking 0.6 +- 0.1 -> 1.6 +- 0.3, whisking 0.8 +- 0.2 -> 2.7 +- 0.5 spikes/s; both paired tests p < 0.05 with light higher")
fsm = pd.DataFrame(rows)
print(fsm.round(3).to_string(index=False))
print("Ranges over the 16 readings:", ranges(fsm, ["ctl_nonwhisk", "light_nonwhisk", "ctl_whisk", "light_whisk"]),
      "; largest paired p:", max(float(x) for x in fsm.p_nonwhisk), "and", max(float(x) for x in fsm.p_whisk))
g = pvE[(pvE["filter"] == "filtfilt") & (pvE.O5 == "a") & (pvE.O11 == "a-i")]
print("Cells dropped for lack of an epoch of each kind (filtfilt, O5 a):",
      int(((g.light_t_nonwhisk == 0) | (g.light_t_whisk == 0) | (g.ctl_t_nonwhisk == 0) | (g.ctl_t_whisk == 0)).sum()))
print("By mouse (filtfilt, O5 a), mean rates:", g.groupby("subject")[["ctl_nonwhisk", "light_nonwhisk", "ctl_whisk", "light_whisk"]].mean().round(2).to_dict("index"))
print("Seconds per cell inside the light, median: nonwhisking", round(g.light_t_nonwhisk.median(), 1), ", whisking", round(g.light_t_whisk.median(), 1))
E cells in PV-IRES-Cre mice with the eNpHR light: 42 (paper 40 of 43), from 9 mice; {'cell-attached': 34, 'whole-cell': 8}
Expected means: nonwhisking 0.6 +- 0.1 -> 1.6 +- 0.3, whisking 0.8 +- 0.2 -> 2.7 +- 0.5 spikes/s; both paired tests p < 0.05 with light higher
  filter O5  O11  n  ctl_nonwhisk  light_nonwhisk  ctl_whisk  light_whisk  sem_light_nonwhisk  sem_light_whisk  nonwhisk_higher p_nonwhisk  whisk_higher p_whisk kruskal_wallis_p
filtfilt  a  a-i 42         1.256           2.263      3.953        6.135               0.374            1.145               33    6.2e-05            33 7.4e-05          0.00011
filtfilt  a a-ii 42         1.271           2.297      4.067        6.272               0.374            1.164               32    7.4e-05            33 9.9e-05          8.2e-05
filtfilt  a  b-i 42         1.269           2.288      4.214        6.551               0.373            1.172               33      7e-05            33 4.9e-05            5e-05
filtfilt  a b-ii 42         1.279           2.317      4.307        6.633               0.373            1.189               32    6.6e-05            33 7.4e-05          4.1e-05
filtfilt  b  a-i 42         1.179           2.263      3.583        6.135               0.374            1.145               33    3.7e-05            33 1.1e-05          6.2e-05
filtfilt  b a-ii 42         1.193           2.297      3.635        6.272               0.374            1.164               33    3.7e-05            34 4.4e-06          5.2e-05
filtfilt  b  b-i 42         1.184           2.288      3.665        6.551               0.373            1.172               33    3.3e-05            33 4.6e-06          2.9e-05
filtfilt  b b-ii 42         1.196           2.317      3.717        6.633               0.373            1.189               33    3.5e-05            34   3e-06          2.7e-05
 lfilter  a  a-i 42         1.291           2.504      3.720        5.690               0.378            1.054               33    4.1e-05            33 0.00018          0.00035
 lfilter  a a-ii 42         1.307           2.539      3.838        5.815               0.378            1.069               33    3.3e-05            31  0.0003           0.0003
 lfilter  a  b-i 42         1.307           2.517      4.001        6.030               0.377            1.069               33    4.4e-05            33 0.00011          0.00016
 lfilter  a b-ii 42         1.315           2.552      4.099        6.154               0.377            1.089               33    3.5e-05            31 0.00019          0.00014
 lfilter  b  a-i 42         1.212           2.504      3.364        5.690               0.378            1.054               36    6.7e-06            32 7.1e-06          0.00018
 lfilter  b a-ii 42         1.219           2.539      3.414        5.815               0.378            1.069               36    6.3e-06            33 4.4e-06          0.00013
 lfilter  b  b-i 42         1.217           2.517      3.453        6.030               0.377            1.069               36    5.9e-06            33 3.2e-06            8e-05
 lfilter  b b-ii 42         1.222           2.552      3.505        6.154               0.377            1.089               36    5.2e-06            33 2.6e-06            7e-05
Ranges over the 16 readings: {'ctl_nonwhisk': (1.18, 1.32), 'light_nonwhisk': (2.26, 2.55), 'ctl_whisk': (3.36, 4.31), 'light_whisk': (5.69, 6.63)} ; largest paired p: 7.4e-05 and 0.0003
Cells dropped for lack of an epoch of each kind (filtfilt, O5 a): 0
By mouse (filtfilt, O5 a), mean rates: {'anm244024': {'ctl_nonwhisk': 1.19, 'light_nonwhisk': 1.98, 'ctl_whisk': 3.71, 'light_whisk': 4.97}, 'anm244025': {'ctl_nonwhisk': 1.22, 'light_nonwhisk': 1.68, 'ctl_whisk': 2.69, 'light_whisk': 4.9}, 'anm244028': {'ctl_nonwhisk': 0.59, 'light_nonwhisk': 1.25, 'ctl_whisk': 1.38, 'light_whisk': 2.83}, 'anm266945': {'ctl_nonwhisk': 0.77, 'light_nonwhisk': 1.46, 'ctl_whisk': 2.8, 'light_whisk': 6.73}, 'anm266951': {'ctl_nonwhisk': 0.6, 'light_nonwhisk': 0.0, 'ctl_whisk': 0.0, 'light_whisk': 0.0}, 'anm300169': {'ctl_nonwhisk': 1.41, 'light_nonwhisk': 2.8, 'ctl_whisk': 1.86, 'light_whisk': 4.83}, 'anm300170': {'ctl_nonwhisk': 1.62, 'light_nonwhisk': 5.34, 'ctl_whisk': 4.49, 'light_whisk': 13.16}, 'anm300171': {'ctl_nonwhisk': 2.8, 'light_nonwhisk': 4.32, 'ctl_whisk': 6.57, 'light_whisk': 7.52}, 'anm300172': {'ctl_nonwhisk': 1.56, 'light_nonwhisk': 2.51, 'ctl_whisk': 9.47, 'light_whisk': 7.88}}
Seconds per cell inside the light, median: nonwhisking 11.7 , whisking 10.5

Result: partially reproduced. In the 42 excitatory cells of PV-IRES-Cre mice the light raises the nonwhisking rate (32 to 36 cells, p at most 7.4e-05) and the whisking rate (31 to 34 cells, p at most 0.0003) under every filter, window and O11 reading, but the rates are higher than the paper's: 1.18 to 1.32 rising to 2.26 to 2.55 spikes/s without whisking (paper 0.6 to 1.6) and 3.36 to 4.31 rising to 5.69 to 6.63 with it (paper 0.8 to 2.7), outside the tolerances. Most of these cells are cell-attached (34 of 42), whose rates are high throughout this dandiset (data check).

fs-enhance-touch-selectivity¶

With PV interneurons suppressed, L4 excitatory neurons become driven by whisker movement (whisking rate above nonwhisking, P = 0.02), while in control trials they are not (P = 0.55); their selectivity for touch is normally maintained in part by FS interneurons.

In [33]:
rows = []
for (filt, o5, o11), g in pvE.groupby(["filter", "O5", "O11"]):
    g = g[(g.light_t_nonwhisk > 0) & (g.light_t_whisk > 0) & (g.ctl_t_nonwhisk > 0) & (g.ctl_t_whisk > 0)]
    rows.append({"filter": filt, "O5": o5, "O11": o11, "n": len(g), "light_whisk_minus_nonwhisk": (g.light_whisk - g.light_nonwhisk).mean(),
                 "light_whisk_higher": int((g.light_whisk > g.light_nonwhisk).sum()), "p_light": "%.2g" % wilcox(g.light_whisk, g.light_nonwhisk),
                 "ctl_whisk_minus_nonwhisk": (g.ctl_whisk - g.ctl_nonwhisk).mean(), "ctl_whisk_higher": int((g.ctl_whisk > g.ctl_nonwhisk).sum()),
                 "p_ctl": "%.2g" % wilcox(g.ctl_whisk, g.ctl_nonwhisk)})
print("Whisking against nonwhisking rate within light trials (paper P = 0.02) and within control trials (paper P = 0.55):")
fse = pd.DataFrame(rows)
print(fse.round(3).to_string(index=False))
print("Ranges over the 16 readings: p with light", (min(float(x) for x in fse.p_light), max(float(x) for x in fse.p_light)),
      "; p in control trials", (min(float(x) for x in fse.p_ctl), max(float(x) for x in fse.p_ctl)))
Whisking against nonwhisking rate within light trials (paper P = 0.02) and within control trials (paper P = 0.55):
  filter O5  O11  n  light_whisk_minus_nonwhisk  light_whisk_higher p_light  ctl_whisk_minus_nonwhisk  ctl_whisk_higher   p_ctl
filtfilt  a  a-i 42                       3.872                  33 2.2e-06                     2.697                31 3.3e-05
filtfilt  a a-ii 42                       3.975                  33 1.9e-06                     2.796                31 2.7e-05
filtfilt  a  b-i 42                       4.263                  33 2.1e-06                     2.945                32   2e-05
filtfilt  a b-ii 42                       4.317                  33 1.7e-06                     3.028                32 1.7e-05
filtfilt  b  a-i 42                       3.872                  33 2.2e-06                     2.404                35 7.6e-06
filtfilt  b a-ii 42                       3.975                  33 1.9e-06                     2.441                35 7.1e-06
filtfilt  b  b-i 42                       4.263                  33 2.1e-06                     2.481                35 7.6e-06
filtfilt  b b-ii 42                       4.317                  33 1.7e-06                     2.521                35 6.7e-06
 lfilter  a  a-i 42                       3.186                  32 3.3e-05                     2.429                29 0.00013
 lfilter  a a-ii 42                       3.276                  31 3.7e-05                     2.531                27 0.00011
 lfilter  a  b-i 42                       3.513                  32 2.2e-05                     2.695                30 7.4e-05
 lfilter  a b-ii 42                       3.602                  32   2e-05                     2.784                28 5.8e-05
 lfilter  b  a-i 42                       3.186                  32 3.3e-05                     2.152                30 7.5e-05
 lfilter  b a-ii 42                       3.276                  31 3.7e-05                     2.196                30 7.5e-05
 lfilter  b  b-i 42                       3.513                  32 2.2e-05                     2.236                31 5.4e-05
 lfilter  b b-ii 42                       3.602                  32   2e-05                     2.283                31 5.1e-05
Ranges over the 16 readings: p with light (1.7e-06, 3.7e-05) ; p in control trials (6.7e-06, 0.00013)

Result: partially reproduced. With the light, whisking rates exceed nonwhisking rates in 31 to 33 of 42 cells (p = 1.7e-06 to 3.7e-05 over the 16 readings), as the paper reports, but in control trials they do too (27 to 35 of 42, p = 6.7e-06 to 0.00013), where the paper found no difference (P = 0.55).

exc-touch-spikes-enphr¶

PV interneuron suppression does not change mean touch-evoked spiking of L4 excitatory neurons significantly (0.27 to 0.32 spikes per touch, P = 0.09); its effect is heterogeneous, with weakly responding neurons responding more and strongly responding neurons unchanged or less.

In [34]:
rows = []
for (filt, o5), g in pvE8[pvE8["filter"] == "filtfilt"].groupby(["filter", "O5"]):
    g = g.dropna(subset=["ctl_spt", "light_spt"])
    rows.append({"O5": o5, "n": len(g), "ctl_mean": g.ctl_spt.mean(), "ctl_sem": g.ctl_spt.sem(), "light_mean": g.light_spt.mean(),
                 "light_sem": g.light_spt.sem(), "higher": int((g.light_spt > g.ctl_spt).sum()), "p": "%.2g" % wilcox(g.light_spt, g.ctl_spt),
                 "touches_light_median": g.light_touches.median()})
print("Spikes per touch (Fig. 8e definition, ITI >= 10 ms), light against control; expected 0.27 +- 0.06 -> 0.32 +- 0.06, P = 0.09 (absence at 0.05)")
print(pd.DataFrame(rows).round(3).to_string(index=False))
g = pvE8[(pvE8["filter"] == "filtfilt") & (pvE8.O5 == "a")].dropna(subset=["ctl_spt", "light_spt"])
lo = g.ctl_spt < g.ctl_spt.median()
print(f"Heterogeneity (described, not tested): cells below the median control response change by {np.mean(g.light_spt[lo] - g.ctl_spt[lo]):+.3f} on average "
      f"({int((g.light_spt[lo] > g.ctl_spt[lo]).sum())} of {int(lo.sum())} rise); cells above it by {np.mean(g.light_spt[~lo] - g.ctl_spt[~lo]):+.3f} "
      f"({int((g.light_spt[~lo] > g.ctl_spt[~lo]).sum())} of {int((~lo).sum())} rise)")
Spikes per touch (Fig. 8e definition, ITI >= 10 ms), light against control; expected 0.27 +- 0.06 -> 0.32 +- 0.06, P = 0.09 (absence at 0.05)
O5  n  ctl_mean  ctl_sem  light_mean  light_sem  higher      p  touches_light_median
 a 40     0.339    0.079       0.448      0.102      25  0.044                  81.5
 b 41     0.304    0.077       0.442      0.100      28 0.0091                  80.0
Heterogeneity (described, not tested): cells below the median control response change by +0.123 on average (14 of 20 rise); cells above it by +0.095 (11 of 20 rise)

Result: not reproduced. Spikes per touch (Fig. 8e definition) rise with the light from 0.339 to 0.448 (40 cells, p = 0.044) or from 0.304 to 0.442 (41 cells, p = 0.0091), so the paper's absence of a significant change is not found. Cells below the median control response rise by 0.123 on average and cells above it by 0.095.

auc-weak-strong¶

Suppressing PV interneurons improves touch detection (AUC) for L4 excitatory neurons with weak touch responses and diminishes it for neurons with robust touch responses.

In [35]:
def auc_inputs(r, d, rng, reading):
    """Per trial, spike counts in the 250 ms before and after the first principal-whisker touch, or (no touch) in two
    adjacent 250 ms windows of the sample epoch: reading (a) at a random time, (b) around the median first-touch time
    of the touch trials. Light trials count when the 500 ms lie inside the light."""
    b, tr = r["beh"], r["trials"]
    sp = np.sort(r["spikes"])
    st = tr.start.to_numpy()
    on_t = b["t"][b["touch_on"]]
    k = yu2016.per_trial_index(on_t, tr)
    first = {}
    for t, kk in zip(on_t, k):
        if kk >= 0 and kk not in first:
            first[kk] = t
    med = np.median([first[kk] - st[kk] for kk in first]) if first else np.nan
    out = {"ctl": {"t": [[], []], "n": [[], []]}, "light": {"t": [[], []], "n": [[], []]}}
    cnt = lambda a, z: int(np.searchsorted(sp, z) - np.searchsorted(sp, a))
    for kk in range(len(tr)):
        if d["ctl"][kk]:
            cond, win = "ctl", None
        elif kk in d["windows"]:
            cond, win = "light", d["windows"][kk]
        else:
            continue
        if kk in first:
            t0, key = first[kk], "t"
        else:
            p0, p1 = tr.pole_in_time.iloc[kk], tr.pole_out_time.iloc[kk]
            if not (np.isfinite(p0) and np.isfinite(p1)) or p1 - p0 < 0.5:
                continue
            t0 = rng.uniform(p0 + 0.25, p1 - 0.25) if reading == "a" else st[kk] + med
            key = "n"
        if win is not None and not (st[kk] + win[0] <= t0 - 0.25 and t0 + 0.25 <= st[kk] + win[1]):
            continue
        if t0 - 0.25 < st[kk] or t0 + 0.25 > st[kk] + yu2016.ANALYSIS_END_S:
            continue
        out[cond][key][0].append(cnt(t0 - 0.25, t0))
        out[cond][key][1].append(cnt(t0, t0 + 0.25))
    return out

def auc_table(reading, rng):
    rows_ = []
    for r in l4:
        c = cells.set_index("path").loc[r["path"]]
        if c.line != "PV-IRES-Cre" or c.cls != "E" or r["beh"]["principal"] is None:
            continue
        d = light_design(r, "vS1")
        if d is None:
            continue
        x = auc_inputs(r, d, rng, reading)
        row = {"path": r["path"], "subject": r["subject"], "reading": reading}
        for cond in ("ctl", "light"):
            tb, ta = np.array(x[cond]["t"][0]), np.array(x[cond]["t"][1])
            nb, na = np.array(x[cond]["n"][0]), np.array(x[cond]["n"][1])
            row[f"{cond}_touch_trials"], row[f"{cond}_notouch_trials"] = len(tb), len(nb)
            row[f"{cond}_auc"] = yu2016.roc_auc((tb, nb), (ta, na), rng)
            row[f"{cond}_after"] = ta.mean() / 0.25 if len(ta) else np.nan
            row[f"{cond}_before"] = tb.mean() / 0.25 if len(tb) else np.nan
        rows_.append(row)
    t = pd.DataFrame(rows_)
    t["evoked_ctl"] = t.ctl_after - t.ctl_before
    t["auc_change"] = t.light_auc - t.ctl_auc
    return t

auc = pd.concat([auc_table(reading, np.random.default_rng(29 if reading == "a" else 31)) for reading in ("a", "b")], ignore_index=True)
for reading, g in auc.groupby("reading"):
    g = g.dropna(subset=["ctl_auc", "light_auc"])
    up, dn = g[g.auc_change > 0], g[g.auc_change < 0]
    print(f"Reading {reading}: {len(g)} cells with an AUC in both conditions (trials per cell, median: control {int(g.ctl_touch_trials.median())} touch, "
          f"{int(g.ctl_notouch_trials.median())} no-touch; light {int(g.light_touch_trials.median())} touch, {int(g.light_notouch_trials.median())} no-touch). "
          f"AUC rises in {len(up)}, falls in {len(dn)}, unchanged in {int((g.auc_change == 0).sum())}. Control touch-evoked rate (0 to 250 ms after minus "
          f"before, spikes/s): rising group mean {up.evoked_ctl.mean():.2f}, falling group mean {dn.evoked_ctl.mean():.2f} "
          f"(rank-sum p = {stats.ranksums(up.evoked_ctl, dn.evoked_ctl).pvalue:.2g}, reported only)")
    print(f"   AUC control median {g.ctl_auc.median():.3f}, light median {g.light_auc.median():.3f}")
    print(f"   Spearman correlation of AUC change with the control touch-evoked rate: rho = {stats.spearmanr(g.evoked_ctl, g.auc_change)[0]:.2f} (reported only)")
print("Cells without an AUC in a condition:", auc[auc.ctl_auc.isna() | auc.light_auc.isna()].groupby("reading").size().to_dict())
Reading a: 36 cells with an AUC in both conditions (trials per cell, median: control 23 touch, 20 no-touch; light 12 touch, 10 no-touch). AUC rises in 23, falls in 12, unchanged in 1. Control touch-evoked rate (0 to 250 ms after minus before, spikes/s): rising group mean 5.93, falling group mean 10.87 (rank-sum p = 0.37, reported only)
   AUC control median 0.660, light median 0.690
   Spearman correlation of AUC change with the control touch-evoked rate: rho = -0.23 (reported only)
Reading b: 34 cells with an AUC in both conditions (trials per cell, median: control 24 touch, 20 no-touch; light 12 touch, 15 no-touch). AUC rises in 17, falls in 16, unchanged in 1. Control touch-evoked rate (0 to 250 ms after minus before, spikes/s): rising group mean 8.14, falling group mean 8.18 (rank-sum p = 1, reported only)
   AUC control median 0.690, light median 0.746
   Spearman correlation of AUC change with the control touch-evoked rate: rho = 0.04 (reported only)
Cells without an AUC in a condition: {'a': 6, 'b': 8}

Result: not reproduced. With the fixed seeds, cells whose AUC rises with the light have a lower control touch-evoked rate than those whose AUC falls under both readings: 5.93 against 10.87 spikes/s (a) and 8.14 against 8.18 (b). The seed check below shows that reading (b) does not decide: over 20 further seeds the rising group's rate is lower in 8 and higher in 12 (8.14 to 8.53 against 7.86 to 8.18 spikes/s), while reading (a) holds in 19 of 20. The criterion needs both readings, and under (b) the sign of a 0.04 spikes/s difference is set by the resampling, so the result is not reproduced. No test separates the groups under either reading with the fixed seeds (rank-sum p = 0.37 and 1, reported only).

auc-groups¶

Neurons whose AUC rose with PV suppression gained touch-evoked spiking (3.4 to 8.7 spikes/s); neurons whose AUC fell kept their touch-evoked response (7.3 to 7.0 spikes/s) but had higher baseline rates (0.9 to 2.6 spikes/s).

In [36]:
rows = []
for reading, g in auc.groupby("reading"):
    g = g.dropna(subset=["ctl_auc", "light_auc"])
    up, dn = g[g.auc_change > 0], g[g.auc_change < 0]
    rows.append({"reading": reading, "n_up": len(up), "n_down": len(dn),
                 "up_after_ctl": up.ctl_after.mean(), "up_after_light": up.light_after.mean(), "p_up_after": "%.2g" % wilcox(up.ctl_after, up.light_after),
                 "down_after_ctl": dn.ctl_after.mean(), "down_after_light": dn.light_after.mean(), "p_down_after": "%.2g" % wilcox(dn.ctl_after, dn.light_after),
                 "down_before_ctl": dn.ctl_before.mean(), "down_before_light": dn.light_before.mean(), "p_down_before": "%.2g" % wilcox(dn.ctl_before, dn.light_before)})
print("Expected: increased-AUC group (19 +- 1.9) 3.4 +- 0.8 -> 8.7 +- 1.9 spikes/s, P = 0.0001; reduced-AUC group (21 +- 2.1) 7.3 +- 1.5 -> 7.0 +- 1.4 "
      "(no change, P = 0.98); its baseline 0.9 +- 0.2 -> 2.6 +- 0.6 (P = 0.0002)")
print(pd.DataFrame(rows).round(3).T.to_string())
Expected: increased-AUC group (19 +- 1.9) 3.4 +- 0.8 -> 8.7 +- 1.9 spikes/s, P = 0.0001; reduced-AUC group (21 +- 2.1) 7.3 +- 1.5 -> 7.0 +- 1.4 (no change, P = 0.98); its baseline 0.9 +- 0.2 -> 2.6 +- 0.6 (P = 0.0002)
                         0        1
reading                  a        b
n_up                    23       17
n_down                  12       16
up_after_ctl         9.991   12.642
up_after_light      15.157   18.101
p_up_after         4.9e-05   0.0026
down_after_ctl      15.982   12.366
down_after_light    17.854   14.946
p_down_after          0.11    0.021
down_before_ctl      5.108    4.187
down_before_light   12.076   11.276
p_down_before       0.0049  0.00065
In [37]:
# Stability over seeds (the verifier's request): the AUC resampling and the random no-touch windows of reading (a) depend on the
# generator. 20 further seeds per reading; for each, the group sizes, the two groups' control touch-evoked rates (auc-weak-strong)
# and the three tests of auc-groups. The outcomes above use the fixed seeds 29 and 31.
srows = []
for reading in ("a", "b"):
    for seed in range(20):
        t = auc_table(reading, np.random.default_rng(1000 + seed)).dropna(subset=["ctl_auc", "light_auc"])
        up, dn = t[t.auc_change > 0], t[t.auc_change < 0]
        srows.append({"reading": reading, "seed": 1000 + seed, "n_up": len(up), "n_down": len(dn), "up_evoked": up.evoked_ctl.mean(),
                      "down_evoked": dn.evoked_ctl.mean(), "weak_strong_holds": bool(up.evoked_ctl.mean() < dn.evoked_ctl.mean()),
                      "p_up_after": wilcox(up.ctl_after, up.light_after), "p_down_after": wilcox(dn.ctl_after, dn.light_after),
                      "p_down_before": wilcox(dn.ctl_before, dn.light_before)})
st_ = pd.DataFrame(srows)
st_["groups_tests_hold"] = (st_.p_up_after < 0.05) & (st_.p_down_after >= 0.05) & (st_.p_down_before < 0.05)
print("Over 20 seeds per reading:")
print(st_.groupby("reading").agg(n_up_min=("n_up", "min"), n_up_max=("n_up", "max"), n_down_min=("n_down", "min"), n_down_max=("n_down", "max"),
                                  up_evoked_mean=("up_evoked", "mean"), down_evoked_mean=("down_evoked", "mean"),
                                  weak_strong_holds=("weak_strong_holds", "sum"), groups_tests_hold=("groups_tests_hold", "sum"),
                                  p_down_after_min=("p_down_after", "min"), p_down_after_max=("p_down_after", "max")).round(3).T.to_string())
print("Reading (b), the two groups' control touch-evoked rates per seed (up, down):",
      st_[st_.reading == "b"][["up_evoked", "down_evoked"]].round(2).values.tolist())
Over 20 seeds per reading:
reading                 a       b
n_up_min           15.000  15.000
n_up_max           22.000  17.000
n_down_min         11.000  16.000
n_down_max         19.000  18.000
up_evoked_mean      6.323   8.283
down_evoked_mean    9.119   8.048
weak_strong_holds  19.000   8.000
groups_tests_hold   9.000   0.000
p_down_after_min    0.005   0.015
p_down_after_max    0.241   0.045
Reading (b), the two groups' control touch-evoked rates per seed (up, down): [[8.14, 8.18], [8.4, 7.93], [8.4, 7.93], [8.14, 8.18], [8.14, 8.18], [8.33, 8.0], [8.53, 7.86], [8.14, 8.18], [8.4, 7.93], [8.4, 7.93], [8.14, 8.18], [8.4, 7.93], [8.31, 8.02], [8.14, 8.18], [8.14, 8.18], [8.4, 7.93], [8.31, 8.02], [8.31, 8.02], [8.31, 8.02], [8.14, 8.18]]

Seeds. The AUC resampling and the random no-touch windows of reading (a) depend on the generator. A reading is taken to match only when it matches under most of the 20 seeds, since an outcome that changes with the seed is not a reproduction; this rule is this attempt's, recorded as a deviation.

Result: not reproduced. With the fixed seeds the readings differ: under (a) the three tests hold (rise in the increased-AUC group, p = 4.9e-05; no change in the reduced-AUC group's touch response, p = 0.11; rise of its baseline, p = 0.0049) with group sizes (23 and 12) and rates outside tolerance, and under (b) the reduced-AUC group's touch response changes (p = 0.021). Over 20 further seeds, the three tests hold in 9 seeds under (a) and in none under (b), where the reduced-AUC group's touch response changes in every seed (p = 0.015 to 0.045). The partial result of (a) with seed 29 is the minority outcome, so neither reading matches the claim and it is not reproduced. Group sizes range from 15 to 22 (rising) and 11 to 19 (falling) over seeds.

behavior-false-alarms¶

With PV interneurons suppressed, most mice (6 of 8) are more likely to report touch with the pole out of reach (false alarms on no-lick trials).

In [38]:
rows = []
for r in l4:
    if line_of[r["subject"]] != "PV-IRES-Cre":
        continue
    d = light_design(r, "vS1")
    if d is None:
        continue
    tr = r["trials"]
    lit = np.zeros(len(tr), bool)
    lit[list(d["windows"])] = True
    nogo = tr.type.str.lower().to_numpy() == "nogo"
    cr = tr.response.to_numpy() == "CR"
    for cond, m in (("light", lit), ("ctl", d["ctl"])):
        rows.append({"subject": r["subject"], "path": r["path"], "cond": cond, "nogo": int((m & nogo).sum()), "correct": int((m & nogo & cr).sum())})
bt = pd.DataFrame(rows)

def chi(g):
    t = g.groupby("cond")[["nogo", "correct"]].sum()
    if not {"light", "ctl"} <= set(t.index):
        return pd.Series({"ctl_correct": np.nan})
    tab = np.array([[t.loc[c, "correct"], t.loc[c, "nogo"] - t.loc[c, "correct"]] for c in ("ctl", "light")])
    ok = tab.sum(axis=1).min() > 0 and tab.sum(axis=0).min() > 0
    p_y = stats.chi2_contingency(tab)[1] if ok else np.nan
    p_n = stats.chi2_contingency(tab, correction=False)[1] if ok else np.nan
    return pd.Series({"ctl_nogo": int(tab[0].sum()), "ctl_correct": tab[0, 0] / tab[0].sum(), "light_nogo": int(tab[1].sum()),
                      "light_correct": tab[1, 0] / tab[1].sum() if tab[1].sum() else np.nan, "p_yates": p_y, "p_no_correction": p_n})

pooled = bt.groupby("subject").apply(chi)
pooled["decrease_yates"] = (pooled.p_yates < 0.05) & (pooled.light_correct < pooled.ctl_correct)
pooled["decrease_plain"] = (pooled.p_no_correction < 0.05) & (pooled.light_correct < pooled.ctl_correct)
pooled["increase_yates"] = (pooled.p_yates < 0.05) & (pooled.light_correct > pooled.ctl_correct)
pooled["increase_plain"] = (pooled.p_no_correction < 0.05) & (pooled.light_correct > pooled.ctl_correct)
print("PV-IRES-Cre mice with the eNpHR light:", len(pooled), "(paper 8 tested). Reading (a), each mouse's trials pooled; correct = CR on no-lick trials:")
print(pooled.round(3).to_string())
print("Reading (a), mice with a significant decrease: (i) without the correction", int(pooled.decrease_plain.sum()), "; (ii) Yates",
      int(pooled.decrease_yates.sum()), "; significant increase: (i)", int(pooled.increase_plain.sum()), ", (ii)", int(pooled.increase_yates.sum()))
per = bt.groupby(["subject", "path"]).apply(chi).reset_index()
per["decrease"] = (per.p_yates < 0.05) & (per.light_correct < per.ctl_correct)
per["decrease_plain"] = (per.p_no_correction < 0.05) & (per.light_correct < per.ctl_correct)
ps = per.groupby("subject").agg(sessions=("path", "size"), any_decrease=("decrease", "any"), any_decrease_plain=("decrease_plain", "any"))
print("Reading (b), per session (file), a mouse counting when any session shows a significant decrease: (i) without the correction",
      int(ps.any_decrease_plain.sum()), "; (ii) Yates", int(ps.any_decrease.sum()), "of", len(ps))
print("Mice counted under (b)(i):", sorted(ps[ps.any_decrease_plain].index), "; sessions per mouse:", ps.sessions.to_dict())
print("Per session, without the correction:")
print(per[["subject", "ctl_nogo", "ctl_correct", "light_nogo", "light_correct", "p_no_correction", "p_yates"]].round(3).to_string(index=False))
print("Expected 6 of 8 mice (reproduced at 6 under a reading, partially at 5 to 7, not reproduced below 5)")
PV-IRES-Cre mice with the eNpHR light: 9 (paper 8 tested). Reading (a), each mouse's trials pooled; correct = CR on no-lick trials:
           ctl_nogo  ctl_correct  light_nogo  light_correct  p_yates  p_no_correction  decrease_yates  decrease_plain  increase_yates  increase_plain
subject                                                                                                                                              
anm244024     200.0        0.800       181.0          0.691    0.019            0.014            True            True           False           False
anm244025     116.0        0.638        85.0          0.671    0.741            0.631           False           False           False           False
anm244028     154.0        0.896        67.0          0.627    0.000            0.000            True            True           False           False
anm266945     340.0        0.568       188.0          0.521    0.350            0.305           False           False           False           False
anm266951       5.0        0.800         2.0          0.500    1.000            0.427           False           False           False           False
anm300169      59.0        0.915        18.0          0.722    0.083            0.033           False            True           False           False
anm300170      86.0        0.895        32.0          0.375    0.000            0.000            True            True           False           False
anm300171      77.0        0.792        66.0          0.879    0.247            0.167           False           False           False           False
anm300172     137.0        0.891        96.0          0.750    0.008            0.005            True            True           False           False
Reading (a), mice with a significant decrease: (i) without the correction 5 ; (ii) Yates 4 ; significant increase: (i) 0 , (ii) 0
Reading (b), per session (file), a mouse counting when any session shows a significant decrease: (i) without the correction 6 ; (ii) Yates 5 of 9
Mice counted under (b)(i): ['anm244024', 'anm244028', 'anm266945', 'anm300169', 'anm300170', 'anm300172'] ; sessions per mouse: {'anm244024': 9, 'anm244025': 5, 'anm244028': 5, 'anm266945': 11, 'anm266951': 1, 'anm300169': 3, 'anm300170': 3, 'anm300171': 4, 'anm300172': 6}
Per session, without the correction:
  subject  ctl_nogo  ctl_correct  light_nogo  light_correct  p_no_correction  p_yates
anm244024      26.0        0.962        25.0          0.960            0.977    1.000
anm244024      66.0        0.939        42.0          0.786            0.017    0.037
anm244024       8.0        0.500        19.0          0.316            0.365    0.639
anm244024      13.0        0.308        16.0          0.688            0.042    0.097
anm244024      23.0        0.739        11.0          0.636            0.538    0.831
anm244024      10.0        1.000        19.0          1.000              NaN      NaN
anm244024      16.0        0.562        12.0          0.250            0.098    0.205
anm244024      12.0        0.333        11.0          0.091            0.159    0.367
anm244024      26.0        0.962        26.0          0.808            0.083    0.193
anm244025       5.0        0.400         9.0          0.556            0.577    1.000
anm244025      33.0        0.364        14.0          0.429            0.675    0.928
anm244025      10.0        1.000         9.0          1.000              NaN      NaN
anm244025      29.0        0.621        24.0          0.625            0.974    1.000
anm244025      39.0        0.821        29.0          0.759            0.532    0.748
anm244028      25.0        0.920        10.0          0.700            0.093    0.252
anm244028      28.0        0.893        12.0          0.917            0.818    1.000
anm244028      20.0        0.850        10.0          0.700            0.333    0.628
anm244028      31.0        0.968        11.0          0.636            0.004    0.018
anm244028      50.0        0.860        24.0          0.417            0.000    0.000
anm266945      42.0        0.762        15.0          0.667            0.472    0.706
anm266945      11.0        0.364         7.0          0.143            0.308    0.631
anm266945      37.0        0.919        23.0          1.000            0.161    0.428
anm266945      66.0        0.636        39.0          0.333            0.003    0.005
anm266945      21.0        0.238         8.0          0.375            0.461    0.785
anm266945      31.0        0.548        15.0          0.333            0.171    0.292
anm266945      30.0        0.367        24.0          0.500            0.325    0.479
anm266945       4.0        0.250         2.0          0.000            0.439    1.000
anm266945      25.0        0.800        20.0          0.800            1.000    1.000
anm266945      20.0        0.250        10.0          0.400            0.398    0.673
anm266945      53.0        0.415        25.0          0.440            0.835    1.000
anm266951       5.0        0.800         2.0          0.500            0.427    1.000
anm300169      14.0        0.714         5.0          0.800            0.709    1.000
anm300169      14.0        0.929         3.0          0.333            0.014    0.105
anm300169      31.0        1.000        10.0          0.800            0.011    0.087
anm300170      44.0        0.795        20.0          0.050            0.000    0.000
anm300170      33.0        1.000         8.0          1.000              NaN      NaN
anm300170       9.0        1.000         4.0          0.750            0.118    0.665
anm300171      18.0        0.778        16.0          0.938            0.189    0.408
anm300171      15.0        0.800        15.0          0.933            0.283    0.591
anm300171      11.0        0.636        18.0          0.778            0.408    0.690
anm300171      33.0        0.848        17.0          0.882            0.744    1.000
anm300172      22.0        0.818        13.0          0.846            0.832    1.000
anm300172      18.0        1.000         3.0          1.000              NaN      NaN
anm300172      43.0        0.930        29.0          0.828            0.174    0.329
anm300172      17.0        0.941        17.0          0.588            0.015    0.043
anm300172      22.0        0.818        20.0          0.750            0.591    0.872
anm300172      15.0        0.800        14.0          0.643            0.344    0.596
Expected 6 of 8 mice (reproduced at 6 under a reading, partially at 5 to 7, not reproduced below 5)

Result: reproduced. Of 9 PV-IRES-Cre mice with the light (the paper tested 8), 6 show a significant decrease of no-lick performance under reading (b)(i), per session without the continuity correction, as the criterion asks (6 of 8). The other readings give 5 ((a)(i), trials pooled, no correction), 4 ((a)(ii), pooled, Yates) and 5 ((b)(ii), per session, Yates); no mouse shows a significant increase when its trials are pooled. Reading (b) counts a mouse when any of its sessions is significant, and mice have 1 to 11 sessions, so it gives more chances to a mouse with many sessions; one session of anm244024 shows a significant rise (p = 0.042) while another shows a fall. Before the answer of 2026-10-20 the claim was blocked because the readings with and without the correction disagreed; the criterion now names both, and the reading without it matches.

Blocked claims¶

exc-inhibition-timing: With L4 excitatory neurons depolarized near 0 mV to reveal inhibition, touch evokes strong, transient inhibition that follows excitation by 0.4 +- 0.8 ms at half maximum.

exc-inhibition-whisking: In L4 excitatory neurons held near 0 mV, inhibitory inputs occur spontaneously but are more prevalent at the onset of whisker movement; after whisking onset, excitation and inhibition increase gradually, with indistinguishable time courses, over about 100 ms.

vpm-photoinhibition: Photoactivating TRN axons in VPM of VGAT-ChR2-EYFP mice reduces thalamic activity to 17% of control.

rabies-vpm-input: Monosynaptic rabies tracing from L4 PV interneurons labels thalamic neurons mostly in VPM (>90%) and few in POM (<10%).

ion-cut-exc-no-touch, ion-cut-exc-whisking, ion-cut-exc-phase, ion-cut-fs-no-touch, sensory-input-drives-fs, ion-cut-fs-phase, ion-cut-behavior: after infraorbital nerve transection, L4 excitatory neurons do not depolarize after touch, their whisking depolarization and phase locking are reduced, touch does not evoke spikes in FS cells, their whisking activation is absent in most cells and their phase locking reduced, and mice perform at chance.

In [39]:
# What the files hold for the blocked claims (see the markdown above).
print("Files with a recording outside barrel cortex L4 (thalamic pipette multiunits of Fig. 6b,c would be):",
      int((meta.region != "barrel cortex").sum() + (meta.layer != "4").sum()), "of", len(meta), "L4 files; VPM files with light:",
      sum(r["has_light"] for r in vpm))
img = [p for r in recs for p in ([] if r["kind"] == "VPM" else r["general_fields"]) if "image" in p.lower()]
print("Files with an image or histology group under general/ (rabies tracing, Supp. Fig. 10):", len(img))
held = mode[(mode.from_trace == "whole-cell") & (mode.trials_near_0mV > 0)]
print("Whole-cell cells with trials held near 0 mV (the 6 E cells of Fig. 3 would be):", len(held), held[["path", "line", "trials_near_0mV"]]
      .assign(path=lambda d: d.path.str.split("/").str[1]).to_dict("records"), "; its class here:",
      cells.set_index("path").cls.get(held.path.iloc[0]) if len(held) else None, "; Supplementary Table 2 gives the one PV-ChR2 cell as FS")
# Descriptive only, not a test: the cells of the flagged mice, which may be among the ION-cut mice
fl = meas[meas.subject.isin(flagged) & (meas.cls != "VPM")]
fv = vm0[vm0.index.isin(fl.path)]
print(f"\nNot a test. Flagged mice {flagged}: {int((fl.cls == 'E').sum())} E cells ({int(((fl.cls == 'E') & (fl['mode'] == 'whole-cell')).sum())} whole-cell), "
      f"{int((fl.cls == 'FS').sum())} FS cells with a principal whisker. Whole-cell touch PSP amplitude mean {fv.amp_mv.mean():.2f} mV "
      f"(other E cells {vm0[~vm0.index.isin(fl.path) & vm0.index.isin(group('E').path)].amp_mv.mean():.2f}); whisking change (b) mean "
      f"{fv.whisk_epoch_change.mean():.2f} mV; Vm phase coupling mean {fv.vm_phase_strength.mean():.2f} mV")
ff = fl[fl.cls == "FS"]
if len(ff):
    print(f"   FS cells: spikes per touch mean {ff.spt.mean():.2f}; nonwhisking {ff['filtfilt:rate_nonwhisk'].mean():.2f}, whisking "
          f"{ff['filtfilt:rate_whisk'].mean():.2f} spikes/s; r2 {ff['filtfilt:r2'].round(2).tolist()}")
Files with a recording outside barrel cortex L4 (thalamic pipette multiunits of Fig. 6b,c would be): 0 of 130 L4 files; VPM files with light: 0
Files with an image or histology group under general/ (rabies tracing, Supp. Fig. 10): 0
Whole-cell cells with trials held near 0 mV (the 6 E cells of Fig. 3 would be): 1 [{'path': 'sub-anm250509_ses-20140817_behavior+icephys+ogen.nwb', 'line': 'PV-ChR2', 'trials_near_0mV': 13}] ; its class here: E ; Supplementary Table 2 gives the one PV-ChR2 cell as FS

Not a test. Flagged mice ['anm273341', 'anm275231', 'anm318530', 'anm318532']: 8 E cells (8 whole-cell), 2 FS cells with a principal whisker. Whole-cell touch PSP amplitude mean 0.35 mV (other E cells 5.54); whisking change (b) mean 0.39 mV; Vm phase coupling mean 0.11 mV
   FS cells: spikes per touch mean 0.09; nonwhisking 8.37, whisking 9.98 spikes/s; r2 [0.06, 0.43]

Result: blocked (data missing): exc-inhibition-timing and exc-inhibition-whisking. Only one whole-cell file has trials held near 0 mV, 13 trials of the PV-ChR2 cell, which Supplementary Table 2 counts as FS, and no other file pairs a whole-cell trace with a depolarizing current (data check); the 6 excitatory neurons recorded at rest and with depolarizing current are not in the dandiset.

Result: blocked (data missing): vpm-photoinhibition. No file holds a thalamic recording made with a pipette during light: the 130 single-cell files are all in barrel cortex layer 4, and the 18 VPM silicon probe files have no photostimulation series.

Result: blocked (data missing): rabies-vpm-input. The dandiset holds no histology or image data.

Result: blocked (metadata insufficient): the seven ION-cut claims. No file marks the ION-cut mice. The 4 flagged mice are consistent with them (their whole-cell excitatory cells have touch PSPs of 0.35 mV on average against 5.54 mV in the others, and their 2 FS cells 0.09 spikes per touch), but they were found by these very properties, so they cannot test the claims.

Summary¶

Claim Outcome Earlier outcome (000005-yu-2016-01)
behavior-touches partially reproduced
vpm-touch not reproduced reproduced
vpm-whisking partially reproduced reproduced
vpm-whisking-phase reproduced
exc-vm-touch-whisking partially reproduced
exc-threshold-touch-vs-whisking partially reproduced
exc-effective-reversal reproduced
exc-inhibition-timing blocked (data missing)
exc-inhibition-whisking blocked (data missing)
fs-touch-response blocked (methods underspecified)
fs-vm-touch-whisking not reproduced
fs-whisking-rate partially reproduced
fs-whisking-phase reproduced
fs-touch-and-whisking reproduced reproduced
exc-mainly-touch not reproduced not reproduced
vpm-photoinhibition blocked (data missing)
exc-thalamus-photoinhibition partially reproduced
thalamus-drives-fs blocked (other: too few cells) reproduced
rabies-vpm-input blocked (data missing)
ion-cut-exc-no-touch blocked (metadata insufficient)
ion-cut-exc-whisking blocked (metadata insufficient)
ion-cut-exc-phase blocked (metadata insufficient)
ion-cut-fs-no-touch blocked (metadata insufficient)
sensory-input-drives-fs blocked (metadata insufficient) blocked (metadata insufficient)
ion-cut-fs-phase blocked (metadata insufficient)
ion-cut-behavior blocked (metadata insufficient)
pv-enphr-suppression partially reproduced
fs-suppress-movement-spiking partially reproduced reproduced
fs-enhance-touch-selectivity partially reproduced not reproduced
exc-touch-spikes-enphr not reproduced
auc-weak-strong not reproduced
auc-groups not reproduced
behavior-false-alarms reproduced

The eight claims that keep their ids have new criteria, written from the full text, so their outcomes are not the same test as before; the attempt record says for each why the outcome differs.

After the first verification round three outcomes changed. fs-touch-response is blocked as methods underspecified, since its FS readings give different outcomes (two of three values within tolerance with the waveform boundary, one without it). auc-weak-strong and auc-groups are not reproduced: over 20 further seeds, reading (b) of auc-weak-strong holds in 8, and the tests of auc-groups hold in 9 seeds under reading (a) and in none under (b). The other outcomes hold under the added readings (O11 for every whisking-epoch claim, O10 and O11 for the whisking-onset claims) and the revised tag test.

A failure to reproduce here may be a failure of this attempt, not of the paper: see the reasoning and the differences from the paper's method.

How the analysis differs from the paper's. The files name no cell type and do not mark the ION-cut mice, so the cell classes are built by the claims file's rules (light tag, the waveform boundary read approximately from Supplementary Fig. 6, PV inactivation, spike width) and every group includes the cells of the ION-cut mice, which the paper left out of its control groups. The rules give 20 FS cells (17 with a principal whisker) against the paper's 26 control FS cells, and 102 excitatory cells (97 measured) against 85; 8 light-driven cells fall outside the boundary as measured here, and 26 VGAT-ChR2 cells have too few cortical light pulses to be tested and count as excitatory; among them are a whole-cell cell with 11,087 spikes 0.37 ms wide and a cell-attached cell with 16,350 spikes, which are likely FS, so the excitatory groups probably hold some FS cells. Only 3 of the paper's 4 PV cells can be found, and they are found by the inactivation the claim tests. The VPM files name one principal whisker per file. Spike detection and the spike-measure windows that the claims file leaves open are standard choices stated in the helper module; the cell-attached detection has 5% of intervals under 1.5 ms in the median E cell, and cell-attached E cells fire at several times the paper's rates; whole-cell E cells, whose detection is clean, also fire 0.70 spikes/s without whisking against the paper's 0.4. The light-driven test (a one-sided binomial test of spikes 0 to 5 ms after each pulse of the 5 and 10 Hz trains against the same 5 ms 50 ms earlier) and the waveform measures are this attempt's; the paper gives neither, and the claims file runs two thresholds and the boundary as readings. Trials of a whole-cell cell held near 0 mV are left out at rest. The derivative-based Vm latencies follow the criterion's rule, which single-sample excursions of dVm/dt and a one-sample transient at light onset cross. Where an outcome depends on a random seed (the AUC claims), a reading counts as matching only when it matches under most of 20 seeds, a rule of this attempt.

What was found about the dandiset. Every finding of the earlier attempt is still present on the same version: no cell types, no ION-cut marks, missing or partial genotypes, layer 4 for every cell, undescribed optogenetic conditions, the wrong recording mode in 24 session descriptions, a first-touch time divided by 1000, a sampling rate stated only by the timestamps, files without a principal whisker and one principal whisker per VPM file, and doubtful light wavelengths. New here: the injected current is stored in nA under a conversion to amperes that assumes uA, and 17 cell-attached files carry a constant stored current of about 1.9; the depolarized recordings of Fig. 3, the thalamic multiunits of Fig. 6b,c and the histology of Supplementary Fig. 10 are not in the dandiset; trains at the VPM site run at 50 Hz in 12 files where the Methods give 100 Hz; and the no-lick trial type is spelled two ways.