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

Paper. Yu J, Gutnisky DA, Hires SA, Svoboda K (2016). Layer 4 fast-spiking interneurons filter thalamocortical signals during active somatosensation. Nature Neuroscience 19:1647-1657. doi:10.1038/nn.4412. The full text is not open access, and no open copy was found (PubMed Central, Europe PMC, Unpaywall, OpenAlex, Semantic Scholar). This attempt used the abstract, the eight figure titles, and the supplementary information, which is public on the publisher's site: the legends of Supplementary Figures 1 to 14 and Supplementary Tables 1 and 2 (the mouse line of each mouse, and the cells per line, recording mode and manipulation). The claims were written from the abstract and figure titles before the supplement was read (in the first verification round). The supplement settles some things: FS cells are defined by spike waveform among light-driven cells in VGAT-ChR2 mice; spike-phase modulation is judged by the r² of a sinusoid fit (cut-off 0.25); the PV-Cre mice express eNpHR in PV cells; the VPM light is a 100 Hz train of 5 ms pulses; and Figure 7 tests the sensory origin of FS whisking responses by transecting the infraorbital nerve (ION). It gives no windows, thresholds or waveform boundaries, so the measures here remain choices made in this attempt. No number from the paper's main text is tested; the supplement's cell counts are compared with the files in the data check.

Dandiset. DANDI:000005, version 0.220126.1853. The paper predates the archive and cites no version. This is the only published version; the draft lists the same 148 assets and sizes, so the most recent published version was pinned.

What the dandiset holds. 18 sessions of silicon probe recordings in VPM (6 mice, sorted units), and 130 sessions with one L4 neuron each, recorded whole-cell or juxtacellularly (49 mice), all during a pole localization task with whisker tracking, and most L4 sessions with optogenetic stimulation. No file states the cell type of its L4 neuron, or whether the mouse had its ION transected, so FS interneurons and excitatory neurons are identified here from the data (see the data check).

Code. Neither the paper nor the dandiset links analysis code; none was found.

Outside input. Supplementary Table 1, read at run time from the supplement PDF (part of doi:10.1038/nn.4412), gives the mouse line of each mouse. It is used only in the data check.

Claim Source Tested
vpm-touch: VPM neurons fire in response to touch Abstract yes
vpm-whisking: VPM neurons fire in response to whisker movement Abstract yes
fs-touch-and-whisking: L4 FS interneurons respond to touch and whisker movement Abstract yes
exc-mainly-touch: L4 excitatory neurons respond mainly to touch Abstract yes
fs-suppress-movement-spiking: silencing FS cells increases movement-related spiking of excitatory cells Abstract, Fig. 8 title yes
fs-enhance-touch-selectivity: FS cells enhance the touch selectivity of excitatory cells Abstract, Fig. 8 title yes
thalamus-drives-fs: thalamic activity drives L4 FS interneurons Fig. 6 title yes
sensory-input-drives-fs: FS movement responses come from sensory input, not corollary discharge Abstract, Fig. 7 title, Supp. Figs. 6, 11, 12 blocked (metadata insufficient)
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"]
print(f"loaded {len(recs)} files from dandiset {yu2016.DANDISET_ID} version {yu2016.DANDISET_VERSION} "
      f"in {time.time() - started:.0f} s")
loaded 148 files from dandiset 000005 version 0.220126.1853 in 156 s

Data check¶

No random numbers are used anywhere in this notebook. Each check of the metadata against the values is stated under its output.

In [2]:
files = pd.DataFrame([{"kind": r["kind"], "subject": r["subject"], "genotype": r["genotype"] or "(none)",
                       "path": r["path"], "trials": len(r["trials"])} for r in recs])
vc = yu2016.versions_check()
print("Published versions:", vc["published"], "; assets in the pinned version:", vc["pinned_assets"],
      "; in the draft:", vc["draft_assets"], "; same paths and sizes:", vc["draft_same_paths_and_sizes"])
print("Files by kind:", files.kind.value_counts().to_dict())
print("Subjects:", files.subject.nunique(), "; VPM:", files[files.kind == "VPM"].subject.nunique(),
      "; L4:", files[files.kind == "L4"].subject.nunique(),
      "; in both:", len(set(files[files.kind == "VPM"].subject) & set(files[files.kind == "L4"].subject)))
print("Sessions (files) per subject: median", files.groupby("subject").size().median(),
      ", max", files.groupby("subject").size().max())
print("\nL4 files by genotype:")
print(files[files.kind == "L4"].genotype.value_counts().to_string())
units = pd.DataFrame([{"path": r["path"], "subject": r["subject"], "unit": u["unit"], "desc": u["desc"],
                       "spikes": len(u["spikes"]), "location": r["location"][0] if r["location"] else ""}
                      for r in vpm for u in r["units"]])
print(f"\nVPM: {len(units)} units in {len(vpm)} files; units per file:",
      units.groupby("path").size().value_counts().sort_index().to_dict())
print("Unit descriptions start with:", units.desc.str.split(".").str[0].value_counts().to_dict())
print("Electrode group locations:", units.location.str.extract(r"brain_subregion: ([^;]*)")[0].value_counts().to_dict())
print("Unit spike time resolution (s):",
      sorted({float(np.round(np.min(np.diff(np.unique(u["spikes"]))), 4)) for r in vpm for u in r["units"]}))
print("Trials: VPM", int(files[files.kind == "VPM"].trials.sum()), ", L4", int(files[files.kind == "L4"].trials.sum()))
Published versions: ['0.220126.1853'] ; assets in the pinned version: 148 ; in the draft: 148 ; same paths and sizes: True
Files by kind: {'L4': 130, 'VPM': 18}
Subjects: 55 ; VPM: 6 ; L4: 49 ; in both: 0
Sessions (files) per subject: median 2.0 , max 11

L4 files by genotype:
genotype
VGAT-ChR2-EYFP    66
PV-Cre            48
C57BL6             8
Ai32               5
(none)             3

VPM: 29 units in 18 files; units per file: {1: 12, 2: 3, 3: 2, 5: 1}
Unit descriptions start with: {'thalamic': 29}
Electrode group locations: {'VPM': 29}
Unit spike time resolution (s): [0.001]
Trials: VPM 7656 , L4 11211

0.220126.1853 is the only published version, and the draft holds the same 148 asset paths and sizes. The archive's summary gives 55 subjects, which matches the files. The VPM recordings come from 6 mice and the L4 recordings from 49 others. The units are all described as "thalamic" and their electrode groups as VPM. Spike times of VPM units are stored at 1 ms resolution.

In [3]:
# What the files say about each L4 recording, and the cells per mouse line against Supplementary
# Tables 1 and 2 of the paper (the mouse line of each mouse; the cells per line and recording mode).
meta = pd.DataFrame([{"path": r["path"], "subject": r["subject"], "genotype": r["genotype"] or "(none)", "mode": r["mode"], "label": r["label_mode"],
                      "general_fields": ",".join(r["general_fields"]), "subject_fields": ",".join(r["subject_fields"]),
                      "region": r["ic_location"].split(";")[0].replace("brain_region:", "").strip(),
                      "layer": r["ic_location"].split("cortical_layer:")[1].split(";")[0].strip(),
                      "dv_mm": float(r["ic_location"].split("coordinate_dv:")[1].split(";")[0])} for r in l4])
print("Fields under general/ in the L4 files:", meta.general_fields.value_counts().to_dict())
print("Fields under general/subject:", meta.subject_fields.value_counts().to_dict())
print("Electrode location: region", meta.region.value_counts().to_dict(), "; cortical layer", meta.layer.value_counts().to_dict(),
      f"; depth (coordinate_dv) {meta.dv_mm.min():.2f} to {meta.dv_mm.max():.2f} mm")
print("Last part of the session descriptions:", pd.Series([r["session_description"].split(";")[-1] for r in l4]).value_counts().to_dict())
# Every text value of each file (session and experiment description, keywords, the optogenetics sites, the
# subject and every other string under general/), searched as whole words
terms = {"ION": r"\bION\b", "infraorbital": r"(?i)infraorbital", "nerve": r"(?i)\bnerves?\b", "transect": r"(?i)transect",
         "lesion": r"(?i)lesion", "cut": r"(?i)\bcut\b", "surgery": r"(?i)surg", "sham": r"(?i)\bsham\b", "control": r"(?i)\bcontrol\b"}
print("Characters of metadata text per L4 file: median", int(np.median([len(r["metadata_text"]) for r in l4])),
      "; files whose metadata text contains each term:",
      {k: sum(bool(re.search(v, r["metadata_text"])) for r in l4) for k, v in terms.items()})
print("Keyword values:", pd.Series([re.search(r"barrel cortex thalamus.*?perturbations", r["metadata_text"]).group(0)
                                    if re.search(r"barrel cortex thalamus.*?perturbations", r["metadata_text"]) else "(other)"
                                    for r in l4]).value_counts().to_dict())

lines = yu2016.supplement_mouse_lines()
print(f"\nSupplementary Table 1 lists {len(lines)} mice; of the {files.subject.nunique()} mice of the dandiset, "
      f"{files.subject.drop_duplicates().isin(lines.subject).sum()} are in it.")
fl = files.merge(lines, on="subject", how="left")
print("Files by genotype in the files against the mouse line in Supplementary Table 1:")
print(pd.crosstab([fl.kind, fl.genotype], fl.line.fillna("(not listed)")).to_string())
print("Mice without a genotype in their files, or given as Ai32:",
      fl[fl.genotype.isin(["(none)", "Ai32"])].drop_duplicates("subject").groupby(["genotype", "line"]).subject.apply(list).to_dict())
meta = meta.merge(lines, on="subject", how="left")
# Cells per mouse line and recording mode in Supplementary Table 2 (whole-cell; cell-attached), all cell types together
paper_counts = pd.DataFrame({"line": ["VGAT-ChR2-EYFP", "PV-IRES-Cre", "PV-IRES-Cre x Ai32", "C57BL/6", "PV-ChR2", "SOM-IRES-Cre x Ai32"],
                             "paper_whole_cell": [23 + 5 + 9, 5, 1, 7 + 1, 1, 1], "paper_cell_attached": [6 + 14 + 10, 38 + 4, 0, 0, 0, 4]})
ours = pd.crosstab(meta.line, meta["mode"]).reindex(paper_counts.line).fillna(0).astype(int)
paper_counts["files_whole_cell"] = ours["whole-cell"].to_numpy()
paper_counts["files_juxtacellular"] = ours["juxtacellular"].to_numpy()
print("\nL4 cells per mouse line: Supplementary Table 2 against the files (mode read from the trace):")
print(paper_counts.to_string(index=False))
print("Totals: paper", int(paper_counts.paper_whole_cell.sum() + paper_counts.paper_cell_attached.sum()), "; files", len(l4))
print("L4 cells by mouse line and session date (year):")
print(pd.crosstab(meta.line, meta.path.str.split("_ses-").str[1].str[:4]).to_string())
print("\nL4 cells by mouse line, mode in the session description (rows) and mode read from the trace (columns):")
print(pd.crosstab([meta.line, meta.label], meta["mode"]).to_string())
Fields under general/ in the L4 files: {'devices,experiment_description,experimenter,institution,intracellular_ephys,keywords,optogenetics,related_publications,subject': 121, 'devices,experiment_description,experimenter,institution,intracellular_ephys,keywords,related_publications,subject': 9}
Fields under general/subject: {'genotype,sex,species,subject_id': 130}
Electrode location: region {'barrel cortex': 130} ; cortical layer {'4': 130} ; depth (coordinate_dv) 0.42 to 0.62 mm
Last part of the session descriptions: {'whole-cell, acute': 79, 'juxtacellular, acute': 51}
Characters of metadata text per L4 file: median 1186 ; files whose metadata text contains each term: {'ION': 0, 'infraorbital': 0, 'nerve': 0, 'transect': 0, 'lesion': 0, 'cut': 0, 'surgery': 0, 'sham': 0, 'control': 0}
Keyword values: {'barrel cortex thalamus whiskers extracellular electrophysiology intracellular electrophysiology optogenetic perturbations': 130}
Supplementary Table 1 lists 55 mice; of the 55 mice of the dandiset, 55 are in it.
Files by genotype in the files against the mouse line in Supplementary Table 1:
line                 C57BL/6  PV-ChR2  PV-IRES-Cre  PV-IRES-Cre x Ai32  SOM-IRES-Cre x Ai32  VGAT-ChR2-EYFP
kind genotype                                                                                              
L4   (none)                0        1            0                   1                    0               1
     Ai32                  0        0            0                   0                    5               0
     C57BL6                8        0            0                   0                    0               0
     PV-Cre                0        0           48                   0                    0               0
     VGAT-ChR2-EYFP        0        0            0                   0                    0              66
VPM  (none)                0        0            0                   0                    0              18
Mice without a genotype in their files, or given as Ai32: {('(none)', 'PV-ChR2'): ['anm250509'], ('(none)', 'PV-IRES-Cre x Ai32'): ['anm223517'], ('(none)', 'VGAT-ChR2-EYFP'): ['anm184389', 'anm186997', 'anm199549', 'anm199551', 'anm199552', 'anm203464', 'anm260074'], ('Ai32', 'SOM-IRES-Cre x Ai32'): ['anm308247', 'anm324650', 'anm337495', 'anm337496', 'anm342684']}

L4 cells per mouse line: Supplementary Table 2 against the files (mode read from the trace):
               line  paper_whole_cell  paper_cell_attached  files_whole_cell  files_juxtacellular
     VGAT-ChR2-EYFP                37                   30                37                   30
        PV-IRES-Cre                 5                   42                 9                   39
 PV-IRES-Cre x Ai32                 1                    0                 1                    0
            C57BL/6                 8                    0                 8                    0
            PV-ChR2                 1                    0                 1                    0
SOM-IRES-Cre x Ai32                 1                    4                 1                    4
Totals: paper 129 ; files 130
L4 cells by mouse line and session date (year):
path                 2013  2014  2015  2016
line                                       
C57BL/6                 3     5     0     0
PV-ChR2                 0     1     0     0
PV-IRES-Cre             1    31    16     0
PV-IRES-Cre x Ai32      0     1     0     0
SOM-IRES-Cre x Ai32     0     0     1     4
VGAT-ChR2-EYFP          4    18    45     0

L4 cells by mouse line, mode in the session description (rows) and mode read from the trace (columns):
mode                               juxtacellular  whole-cell
line                label                                   
C57BL/6             whole-cell                 0           8
PV-ChR2             whole-cell                 0           1
PV-IRES-Cre         juxtacellular             16           1
                    whole-cell                23           8
PV-IRES-Cre x Ai32  whole-cell                 0           1
SOM-IRES-Cre x Ai32 juxtacellular              4           0
                    whole-cell                 0           1
VGAT-ChR2-EYFP      juxtacellular             30           0
                    whole-cell                 0          37

No L4 file records a surgery, lesion, virus, opsin or notes. A search of every text value of each file (session and experiment description, keywords, optogenetics sites, subject and the rest of general/) finds none of ION, infraorbital, nerve, transection, lesion, cut, surgery, sham or control, and every file has the same keywords. The fields under general/ are the same in every file (9 files have no optogenetics group), the subject has only genotype, sex, species and ID, and every electrode location reads barrel cortex, layer 4, at 0.42 to 0.62 mm depth. The files therefore cannot say which cells come from ION-transected mice, and they put every cell in L4 (Supp. Fig. 6 of the paper includes some L5 cells, which cannot be found here).

All 55 mice are in Supplementary Table 1. Where the files give a genotype it agrees with the mouse line, but it is missing for the 6 VPM mice (VGAT-ChR2-EYFP in the table) and for 3 L4 mice (one each of VGAT-ChR2-EYFP, PV-IRES-Cre x Ai32 and PV-ChR2), and the SOM-IRES-Cre x Ai32 mice are given as Ai32. The cells per line and recording mode match Supplementary Table 2 for every line except PV-IRES-Cre: 48 files against 47 cells, and 9 whole-cell by the trace against 5 in the table. All 24 files whose session description disagrees with the trace are PV-IRES-Cre files. For VGAT-ChR2-EYFP the table's 37 whole-cell and 30 cell-attached cells include 9 whole-cell excitatory cells and 10 cell-attached FS cells from ION-transected mice; they are in the dandiset, but not marked.

In [4]:
mode = pd.DataFrame([{"path": r["path"], "genotype": r["genotype"], "label": r["label_mode"], "from_data": r["mode"],
                      "vm_median": r["vm_median"], "vm_hz": r["vm_hz"], "trial_s": r["vm_trial_s"],
                      "spikes": len(r["spikes"]), "fwhm_ms": r["fwhm_ms"]} for r in l4])
print("Recording mode in session_description against the mode read from the trace "
      "(whole-cell when the median trace is below -20 mV):")
print(pd.crosstab(mode.label, mode.from_data).to_string())
print("\nMedian of the trace (stored values, unit 'volts' with conversion 0.001, so mV):")
print(mode.groupby("from_data").vm_median.describe().round(1).to_string())
print("\nTrace sampling rate (Hz) from the timestamps, and trial length of the trace (s):")
print(mode.groupby(["vm_hz", "trial_s"]).size().to_string())
print("First file at 20 kHz (session date):", min(p.split("_ses-")[1][:8] for p in mode[mode.vm_hz == 20000].path))
print("Last file at 10 kHz (session date):", max(p.split("_ses-")[1][:8] for p in mode[mode.vm_hz == 10000].path))
ts = pd.DataFrame([{"outside_trials": r["vm_outside_trials"], "block_start_offset_ms": r["vm_block_first_rel"] * 1000,
                    "irregular_steps": r["vm_dt_irregular"]} for r in l4])
print("\nTrace samples outside every trial:", int(ts.outside_trials.sum()),
      "; largest offset of a trial's first sample from the trial start (ms):", round(ts.block_start_offset_ms.max(), 2),
      "; steps within a trial that differ from 1/rate:", int(ts.irregular_steps.sum()))
print("Files with every trace distinct (hash of the whole trace):", mode.shape[0] == len({r['vm_hash'] for r in l4}))
Recording mode in session_description against the mode read from the trace (whole-cell when the median trace is below -20 mV):
from_data      juxtacellular  whole-cell
label                                   
juxtacellular             50           1
whole-cell                23          56

Median of the trace (stored values, unit 'volts' with conversion 0.001, so mV):
               count  mean  std   min   25%   50%   75%   max
from_data                                                    
juxtacellular   73.0   1.8  5.2 -16.9   0.1   2.3   4.4  13.0
whole-cell      57.0 -60.8  9.1 -76.8 -67.4 -62.5 -56.0 -39.2

Trace sampling rate (Hz) from the timestamps, and trial length of the trace (s):
vm_hz    trial_s
10000.0  5.2000     29
         5.4999      1
         5.5000     62
20000.0  5.9000     38
First file at 20 kHz (session date): 20150827
Last file at 10 kHz (session date): 20150320

Trace samples outside every trial: 0 ; largest offset of a trial's first sample from the trial start (ms): 4.76 ; steps within a trial that differ from 1/rate: 0
Files with every trace distinct (hash of the whole trace): True

The session description says "whole-cell" for 79 files and "juxtacellular" for 51, but the traces disagree for 24 of them: 23 files described as whole-cell have a trace that sits near 0 mV with small extracellular spikes, as juxtacellular (cell-attached) recordings do, and one described as juxtacellular sits near -65 mV. The mode read from the trace is used below. The stored trace values are in mV (conversion 0.001 to volts), which the whole-cell resting potentials confirm. The trace is sampled at 10 kHz (5.2 or 5.5 s per trial) in files up to March 2015 and at 20 kHz (5.9 s per trial) from late August 2015 on; the files do not state the rate except through their timestamps, which are regular within trials and start at the trial start.

In [5]:
wh = []
for r in recs:
    for name, w in r["whiskers"].items():
        row = {"kind": r["kind"], "path": r["path"], "whisker": name, "principal": w["principal"],
               "touches": len(w["touch_on"])}
        if w["principal"]:
            row.update(dt_ms=w["dt_median"] * 1000, angle_min=w["angle_range"][0], angle_max=w["angle_range"][1],
                       angle_nan=w["angle_nan"])
        wh.append(row)
wh = pd.DataFrame(wh)
p = wh[wh.principal]
print("Files with a whisker named principal:", p.path.nunique(), "of", len(recs),
      "; files without one:", sorted(set(wh.path) - set(p.path)))
print("Whisker sampling interval (ms):", p.dt_ms.round(3).unique().tolist())
span = pd.Series([round(float(np.median(np.bincount(w["trial"][w["trial"] >= 0])[np.bincount(w["trial"][w["trial"] >= 0]) > 0]))
                        * w["dt_median"], 2)
                  for r in recs for w in r["whiskers"].values() if w["principal"]])
print("Whisker samples per trial times the interval (s), files by value:", span.value_counts().sort_index().to_dict())
print("Principal whisker angle (degrees): min", round(p.angle_min.min(), 1), ", max", round(p.angle_max.max(), 1),
      "; median fraction of NaN samples", round(p.angle_nan.median(), 4))
print("Touch onsets on the principal whisker per file: median", int(p.touches.median()), ", range",
      int(p.touches.min()), "to", int(p.touches.max()))

# The trial table's first_touch_time against the first touch onset of the principal whisker
rows = []
for r in recs:
    if r["principal"] is None:
        continue
    w = r["whiskers"][r["principal"]]
    on_t = w["t"][w["touch_on"]]
    tr = r["trials"]
    for i, (s, e, ft) in enumerate(zip(tr.start, tr.stop, tr.first_touch_time)):
        x = on_t[(on_t >= s) & (on_t < e)]
        if len(x) and np.isfinite(ft):
            rows.append({"kind": r["kind"], "table_rel": ft - s, "series_rel": x[0] - s})
ft = pd.DataFrame(rows)
for kind, g in ft.groupby("kind"):
    print(f"\n{kind}: {len(g)} trials. first_touch_time minus trial start: median {g.table_rel.median():.4f} s; "
          f"first touch onset in the series: median {g.series_rel.median():.3f} s; "
          f"fraction where the table value equals the series value: {np.mean(np.abs(g.table_rel - g.series_rel) < 0.002):.3f}; "
          f"fraction where the table value x 1000 is within 2 ms of it: {np.mean(np.abs(g.table_rel * 1000 - g.series_rel) < 0.002):.3f}")
Files with a whisker named principal: 140 of 148 ; files without one: ['sub-anm230103/sub-anm230103_ses-20140131_behavior+icephys+ogen.nwb', 'sub-anm266945/sub-anm266945_ses-20141222_obj-ianjb9_behavior+icephys+ogen.nwb', 'sub-anm281589/sub-anm281589_ses-20150320_behavior+icephys+ogen.nwb', 'sub-anm300172/sub-anm300172_ses-20150908_obj-n5zk5z_behavior+icephys+ogen.nwb', 'sub-anm318530/sub-anm318530_ses-20151128_behavior+icephys+ogen.nwb', 'sub-anm318532/sub-anm318532_ses-20151125_obj-1kqnryo_behavior+icephys+ogen.nwb', 'sub-anm324650/sub-anm324650_ses-20160422_behavior+icephys+ogen.nwb', 'sub-anm337496/sub-anm337496_ses-20160524_behavior+icephys+ogen.nwb']
Whisker sampling interval (ms): [1.0]
Whisker samples per trial times the interval (s), files by value: {5.0: 140}
Principal whisker angle (degrees): min -98.4 , max 94.4 ; median fraction of NaN samples 0.0
Touch onsets on the principal whisker per file: median 354 , range 0 to 4265

L4: 5627 trials. first_touch_time minus trial start: median 0.0014 s; first touch onset in the series: median 1.373 s; fraction where the table value equals the series value: 0.000; fraction where the table value x 1000 is within 2 ms of it: 0.956

VPM: 3154 trials. first_touch_time minus trial start: median 1.3240 s; first touch onset in the series: median 1.324 s; fraction where the table value equals the series value: 1.000; fraction where the table value x 1000 is within 2 ms of it: 0.000

The whisker variables are sampled at 1 kHz (5 s per trial), in degrees with a plausible range. In the VPM files, first_touch_time is the first touch onset of the series. In the L4 files it is not: it lies a few ms after the trial start, and multiplied by 1000 its offset from the trial start matches the first touch onset in most trials, so the latency was divided by 1000 once too often when the files were made. The analysis uses the touch onset series only. Files without a named principal whisker are left out of every measure that needs whisking or touch.

In [6]:
light = []
for r in l4:
    tr = r["trials"]
    for site, info in r["sites"].items():
        w = yu2016.light_windows(r, site)
        lt = r["light"][site]
        modes = tr.photo_stim_mode.to_numpy()[w.trial.to_numpy(int)] if len(w) else []
        light.append({"genotype": r["genotype"], "site_region": info["region"], "lambda_nm": info["lambda_nm"],
                      "power_max": lt["power_max"], "lit_trials": len(w),
                      "isolated_pulses": len(yu2016.isolated_pulses(r, site)),
                      "median_pulses_per_lit_trial": w.n_pulses.median() if len(w) else 0,
                      "modes": ",".join(sorted(set(modes)))})
light = pd.DataFrame(light)
print("Photostimulation sites, by genotype and labelled region and wavelength:")
print(light.groupby(["genotype", "site_region", "lambda_nm"]).agg(
    files=("lit_trials", "size"), files_with_light=("lit_trials", lambda x: int((x > 0).sum())),
    lit_trials=("lit_trials", "sum"), isolated_pulses=("isolated_pulses", "sum"),
    pulses_per_trial=("median_pulses_per_lit_trial", "median"), power_max=("power_max", "max")).to_string())
print("\nphoto_stim_mode values of the lit trials of each site region:")
print(light[light.lit_trials > 0].groupby("site_region").modes.value_counts().to_string())
print("Trial table: photo_stim_mode has an empty description; photo_stim_power is NaN in",
      sum(int(r["trials"].photo_stim_power.isna().all()) for r in l4), "of", len(l4), "L4 files")
Photostimulation sites, by genotype and labelled region and wavelength:
                                      files  files_with_light  lit_trials  isolated_pulses  pulses_per_trial  power_max
genotype       site_region lambda_nm                                                                                   
               VPM         594.0          2                 2          76                0             250.0       15.0
               VPM and vS1 473.0          1                 1           6                0             262.5       15.0
               vS1         594.0          1                 0           0                0               0.0        0.0
Ai32           vM1         473.0          3                 3          54                0             100.0       15.0
               vS1         594.0          5                 3          16              100               5.0       15.0
PV-Cre         vS1         594.0         47                47        1490                0               1.0        3.0
VGAT-ChR2-EYFP VPM         594.0         33                31         742               32             250.0       15.0
               VPM and vS1 473.0         23                20         113              689              12.0       15.0
               vM1         473.0         25                23         360               25             100.0       15.0
               vS1         594.0         33                29         431             1449              10.0       15.0

photo_stim_mode values of the lit trials of each site region:
site_region  modes                            
VPM          shift                                21
             5ms_pulse                            10
             5ms_pulse,constant                    1
             constant_shift,shift                  1
VPM and vS1  5ms_pulse                            19
             5ms_pulse,constant,constant_shift     1
             5ms_pulse,constant                    1
vM1          5ms_pulse                            18
             shift                                 7
             5ms_grid,5ms_pulse                    1
vS1          constant                             47
             shift                                17
             5ms_pulse                            14
             shift,shiftgrid                       1
Trial table: photo_stim_mode has an empty description; photo_stim_power is NaN in 130 of 130 L4 files

The trial table's photo_stim_mode has no description, and its values do not map to one kind of light: in early VGAT-ChR2 files the VPM site gives pulse trains under the label 5ms_pulse, in later ones under shift. The light epochs are therefore read from the photostimulation series themselves. Three kinds of light are used below: isolated 5 ms pulses (for tagging), long epochs at the vS1 site in PV-Cre mice, and pulse trains at the VPM site in VGAT-ChR2 mice. What each light does is not stated in the files; the claims say what is assumed.

In [7]:
# Alignment of spikes and behaviour. Spikes are compared with the touch onsets and whisking phase
# of their own trial, and with those of the next trial (spikes moved to the next trial at the same
# time after its start). With the true alignment touch responses and phase locking should be
# larger.
rows = []
for r in recs:
    if r["principal"] is None:
        continue
    cells = [(u["unit"], u["spikes"]) for u in r["units"]] if r["kind"] == "VPM" else [(0, r["spikes"])]
    for unit, sp in cells:
        if len(sp) < yu2016.MIN_SPIKES:
            continue
        a = yu2016.response_measures(sp, r)
        b = yu2016.response_measures(yu2016.shift_spikes(sp, r["trials"]), r)
        rows.append({"kind": r["kind"], "path": r["path"], "unit": unit,
                     "touch_true": a["touch_resp_hz"], "touch_shift": b["touch_resp_hz"],
                     "amp_true": a["whisk_amp_raw_hz"], "amp_shift": b["whisk_amp_raw_hz"],
                     "vs_true": a["vector_strength"], "vs_shift": b["vector_strength"],
                     "n_whisk_true": a["n_whisk_spikes"], "n_whisk_shift": b["n_whisk_spikes"]})
align = pd.DataFrame(rows)
for kind, g in align.groupby("kind"):
    t = g.dropna(subset=["touch_true", "touch_shift"])
    v = g[(g.n_whisk_true >= 20) & (g.n_whisk_shift >= 20)]
    print(f"{kind}: {len(t)} cells or units. Touch response (Hz), median true {t.touch_true.median():.2f}, "
          f"shifted {t.touch_shift.median():.2f}; true larger in {(t.touch_true > t.touch_shift).sum()} "
          f"(Wilcoxon p = {stats.wilcoxon(t.touch_true, t.touch_shift).pvalue:.2g}). "
          f"Whisking phase modulation (Hz, occupancy corrected, {len(v)} with 20 whisking spikes), median true "
          f"{v.amp_true.median():.3f}, shifted {v.amp_shift.median():.3f} (p = {stats.wilcoxon(v.amp_true, v.amp_shift).pvalue:.2g}); "
          f"vector strength of spike phases, true {v.vs_true.median():.3f}, shifted {v.vs_shift.median():.3f}.")
bad = align[(align.touch_true <= align.touch_shift) & (align.amp_true <= align.amp_shift)]
print("Cells or units where neither the touch response nor the phase modulation is larger with the true alignment:",
      len(bad), "of", len(align))
print(bad[["kind", "path", "unit"]].to_string(index=False))
L4: 95 cells or units. Touch response (Hz), median true 8.40, shifted 0.34; true larger in 78 (Wilcoxon p = 9e-13). Whisking phase modulation (Hz, occupancy corrected, 84 with 20 whisking spikes), median true 2.160, shifted 0.415 (p = 1.3e-12); vector strength of spike phases, true 0.167, shifted 0.084.
VPM: 29 cells or units. Touch response (Hz), median true 6.91, shifted 0.00; true larger in 23 (Wilcoxon p = 1.8e-05). Whisking phase modulation (Hz, occupancy corrected, 29 with 20 whisking spikes), median true 1.236, shifted 0.283 (p = 9.3e-08); vector strength of spike phases, true 0.126, shifted 0.099.
Cells or units where neither the touch response nor the phase modulation is larger with the true alignment: 3 of 124
kind                                                                           path  unit
  L4 sub-anm266945/sub-anm266945_ses-20141220_obj-1g7fwne_behavior+icephys+ogen.nwb     0
  L4             sub-anm273341/sub-anm273341_ses-20150308_behavior+icephys+ogen.nwb     0
  L4  sub-anm300170/sub-anm300170_ses-20150918_obj-1c4fz8_behavior+icephys+ogen.nwb     0

For both VPM units and L4 cells the touch response and the whisking phase locking are larger with the spikes in their own trial than moved to the next trial, so spikes and behaviour share a clock in the data as a whole. This cannot be shown for a cell that responds to neither; such cells are counted in the last line, and their alignment is consistent with the data, not shown.

In [8]:
# Cell types. FS (proxy): tagged by isolated 5 ms light pulses in VGAT-ChR2 mice.
# Excitatory (proxy): whole-cell, action potentials at least EXC_MIN_FWHM_MS wide, not tagged.
def classify(r, min_diff=yu2016.TAG_MIN_DIFF, min_fwhm=yu2016.EXC_MIN_FWHM_MS):
    best = None
    for site, info in r["sites"].items():
        lr = yu2016.light_pulse_response(r, site)
        if lr["n_pulses"] >= yu2016.TAG_MIN_PULSES:
            lr["site"] = info["region"]
            if best is None or lr["p_evoked"] - lr["p_base"] > best["p_evoked"] - best["p_base"]:
                best = lr
    tagged = best is not None and best["p_evoked"] - best["p_base"] >= min_diff and best["p_value"] < yu2016.TAG_MAX_P
    if r["genotype"] == "VGAT-ChR2-EYFP" and tagged:
        cls = "FS"
    elif r["mode"] == "whole-cell" and r["fwhm_ms"] >= min_fwhm and not tagged:
        cls = "EXC"
    else:
        cls = "other"
    out = {"path": r["path"], "subject": r["subject"], "genotype": r["genotype"], "mode": r["mode"],
           "spikes": len(r["spikes"]), "fwhm_ms": r["fwhm_ms"], "tag_tested": best is not None, "tagged": tagged, "cls": cls}
    if best is not None:
        out.update(tag_site=best["site"], pulses=best["n_pulses"], p_evoked=best["p_evoked"], p_base=best["p_base"],
                   tag_p=best["p_value"], latency_ms=best["median_latency_ms"])
    return out

cl = pd.DataFrame([classify(r) for r in l4])
print("Cells by genotype, mode and class:")
print(pd.crosstab([cl.genotype.replace("", "(none)"), cl["mode"]], cl.cls).to_string())
vg = cl[(cl.genotype == "VGAT-ChR2-EYFP") & cl.tag_tested]
print(f"\nVGAT-ChR2 cells tested with isolated pulses: {len(vg)}; tagged {int(vg.tagged.sum())}")
print("Tagged cells: evoked spike probability median", round(vg[vg.tagged].p_evoked.median(), 2), "(range",
      round(vg[vg.tagged].p_evoked.min(), 2), "to", round(vg[vg.tagged].p_evoked.max(), 2), "), baseline median",
      round(vg[vg.tagged].p_base.median(), 2), ", first-spike latency median", round(vg[vg.tagged].latency_ms.median(), 1),
      "ms (range", round(vg[vg.tagged].latency_ms.min(), 1), "to", round(vg[vg.tagged].latency_ms.max(), 1), ")")
un = vg[~vg.tagged]
print("Untagged tested cells with evoked minus baseline probability of 0.2 or more (too few pulses or p >= 0.001):")
print(un[(un.p_evoked - un.p_base) >= 0.2][["path", "mode", "pulses", "p_evoked", "p_base", "tag_p", "latency_ms"]].round(4).to_string(index=False))
print("Tagged cells by mode:", vg[vg.tagged]["mode"].value_counts().to_dict(), "; by subject:",
      vg[vg.tagged].subject.value_counts().to_dict())
wc = cl[(cl["mode"] == "whole-cell")]
print("\nWhole-cell action potential width at half maximum (ms), cells with at least 5 spikes:")
print(wc.dropna(subset=["fwhm_ms"]).groupby("tagged").fwhm_ms.describe().round(2).to_string())
print("Whole-cell cells with fewer than 5 spikes (no width):", int(wc.fwhm_ms.isna().sum()), "of", len(wc))
print("Excitatory cells by subject:", cl[cl.cls == "EXC"].subject.value_counts().to_dict())
for d in (0.1, 0.3):
    for f in (0.5, 0.7):
        c = pd.DataFrame([classify(r, d, f) for r in l4]).cls.value_counts().to_dict()
        print(f"Variant: tag threshold {d}, width threshold {f} ms ->", c)

fig, ax = plt.subplots(1, 2, figsize=(10, 3.2))
ax[0].hist(wc.fwhm_ms.dropna(), bins=np.arange(0, 2.4, 0.1), color="0.6", label="whole-cell, untagged")
ax[0].hist(wc[wc.tagged].fwhm_ms.dropna(), bins=np.arange(0, 2.4, 0.1), color="C3", label="whole-cell, tagged")
ax[0].axvline(yu2016.EXC_MIN_FWHM_MS, color="k", lw=0.8)
ax[0].set_xlabel("AP width at half maximum (ms)"); ax[0].set_ylabel("cells"); ax[0].legend(fontsize=7)
ax[1].scatter(vg.p_base, vg.p_evoked, c=np.where(vg.tagged, "C3", "0.5"), s=14)
ax[1].plot([0, 1], [0, 1], "k:", lw=0.8)
ax[1].set_xlabel("spike probability before pulse"); ax[1].set_ylabel("after pulse (0-6 ms)")
plt.tight_layout(); plt.show()
print("Axes: widths 0 to 2.4 ms hold all", int(wc.fwhm_ms.notna().sum()), "whole-cell widths (max",
      round(wc.fwhm_ms.max(), 2), "); probabilities are within 0 to 1.")

print("\nTagged cells, one row each. The tag site is the light site with the largest evoked minus baseline probability:")
print(cl[cl.tagged][["subject", "mode", "spikes", "tag_site", "pulses", "p_evoked", "p_base", "tag_p", "latency_ms"]].round(4).to_string(index=False))
hz = pd.Series({r["path"]: r["vm_hz"] for r in l4})
print("\nWhole-cell action potential width (ms) by trace sampling rate (Hz) and tag:")
print(wc.assign(hz=wc.path.map(hz)).dropna(subset=["fwhm_ms"]).groupby(["hz", "tagged"]).fwhm_ms.describe()[["count", "min", "50%", "max"]].round(2).to_string())
Cells by genotype, mode and class:
cls                           EXC  FS  other
genotype       mode                         
(none)         whole-cell       1   0      2
Ai32           juxtacellular    0   0      4
               whole-cell       0   0      1
C57BL6         whole-cell       4   0      4
PV-Cre         juxtacellular    0   0     39
               whole-cell       8   0      1
VGAT-ChR2-EYFP juxtacellular    0  19     11
               whole-cell       9   4     23

VGAT-ChR2 cells tested with isolated pulses: 38; tagged 23
Tagged cells: evoked spike probability median 1.0 (range 0.37 to 1.0 ), baseline median 0.08 , first-spike latency median 2.3 ms (range 1.6 to 4.6 )
Untagged tested cells with evoked minus baseline probability of 0.2 or more (too few pulses or p >= 0.001):
                                                                          path          mode  pulses  p_evoked  p_base  tag_p  latency_ms
 sub-anm273347/sub-anm273347_ses-20150224_obj-i6vwao_behavior+icephys+ogen.nwb juxtacellular    36.0    0.3333  0.0278 0.0017        2.25
            sub-anm273348/sub-anm273348_ses-20150227_behavior+icephys+ogen.nwb juxtacellular    12.0    1.0000  0.2500 0.0020        1.60
sub-anm318531/sub-anm318531_ses-20151230_obj-1jbfh1x_behavior+icephys+ogen.nwb juxtacellular    10.0    1.0000  0.1000 0.0020        1.65
Tagged cells by mode: {'juxtacellular': 19, 'whole-cell': 4} ; by subject: {'anm260070': 6, 'anm273348': 3, 'anm275231': 3, 'anm281589': 2, 'anm281592': 2, 'anm220466': 1, 'anm220470': 1, 'anm273341': 1, 'anm305599': 1, 'anm307766': 1, 'anm318530': 1, 'anm318532': 1}

Whole-cell action potential width at half maximum (ms), cells with at least 5 spikes:
        count  mean   std   min   25%   50%   75%   max
tagged                                                 
False    27.0  0.89  0.41  0.25  0.70  0.83  1.04  2.12
True      4.0  0.51  0.10  0.38  0.46  0.52  0.57  0.61
Whole-cell cells with fewer than 5 spikes (no width): 26 of 57
Excitatory cells by subject: {'anm266945': 4, 'anm214848': 2, 'anm318530': 2, 'anm190964': 1, 'anm196837': 1, 'anm220470': 1, 'anm223517': 1, 'anm234411': 1, 'anm236459': 1, 'anm236462': 1, 'anm244028': 1, 'anm266951': 1, 'anm281592': 1, 'anm300172': 1, 'anm307766': 1, 'anm309008': 1, 'anm318532': 1}
Variant: tag threshold 0.1, width threshold 0.5 ms -> {'other': 85, 'FS': 23, 'EXC': 22}
Variant: tag threshold 0.1, width threshold 0.7 ms -> {'other': 87, 'FS': 23, 'EXC': 20}
Variant: tag threshold 0.3, width threshold 0.5 ms -> {'other': 85, 'FS': 23, 'EXC': 22}
Variant: tag threshold 0.3, width threshold 0.7 ms -> {'other': 87, 'FS': 23, 'EXC': 20}
No description has been provided for this image
Axes: widths 0 to 2.4 ms hold all 31 whole-cell widths (max 2.12 ); probabilities are within 0 to 1.

Tagged cells, one row each. The tag site is the light site with the largest evoked minus baseline probability:
  subject          mode  spikes    tag_site  pulses  p_evoked  p_base  tag_p  latency_ms
anm220466    whole-cell    4221         vS1   190.0    0.5474  0.0474 0.0000       3.600
anm220470    whole-cell    2040         vS1   210.0    0.3714  0.0238 0.0000       4.600
anm260070 juxtacellular   10110 VPM and vS1    35.0    1.0000  0.1429 0.0000       1.600
anm260070 juxtacellular    9966 VPM and vS1    48.0    1.0000  0.0833 0.0000       2.100
anm260070 juxtacellular    7678 VPM and vS1    72.0    0.8194  0.1389 0.0000       2.600
anm260070 juxtacellular    6575 VPM and vS1    72.0    0.6944  0.1528 0.0000       2.500
anm260070 juxtacellular    5774 VPM and vS1    24.0    0.5417  0.0833 0.0005       2.300
anm260070 juxtacellular   10209 VPM and vS1    24.0    0.5000  0.0417 0.0005       2.550
anm273341 juxtacellular    2036         vS1    76.0    0.6974  0.0395 0.0000       2.300
anm273348 juxtacellular    3565 VPM and vS1    24.0    1.0000  0.0000 0.0000       2.100
anm273348 juxtacellular    4765 VPM and vS1    48.0    1.0000  0.0417 0.0000       2.500
anm273348 juxtacellular    8884 VPM and vS1    36.0    1.0000  0.1111 0.0000       1.900
anm275231 juxtacellular    2579         vS1    60.0    1.0000  0.0000 0.0000       1.900
anm275231 juxtacellular    4281         vS1    12.0    1.0000  0.0000 0.0002       1.900
anm275231 juxtacellular   18665         vS1    41.0    1.0000  0.0976 0.0000       2.000
anm281589 juxtacellular   16794 VPM and vS1    24.0    1.0000  0.1250 0.0000       2.300
anm281589 juxtacellular    9055         vS1   180.0    1.0000  0.1333 0.0000       2.200
anm281592 juxtacellular    4148 VPM and vS1   120.0    1.0000  0.0833 0.0000       2.300
anm281592 juxtacellular     286         vS1    48.0    1.0000  0.0417 0.0000       2.250
anm305599    whole-cell    5806         vS1    58.0    1.0000  0.2069 0.0000       2.175
anm307766    whole-cell    6411         vS1   108.0    0.9722  0.0926 0.0000       2.150
anm318530 juxtacellular   10401         vS1    20.0    0.7000  0.0000 0.0001       2.500
anm318532 juxtacellular     166         vS1    10.0    1.0000  0.0000 0.0010       2.900

Whole-cell action potential width (ms) by trace sampling rate (Hz) and tag:
                count   min   50%   max
hz      tagged                         
10000.0 False    20.0  0.25  0.82  1.46
        True      2.0  0.49  0.55  0.61
20000.0 False     7.0  0.35  1.06  2.12
        True      2.0  0.38  0.47  0.55

Nothing in the files names a cell type. In VGAT-ChR2 mice, isolated 5 ms light pulses make some cells spike within a few ms at high probability; these GABAergic cells are the FS proxy. The paper's FS interneurons are cells driven by the light in VGAT-ChR2 mice that also fall inside a boundary of spike peak-to-trough ratio and duration (Supp. Fig. 6), which is not given in numbers, so every tagged cell is used here (in L4, most GABAergic neurons are PV-positive FS cells, but the tag cannot exclude other interneurons). All tags come from the vS1 site or the site labelled "VPM and vS1", at 1.6 to 4.6 ms latency, which suggests direct activation. Three untagged juxtacellular cells spike after the pulses but miss p < 0.001 with 10 to 36 pulses; they are in neither class. Whole-cell cells with broad action potentials that are not tagged are the excitatory proxy. Some tagged whole-cell cells have spikes 0.4 to 0.6 ms wide, so width alone would not separate the classes cleanly. The 85 other cells (juxtacellular cells in PV-Cre, Ai32 and untagged VGAT-ChR2 mice, and whole-cell cells without spikes or with narrow spikes) are not used by the main tests. Many whole-cell recordings have almost no spikes; their membrane potential, which Figures 2 to 4 of the paper are about, is not analysed here.

In [9]:
# Long light epochs at the vS1 site in PV-Cre mice: firing inside the epoch, light trials against
# trials without light, every PV-Cre cell with at least 20 spikes in these trials.
def light_effect(r, site):
    w = yu2016.light_windows(r, site)
    if len(w) == 0:
        return None
    lit = np.zeros(len(r["trials"]), bool)
    lit[w.trial.to_numpy(int)] = True
    dark = ~yu2016.light_trials(r)
    win = (round(float(np.median(w.rel_start)), 3), round(float(np.median(w.rel_stop)), 3))
    rl, nl = yu2016.window_rate(r, lit, win)
    rd, nd = yu2016.window_rate(r, dark, win)
    starts = r["trials"]["start"].to_numpy()
    sp = np.sort(r["spikes"])
    per_trial = lambda m: np.array([np.searchsorted(sp, s + win[1]) - np.searchsorted(sp, s + win[0]) for s in starts[m]])
    p = stats.mannwhitneyu(per_trial(lit), per_trial(dark)).pvalue if lit.sum() and dark.sum() else np.nan
    return {"window": win, "lit_trials": int(lit.sum()), "dark_trials": int(dark.sum()), "rate_light": rl,
            "rate_dark": rd, "spikes": nl + nd, "p": p}

pv = []
for r in l4:
    if r["genotype"] != "PV-Cre":
        continue
    for site, info in r["sites"].items():
        e = light_effect(r, site)
        if e and e["spikes"] >= yu2016.MIN_SPIKES:
            pv.append({"path": r["path"], "mode": r["mode"], "cls": cl.set_index("path").cls[r["path"]], **e})
pv = pd.DataFrame(pv)
pv["ratio"] = pv.rate_light / pv.rate_dark
pv["suppressed"] = (pv.ratio < 0.5) & (pv.p < 0.01)
pv["raised"] = (pv.ratio > 1) & (pv.p < 0.01)
print("PV-Cre cells with light at the vS1 site:", len(pv), "; light window (s after trial start):",
      sorted({tuple(float(x) for x in np.round(w, 2)) for w in pv.window}))
print("Suppressed (rate ratio below 0.5, p < 0.01):", int(pv.suppressed.sum()), "; raised (p < 0.01):", int(pv.raised.sum()))
print(pv.groupby("mode").agg(cells=("ratio", "size"), suppressed=("suppressed", "sum"), raised=("raised", "sum"),
                             median_ratio=("ratio", "median")).to_string())
print("\nSuppressed cells:")
print(pv[pv.suppressed][["path", "mode", "rate_dark", "rate_light", "p"]].round(4).to_string(index=False))
PV-Cre cells with light at the vS1 site: 46 ; light window (s after trial start): [(0.73, 2.73), (0.82, 2.32), (0.82, 2.82), (0.82, 3.32), (0.92, 1.92), (0.92, 2.42), (0.92, 3.42), (0.92, 4.12), (1.02, 2.02), (1.02, 2.52), (1.07, 4.12)]
Suppressed (rate ratio below 0.5, p < 0.01): 3 ; raised (p < 0.01): 13
               cells  suppressed  raised  median_ratio
mode                                                  
juxtacellular     39           3       9      1.057043
whole-cell         7           0       4      1.577365

Suppressed cells:
                                                                         path          mode  rate_dark  rate_light   p
sub-anm244024/sub-anm244024_ses-20141112_obj-vigb9a_behavior+icephys+ogen.nwb juxtacellular    17.5610      7.5385 0.0
sub-anm266945/sub-anm266945_ses-20141220_obj-7o69ev_behavior+icephys+ogen.nwb juxtacellular    41.3810      3.9649 0.0
sub-anm300169/sub-anm300169_ses-20150926_obj-rp0tw4_behavior+icephys+ogen.nwb juxtacellular    28.7903      1.4583 0.0

In PV-Cre mice the long light epoch at the vS1 site (594 nm in the metadata) cuts the firing of a few juxtacellular cells by more than half and raises that of others. This is what an inhibitory opsin in PV interneurons would do. The files do not name the opsin; the paper's supplement does (eNpHR in PV cells, Supp. Fig. 13, where 3 of 4 PV cells are inhibited), and the PV-Cre experiments are read below on that basis.

In [10]:
# Spike detection. Light artifacts: the fraction of a cell's spikes within 0.3 ms of a light pulse onset or offset,
# against the fraction expected if spikes were placed at random in the analysed span of each trial.
rows = []
for r in l4:
    sp = np.sort(r["spikes"])
    edges = np.sort(np.concatenate([np.r_[r["light"][s]["pulses"][:, 0], r["light"][s]["pulses"][:, 1]] for s in r["light"]] or [np.zeros(0)]))
    if len(sp) < yu2016.MIN_SPIKES or len(edges) == 0:
        continue
    j = np.searchsorted(edges, sp)
    d = np.minimum(np.abs(sp - edges[np.clip(j - 1, 0, len(edges) - 1)]), np.abs(edges[np.clip(j, 0, len(edges) - 1)] - sp))
    chance = min(1.0, 2 * 0.0003 * len(edges) / (len(r["trials"]) * yu2016.ANALYSIS_END_S))
    rows.append({"path": r["path"], "mode": r["mode"], "cls": cl.set_index("path").cls[r["path"]], "spikes": len(sp),
                 "light_edges": len(edges), "near_edge": float(np.mean(d < 0.0003)), "chance": chance})
art = pd.DataFrame(rows)
art["excess"] = art.near_edge / art.chance
print(f"Cells with light and at least {yu2016.MIN_SPIKES} spikes: {len(art)}. Fraction of spikes within 0.3 ms of a light edge, "
      f"median {art.near_edge.median():.4f} (chance median {art.chance.median():.4f}); max {art.near_edge.max():.4f}")
print("Cells with more than 1% of spikes at a light edge and more than 3 times chance:")
print(art[(art.near_edge > 0.01) & (art.excess > 3)].round(4).to_string(index=False))

# Example traces: the first trial of one tagged juxtacellular cell and one excitatory whole-cell cell, with the
# detected spikes and the light.
ex_paths = [cl[(cl.cls == "FS") & (cl["mode"] == "juxtacellular")].sort_values("spikes").path.iloc[-1],
            cl[cl.cls == "EXC"].sort_values("spikes").path.iloc[-1]]
fig, ax = plt.subplots(2, 1, figsize=(10, 4.5))
for a, pth in zip(ax, ex_paths):
    r = next(x for x in l4 if x["path"] == pth)
    s0, n = r["trials"].start.iloc[0], min(int(r["vm_trial_s"] * r["vm_hz"]), len(r["vm_head"]))
    t = np.arange(n) / r["vm_hz"]
    a.plot(t, r["vm_head"][:n], lw=0.4, color="0.3")
    sp = r["spikes"][(r["spikes"] >= s0) & (r["spikes"] < s0 + t[-1])] - s0
    a.plot(sp, np.interp(sp, t, r["vm_head"][:n]), "r.", ms=3)
    for s in r["light"]:
        for p0, p1 in r["light"][s]["pulses"]:
            if s0 <= p0 < s0 + t[-1]:
                a.axvspan(p0 - s0, p1 - s0, color="C0", alpha=0.3)
    a.set_title(f"{pth.split('/')[-1]} ({r['mode']}, {cl.set_index('path').cls[pth]}): {len(sp)} spikes in trial 1", fontsize=8)
    a.set_ylabel("stored value (mV)")
ax[1].set_xlabel("s after trial start"); plt.tight_layout(); plt.show()
print("Axes: each panel shows the first trial (up to the first 100,000 samples) and its full range.")
Cells with light and at least 20 spikes: 93. Fraction of spikes within 0.3 ms of a light edge, median 0.0005 (chance median 0.0003); max 0.0465
Cells with more than 1% of spikes at a light edge and more than 3 times chance:
                                                                         path          mode cls  spikes  light_edges  near_edge  chance  excess
           sub-anm220466/sub-anm220466_ses-20140403_behavior+icephys+ogen.nwb    whole-cell  FS    4221         1980     0.0102  0.0016  6.4313
sub-anm220470/sub-anm220470_ses-20140409_obj-bb09nm_behavior+icephys+ogen.nwb    whole-cell  FS    2040         3620     0.0123  0.0037  3.3007
           sub-anm281589/sub-anm281589_ses-20150319_behavior+icephys+ogen.nwb juxtacellular  FS   16794         3298     0.0111  0.0023  4.7575
No description has been provided for this image
Axes: each panel shows the first trial (up to the first 100,000 samples) and its full range.

Light artifacts are not counted as spikes to any extent that matters. Three cells have more than 1% of their spikes within 0.3 ms of a light onset or offset and more than 3 times chance; all three are tagged cells, whose light-evoked spikes come 1.6 to 4.6 ms after the onset of a 5 ms pulse, near its offset. The largest fraction in any cell (0.047) is below 3 times chance for that cell. The example traces show the detected spikes on the action potentials of a juxtacellular and a whole-cell recording. Spike detection was not checked against any ground truth.

Measures for every unit and cell¶

Each VPM unit and each L4 cell with a named principal whisker, over its trials without light: the touch response, the whisking modulation and the touch selectivity, as defined in the claims file and the helper module. A unit or cell enters a test when it has at least 20 spikes and at least 10 touches in those trials.

In [11]:
rows = []
for r in vpm:
    dark = (r["trials"].photo_stim_mode == "").to_numpy()
    for u in r["units"]:
        if r["principal"] is None:
            rows.append({"path": r["path"], "subject": r["subject"], "unit": u["unit"], "cls": "VPM"})
            continue
        rows.append({"path": r["path"], "subject": r["subject"], "unit": u["unit"], "cls": "VPM",
                     **yu2016.response_measures(u["spikes"], r, trial_mask=dark)})
for r in l4:
    c = cl.set_index("path").loc[r["path"]]
    if r["principal"] is None:
        rows.append({"path": r["path"], "subject": r["subject"], "unit": 0, "cls": c.cls})
        continue
    rows.append({"path": r["path"], "subject": r["subject"], "unit": 0, "cls": c.cls,
                 **yu2016.response_measures(r["spikes"], r, trial_mask=~yu2016.light_trials(r))})
meas = pd.DataFrame(rows)
meas["included"] = (meas.n_spikes >= yu2016.MIN_SPIKES) & (meas.n_touches >= yu2016.MIN_TOUCHES)
meas["touch_resp"] = meas.touch_p < yu2016.ALPHA_CELL
meas["whisk_mod"] = meas.whisk_p < yu2016.ALPHA_CELL
meas["whisk_mod_rayleigh"] = meas.rayleigh_p < yu2016.ALPHA_CELL
print("Included and dropped, by class (dropped: no principal whisker, or too few spikes or touches):")
print(meas.groupby("cls").agg(total=("included", "size"), included=("included", "sum"),
                              no_principal=("n_spikes", lambda x: int(x.isna().sum()))).to_string())
meas["whisk_fit"] = meas.whisk_r2 > 0.25
inc = meas[meas.included]
summary = inc.groupby("cls").agg(
    n=("included", "size"), touch_responsive=("touch_resp", "mean"), whisk_modulated=("whisk_mod", "mean"),
    whisk_mod_rayleigh=("whisk_mod_rayleigh", "mean"),
    touch_resp_hz=("touch_resp_hz", "median"), whisk_mod_hz=("whisk_mod_hz", "median"),
    rate_whisk=("rate_whisk", "median"), rate_quiet=("rate_quiet", "median"),
    touch_selectivity=("touch_selectivity", "median"), touches=("n_touches", "median"))
print("\nMedians (fractions for the two significance columns):")
print(summary.round(3).to_string())
print("\nRanges over included cells and units:")
print(inc.groupby("cls")[["touch_resp_hz", "whisk_mod_hz", "rate_whisk", "touch_selectivity"]].agg(["min", "max"]).round(2).to_string())

# Null for the L4 cells: the same measures with spikes moved to the next trial
nrows = []
for r in l4:
    if r["principal"] is None:
        continue
    m = yu2016.response_measures(yu2016.shift_spikes(r["spikes"], r["trials"]), r, trial_mask=~yu2016.light_trials(r))
    nrows.append({"path": r["path"], "null_touch": m["touch_p"] < yu2016.ALPHA_CELL, "null_whisk": m["whisk_p"] < yu2016.ALPHA_CELL,
                  "null_fit": m["whisk_r2"] > 0.25, "null_ok": m["n_spikes"] >= yu2016.MIN_SPIKES and m["n_touches"] >= yu2016.MIN_TOUCHES})
nl = inc.merge(pd.DataFrame(nrows), on="path")
nl = nl[nl.null_ok]
print("\nL4 cells, null with spikes moved to the next trial (fractions; cells with enough spikes and touches after the move):")
print(nl.groupby("cls").agg(n=("path", "size"), touch_responsive=("null_touch", "mean"), whisk_modulated=("null_whisk", "mean"),
                            r2_above_025=("null_fit", "mean")).round(2).to_string())
print("\nThe paper's phase measure (r2 of a sinusoid fit above 0.25), fraction of included cells:",
      inc.groupby("cls").whisk_fit.mean().round(2).to_dict())
Included and dropped, by class (dropped: no principal whisker, or too few spikes or touches):
       total  included  no_principal
cls                                 
EXC       22        18             0
FS        23        20             3
VPM       29        24             0
other     85        53             5

Medians (fractions for the two significance columns):
        n  touch_responsive  whisk_modulated  whisk_mod_rayleigh  touch_resp_hz  whisk_mod_hz  rate_whisk  rate_quiet  touch_selectivity  touches
cls                                                                                                                                              
EXC    18             0.556            0.278               0.167          2.074         0.027       0.232       0.108              0.980    370.0
FS     20             0.650            0.800               0.750         34.905         2.636      13.571       5.323              0.916    336.0
VPM    24             0.833            0.875               0.917          7.520         1.300      12.781       3.781              0.883    943.5
other  53             0.623            0.604               0.679         11.124         1.880       5.883       1.386              0.836    180.0

Ranges over included cells and units:
      touch_resp_hz         whisk_mod_hz        rate_whisk        touch_selectivity     
                min     max          min    max        min    max               min  max
cls                                                                                     
EXC           -3.10   24.18          0.0   2.96       0.00   2.91               0.0  1.0
FS            -2.94  112.73          0.0   7.65       1.95  40.21               0.0  1.0
VPM           -0.83   34.81          0.0   7.22       0.30  36.57               0.0  1.0
other         -8.31   90.78          0.0  29.76       0.36  61.31               0.0  1.0
L4 cells, null with spikes moved to the next trial (fractions; cells with enough spikes and touches after the move):
        n  touch_responsive  whisk_modulated  r2_above_025
cls                                                       
EXC    18              0.00             0.06          0.33
FS     20              0.00             0.00          0.40
other  53              0.04             0.00          0.34

The paper's phase measure (r2 of a sinusoid fit above 0.25), fraction of included cells: {'EXC': 0.56, 'FS': 0.8, 'VPM': 0.96, 'other': 0.7}

With the spikes moved to the next trial, no FS cell and 0.06 of excitatory cells pass the whisking test, and no FS or excitatory cell passes the touch test (0.04 of the other cells). The paper's phase measure, the r² of a sinusoid fit above 0.25, marks 0.33 to 0.40 of L4 cells with the spikes moved to the next trial: with 12 phase bins it does not separate modulation from chance here, so it is shown only as a variant.

In [12]:
# Touch input per mouse. ION transection removes the input from the whiskers; Supplementary Table 2 gives 9 such
# VGAT-ChR2 mice (9 whole-cell excitatory and 10 cell-attached FS cells), but no file says which. A cell has a
# touch input when its spikes respond to touch (p < 0.01) or, in whole-cell, its membrane potential rises after
# touch (median at least 0.5 mV, p < 0.01). Mice where no tested cell has one are flagged.
vmt = pd.DataFrame([{"path": r["path"], "vm_touch_n": r["vm_touch_n"], "vm_touch_mv": r["vm_touch_mv"],
                     "vm_touch_p": r["vm_touch_p"]} for r in l4])
cells = meas[meas.cls != "VPM"].merge(vmt, on="path").merge(meta[["path", "mode", "line"]], on="path")
cells["spike_touch"] = cells.included & cells.touch_resp
cells["vm_touch"] = (cells.vm_touch_mv >= 0.5) & (cells.vm_touch_p < 0.01)
cells["vm_tested"] = cells.vm_touch_n >= 10
cells["tested"] = cells.included | cells.vm_tested
wcv = cells[cells.vm_tested]
print(f"Whole-cell cells with at least 10 touches: {len(wcv)}; touch-evoked change median {wcv.vm_touch_mv.median():.2f} mV "
      f"(range {wcv.vm_touch_mv.min():.2f} to {wcv.vm_touch_mv.max():.2f}); with a touch input {int(wcv.vm_touch.sum())}")
mice = cells.groupby("subject").agg(line=("line", "first"), cells=("path", "size"), tested=("tested", "sum"),
                                    spike_touch=("spike_touch", "sum"), vm_tested=("vm_tested", "sum"), vm_touch=("vm_touch", "sum"),
                                    median_vm_touch_mv=("vm_touch_mv", "median"))
mice["no_touch_input"] = (mice.tested > 0) & (mice.spike_touch + mice.vm_touch == 0)
print("Mice with no tested cell:", list(mice[mice.tested == 0].index))
print("\nMice where no cell has a touch input, and mice where the membrane potential of every tested whole-cell cell shows none:")
print(mice[mice.no_touch_input | ((mice.vm_tested > 0) & (mice.vm_touch == 0))].round(2).to_string())
no_input = set(mice[mice.no_touch_input].index)
fl_cells = cells[cells.subject.isin(no_input)]
print(f"\nFlagged mice: {len(no_input)} (lines {mice.loc[sorted(no_input)].line.value_counts().to_dict()}); their cells by mode and class:")
print(pd.crosstab(fl_cells["mode"], fl_cells.cls).to_string())
print("For comparison, Supplementary Table 2: 9 ION-transected VGAT-ChR2 mice, 9 whole-cell excitatory and 10 cell-attached FS cells.")
print("Whole-cell cells of the flagged mice:", fl_cells[fl_cells["mode"] == "whole-cell"][["subject", "cls", "vm_touch_n", "vm_touch_mv", "vm_touch_p"]].round(3).to_dict("records"))
amb = mice[~mice.no_touch_input & (mice.vm_tested > 0) & (mice.vm_touch == 0)].index
print("Cells of the mice whose tested whole-cell cells all lack a touch input but that are not flagged:")
print(cells[cells.subject.isin(amb)][["subject", "mode", "cls", "included", "touch_resp_hz", "touch_p", "vm_touch_n", "vm_touch_mv", "vm_touch_p"]].round(4).to_string(index=False))
Whole-cell cells with at least 10 touches: 53; touch-evoked change median 2.82 mV (range -1.84 to 10.30); with a touch input 40
Mice with no tested cell: ['anm223517', 'anm324650', 'anm337496']

Mice where no cell has a touch input, and mice where the membrane potential of every tested whole-cell cell shows none:
                     line  cells  tested  spike_touch  vm_tested  vm_touch  median_vm_touch_mv  no_touch_input
subject                                                                                                       
anm273341  VGAT-ChR2-EYFP      2       2            0          0         0                 NaN            True
anm275231  VGAT-ChR2-EYFP      3       3            0          0         0                 NaN            True
anm281592  VGAT-ChR2-EYFP      3       3            1          1         0               -1.84           False
anm318530  VGAT-ChR2-EYFP      5       4            0          4         0               -0.00            True
anm318531  VGAT-ChR2-EYFP      2       2            1          1         0                0.09           False
anm318532  VGAT-ChR2-EYFP      5       3            0          3         0                0.09            True

Flagged mice: 4 (lines {'VGAT-ChR2-EYFP': 4}); their cells by mode and class:
cls            EXC  FS  other
mode                         
juxtacellular    0   6      1
whole-cell       3   0      5
For comparison, Supplementary Table 2: 9 ION-transected VGAT-ChR2 mice, 9 whole-cell excitatory and 10 cell-attached FS cells.
Whole-cell cells of the flagged mice: [{'subject': 'anm318530', 'cls': 'EXC', 'vm_touch_n': 57, 'vm_touch_mv': 0.1, 'vm_touch_p': 0.626}, {'subject': 'anm318530', 'cls': 'other', 'vm_touch_n': 83, 'vm_touch_mv': -0.108, 'vm_touch_p': 0.4}, {'subject': 'anm318530', 'cls': 'other', 'vm_touch_n': 194, 'vm_touch_mv': 0.316, 'vm_touch_p': 0.001}, {'subject': 'anm318530', 'cls': 'EXC', 'vm_touch_n': 169, 'vm_touch_mv': -0.184, 'vm_touch_p': 0.882}, {'subject': 'anm318532', 'cls': 'EXC', 'vm_touch_n': 133, 'vm_touch_mv': 0.091, 'vm_touch_p': 0.3}, {'subject': 'anm318532', 'cls': 'other', 'vm_touch_n': 41, 'vm_touch_mv': 0.135, 'vm_touch_p': 0.526}, {'subject': 'anm318532', 'cls': 'other', 'vm_touch_n': 0, 'vm_touch_mv': nan, 'vm_touch_p': nan}, {'subject': 'anm318532', 'cls': 'other', 'vm_touch_n': 33, 'vm_touch_mv': -0.034, 'vm_touch_p': 0.844}]
Cells of the mice whose tested whole-cell cells all lack a touch input but that are not flagged:
  subject          mode   cls  included  touch_resp_hz  touch_p  vm_touch_n  vm_touch_mv  vm_touch_p
anm281592 juxtacellular    FS      True        49.6970   0.0000           0          NaN         NaN
anm281592    whole-cell   EXC      True        -3.1016   0.9997         374      -1.8418      1.0000
anm281592 juxtacellular    FS      True        -0.6349   0.8413           0          NaN         NaN
anm318531 juxtacellular other      True        11.7460   0.0063           0          NaN         NaN
anm318531    whole-cell other     False         0.0000      NaN          69       0.0912      0.3791

Almost every whole-cell cell depolarizes after touch (median 2.82 mV; 40 of 53 tested cells have a touch input by this rule). In 4 VGAT-ChR2 mice no tested cell has one: anm318530 and anm318532 (8 whole-cell cells, change -0.18 to 0.32 mV, and one tagged cell each, without a named principal whisker) and anm273341 and anm275231 (juxtacellular cells only, none responding to touch). anm318531 is ambiguous: its one tested whole-cell cell has no touch input, but a juxtacellular cell responds to touch (p = 0.006, at the edge of the rule). With it, these three mice hold 9 whole-cell cells, the number of ION-cut whole-cell cells in Supplementary Table 2; 8 of them were tested and lack a touch input, and one (in anm318532) has too few touches to be tested. These mice are consistent with the ION-transected mice of Figure 7, which the files do not mark. The table lists 9 such mice, so others may be among the mice with too few tested cells. The 0.5 mV threshold was chosen after seeing these values. Because the flag is drawn from touch responses, leaving these mice out favours any claim about touch, so it is used only in variants.

vpm-touch¶

Neurons in the ventral posteromedial nucleus (VPM) fire in response to touch in mice locating objects with their whiskers.

In [13]:
v = inc[inc.cls == "VPM"]
frac = v.touch_resp.mean()
print(f"VPM units included: {len(v)} of {int((meas.cls == 'VPM').sum())}; touch-responsive (p < 0.01): "
      f"{int(v.touch_resp.sum())} ({frac:.2f}). Criterion: >= 0.5 reproduced, 0.25-0.5 partially.")
print(f"Touch response (Hz): median {v.touch_resp_hz.median():.1f}, range {v.touch_resp_hz.min():.1f} to "
      f"{v.touch_resp_hz.max():.1f}; rate 0-25 ms after touch median {v.touch_post_hz.median():.1f} Hz, "
      f"before {v.touch_pre_hz.median():.1f} Hz")
print("Variant, p < 0.05:", round((v.touch_p < 0.05).mean(), 2))
print("\nBy mouse:")
print(v.groupby("subject").agg(units=("touch_resp", "size"), responsive=("touch_resp", "sum"),
                               median_touch_resp_hz=("touch_resp_hz", "median")).round(1).to_string())
print("\nNull: the same test with spikes moved to the next trial gives a touch-responsive fraction of", end=" ")
shifted = []
for r in vpm:
    if r["principal"] is None:
        continue
    dark = (r["trials"].photo_stim_mode == "").to_numpy()
    for u in r["units"]:
        m = yu2016.response_measures(yu2016.shift_spikes(u["spikes"], r["trials"]), r, trial_mask=dark)
        if m["n_spikes"] >= yu2016.MIN_SPIKES and m["n_touches"] >= yu2016.MIN_TOUCHES:
            shifted.append(m["touch_p"] < yu2016.ALPHA_CELL)
print(round(np.mean(shifted), 2), f"({len(shifted)} units)")
VPM units included: 24 of 29; touch-responsive (p < 0.01): 20 (0.83). Criterion: >= 0.5 reproduced, 0.25-0.5 partially.
Touch response (Hz): median 7.5, range -0.8 to 34.8; rate 0-25 ms after touch median 25.0 Hz, before 14.4 Hz
Variant, p < 0.05: 0.88

By mouse:
           units  responsive  median_touch_resp_hz
subject                                           
anm184389      7           5                   4.9
anm186997      2           2                  22.3
anm199549      5           5                  25.8
anm199551      1           1                   4.8
anm199552      7           6                   7.3
anm203464      2           1                  24.1

Null: the same test with spikes moved to the next trial gives a touch-responsive fraction of 
0.0 (24 units)

Result: reproduced. 20 of the 24 included VPM units (0.83) fire more in the 25 ms after a touch than in the 25 ms before it (p < 0.01), against 0 of 24 when the spikes are moved to the next trial, and at least one unit responds in each of the 6 mice. The median touch response is 7.5 Hz.

vpm-whisking¶

VPM neurons fire in response to whisker movement.

In [14]:
frac = v.whisk_mod.mean()
print(f"VPM units included: {len(v)}; modulated by whisking phase (trial-shift null, p < 0.01): {int(v.whisk_mod.sum())} "
      f"({frac:.2f}). Criterion: >= 0.5 reproduced, 0.25-0.5 partially.")
print(f"Whisking modulation above the null (Hz): median {v.whisk_mod_hz.median():.2f}, range {v.whisk_mod_hz.min():.2f} to "
      f"{v.whisk_mod_hz.max():.2f}; null median per unit, median {v.whisk_null_median_hz.median():.2f} Hz; null draws per unit "
      f"{int(v.n_null.min())} to {int(v.n_null.max())}; spikes during non-touch whisking per unit median {int(v.n_whisk_spikes.median())}")
print("Variant, p < 0.05:", round((v.whisk_p < 0.05).mean(), 2))
print("First version (Rayleigh test of spike phases, p < 0.01):", round(v.whisk_mod_rayleigh.mean(), 2))
rq = v.dropna(subset=["rate_whisk", "rate_quiet"])
print(f"Variant, rate during non-touch whisking against quiet (amplitude < {yu2016.QUIET_AMP_DEG} deg): "
      f"median {rq.rate_whisk.median():.2f} against {rq.rate_quiet.median():.2f} Hz, higher in "
      f"{(rq.rate_whisk > rq.rate_quiet).sum()} of {len(rq)} (Wilcoxon p = {stats.wilcoxon(rq.rate_whisk, rq.rate_quiet).pvalue:.2g})")
alt = []
for r in vpm:
    if r["principal"] is None:
        continue
    dark = (r["trials"].photo_stim_mode == "").to_numpy()
    for u in r["units"]:
        old = yu2016.WHISK_AMP_DEG
        yu2016.WHISK_AMP_DEG = 5.0
        m = yu2016.response_measures(u["spikes"], r, trial_mask=dark)
        yu2016.WHISK_AMP_DEG = old
        if m["n_spikes"] >= yu2016.MIN_SPIKES and m["n_touches"] >= yu2016.MIN_TOUCHES:
            alt.append(m["whisk_p"] < yu2016.ALPHA_CELL)
print("Variant, whisking threshold 5 degrees:", round(np.mean(alt), 2), f"({len(alt)} units)")
print("\nBy mouse:")
print(v.groupby("subject").agg(units=("whisk_mod", "size"), modulated=("whisk_mod", "sum"),
                               median_whisk_mod_hz=("whisk_mod_hz", "median")).round(2).to_string())
print("Null (spikes moved to the next trial): fraction modulated by the trial-shift test, and by the Rayleigh test,", end=" ")
null, null_r = [], []
for r in vpm:
    if r["principal"] is None:
        continue
    dark = (r["trials"].photo_stim_mode == "").to_numpy()
    for u in r["units"]:
        m = yu2016.response_measures(yu2016.shift_spikes(u["spikes"], r["trials"]), r, trial_mask=dark)
        if m["n_spikes"] >= yu2016.MIN_SPIKES and m["n_touches"] >= yu2016.MIN_TOUCHES:
            null.append(m["whisk_p"] < yu2016.ALPHA_CELL)
            null_r.append(m["rayleigh_p"] < yu2016.ALPHA_CELL)
print(round(np.mean(null), 2), "and", round(np.mean(null_r), 2), f"({len(null)} units)")
VPM units included: 24; modulated by whisking phase (trial-shift null, p < 0.01): 21 (0.88). Criterion: >= 0.5 reproduced, 0.25-0.5 partially.
Whisking modulation above the null (Hz): median 1.30, range 0.00 to 7.22; null median per unit, median 0.20 Hz; null draws per unit 399 to 399; spikes during non-touch whisking per unit median 4658
Variant, p < 0.05: 0.92
First version (Rayleigh test of spike phases, p < 0.01): 0.92
Variant, rate during non-touch whisking against quiet (amplitude < 1.0 deg): median 12.78 against 3.78 Hz, higher in 23 of 24 (Wilcoxon p = 1.2e-06)
Variant, whisking threshold 5 degrees: 0.92 (24 units)

By mouse:
           units  modulated  median_whisk_mod_hz
subject                                         
anm184389      7          7                 1.00
anm186997      2          1                 0.62
anm199549      5          5                 1.68
anm199551      1          1                 0.58
anm199552      7          7                 2.93
anm203464      2          0                 0.08
Null (spikes moved to the next trial): fraction modulated by the trial-shift test, and by the Rayleigh test, 
0.12 and 0.88 (24 units)

Result: reproduced. 21 of the 24 included VPM units (0.88) are modulated by whisking phase away from touch under the corrected test, against 0.12 with the spikes moved to the next trial; 23 of 24 also fire more during whisking than at rest (median 12.78 against 3.78 Hz). The first version of the test (Rayleigh) gives 0.92 but also 0.88 with the wrong alignment, so it is not used. One mouse (2 units) has no modulated unit.

fs-touch-and-whisking¶

L4 fast-spiking (FS) interneurons inherit the responses of VPM, firing in response to both touch and whisker movement.

In [15]:
fs = inc[inc.cls == "FS"]
print(f"FS (tagged) cells included: {len(fs)} of {int((meas.cls == 'FS').sum())} "
      f"(dropped: {int(meas[(meas.cls == 'FS')].n_spikes.isna().sum())} without a named principal whisker)")
print(f"Touch-responsive: {int(fs.touch_resp.sum())} ({fs.touch_resp.mean():.2f}); whisking-modulated: "
      f"{int(fs.whisk_mod.sum())} ({fs.whisk_mod.mean():.2f}); both: {int((fs.touch_resp & fs.whisk_mod).sum())}. "
      "Criterion: both fractions >= 0.5 reproduced; one of them partially.")
print(f"Touch response (Hz) median {fs.touch_resp_hz.median():.1f} (range {fs.touch_resp_hz.min():.1f} to "
      f"{fs.touch_resp_hz.max():.1f}); whisking modulation (Hz) median {fs.whisk_mod_hz.median():.2f} "
      f"(range {fs.whisk_mod_hz.min():.2f} to {fs.whisk_mod_hz.max():.2f})")
print("Variant p < 0.05: touch", round((fs.touch_p < 0.05).mean(), 2), ", whisking", round((fs.whisk_p < 0.05).mean(), 2))
print("First version (Rayleigh test, p < 0.01): whisking", round(fs.whisk_mod_rayleigh.mean(), 2))
print("Null draws per cell:", int(fs.n_null.min()), "to", int(fs.n_null.max()),
      "; cells with fewer than 99 (smallest possible p above 0.01):", int((fs.n_null < 99).sum()))
for d in (0.1, 0.3):
    alt_cls = pd.DataFrame([classify(r, d) for r in l4]).set_index("path").cls
    a = inc[inc.path.map(alt_cls) == "FS"]
    print(f"Variant, tag threshold {d}: {len(a)} cells, touch {a.touch_resp.mean():.2f}, whisking {a.whisk_mod.mean():.2f}")
print("For comparison, VPM: touch", round(v.touch_resp.mean(), 2), ", whisking", round(v.whisk_mod.mean(), 2),
      "; whisking modulation (Hz) FS against VPM, Mann-Whitney p =",
      round(stats.mannwhitneyu(fs.whisk_mod_hz, v.whisk_mod_hz).pvalue, 3))
print("\nBy mouse:")
print(fs.groupby("subject").agg(cells=("touch_resp", "size"), touch=("touch_resp", "sum"), whisking=("whisk_mod", "sum"),
                                median_touch_hz=("touch_resp_hz", "median"), median_whisk_hz=("whisk_mod_hz", "median")).round(2).to_string())
a = fs[~fs.subject.isin(no_input)]
print(f"\nVariant, without the mice flagged as lacking touch input ({len(fs) - len(a)} cells left out): {len(a)} cells, "
      f"touch {a.touch_resp.mean():.2f}, whisking {a.whisk_mod.mean():.2f}")
print("Variant, the paper's phase measure (r2 > 0.25):", round(fs.whisk_fit.mean(), 2), "; without flagged mice:", round(a.whisk_fit.mean(), 2))
FS (tagged) cells included: 20 of 23 (dropped: 3 without a named principal whisker)
Touch-responsive: 13 (0.65); whisking-modulated: 16 (0.80); both: 11. Criterion: both fractions >= 0.5 reproduced; one of them partially.
Touch response (Hz) median 34.9 (range -2.9 to 112.7); whisking modulation (Hz) median 2.64 (range 0.00 to 7.65)
Variant p < 0.05: touch 0.65 , whisking 0.85
First version (Rayleigh test, p < 0.01): whisking 0.75
Null draws per cell: 31 to 399 ; cells with fewer than 99 (smallest possible p above 0.01): 2
Variant, tag threshold 0.1: 20 cells, touch 0.65, whisking 0.80
Variant, tag threshold 0.3: 20 cells, touch 0.65, whisking 0.80
For comparison, VPM: touch 0.83 , whisking 0.88 ; whisking modulation (Hz) FS against VPM, Mann-Whitney p = 0.085

By mouse:
           cells  touch  whisking  median_touch_hz  median_whisk_hz
subject                                                            
anm220466      1      0         1             3.35             3.07
anm220470      1      0         1            -2.94             1.44
anm260070      6      6         6            55.29             4.03
anm273341      1      0         1             1.37             1.05
anm273348      3      3         3            74.33             2.24
anm275231      3      0         2             3.01             1.19
anm281589      1      1         1            17.77             2.38
anm281592      2      1         1            24.53             1.45
anm305599      1      1         0            40.75             0.09
anm307766      1      1         0           112.73             7.65

Variant, without the mice flagged as lacking touch input (4 cells left out): 16 cells, touch 0.81, whisking 0.81
Variant, the paper's phase measure (r2 > 0.25): 0.8 ; without flagged mice: 0.75

Result: reproduced. Of the 20 included tagged cells, 13 (0.65) are touch-responsive and 16 (0.80) are modulated by whisking phase, and 11 are both. Their median touch response (34.9 Hz) is larger than that of VPM units (7.5 Hz). The tagged cells are GABAergic cells identified here by light pulses in VGAT-ChR2 mice, not FS cells identified by waveform as the paper did, and 3 tagged cells without a named principal whisker are left out. 4 of the 20 come from 2 mice flagged as lacking touch input (probably ION-transected), and none of the 4 responds to touch; without them, 0.81 of 16 are touch-responsive and 0.81 are modulated by whisking.

exc-mainly-touch¶

In contrast with VPM neurons and L4 FS interneurons, L4 excitatory neurons respond mainly to touch.

In [16]:
ex = inc[inc.cls == "EXC"]
print(f"Excitatory cells included: {len(ex)} of {int((meas.cls == 'EXC').sum())}")
groups = {"EXC": ex, "FS": fs, "VPM": v}
for k, g in groups.items():
    s = g.touch_selectivity.dropna()
    print(f"{k}: n = {len(g)}, selectivity defined for {len(s)}, median {s.median():.3f} "
          f"(IQR {s.quantile(0.25):.3f} to {s.quantile(0.75):.3f}); touch-responsive {g.touch_resp.mean():.2f}; "
          f"whisking-modulated {g.whisk_mod.mean():.2f} (cells with under 99 null draws: {int((g.n_null < 99).sum())}); touch response median {g.touch_resp_hz.median():.2f} Hz; "
          f"whisking modulation median {g.whisk_mod_hz.median():.2f} Hz; rate during whisking median {g.rate_whisk.median():.2f} Hz")
p_fs = stats.mannwhitneyu(ex.touch_selectivity.dropna(), fs.touch_selectivity.dropna(), alternative="greater").pvalue
p_vpm = stats.mannwhitneyu(ex.touch_selectivity.dropna(), v.touch_selectivity.dropna(), alternative="greater").pvalue
print(f"\nMain test, one-sided Mann-Whitney: EXC more touch selective than FS p = {p_fs:.3f}; than VPM p = {p_vpm:.3f}. "
      "Criterion: both < 0.05 reproduced, one partially.")
print("Cells with selectivity 0 (no touch increase):", {k: int((g.touch_selectivity == 0).sum()) for k, g in groups.items()},
      "; selectivity 1 (no whisking modulation):", {k: int((g.touch_selectivity == 1).sum()) for k, g in groups.items()})

# Variants (do not decide the outcome)
def fisher(a, b, col):
    t = [[int(a[col].sum()), int((~a[col]).sum())], [int(b[col].sum()), int((~b[col]).sum())]]
    return stats.fisher_exact(t, alternative="less").pvalue
print(f"\nVariant: fraction whisking-modulated EXC {ex.whisk_mod.mean():.2f} against FS {fs.whisk_mod.mean():.2f} "
      f"(Fisher one-sided p = {fisher(ex, fs, 'whisk_mod'):.2g}) and VPM {v.whisk_mod.mean():.2f} (p = {fisher(ex, v, 'whisk_mod'):.2g})")
for k, g in groups.items():
    g = g.assign(rel=g.whisk_mod_hz / g.rate_whisk.where(g.rate_whisk > 0))
    groups[k] = g
print("Variant: whisking modulation above the null relative to the whisking rate, medians",
      {k: round(float(g.rel.median()), 3) for k, g in groups.items()},
      f"; EXC below FS p = {stats.mannwhitneyu(groups['EXC'].rel.dropna(), groups['FS'].rel.dropna(), alternative='less').pvalue:.3f}"
      f", below VPM p = {stats.mannwhitneyu(groups['EXC'].rel.dropna(), groups['VPM'].rel.dropna(), alternative='less').pvalue:.3f}")
def rate_sel(g):
    tr = g.touch_resp_hz.clip(lower=0)
    wr = (g.rate_whisk - g.rate_quiet).clip(lower=0)
    return (tr / (tr + wr)).where((tr + wr) > 0)
rs = {k: rate_sel(g).dropna() for k, g in groups.items()}
print("Variant: selectivity with the whisking-minus-quiet rate in place of the phase modulation, medians",
      {k: round(float(x.median()), 3) for k, x in rs.items()},
      f"; EXC above FS p = {stats.mannwhitneyu(rs['EXC'], rs['FS'], alternative='greater').pvalue:.3f}, above VPM p = "
      f"{stats.mannwhitneyu(rs['EXC'], rs['VPM'], alternative='greater').pvalue:.3f}")
r0 = {k: g.touch_selectivity_raw.dropna() for k, g in groups.items()}
print("First version (selectivity with 2 x vector strength x rate, uncorrected), medians",
      {k: round(float(x.median()), 3) for k, x in r0.items()},
      f"; EXC above FS p = {stats.mannwhitneyu(r0['EXC'], r0['FS'], alternative='greater').pvalue:.3f}, above VPM p = "
      f"{stats.mannwhitneyu(r0['EXC'], r0['VPM'], alternative='greater').pvalue:.3f}")
for f in (0.5, 0.7):
    alt_cls = pd.DataFrame([classify(r, yu2016.TAG_MIN_DIFF, f) for r in l4]).set_index("path").cls
    a = inc[inc.path.map(alt_cls) == "EXC"].touch_selectivity.dropna()
    print(f"Variant: width threshold {f} ms, {len(a)} EXC, median selectivity {a.median():.3f}, above FS p = "
          f"{stats.mannwhitneyu(a, fs.touch_selectivity.dropna(), alternative='greater').pvalue:.3f}, above VPM p = "
          f"{stats.mannwhitneyu(a, v.touch_selectivity.dropna(), alternative='greater').pvalue:.3f}")
ug = inc[inc.path.isin(cl[(cl.genotype == "VGAT-ChR2-EYFP") & cl.tag_tested & ~cl.tagged].path)]
print(f"Variant: untagged VGAT-ChR2 cells of any mode as excitatory ({len(ug)}), median selectivity "
      f"{ug.touch_selectivity.median():.3f}, whisking-modulated {ug.whisk_mod.mean():.2f}")
print("By mouse (cells, median selectivity):", ex.groupby("subject").touch_selectivity.agg(["size", "median"]).round(3).to_dict("index"))
print("\nExcitatory cells, one row each:")
print(ex[["subject", "n_spikes", "n_touches", "touch_resp_hz", "touch_p", "whisk_amp_raw_hz", "whisk_null_median_hz",
          "whisk_mod_hz", "whisk_p", "rate_whisk", "touch_selectivity"]].round(3).to_string(index=False))

fig, ax = plt.subplots(1, 2, figsize=(10, 3.2))
for i, (k, g) in enumerate(groups.items()):
    ax[0].scatter(np.full(len(g), i) + np.linspace(-0.15, 0.15, len(g)), g.touch_selectivity, s=12)
ax[0].set_xticks(range(3)); ax[0].set_xticklabels(list(groups)); ax[0].set_ylabel("touch selectivity"); ax[0].set_ylim(-0.05, 1.05)
for k, g in groups.items():
    ax[1].scatter(g.touch_resp_hz.clip(lower=0.01), g.whisk_mod_hz.clip(lower=0.01), s=12, label=k)
ax[1].set_xscale("log"); ax[1].set_yscale("log"); ax[1].set_xlabel("touch response (Hz)"); ax[1].set_ylabel("whisking modulation (Hz)")
ax[1].legend(fontsize=7); plt.tight_layout(); plt.show()
allg = pd.concat(groups.values())
print("Right panel: values at or below 0.01 Hz are drawn at 0.01:", int((allg.touch_resp_hz <= 0.01).sum()), "touch,",
      int((allg.whisk_mod_hz <= 0.01).sum()), "whisking; selectivity is within 0 to 1.")

# Variants on the data that bear on the selectivity test
print("\nSpikes in the 25 ms after touches, per excitatory cell: median", int(ex.touch_spikes_post.median()), ", range",
      int(ex.touch_spikes_post.min()), "to", int(ex.touch_spikes_post.max()), "; cells with selectivity 0 or 1:",
      int(ex.touch_selectivity.isin([0, 1]).sum()), "of", len(ex))
def sel_test(e, label):
    pf = stats.mannwhitneyu(e.touch_selectivity.dropna(), fs.touch_selectivity.dropna(), alternative="greater").pvalue
    pv_ = stats.mannwhitneyu(e.touch_selectivity.dropna(), v.touch_selectivity.dropna(), alternative="greater").pvalue
    print(f"Variant, {label}: {len(e)} EXC, median selectivity {e.touch_selectivity.median():.3f}, above FS p = {pf:.3f}, above VPM p = {pv_:.3f}")
sel_test(ex[(ex.touch_spikes_post + ex.touch_spikes_pre) >= 20], "EXC with at least 20 spikes in the touch windows")
sel_test(ex[~ex.subject.isin(no_input)], "EXC without the mice flagged as lacking touch input")
fs_k, v_k = fs[~fs.subject.isin(no_input)], v
pf = stats.mannwhitneyu(ex[~ex.subject.isin(no_input)].touch_selectivity.dropna(), fs_k.touch_selectivity.dropna(), alternative="greater").pvalue
print(f"Variant, flagged mice left out of both EXC and FS: above FS p = {pf:.3f}")
iso = []
for r in recs:
    if r["principal"] is None:
        continue
    if r["kind"] == "VPM":
        dark = (r["trials"].photo_stim_mode == "").to_numpy()
        cand = [(u["unit"], u["spikes"]) for u in r["units"]]
    else:
        dark = ~yu2016.light_trials(r)
        cand = [(0, r["spikes"])]
    for unit, sp in cand:
        row = inc[(inc.path == r["path"]) & (inc.unit == unit)]
        if len(row) == 0 or row.cls.iloc[0] not in groups:
            continue
        m = yu2016.response_measures(sp, r, trial_mask=dark, isolated_touch_s=yu2016.TOUCH_WIN_S)
        iso.append({"cls": row.cls.iloc[0], "sel": m["touch_selectivity"], "touch_p": m["touch_p"]})
iso = pd.DataFrame(iso)
ie, ifs, iv = (iso[iso.cls == k].sel.dropna() for k in ("EXC", "FS", "VPM"))
print(f"Variant, only touches with no touch onset in the 25 ms before: medians EXC {ie.median():.3f}, FS {ifs.median():.3f}, "
      f"VPM {iv.median():.3f}; above FS p = {stats.mannwhitneyu(ie, ifs, alternative='greater').pvalue:.3f}, above VPM p = "
      f"{stats.mannwhitneyu(ie, iv, alternative='greater').pvalue:.3f}")
print("Variant, the paper's phase measure (r2 > 0.25): fraction EXC", round(ex.whisk_fit.mean(), 2), ", FS", round(fs.whisk_fit.mean(), 2),
      ", VPM", round(v.whisk_fit.mean(), 2), f"; EXC below FS Fisher p = {fisher(ex, fs, 'whisk_fit'):.2g}, below VPM p = {fisher(ex, v, 'whisk_fit'):.2g}")
# Inclusion: untagged whole-cell cells that are not in the EXC group
wcu = cl[(cl["mode"] == "whole-cell") & ~cl.tagged].merge(meas[["path", "n_spikes", "included"]], on="path", how="left")
print("\nUntagged whole-cell cells:", len(wcu), "; in the EXC group and included:", int(((wcu.cls == "EXC") & wcu.included.fillna(False).astype(bool)).sum()),
      "; without a width (under 5 spikes):", int(wcu.fwhm_ms.isna().sum()), "; with a width but under 20 spikes or 10 touches in trials without light:",
      int((wcu.fwhm_ms.notna() & ~wcu.included.fillna(False).astype(bool)).sum()),
      "; median spikes in trials without light of those left out:", wcu[~wcu.included.fillna(False).astype(bool)].n_spikes.median())
Excitatory cells included: 18 of 22
EXC: n = 18, selectivity defined for 16, median 0.980 (IQR 0.027 to 1.000); touch-responsive 0.56; whisking-modulated 0.28 (cells with under 99 null draws: 3); touch response median 2.07 Hz; whisking modulation median 0.03 Hz; rate during whisking median 0.23 Hz
FS: n = 20, selectivity defined for 19, median 0.916 (IQR 0.716 to 0.948); touch-responsive 0.65; whisking-modulated 0.80 (cells with under 99 null draws: 2); touch response median 34.90 Hz; whisking modulation median 2.64 Hz; rate during whisking median 13.57 Hz
VPM: n = 24, selectivity defined for 24, median 0.883 (IQR 0.769 to 0.938); touch-responsive 0.83; whisking-modulated 0.88 (cells with under 99 null draws: 0); touch response median 7.52 Hz; whisking modulation median 1.30 Hz; rate during whisking median 12.78 Hz

Main test, one-sided Mann-Whitney: EXC more touch selective than FS p = 0.074; than VPM p = 0.090. Criterion: both < 0.05 reproduced, one partially.
Cells with selectivity 0 (no touch increase): {'EXC': 4, 'FS': 1, 'VPM': 2} ; selectivity 1 (no whisking modulation): {'EXC': 4, 'FS': 0, 'VPM': 1}

Variant: fraction whisking-modulated EXC 0.28 against FS 0.80 (Fisher one-sided p = 0.0016) and VPM 0.88 (p = 0.00011)
Variant: whisking modulation above the null relative to the whisking rate, medians {'EXC': 0.272, 'FS': 0.191, 'VPM': 0.15} ; EXC below FS p = 0.836, below VPM p = 0.861
Variant: selectivity with the whisking-minus-quiet rate in place of the phase modulation, medians {'EXC': 0.992, 'FS': 0.754, 'VPM': 0.568} ; EXC above FS p = 0.047, above VPM p = 0.032
First version (selectivity with 2 x vector strength x rate, uncorrected), medians {'EXC': 0.923, 'FS': 0.858, 'VPM': 0.785} ; EXC above FS p = 0.176, above VPM p = 0.258
Variant: width threshold 0.5 ms, 16 EXC, median selectivity 0.980, above FS p = 0.074, above VPM p = 0.090
Variant: width threshold 0.7 ms, 15 EXC, median selectivity 0.980, above FS p = 0.112, above VPM p = 0.126
Variant: untagged VGAT-ChR2 cells of any mode as excitatory (10), median selectivity 0.699, whisking-modulated 0.80
By mouse (cells, median selectivity): {'anm196837': {'size': 1, 'median': nan}, 'anm214848': {'size': 2, 'median': 0.972}, 'anm220470': {'size': 1, 'median': 0.998}, 'anm234411': {'size': 1, 'median': 0.98}, 'anm236459': {'size': 1, 'median': 1.0}, 'anm236462': {'size': 1, 'median': 1.0}, 'anm244028': {'size': 1, 'median': 0.0}, 'anm266945': {'size': 4, 'median': 0.036}, 'anm266951': {'size': 1, 'median': 1.0}, 'anm281592': {'size': 1, 'median': 0.0}, 'anm300172': {'size': 1, 'median': 1.0}, 'anm309008': {'size': 1, 'median': 1.0}, 'anm318530': {'size': 1, 'median': 0.0}, 'anm318532': {'size': 1, 'median': 0.902}}

Excitatory cells, one row each:
  subject  n_spikes  n_touches  touch_resp_hz  touch_p  whisk_amp_raw_hz  whisk_null_median_hz  whisk_mod_hz  whisk_p  rate_whisk  touch_selectivity
anm196837      53.0      375.0          1.920    0.001               NaN                   NaN           NaN      NaN       0.000                NaN
anm214848     245.0      366.0         16.831    0.000             0.452                 0.096         0.356    0.005       0.270              0.979
anm214848      34.0      913.0          0.219    0.083             0.021                 0.013         0.008    0.343       0.012              0.965
anm220470     137.0      110.0         10.182    0.000             0.073                 0.056         0.017    0.312       0.099              0.998
anm234411     162.0      495.0          3.636    0.000             0.137                 0.063         0.074    0.067       0.164              0.980
anm236459     991.0      518.0          5.405    0.000             0.075                 0.154         0.000    0.876       2.065              1.000
anm236462     626.0      337.0          2.730    0.003             0.130                 0.167         0.000    0.649       0.454              1.000
anm244028     500.0      617.0         -0.324    0.751             0.252                 0.152         0.100    0.138       0.970              0.000
anm266945      56.0      352.0         -0.341    0.958             0.044                 0.057         0.000    0.631       0.067                NaN
anm266945      31.0      644.0          0.000    0.500             0.045                 0.018         0.027    0.042       0.034              0.000
anm266945      70.0       91.0         24.176    0.000             0.548                 0.195         0.353    0.045       0.468              0.986
anm266945     669.0      366.0          0.109    0.353             3.326                 0.366         2.961    0.004       2.914              0.036
anm266951     120.0      149.0          4.295    0.004             0.259                 0.416         0.000    0.733       0.182              1.000
anm281592     434.0      374.0         -3.102    1.000             0.603                 0.092         0.511    0.004       0.789              0.000
anm300172     280.0      449.0          2.227    0.002             0.053                 0.104         0.000    0.845       0.353              1.000
anm309008      88.0      408.0          5.784    0.000             0.017                 0.016         0.002    0.363       0.008              1.000
anm318530      21.0      169.0          0.000      NaN             0.203                 0.061         0.142    0.006       0.193              0.000
anm318532     132.0      133.0          1.504    0.066             0.240                 0.077         0.163    0.003       0.599              0.902
No description has been provided for this image
Right panel: values at or below 0.01 Hz are drawn at 0.01: 9 touch, 9 whisking; selectivity is within 0 to 1.

Spikes in the 25 ms after touches, per excitatory cell: median 23 , range 0 to 172 ; cells with selectivity 0 or 1: 8 of 18
Variant, EXC with at least 20 spikes in the touch windows: 12 EXC, median selectivity 0.998, above FS p = 0.003, above VPM p = 0.005
Variant, EXC without the mice flagged as lacking touch input: 16 EXC, median selectivity 0.983, above FS p = 0.024, above VPM p = 0.038
Variant, flagged mice left out of both EXC and FS: above FS p = 0.033
Variant, only touches with no touch onset in the 25 ms before: medians EXC 0.980, FS 0.917, VPM 0.882; above FS p = 0.084, above VPM p = 0.131
Variant, the paper's phase measure (r2 > 0.25): fraction EXC 0.56 , FS 0.8 , VPM 0.96 ; EXC below FS Fisher p = 0.1, below VPM p = 0.0025

Untagged whole-cell cells: 53 ; in the EXC group and included: 18 ; without a width (under 5 spikes): 26 ; with a width but under 20 spikes or 10 touches in trials without light: 5 ; median spikes in trials without light of those left out: 1.0

Result: not reproduced. By the criterion's test the excitatory cells are not significantly more touch selective than FS cells (p = 0.074) or VPM units (p = 0.090), though their median selectivity is the highest (0.980 against 0.916 and 0.883). The test is fragile. Selectivity rests on few spikes in many excitatory cells (median 23 in the 25 ms after touches; 8 of 18 cells sit at exactly 0 or 1), and the groups mix in cells from mice flagged as lacking touch input. Without those mice the excitatory cells are more touch selective than both groups (p = 0.024 and 0.038), and so are the 12 excitatory cells with at least 20 spikes in the touch windows (p = 0.003 and 0.005). These variants do not decide the outcome: the flag is inferred from touch responses, and both selections favour the claim. Counting only isolated touches gives p = 0.084 and 0.131. The other measures point the way the claim does: 0.28 of excitatory cells are modulated by whisking against 0.80 of FS cells (p = 0.0016) and 0.88 of VPM units (p = 0.00011), and a selectivity built from the whisking-minus-rest rate is higher than in both (p = 0.047 and 0.032). The excitatory group holds the 18 broad-spiking whole-cell cells with enough spikes. Of the 53 untagged whole-cell cells, 26 have fewer than 5 spikes and 5 more have too few spikes or touches; the cells left out fire a median of 1 spike.

fs-suppress-movement-spiking¶

FS interneurons reduce movement-related spiking in L4 excitatory neurons; silencing them increases the spiking of excitatory neurons during whisker movement.

In [17]:
def light_measures(r, site):
    w = yu2016.light_windows(r, site)
    lit = np.zeros(len(r["trials"]), bool)
    lit[w.trial.to_numpy(int)] = True
    dark = ~yu2016.light_trials(r)
    win = [(float(np.median(w.rel_start)), float(np.median(w.rel_stop)))] * len(r["trials"])
    a = yu2016.response_measures(r["spikes"], r, trial_mask=lit, time_window=win)
    b = yu2016.response_measures(r["spikes"], r, trial_mask=dark, time_window=win)
    return a, b, win[0], int(lit.sum()), int(dark.sum())

rows = []
for r in l4:
    if r["genotype"] != "PV-Cre" or r["principal"] is None:
        continue
    for site in r["sites"]:
        if len(yu2016.light_windows(r, site)) == 0:
            continue
        a, b, win, nlit, ndark = light_measures(r, site)
        rows.append({"path": r["path"], "subject": r["subject"], "mode": r["mode"],
                     "cls": cl.set_index("path").cls[r["path"]], "window": tuple(float(x) for x in np.round(win, 2)),
                     "lit_trials": nlit, "dark_trials": ndark,
                     "spikes": a["n_spikes"] + b["n_spikes"], "touches": a["n_touches"] + b["n_touches"],
                     "touches_light": a["n_touches"], "touches_dark": b["n_touches"],
                     "whisk_spk_light": a["n_whisk_spikes"], "whisk_spk_dark": b["n_whisk_spikes"],
                     "touch_spk_light": a["touch_spikes_post"], "touch_spk_dark": b["touch_spikes_post"],
                     "whisk_s_light": a["whisk_s"], "whisk_s_dark": b["whisk_s"],
                     "rate_whisk_light": a["rate_whisk"], "rate_whisk_dark": b["rate_whisk"],
                     "rate_light": a["rate"], "rate_dark": b["rate"],
                     "sel_light": a["touch_selectivity"], "sel_dark": b["touch_selectivity"],
                     "sel_raw_light": a["touch_selectivity_raw"], "sel_raw_dark": b["touch_selectivity_raw"],
                     "touch_light": a["touch_resp_hz"], "touch_dark": b["touch_resp_hz"],
                     "wmod_light": a["whisk_mod_hz"], "wmod_dark": b["whisk_mod_hz"]})
pvm = pd.DataFrame(rows)
pvm["included"] = (pvm.spikes >= yu2016.MIN_SPIKES) & (pvm.touches >= yu2016.MIN_TOUCHES)
pvx = pvm[(pvm.cls == "EXC") & pvm.included]
print(f"PV-Cre excitatory cells with light: {int((pvm.cls == 'EXC').sum())}, included {len(pvx)}")
d = pvx.rate_whisk_light - pvx.rate_whisk_dark
p = stats.wilcoxon(pvx.rate_whisk_light, pvx.rate_whisk_dark, alternative="greater").pvalue
print(f"Rate during non-touch whisking inside the light window: rises in {(d > 0).sum()} of {len(pvx)}; median "
      f"{pvx.rate_whisk_dark.median():.2f} Hz without light, {pvx.rate_whisk_light.median():.2f} Hz with; median change "
      f"{d.median():.2f} Hz; one-sided Wilcoxon p = {p:.3f}. Criterion: rises in most and p < 0.05 reproduced.")
print(pvx[["subject", "window", "lit_trials", "dark_trials", "whisk_s_dark", "whisk_s_light", "whisk_spk_dark", "whisk_spk_light",
           "rate_whisk_dark", "rate_whisk_light", "rate_dark", "rate_light", "wmod_dark", "wmod_light"]].round(2).to_string(index=False))
print("\nBehaviour inside the light window, per trial (median over cells), without and with light: whisking time (s)",
      round((pvx.whisk_s_dark / pvx.dark_trials).median(), 3), "and", round((pvx.whisk_s_light / pvx.lit_trials).median(), 3),
      "; touches", round((pvx.touches_dark / pvx.dark_trials).median(), 2), "and", round((pvx.touches_light / pvx.lit_trials).median(), 2))
print("\nVariant, rate over the whole light window:", (pvx.rate_light > pvx.rate_dark).sum(), "of", len(pvx), "rise, p =",
      round(stats.wilcoxon(pvx.rate_light, pvx.rate_dark, alternative="greater").pvalue, 3))
dd = (pvx.wmod_light - pvx.wmod_dark).dropna()
print("Variant, whisking modulation (Hz):", (dd > 0).sum(), "of", len(dd), "rise, median change", round(dd.median(), 2),
      ", p =", round(stats.wilcoxon(dd, alternative="greater").pvalue, 3))
rest = pvx[pvx.subject != pvx.subject.value_counts().index[0]]
dr = rest.rate_whisk_light - rest.rate_whisk_dark
print(f"Variant, without the mouse with most cells ({pvx.subject.value_counts().index[0]}): rises in {(dr > 0).sum()} of {len(rest)}, "
      f"p = {stats.wilcoxon(rest.rate_whisk_light, rest.rate_whisk_dark, alternative='greater').pvalue:.3f}")
print("By mouse (rise / cells):", pvx.assign(rise=d > 0).groupby("subject").rise.agg(["sum", "size"]).to_dict("index"))
jx = pvm[(pvm["mode"] == "juxtacellular") & pvm.included]
jx = jx[~jx.path.isin(pv[pv.suppressed].path)]
dj = (jx.rate_whisk_light - jx.rate_whisk_dark).dropna()
print(f"\nVariant: PV-Cre juxtacellular cells not suppressed by light, {len(dj)} from {jx.subject.nunique()} mice: "
      f"rise in {(dj > 0).sum()}, median {jx.rate_whisk_dark.median():.2f} Hz without light and {jx.rate_whisk_light.median():.2f} Hz with, "
      f"median change {dj.median():.2f} Hz, p = {stats.wilcoxon(dj, alternative='greater').pvalue:.3g}; spikes during whisking per cell "
      f"without light median {int(jx.whisk_spk_dark.median())}")
PV-Cre excitatory cells with light: 7, included 7
Rate during non-touch whisking inside the light window: rises in 6 of 7; median 0.05 Hz without light, 1.07 Hz with; median change 0.56 Hz; one-sided Wilcoxon p = 0.016. Criterion: rises in most and p < 0.05 reproduced.
  subject       window  lit_trials  dark_trials  whisk_s_dark  whisk_s_light  whisk_spk_dark  whisk_spk_light  rate_whisk_dark  rate_whisk_light  rate_dark  rate_light  wmod_dark  wmod_light
anm244028 (0.82, 3.32)          58          112         51.68          32.73              58               55             1.12              1.68       0.98        1.54       0.24        0.19
anm266945 (0.82, 2.32)          35           79         20.25           8.38               1                9             0.05              1.07       0.18        1.28       0.01        0.00
anm266945 (0.82, 2.32)          73          122         41.27          25.41               1               10             0.02              0.39       0.04        0.71       0.05        0.05
anm266945 (0.82, 2.32)           5           11          2.88           1.15               0                0             0.00              0.00       2.48        1.07        NaN        0.00
anm266945 (0.82, 2.32)          33           56         10.79           5.15              30               27             2.78              5.24       3.37        2.81       2.01        4.55
anm266951 (0.92, 1.92)           5           15          4.83           2.10               0                1             0.00              0.48       1.67        2.20       0.00        1.09
anm300172 (0.73, 2.73)          61           84         24.20          21.18               6               46             0.25              2.17       0.88        2.58       0.00        0.81

Behaviour inside the light window, per trial (median over cells), without and with light: whisking time (s) 0.288 and 0.347 ; touches 3.59 and 3.4

Variant, rate over the whole light window: 5 of 7 rise, p = 0.188
Variant, whisking modulation (Hz): 3 of 6 rise, median change 0.4 , p = 0.219
Variant, without the mouse with most cells (anm266945): rises in 3 of 3, p = 0.125
By mouse (rise / cells): {'anm244028': {'sum': 1, 'size': 1}, 'anm266945': {'sum': 3, 'size': 4}, 'anm266951': {'sum': 1, 'size': 1}, 'anm300172': {'sum': 1, 'size': 1}}

Variant: PV-Cre juxtacellular cells not suppressed by light, 32 from 8 mice: rise in 24, median 5.27 Hz without light and 6.61 Hz with, median change 1.74 Hz, p = 0.000419; spikes during whisking per cell without light median 61

Result: reproduced. In PV-Cre mice, with the vS1 light on, the spike rate of excitatory cells during non-touch whisking rises in 6 of 7 cells (median 0.05 to 1.07 Hz, p = 0.016), from 4 mice. The evidence is thin: 4 cells come from anm266945, and without that mouse the rate rises in 3 of 3 (p = 0.125, the smallest possible with 3 cells). Several rates rest on 0 to 10 spikes during whisking. Over the whole light window the rise is not significant (5 of 7, p = 0.188), nor is the rise of the phase modulation (3 of 6, p = 0.219). The supplement confirms that this light drives eNpHR in PV cells; it cuts the firing of 3 PV-Cre cells by more than half (data check), as in Supp. Fig. 13a (3 of 4 PV cells). The mice whisk and touch about as much with the light as without (0.35 against 0.29 s of whisking and 3.4 against 3.6 touches per trial). The PV-Cre juxtacellular cells not suppressed by light, which Supplementary Table 2 suggests are mostly excitatory (38 of 42 cell-attached PV-Cre cells), show the same rise in 24 of 32 (p = 0.00042). As the claims file says, that selection is biased toward a rise, so it does not decide the outcome.

fs-enhance-touch-selectivity¶

By suppressing movement-related spiking, FS interneurons enhance the selectivity of L4 excitatory neurons for touch.

In [18]:
s = pvx.dropna(subset=["sel_light", "sel_dark"])
ds = s.sel_light - s.sel_dark
p = stats.wilcoxon(s.sel_light, s.sel_dark, alternative="less").pvalue if (ds != 0).any() else np.nan
print(f"Excitatory cells with selectivity defined in both conditions: {len(s)} of {len(pvx)}")
print(f"Touch selectivity lower with light in {(ds < 0).sum()} of {len(s)}; median {s.sel_dark.median():.3f} without light, "
      f"{s.sel_light.median():.3f} with; one-sided Wilcoxon p = {p:.3f}. Criterion: lower in most and p < 0.05 reproduced.")
print(s[["subject", "touch_dark", "touch_light", "wmod_dark", "wmod_light", "sel_dark", "sel_light"]].round(3).to_string(index=False))
t = pvx.dropna(subset=["touch_light", "touch_dark"])
s0 = pvx.dropna(subset=["sel_raw_light", "sel_raw_dark"])
print("First version (uncorrected selectivity): lower with light in", (s0.sel_raw_light < s0.sel_raw_dark).sum(), "of", len(s0),
      ", median", round(s0.sel_raw_dark.median(), 3), "->", round(s0.sel_raw_light.median(), 3))
print("\nVariant, touch response (Hz) with and without light: rises in", (t.touch_light > t.touch_dark).sum(), "of", len(t),
      ", median", round(t.touch_dark.median(), 2), "->", round(t.touch_light.median(), 2))
print("\nSpikes in the 25 ms after touches inside the light window, per cell (without light, with):",
      list(zip(pvx.touch_spk_dark, pvx.touch_spk_light)), "; touches:", list(zip(pvx.touches_dark, pvx.touches_light)))
sj = jx.dropna(subset=["sel_light", "sel_dark"])
dsj = sj.sel_light - sj.sel_dark
print(f"Variant: PV-Cre juxtacellular cells not suppressed by light ({len(sj)}): selectivity lower with light in {(dsj < 0).sum()}, "
      f"median {sj.sel_dark.median():.3f} without light and {sj.sel_light.median():.3f} with, one-sided p = "
      f"{stats.wilcoxon(sj.sel_light, sj.sel_dark, alternative='less').pvalue:.3g}; touch response rises in "
      f"{(sj.touch_light > sj.touch_dark).sum()}, whisking modulation rises in {(sj.wmod_light > sj.wmod_dark).sum()}")
Excitatory cells with selectivity defined in both conditions: 6 of 7
Touch selectivity lower with light in 2 of 6; median 0.083 without light, 0.949 with; one-sided Wilcoxon p = 0.953. Criterion: lower in most and p < 0.05 reproduced.
  subject  touch_dark  touch_light  wmod_dark  wmod_light  sel_dark  sel_light
anm244028      -0.344        4.642      0.241       0.188     0.000      0.961
anm266945      -0.574        8.615      0.006       0.000     0.000      1.000
anm266945      -0.105        2.258      0.049       0.047     0.000      0.980
anm266945       0.398       15.556      2.012       4.547     0.165      0.774
anm266951      19.394       16.000      0.000       1.089     1.000      0.936
anm300172       2.046        4.238      0.000       0.808     1.000      0.840
First version (uncorrected selectivity): lower with light in 1 of 5 , median 0.0 -> 0.87

Variant, touch response (Hz) with and without light: rises in 5 of 7 , median 0.4 -> 8.62

Spikes in the 25 ms after touches inside the light window, per cell (without light, with): [(13, 37), (1, 29), (0, 20), (40, 5), (4, 25), (18, 8), (34, 45)] ; touches: [(582, 293), (209, 130), (380, 248), (52, 15), (201, 54), (33, 15), (391, 302)]
Variant: PV-Cre juxtacellular cells not suppressed by light (30): selectivity lower with light in 14, median 0.885 without light and 0.881 with, one-sided p = 0.613; touch response rises in 20, whisking modulation rises in 18

Result: not reproduced. With the vS1 light on, touch selectivity is lower in 2 of 6 excitatory cells (median 0.083 without light, 0.949 with, p = 0.953). The touch response rises with the light (median 0.4 to 8.62 Hz) more than the whisking modulation does; the uncorrected version gives the same (lower in 1 of 5). Without light, three cells sit at selectivity 0 because their touch response inside the window is below zero, on 0 to 13 spikes after touches, so the measure is near its floor there. In the larger juxtacellular PV-Cre population not suppressed by light, selectivity is lower with light in 14 of 30 (median 0.885 against 0.881, p = 0.613), so no effect in the claim's direction is seen there either.

thalamus-drives-fs¶

Thalamic activity drives L4 FS interneurons.

In [19]:
rows = []
for r in l4:
    for site, info in r["sites"].items():
        if info["region"] != "VPM":
            continue
        e = light_effect(r, site)
        if e is None:
            continue
        w = yu2016.light_windows(r, site)
        row = {"path": r["path"], "subject": r["subject"], "genotype": r["genotype"], "mode": r["mode"],
               "cls": cl.set_index("path").cls[r["path"]], "pulses_per_trial": w.n_pulses.median(), **e}
        if r["principal"] is not None:
            a, b, _, nlit, ndark = light_measures(r, site)
            row.update(rate_whisk_light=a["rate_whisk"], rate_whisk_dark=b["rate_whisk"],
                       whisk_s_light=a["whisk_s"] / nlit, whisk_s_dark=b["whisk_s"] / ndark,
                       touches_light=a["n_touches"] / nlit, touches_dark=b["n_touches"] / ndark)
        rows.append(row)
th = pd.DataFrame(rows)
th["included"] = th.spikes >= yu2016.MIN_SPIKES
f = th[(th.cls == "FS") & th.included]
d = f.rate_light - f.rate_dark
p = stats.wilcoxon(f.rate_light, f.rate_dark, alternative="less").pvalue
print(f"Cells with light at the VPM site: {len(th)} (genotypes {th.genotype.replace('', '(none)').value_counts().to_dict()}); "
      f"tagged FS among them: {int((th.cls == 'FS').sum())}, included {len(f)}")
print(f"FS rate inside the light window: falls in {(d < 0).sum()} of {len(f)}; median {f.rate_dark.median():.1f} Hz without light, "
      f"{f.rate_light.median():.1f} Hz with (median ratio {(f.rate_light / f.rate_dark).median():.2f}); one-sided Wilcoxon p = {p:.3g}. "
      "Criterion: falls in most and p < 0.05 reproduced.")
print(f[["subject", "window", "pulses_per_trial", "lit_trials", "dark_trials", "rate_dark", "rate_light", "p"]].round(3).to_string(index=False))
fw = f.dropna(subset=["rate_whisk_light", "rate_whisk_dark"])
print(f"\nVariant, rate during non-touch whisking: falls in {(fw.rate_whisk_light < fw.rate_whisk_dark).sum()} of {len(fw)}, "
      f"p = {stats.wilcoxon(fw.rate_whisk_light, fw.rate_whisk_dark, alternative='less').pvalue:.3g}")
o = th[th.included]
print(f"Variant, every cell with VPM light and at least 20 spikes ({len(o)}): falls in {(o.rate_light < o.rate_dark).sum()}, "
      f"median ratio {(o.rate_light / o.rate_dark).median():.2f}; by class:",
      o.assign(fall=o.rate_light < o.rate_dark).groupby("cls").fall.agg(["sum", "size"]).to_dict("index"))
print("By mouse (FS falls / cells):", f.assign(fall=d < 0).groupby("subject").fall.agg(["sum", "size"]).to_dict("index"))
print("Behaviour inside the light window, per trial (median over FS cells), without and with light: whisking time (s)",
      round(fw.whisk_s_dark.median(), 3), "and", round(fw.whisk_s_light.median(), 3), "; touches",
      round(fw.touches_dark.median(), 2), "and", round(fw.touches_light.median(), 2))
Cells with light at the VPM site: 33 (genotypes {'VGAT-ChR2-EYFP': 31, '(none)': 2}); tagged FS among them: 9, included 9
FS rate inside the light window: falls in 9 of 9; median 17.9 Hz without light, 7.7 Hz with (median ratio 0.40); one-sided Wilcoxon p = 0.00195. Criterion: falls in most and p < 0.05 reproduced.
  subject         window  pulses_per_trial  lit_trials  dark_trials  rate_dark  rate_light     p
anm260070 (0.324, 2.818)             250.0          31           71     24.476       7.657 0.000
anm260070 (0.324, 2.818)             250.0          35           84     19.633       7.928 0.000
anm260070 (0.324, 2.818)             250.0          38          110      9.824       6.637 0.055
anm260070 (0.323, 2.818)             250.0          23           69     17.949       5.106 0.000
anm260070 (0.323, 2.818)             250.0          27           64     15.813       7.853 0.000
anm260070 (0.324, 2.819)             250.0          24           57     34.687       7.699 0.000
anm273348 (0.324, 2.818)             250.0          20           57     12.296       3.348 0.001
anm273348 (0.324, 2.818)             250.0          23           54     14.613       9.379 0.002
anm273348 (0.323, 2.818)             250.0          26           67     21.751      10.128 0.000

Variant, rate during non-touch whisking: falls in 9 of 9, p = 0.00195
Variant, every cell with VPM light and at least 20 spikes (21): falls in 18, median ratio 0.59; by class: {'EXC': {'sum': 1, 'size': 1}, 'FS': {'sum': 9, 'size': 9}, 'other': {'sum': 8, 'size': 11}}
By mouse (FS falls / cells): {'anm260070': {'sum': 6, 'size': 6}, 'anm273348': {'sum': 3, 'size': 3}}
Behaviour inside the light window, per trial (median over FS cells), without and with light: whisking time (s) 0.992 and 0.595 ; touches 4.45 and 5.13

Result: reproduced. With light at the VPM site, the firing of all 9 tagged FS cells falls (median 17.9 to 7.7 Hz, median ratio 0.40, p = 0.00195), and their rate during non-touch whisking falls in 9 of 9. The light is 250 pulses per trial (100 Hz, as in Supp. Fig. 9i). The mice whisk less with the light (0.60 against 0.99 s per trial in the window), so the fall of the whisking-period rate is the stronger evidence. The cells come from 2 mice, and the drop is not specific to FS cells: 18 of 21 cells recorded with this light fire less.

sensory-input-drives-fs¶

The movement-related activity of L4 FS interneurons is driven by sensory input (reafference through the thalamus), not by a corollary discharge from motor areas.

Not tested. The supplement shows the experiment: Figure 7 compares the spike-phase relationship of FS cells in control mice and in mice whose infraorbital nerve was transected (Supp. Figs. 11, 12). Supplementary Table 2 says 9 VGAT-ChR2 mice were transected, and their cells are in the dandiset, but no file says which mice they are (data check). Blocked: metadata insufficient. The cell below is not a test: it compares the tagged cells of the mice flagged in the data check as lacking touch input with the others.

In [20]:
# Not a test of the claim (blocked): tagged FS cells in mice flagged as lacking touch input, against the others.
fsx = inc[inc.cls == "FS"].assign(flagged=lambda d: d.subject.isin(no_input))
print(fsx.groupby("flagged").agg(cells=("path", "size"), mice=("subject", "nunique"), touch_responsive=("touch_resp", "mean"),
                                 whisk_modulated=("whisk_mod", "mean"), whisk_mod_hz=("whisk_mod_hz", "median"),
                                 r2=("whisk_r2", "median"), r2_above_025=("whisk_fit", "mean"), rate_whisk=("rate_whisk", "median")).round(3).to_string())
a_, b_ = fsx[fsx.flagged], fsx[~fsx.flagged]
if len(a_) and len(b_):
    print(f"Whisking modulation (Hz) lower in flagged mice: Mann-Whitney p = {stats.mannwhitneyu(a_.whisk_mod_hz, b_.whisk_mod_hz, alternative='less').pvalue:.3f}; "
          f"r2 lower: p = {stats.mannwhitneyu(a_.whisk_r2, b_.whisk_r2, alternative='less').pvalue:.3f}")
print(fsx[fsx.flagged][["subject", "n_spikes", "touch_resp_hz", "touch_p", "whisk_mod_hz", "whisk_p", "whisk_r2"]].round(4).to_string(index=False))
         cells  mice  touch_responsive  whisk_modulated  whisk_mod_hz     r2  r2_above_025  rate_whisk
flagged                                                                                               
False       16     8             0.812            0.812         2.898  0.534          0.75      14.880
True         4     2             0.000            0.750         1.121  0.497          1.00      10.795
Whisking modulation (Hz) lower in flagged mice: Mann-Whitney p = 0.061; r2 lower: p = 0.625
  subject  n_spikes  touch_resp_hz  touch_p  whisk_mod_hz  whisk_p  whisk_r2
anm273341    1604.0         1.3699   0.2696        1.0527   0.0066    0.3318
anm275231    1618.0         4.0000   0.1392        2.9756   0.0081    0.6663
anm275231    3075.0         1.6970   0.1273        1.1892   0.0057    0.6619
anm275231   12710.0         3.0126   0.2033        0.4982   0.2443    0.3138

In the 2 flagged mice that have included tagged cells, none of the 4 cells responds to touch, 3 are still modulated by whisking phase, and their median modulation is lower (1.12 against 2.90 Hz in 16 cells of 8 other mice, p = 0.061); the r² of the sinusoid fit is not lower (0.50 against 0.53). With 4 cells and an inferred label this says little either way.

Summary¶

Claim Outcome Main number
vpm-touch Reproduced 20 of 24 VPM units touch-responsive (shifted null 0 of 24)
vpm-whisking Reproduced 21 of 24 units modulated by whisking phase (shifted null 0.12)
fs-touch-and-whisking Reproduced tagged FS cells: touch 0.65, whisking 0.80 of 20
exc-mainly-touch Not reproduced selectivity above FS p = 0.074, above VPM p = 0.090
fs-suppress-movement-spiking Reproduced whisking rate rises in 6 of 7 excitatory cells, p = 0.016
fs-enhance-touch-selectivity Not reproduced selectivity lower with light in 2 of 6, p = 0.953
thalamus-drives-fs Reproduced FS rate falls with VPM light in 9 of 9, p = 0.00195
sensory-input-drives-fs Blocked (metadata insufficient) ION-transected mice not marked in the files

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

How the analysis differs from the paper's. The full text was not available; the claims are those of the abstract and figure titles, and the supplement was read only after the first run. No number of the paper's main text is tested. Every measure is a choice made here: the touch window (25 ms), the whisking band and threshold, the phase modulation and its null (the paper's sinusoid r² is shown as a variant), the touch selectivity index and the inclusion rules. FS cells are light-tagged GABAergic cells in VGAT-ChR2 mice, not cells inside the paper's waveform boundary, and excitatory cells are broad-spiking untagged whole-cell cells; the paper's excitatory groups also hold cell-attached cells. Cells from ION-transected mice, which the paper keeps apart, may be in both groups because the files do not mark them; the 4 mice inferred here to be transected contribute 3 excitatory and 4 included FS cells. Leaving them out is shown as a variant. Under it the selectivity test of exc-mainly-touch passes (p = 0.024 and 0.038), but the outcome stays not reproduced, because the flag is drawn from touch responses and favours the claim. The whisking test of the claims file (Rayleigh) was replaced after the first run, when its null control failed; both are shown. Membrane potential, the subject of Figures 2 to 4, is not analysed except to find cells with a touch input.

What was found about the dandiset.

  • No file names the cell type of its L4 neuron, the opsin, or what each light condition does; photo_stim_mode has no description, its labels do not map to one kind of light, and photo_stim_power is empty in every L4 file.
  • No file marks the ION-transected mice of Figure 7 (9 VGAT-ChR2 mice in Supplementary Table 2); 4 mice whose cells have no touch input are probably among them.
  • The genotype is missing for the 6 VPM mice and 3 L4 mice, and SOM-IRES-Cre x Ai32 mice are labelled Ai32.
  • Every L4 file gives layer 4, though the paper's waveform analysis includes L5 cells.
  • The session description gives the wrong recording mode for 24 of 130 L4 files, all from PV-Cre mice (23 described as whole-cell hold juxtacellular traces); by the trace, PV-Cre has 9 whole-cell cells where Supplementary Table 2 has 5.
  • The L4 trace is sampled at 10 kHz in files up to March 2015 and at 20 kHz from late August 2015, which only the timestamps show.
  • In the L4 files first_touch_time is the trial start plus the true latency divided by 1000; in the VPM files it is correct.
  • 8 files name no principal whisker.
  • No recording is deposited twice: every trace is distinct, and files of equal length differ.