Mouse eyepuff behaviour: Kauvar, Richman, Liu et al. (2025), dandiset 001347¶

Paper. Kauvar IV, Richman EB, Liu TX, et al. (2025). Conserved brain-wide emergence of emotional response from sensory experience in humans and mice. Science 388, eadt3971. doi:10.1126/science.adt3971. The claims file was written from the version of record (29 May 2025) with Materials and Methods, figure legends and both supplement PDFs, including Table S3.

Dandiset. DANDI:001347, version 0.250528.0702, the only published version. The paper cites the dandiset without a version, so the latest published version is pinned (see dandi_io.py). The mouse spiking data of the paper are in dandiset 001326; this notebook reads it only to count its sessions.

What this notebook reads. The 142 head-fixed mouse behaviour files sub-mouse/sub-mouse_ses-<experiment id>_behavior.nwb (one eye-closure trace at 200 Hz each) and the 10 Neuropixels-rig behaviour files sub-m<mouse>-<id>/..._ses-puff_behavior.nwb.

Outside input. The sub-mouse files carry no drug, dose, mouse, sex, day or pressure. The experiment lists that give these labels are in the paper's scripts (mouse_eyepuff/scripts/fig1_*.py) in the code record Zenodo 10.5281/zenodo.14920570 (kauvar-richman-liu-2025_zenodo.zip, md5 bd1fb567121aaf0291337783df57bcfc, git commit b873ca5). The notebook fetches the zip at run time, checks its md5 and parses the lists with ast. These lists only name codes the dataset holds, so the claims that use them are data: with_labels, and the link claim mouse-session-labels-link tests them. The processing follows ketmouse/eye_trace_utils.py of the same record, reimplemented in mouse_eye.py.

No random numbers are drawn anywhere in this notebook.

Claim Data Status
mouse-session-labels-link with labels tested
fig1k-mouse-late-eye-closure-ketamine with labels tested
fig1l-mouse-dissociatives-reduce-late with labels tested
figs2bc-mouse-ketamine-early-present-late-abolished with labels tested
fig1m-mouse-late-closure-accumulates-and-persists with labels tested
figs2g-mouse-scaling-with-puff-number-not-pressure with labels tested
figs2h-mouse-dose-threshold with labels tested
figs5c-mouse-other-drugs-no-disconnection with labels tested
fig1p-mouse-anesthesia-abolishes-both with labels tested
fig1no-mouse-dissociatives-no-accumulation with labels tested
figs2l-mouse-repeatable-across-days with labels tested
figs2n-mouse-no-sex-difference with labels tested
figs2o-mouse-no-anticipation with labels tested
figs4-mouse-reward-seeking-reduced dataset blocked: lick data not in the dataset
figs6-mouse-vpm-inhibition-reduces-affective with labels tested
fig4b-mouse-recorded-affective-reduced dataset tested (10 of 13 sessions)
In [1]:
import collections
import hashlib
import time
import warnings

import numpy as np
import pandas as pd
import scipy.stats as st
import yaml
import matplotlib.pyplot as plt
from lifelines import KaplanMeierFitter
from statsmodels.stats.multitest import multipletests

import dandi_io
import mouse_eye as me

T0 = time.time()
warnings.filterwarnings("ignore", message="Mean of empty slice")   # NaN frames in the DeepLabCut traces
pd.set_option("display.width", 200)
np.set_printoptions(precision=3, suppress=True, linewidth=160)

CLAIMS = yaml.safe_load(open("../../claims/001347/10.1126-science.adt3971.yaml"))
CLAIM = {c["id"]: c for c in CLAIMS["claims"]}


def expected(cid):
    # Expected values of a claim, read from the claims file, for comparison only.
    return [(e["quantity"], e["value"], e["tolerance"]) for e in CLAIM[cid].get("expected", [])]


def bh(p):
    return multipletests(p, method="fdr_bh")[1]


def within(x, v, tol):
    return abs(x - v) <= tol + 1e-12


print("dandiset", dandi_io.DANDISET, dandi_io.VERSION)
dandiset 001347 0.250528.0702

Data check¶

The code record and its experiment lists¶

The zip is fetched from Zenodo and its md5 checked. Each script's literal assignments are read (experiments, ntrials, SKIP_PUFFS, expids). The trial selections and windows of each script are not literals; mouse_eye.py gives them with the script they come from.

In [2]:
zf = me.fetch_code_zip()
LISTS = me.code_lists(zf)
for script, v in LISTS.items():
    keys = {k: v[k] for k in ("ntrials",) if k in v}
    if "experiments" in v:
        print(f"{script}: ntrials={keys.get('ntrials')}")
        for cond, ids in v["experiments"].items():
            print(f"    {cond:9s} {ids}")
    if "SKIP_PUFFS" in v:
        print(f"    SKIP_PUFFS {v['SKIP_PUFFS']}")
    if "expids" in v:
        print(f"{script}: expids {v['expids']}")
fig1_drugs: ntrials=40
    ket50     [275, 276, 277, 280, 281]
    pcp20     [287, 288, 289, 290, 291]
    sal       [282, 283, 284, 285, 286]
    kx        [316, 338, 339, 343, 344]
    dzp       [325, 326, 327, 333, 334]
    bup       [340, 341, 342, 347, 348]
fig1_drugs_first_puff: ntrials=40
    ket50     [275, 276, 277, 280, 281]
    pcp20     [287, 288, 289, 290, 291]
    sal       [282, 283, 284, 285, 286]
    kx        [316, 338, 339, 343, 344]
    dzp       [325, 326, 327, 333, 334]
    bup       [340, 341, 342, 347, 348]
fig1_dose: ntrials=40
    ket50     [275, 276, 277, 280, 281]
    pcp20     [287, 288, 289, 290, 291]
    sal       [282, 283, 284, 285, 286]
    kx        [316, 338, 339, 343, 344]
    ket25     [355, 356, 357, 362, 363]
    pcp10     [358, 359, 360, 361, 364]
    ket13     [401, 400, 399, 398, 397]
    ket6      [403, 405, 404, 406, 426]
    pcp5      [370, 368, 366, 365, 434]
    pcp1      [430, 432, 431, 435, 436]
    ket35     [1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027]
fig1_psi: ntrials=20
    18 PSI    [1004, 1001, 1002, 1003, 1030, 1031, 1029, 1028]
    8 PSI     [1034, 1033, 1032, 1035, 1008, 1005, 1007, 1006]
    5 PSI     [1014, 1016, 1019, 1017, 1011, 1012, 1013, 1009]
    28 PSI    [1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027]
fig1_days: ntrials=40
    day1      [668, 669, 670, 671, 674]
    day2      [672, 673, 677, 678, 679]
    day3      [675, 676, 682, 683, 684]
    day4      [680, 681, 687, 688, 689]
    day5      [685, 686, 690, 691, 692]
fig1_sex: ntrials=40
    female    [282, 283, 284, 285, 286]
    male      [700, 701, 702, 708, 709]
fig1_muscimol: ntrials=20
    intact    [975, 971, 972, 973, 974]
    sal       [984, 985, 980, 981, 982]
    vpm_musc  [989, 990, 986, 987, 988]
fig1_persistence: ntrials=40
    ket50     [275, 276, 277, 280, 281]
    pcp20     [287, 288, 289, 290, 291]
    sal       [282, 283, 284, 285, 286]
    kx        [316, 338, 339, 343, 344]
    dzp       [325, 326, 327, 333, 334]
    bup       [340, 341, 342, 347, 348]
fig1_skippuff: ntrials=40
    ket50     [411, 412, 413, 420, 421]
    SKIP_PUFFS [6, 6, 5, 6, 5, 5, 5, 6, 6, 5, 5, 6, 5, 6, 5, 6, 6, 5, 6, 5]
fig1_npx_behavior: expids ['m94-549', 'm304-876', 'm11208-959', 'm11208-962', 'm11210-960', 'm11219-1108', 'm11221-1119', 'm11223-1125', 'm11225-1131', 'm11227-1132']
convert_npx_mouse_airpuff_to_nwb: expids ['20220228_m94_549', '20220831_m304_876', '20230218_m11208_959', '20230226_m11208_962', '20230218_m11210_960', '20230823_m11219_1108', '20230901_m11221_1119', '20230902_m11223_1125', '20230907_m11225_1131', '20230908_m11227_1132']

The head-fixed mouse files¶

All 142 sub-mouse files are streamed (about a minute). For each file the check prints the NWB metadata that could label it, the sampling rate, the number of 81-s trials (16200 frames), the range of the trace and missing values.

In [3]:
IDS = me.mouse_ids()
DATA, META = me.load_all(IDS)
meta = pd.DataFrame(META).T
meta["n_frames"] = [len(DATA[e]) for e in meta.index]
meta["n_trials"] = meta["n_frames"] / me.FRAMES_PER_TRIAL
meta["min"] = [DATA[e].min() for e in meta.index]
meta["max"] = [DATA[e].max() for e in meta.index]
meta["n_nan"] = [int(np.isnan(DATA[e]).sum()) for e in meta.index]
print("files:", len(IDS))
print("identifier equals file session id:", all(meta.loc[e, "identifier"] == str(e) for e in IDS))
print("subject_id equals session id:", all(meta.loc[e, "subject_id"] == str(e) for e in IDS))
for col in ["rate", "unit", "sex", "age", "subject_description"]:
    print(f"{col}: {dict(collections.Counter(meta[col]))}")
print("session_description examples:", list(meta["session_description"][:3]))
print("trials per file:", dict(collections.Counter(meta["n_trials"])))
print("whole trials only:", bool((meta["n_frames"] % me.FRAMES_PER_TRIAL == 0).all()))
print("20-trial sessions:", [e for e in IDS if meta.loc[e, "n_trials"] == 20])
print(f"eye closure range {meta['min'].min():.3f} to {meta['max'].max():.3f} (correlation with a "
      f"closed-eye template, no unit); missing values {meta['n_nan'].sum()}")
files: 142
identifier equals file session id: True
subject_id equals session id: True
rate: {200.0: 142}
unit: {'Eye closure, sampled at 200.0Hz': 142}
sex: {'O': 142}
age: {'P8W/': 142}
subject_description: {'mouse_airpuff': 142}
session_description examples: ['Airpuff session 275', 'Airpuff session 276', 'Airpuff session 277']
trials per file: {40.0: 103, 20.0: 39}
whole trials only: True
20-trial sessions: [971, 972, 973, 974, 975, 980, 981, 982, 984, 985, 986, 987, 988, 989, 990, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1011, 1012, 1013, 1014, 1016, 1017, 1019, 1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035]
eye closure range -0.665 to 0.997 (correlation with a closed-eye template, no unit); missing values 0

The files hold one trace each. Sex is "O" and age "P8W/" in every file, the same placeholders the paper's converter writes (mouse_data_to_nwb), so the files say nothing about the mouse; the subject id is the session id. Drug, dose, day, pressure and mouse identity are not in the files.

Recordings deposited twice. Every pair of sessions is compared: identical arrays (md5), identical first 20 trials, any identical 81-s trial, and the Pearson correlation of the first 20 trials.

In [4]:
h_all = collections.defaultdict(list); h_20 = collections.defaultdict(list); h_trial = collections.defaultdict(list)
for e in IDS:
    x = DATA[e]
    h_all[hashlib.md5(x.tobytes()).hexdigest()].append(e)
    h_20[hashlib.md5(x[:20 * me.FRAMES_PER_TRIAL].tobytes()).hexdigest()].append(e)
    for i in range(len(x) // me.FRAMES_PER_TRIAL):
        h_trial[hashlib.md5(x[i * me.FRAMES_PER_TRIAL:(i + 1) * me.FRAMES_PER_TRIAL].tobytes()).hexdigest()].append((e, i))
print("identical files:", [v for v in h_all.values() if len(v) > 1])
print("identical first 20 trials:", [v for v in h_20.values() if len(v) > 1])
print("identical single trials across or within files:", [v for v in h_trial.values() if len(v) > 1])
X = np.stack([DATA[e][:20 * me.FRAMES_PER_TRIAL] for e in IDS])
X = X - X.mean(1, keepdims=True); X /= np.linalg.norm(X, axis=1, keepdims=True)
C = X @ X.T; np.fill_diagonal(C, -1)
order = np.dstack(np.unravel_index(np.argsort(C, axis=None)[::-1], C.shape))[0]
top = [(IDS[a], IDS[b], round(float(C[a, b]), 3)) for a, b in order[:10] if a < b]
print("highest correlations between sessions (first 20 trials):", top)
del X, C
identical files: []
identical first 20 trials: []
identical single trials across or within files: []
highest correlations between sessions (first 20 trials): [(1023, 1026, 0.912), (1026, 1029, 0.889), (681, 1023, 0.874), (343, 1023, 0.871), (343, 681, 0.864)]

Trial clock. The files have no timestamps or trial table; the code takes each 81-s block to start 6 s before the first puff, with puffs every 3 s. If that holds, the early response (0.1 to 0.2 s after a puff minus the 0.1 s before it) is large at the eight puff times and small at the mid-points between puffs. This is checked on the preinfusion trials of every 40-trial session and every trial of the 20-trial sessions.

In [5]:
def jump(tr, t0):
    return tr[:, me.window(t0, (0.1, 0.2))].mean(1) - tr[:, me.window(t0, (-0.1, 0.0))].mean(1)

rows = []
for e in IDS:
    nt = int(meta.loc[e, "n_trials"])
    tr = me.prox_norm(me.trials(DATA[e], nt))[:20]
    at = np.mean([jump(tr, 3 * k).mean() for k in range(8)])
    mid = np.mean([jump(tr, 3 * k + 1.5).mean() for k in range(7)])
    rows.append((e, at, mid))
clock = pd.DataFrame(rows, columns=["session", "jump_at_puffs", "jump_between_puffs"]).set_index("session")
print(clock.describe().round(3))
print("sessions where the response at puff times is not larger than between puffs:",
      list(clock.index[clock.jump_at_puffs <= clock.jump_between_puffs]))
       jump_at_puffs  jump_between_puffs
count        142.000             142.000
mean           0.447              -0.005
std            0.168               0.004
min            0.058              -0.015
25%            0.324              -0.007
50%            0.428              -0.004
75%            0.558              -0.002
max            1.041               0.006
sessions where the response at puff times is not larger than between puffs: []

The Neuropixels-rig files¶

Every acquisition of the 10 files is listed with its shape and, for scalars and short arrays, its value. events-on are trial onsets in frames at fps; the check confirms that eyeclosure_by_trial is eyeclosure cut from 3 s before each onset (w_pre) to 70 s after (w_post), with t_trial_eyeclosure running from -8 s, so that the first puff is 5 s (led_delay) after each onset, as analysis.py assumes (events['on'] + 5*fps).

In [6]:
NPX = {p: me.npx_load(p) for p in me.npx_paths()}
b0 = NPX[me.npx_paths()[0]]
for k, v in b0.items():
    if isinstance(v, np.ndarray):
        print(f"  {k:22s} shape {v.shape}" + (f" value {v}" if v.size <= 8 else ""))
rows = []
for p, b in NPX.items():
    fps = int(b["fps"][0]); on = b["events-on"]; bt = b["eyeclosure_by_trial"].T
    start = on[0] - int(b["w_pre"][0]) * fps
    seg = b["eyeclosure"][start:start + bt.shape[1]]
    ok = np.allclose(seg, bt[0], equal_nan=True)
    gaps = np.diff(on[:40]) / fps
    rows.append(dict(file=p.split("/")[0], subject=b["subject_id"], n_onsets=len(on),
                     n_puff_trials=b["eyeclosure_by_puff"].shape[0] // 8, ket_trial=int(b["ket_trial"][0]),
                     pre=f"{b['trial_range_preket'][0]}-{b['trial_range_preket'][-1]}",
                     ket=f"{b['trial_range_ket'][0]}-{b['trial_range_ket'][-1]}", fps=fps,
                     ref_w=tuple(b["ref_w"]), aff_w=tuple(b["aff_w"]), led_delay=int(b["led_delay"][0]),
                     by_trial_matches=ok, t_trial_first=b["t_trial_eyeclosure"][0],
                     median_gap_s=np.median(gaps), longest_gap_s=gaps.max(), longest_gap_after=int(gaps.argmax()) + 1,
                     nan_frac=np.isnan(b["eyeclosure"]).mean(),
                     ec_min=np.nanmin(b["eyeclosure"]), ec_max=np.nanmax(b["eyeclosure"])))
npx_tab = pd.DataFrame(rows)
print(npx_tab.to_string())
print("identical eye traces between files:",
      [v for v in collections.Counter(hashlib.md5(b["eyeclosure"].tobytes()).hexdigest() for b in NPX.values()).items() if v[1] > 1])
  aff_w                  shape (2,) value [0.3 0.8]
  events-off             shape (48,)
  events-on              shape (48,)
  eyeclosure             shape (558508,)
  eyeclosure_by_puff     shape (384, 400)
  eyeclosure_by_trial    shape (7300, 48)
  eyesize                shape (558508,)
  eyesize_by_puff        shape (384, 400)
  eyesize_by_trial       shape (7300, 48)
  fps                    shape (1,) value [100]
  ket_puff               shape (1,) value [160]
  ket_trial              shape (1,) value [20]
  led                    shape (558508,)
  led_delay              shape (1,) value [5]
  npuffs_per_trial       shape (1,) value [8]
  p_post                 shape (1,) value [2]
  p_pre                  shape (1,) value [2]
  puff_aff               shape (384,)
  puff_ref               shape (384,)
  ref_w                  shape (2,) value [0.  0.3]
  t_eyeclosure           shape (400,)
  t_trial_eyeclosure     shape (7300,)
  trial_range_ket        shape (8,) value [22 23 24 25 26 27 28 29]
  trial_range_preket     shape (20,)
  w_post                 shape (1,) value [70]
  w_pre                  shape (1,) value [3]
              file      subject  n_onsets  n_puff_trials  ket_trial   pre    ket  fps       ref_w       aff_w  led_delay  by_trial_matches  t_trial_first  median_gap_s  longest_gap_s  longest_gap_after  nan_frac  ec_min  ec_max
0   sub-m11208-959   m11208-959        48             48         20  0-19  22-29  100  (0.0, 0.3)  (0.3, 0.8)          5              True           -8.0        110.61         154.82                 20  0.002414     0.0     1.0
1   sub-m11208-962   m11208-962        59             59         20  0-19  22-29  100  (0.0, 0.3)  (0.3, 0.8)          5              True           -8.0        124.48         434.09                 20  0.008956     0.0     1.0
2   sub-m11210-960   m11210-960        48             48         20  0-19  22-29  100  (0.0, 0.3)  (0.3, 0.8)          5              True           -8.0        110.60         180.81                 20  0.000025     0.0     1.0
3  sub-m11219-1108  m11219-1108        58             58         20  0-19  22-29  100  (0.0, 0.3)  (0.3, 0.8)          5              True           -8.0        125.65         165.93                 20  0.011032     0.0     1.0
4  sub-m11221-1119  m11221-1119        58             58         20  0-19  22-29  100  (0.0, 0.3)  (0.3, 0.8)          5              True           -8.0        125.66         150.00                 20  0.008903     0.0     1.0
5  sub-m11223-1125  m11223-1125        58             58         20  0-19  22-29  100  (0.0, 0.3)  (0.3, 0.8)          5              True           -8.0        125.66         154.01                 20  0.011217     0.0     1.0
6  sub-m11225-1131  m11225-1131        58             58         20  0-19  22-29  100  (0.0, 0.3)  (0.3, 0.8)          5              True           -8.0        125.67         149.91                 20  0.008292     0.0     1.0
7  sub-m11227-1132  m11227-1132        58             58         20  0-19  22-29  100  (0.0, 0.3)  (0.3, 0.8)          5              True           -8.0        125.66         298.04                 20  0.011044     0.0     1.0
8     sub-m304-876     m304-876        50             50         20  0-19  22-29  100  (0.0, 0.3)  (0.3, 0.8)          5              True           -8.0        110.69         475.72                 20  0.000034     0.0     1.0
9      sub-m94-549      m94-549        79             40         20  0-19  22-29  100  (0.0, 0.3)  (0.3, 0.8)          5              True           -8.0        110.66         216.39                 20  0.000221     0.0     1.0
identical eye traces between files: []

Which Neuropixels sessions have behaviour. The paper's converter (convert_npx_mouse_airpuff_to_nwb.py) names each behaviour session by date, mouse and id; the Neuropixels dandiset 001326 names its 13 sessions by date. Matching dates shows which recorded sessions have no behaviour file here.

In [7]:
conv = LISTS["convert_npx_mouse_airpuff_to_nwb"]["expids"]
conv_dates = collections.defaultdict(list)
for x in conv:
    conv_dates[x.split("_")[0]].append(f"{x.split('_')[1]}-{x.split('_')[2]}")
npx_sessions = sorted(dandi_io.list_assets(dandi_io.NPX_DANDISET, dandi_io.NPX_VERSION))
print(f"001326 sessions: {len(npx_sessions)}")
have = set(p.split("/")[0].replace("sub-", "") for p in NPX)
for p in npx_sessions:
    date = p.split("ses-")[1].split("_")[0]
    beh = conv_dates.get(date, [])
    print(f"   {p:45s} behaviour session(s) in the converter for this date: {beh}; "
          f"in 001347: {bool(beh) and all(x in have for x in beh)}")
print("converter sessions:", conv, "; all in 001347:", all(f"{x.split('_')[1]}-{x.split('_')[2]}" in have for x in conv))
conv_src = zf.read(me.SCRIPTS + "convert_npx_mouse_airpuff_to_nwb.py").decode()
print("converter lines that name sessions it does not convert:")
print(me.grep_lines(conv_src, ["# extra", "#   "]))
001326 sessions: 13
   sub-0/sub-0_ses-20230901_ecephys.nwb          behaviour session(s) in the converter for this date: ['m11221-1119']; in 001347: True
   sub-1/sub-1_ses-20230218_ecephys.nwb          behaviour session(s) in the converter for this date: ['m11208-959', 'm11210-960']; in 001347: True
   sub-1/sub-1_ses-20230226_ecephys.nwb          behaviour session(s) in the converter for this date: ['m11208-962']; in 001347: True
   sub-2/sub-2_ses-20230902_ecephys.nwb          behaviour session(s) in the converter for this date: ['m11223-1125']; in 001347: True
   sub-3/sub-3_ses-20230218_ecephys.nwb          behaviour session(s) in the converter for this date: ['m11208-959', 'm11210-960']; in 001347: True
   sub-3/sub-3_ses-20230225_ecephys.nwb          behaviour session(s) in the converter for this date: []; in 001347: False
   sub-4/sub-4_ses-20230907_ecephys.nwb          behaviour session(s) in the converter for this date: ['m11225-1131']; in 001347: True
   sub-5/sub-5_ses-20230706_ecephys.nwb          behaviour session(s) in the converter for this date: []; in 001347: False
   sub-5/sub-5_ses-20230822_ecephys.nwb          behaviour session(s) in the converter for this date: []; in 001347: False
   sub-6/sub-6_ses-20230823_ecephys.nwb          behaviour session(s) in the converter for this date: ['m11219-1108']; in 001347: True
   sub-7/sub-7_ses-20220831_ecephys.nwb          behaviour session(s) in the converter for this date: ['m304-876']; in 001347: True
   sub-8/sub-8_ses-20220228_ecephys.nwb          behaviour session(s) in the converter for this date: ['m94-549']; in 001347: True
   sub-9/sub-9_ses-20230908_ecephys.nwb          behaviour session(s) in the converter for this date: ['m11227-1132']; in 001347: True
converter sessions: ['20220228_m94_549', '20220831_m304_876', '20230218_m11208_959', '20230226_m11208_962', '20230218_m11210_960', '20230823_m11219_1108', '20230901_m11221_1119', '20230902_m11223_1125', '20230907_m11225_1131', '20230908_m11227_1132'] ; all in 001347: True
converter lines that name sessions it does not convert:
   24  # extra = ['20220223_m95_544', '20230225_m11210_961',
   25  #          '20230706_m11217_1104', '20230822_m11217_1107',]

The longest gap between trial onsets falls after trial 20 in each file, where the injection is stored (ket_trial = 20); this is the only timing of the injection in either kind of file. The sub-mouse files have no trial onsets, so the Methods' rule (trials at least 3 min after the injection) cannot be applied to them (Open I).

Puff clock on the Neuropixels rig. Mean eye closure is compared at 0.1 to 0.3 s after each expected puff and in the 0.2 s before it, over the preinfusion trials.

In [8]:
for p, b in NPX.items():
    fps = int(b["fps"][0]); ec = b["eyeclosure"]
    on = b["events-on"][:20] + 5 * fps
    d = []
    for shift, puffs in ((0.0, range(8)), (1.5, range(7)), (0.0, [0]), (1.5, [0])):
        vals = []
        for o in on:
            for k in puffs:
                q = int(o + (3 * k + shift) * fps)
                vals.append(np.nanmean(ec[q + int(0.1 * fps):q + int(0.3 * fps)]) - np.nanmean(ec[q - int(0.2 * fps):q]))
        d.append(np.nanmean(vals))
    print(f"{p.split('/')[0]:18s} response at puff times {d[0]:.3f}, at mid-points {d[1]:.3f}; "
          f"first puff only {d[2]:.3f}, 1.5 s after it {d[3]:.3f}")
sub-m11208-959     response at puff times 0.195, at mid-points -0.009; first puff only 0.208, 1.5 s after it -0.006
sub-m11208-962     response at puff times 0.124, at mid-points -0.011; first puff only 0.125, 1.5 s after it -0.008
sub-m11210-960     response at puff times 0.286, at mid-points -0.009; first puff only 0.331, 1.5 s after it 0.001
sub-m11219-1108    response at puff times 0.098, at mid-points -0.009; first puff only 0.258, 1.5 s after it -0.017
sub-m11221-1119    response at puff times -0.005, at mid-points -0.004; first puff only 0.069, 1.5 s after it -0.009
sub-m11223-1125    response at puff times 0.251, at mid-points -0.026; first puff only 0.250, 1.5 s after it -0.039
sub-m11225-1131    response at puff times 0.180, at mid-points -0.007; first puff only 0.211, 1.5 s after it -0.006
sub-m11227-1132    response at puff times 0.225, at mid-points -0.021; first puff only 0.269, 1.5 s after it -0.002
sub-m304-876       response at puff times 0.297, at mid-points -0.033; first puff only 0.518, 1.5 s after it -0.031
sub-m94-549        response at puff times 0.212, at mid-points -0.012; first puff only 0.300, 1.5 s after it -0.035

In nine sessions the response at the expected puff times is clear and the mid-points show none. In m11221-1119 the response averaged over the eight puffs is near zero: there the eye closes at the first puff and stays closed through the series (first-puff value printed above), so the clock is consistent with the data but rests on the first puff only in that session.

Link claim: mouse-session-labels-link¶

The experiment lists in the paper's mouse scripts (code record, mouse_eyepuff/scripts/fig1_*.py) name sessions the dandiset holds, with the session lengths the scripts read, and the conditions they assign agree with what the eye-closure data show.

Criterion (written for this attempt): the match is by experiment id. It holds when (1) every listed id is a sub-mouse file; (2) every session has at least the number of trials its script reads (ntrials); (3) the data agree with the labels that leave a mark in them: (a) every ketamine 50 mg/kg and PCP 20 mg/kg session has lower late/early on the eighth puff in infusion trials (23 to 37) than in preinfusion trials (6 to 20), and the median change of the saline sessions is smaller in magnitude than that of either group; (b) in every K/X session, early and late eye closure on the eighth puff in infusion trials are below 0.1; (c) in the skip-puff sessions the puff with the smallest early response in each infusion trial is the one SKIP_PUFFS names, in at least 90% of trials. It holds in part when (1) and (2) hold and part of (3) fails. Mouse identity across lists (the code pairs sessions by list position), sex, day and dose cannot be checked from the data.

In [9]:
listed = {}
for script, v in LISTS.items():
    for cond, ids in v.get("experiments", {}).items():
        for e in ids:
            listed.setdefault(e, []).append((script, cond, v["ntrials"]))
in_data = set(IDS)
missing = sorted(set(listed) - in_data)
unlisted = sorted(in_data - set(listed))
print(f"(1) listed ids: {len(listed)}; in the dataset: {len(set(listed) & in_data)}; listed but missing: {missing}")
print(f"    sessions in the dataset in no list: {unlisted}")
short = [(e, s, c, n, int(meta.loc[e, 'n_trials'])) for e, uses in listed.items() for s, c, n in uses
         if e in in_data and meta.loc[e, "n_trials"] < n]
print(f"(2) sessions shorter than their script reads: {short}")
multi = {e: sorted({(s.replace('fig1_', ''), c) for s, c, _ in u}) for e, u in listed.items()
         if len({c for _, c, _ in u}) > 1}
print("    ids under more than one condition name:")
for e, u in sorted(multi.items()):
    print(f"      {e}: {u}")
twenty = sorted(e for e in IDS if meta.loc[e, "n_trials"] == 20)
twenty_scripts = {s for e in twenty for s, _, _ in listed[e]}
print(f"    the 20-trial sessions are listed only by: {sorted(twenty_scripts)}")
(1) listed ids: 142; in the dataset: 142; listed but missing: []
    sessions in the dataset in no list: []
(2) sessions shorter than their script reads: []
    ids under more than one condition name:
      282: [('dose', 'sal'), ('drugs', 'sal'), ('drugs_first_puff', 'sal'), ('persistence', 'sal'), ('sex', 'female')]
      283: [('dose', 'sal'), ('drugs', 'sal'), ('drugs_first_puff', 'sal'), ('persistence', 'sal'), ('sex', 'female')]
      284: [('dose', 'sal'), ('drugs', 'sal'), ('drugs_first_puff', 'sal'), ('persistence', 'sal'), ('sex', 'female')]
      285: [('dose', 'sal'), ('drugs', 'sal'), ('drugs_first_puff', 'sal'), ('persistence', 'sal'), ('sex', 'female')]
      286: [('dose', 'sal'), ('drugs', 'sal'), ('drugs_first_puff', 'sal'), ('persistence', 'sal'), ('sex', 'female')]
      1020: [('dose', 'ket35'), ('psi', '28 PSI')]
      1021: [('dose', 'ket35'), ('psi', '28 PSI')]
      1022: [('dose', 'ket35'), ('psi', '28 PSI')]
      1023: [('dose', 'ket35'), ('psi', '28 PSI')]
      1024: [('dose', 'ket35'), ('psi', '28 PSI')]
      1025: [('dose', 'ket35'), ('psi', '28 PSI')]
      1026: [('dose', 'ket35'), ('psi', '28 PSI')]
      1027: [('dose', 'ket35'), ('psi', '28 PSI')]
    the 20-trial sessions are listed only by: ['fig1_muscimol', 'fig1_psi']
In [10]:
E6 = LISTS["fig1_drugs"]["experiments"]
link_rows = []
for cond in ["sal", "ket50", "pcp20", "bup", "dzp", "kx"]:
    R = me.condition_values(DATA, E6[cond], 40, me.PRE_CODE, me.POST_CODE, 21)
    for i, e in enumerate(E6[cond]):
        link_rows.append(dict(cond=cond, session=e, pre_ratio=R["pre_ratio"][i], post_ratio=R["post_ratio"][i],
                              change=R["post_ratio"][i] - R["pre_ratio"][i],
                              post_early=R["post_early"][i], post_late=R["post_late"][i]))
link = pd.DataFrame(link_rows)
print(link.round(3).to_string(index=False))
med = link.groupby("cond")["change"].median()
a1 = bool((link[link.cond.isin(["ket50", "pcp20"])].change < 0).all())
a2 = bool(abs(med["sal"]) < min(abs(med["ket50"]), abs(med["pcp20"])))
kx = link[link.cond == "kx"]
b_ok = bool((kx.post_early < 0.1).all() and (kx.post_late < 0.1).all())
print(f"(3a) every ketamine and PCP session drops: {a1}; median change saline {med['sal']:.3f}, "
      f"ketamine {med['ket50']:.3f}, PCP {med['pcp20']:.3f}; saline smaller: {a2}")
print(f"(3b) K/X infusion early max {kx.post_early.max():.3f}, late max {kx.post_late.max():.3f}; both below 0.1: {b_ok}")

skip = np.array(LISTS["fig1_skippuff"]["SKIP_PUFFS"])
match_post, match_pre = [], []
for e in LISTS["fig1_skippuff"]["experiments"]["ket50"]:
    tr = me.prox_norm(me.trials(DATA[e], 40))
    J = np.stack([jump(tr, 3 * k) for k in range(8)], 1)
    inferred = J.argmin(1)
    match_pre.append((inferred[:20] == skip).mean()); match_post.append((inferred[20:] == skip).mean())
    print(f"     skip session {e}: SKIP_PUFFS matched in {match_pre[-1]:.0%} of preinfusion and {match_post[-1]:.0%} of infusion trials")
c_ok = bool(np.mean(match_post) >= 0.9)
print(f"(3c) skip-puff labels, infusion trials: {np.mean(match_post):.0%} matched; preinfusion {np.mean(match_pre):.0%}; holds: {c_ok}")
for e in [282, 316]:
    tr = me.prox_norm(me.trials(DATA[e], 40))
    J = np.stack([jump(tr, 3 * k) for k in range(8)], 1)
    print(f"     session {e} (also listed as sal_skip / kx_skip by the converter): smallest early response "
          f"over puffs in preinfusion trials, min over trials {J[:20].min(1).min():.3f}")
LINK_HOLDS = (not missing) and (not short) and a1 and a2 and b_ok and c_ok
print("link holds:", LINK_HOLDS)
 cond  session  pre_ratio  post_ratio  change  post_early  post_late
  sal      282      0.535       0.473  -0.062       0.795      0.376
  sal      283      0.594       0.632   0.038       0.796      0.503
  sal      284      0.617       0.622   0.006       0.624      0.388
  sal      285      0.621       0.849   0.228       0.918      0.780
  sal      286      0.920       0.947   0.027       0.761      0.720
ket50      275      0.678       0.212  -0.467       0.398      0.084
ket50      276      0.672      -0.130  -0.802       0.474     -0.062
ket50      277      0.666      -0.267  -0.933       0.250     -0.067
ket50      280      0.542      -0.035  -0.578       0.467     -0.017
ket50      281      0.734       0.039  -0.695       0.593      0.023
pcp20      287      0.702       0.244  -0.458       0.466      0.114
pcp20      288      0.704      -0.094  -0.798       0.393     -0.037
pcp20      289      0.616      -0.052  -0.668       0.410     -0.021
pcp20      290      0.591       0.060  -0.530       0.446      0.027
pcp20      291      0.935      -0.190  -1.125       0.427     -0.081
  bup      340      0.637       0.846   0.209       0.348      0.294
  bup      341      0.818       0.513  -0.306       0.743      0.381
  bup      342      0.839       0.784  -0.055       0.463      0.363
  bup      347      0.315       0.430   0.115       0.379      0.163
  bup      348      0.624       0.622  -0.002       0.420      0.261
  dzp      325      0.746       0.602  -0.144       0.520      0.313
  dzp      326      0.725       0.696  -0.029       0.592      0.412
  dzp      327      0.832       0.440  -0.392       0.407      0.179
  dzp      333      0.614       0.711   0.097       0.473      0.336
  dzp      334      0.822       0.631  -0.191       0.406      0.256
   kx      316      0.787       0.225  -0.562       0.041      0.009
   kx      338      0.563       0.818   0.255       0.062      0.051
   kx      339      0.586      -1.660  -2.246      -0.020      0.034
   kx      343      0.473       0.313  -0.160       0.046      0.014
   kx      344      0.728     -11.006 -11.735      -0.001      0.013
(3a) every ketamine and PCP session drops: True; median change saline 0.027, ketamine -0.695, PCP -0.668; saline smaller: True
(3b) K/X infusion early max 0.062, late max 0.051; both below 0.1: True
     skip session 411: SKIP_PUFFS matched in 100% of preinfusion and 100% of infusion trials
     skip session 412: SKIP_PUFFS matched in 100% of preinfusion and 100% of infusion trials
     skip session 413: SKIP_PUFFS matched in 95% of preinfusion and 100% of infusion trials
     skip session 420: SKIP_PUFFS matched in 80% of preinfusion and 100% of infusion trials
     skip session 421: SKIP_PUFFS matched in 100% of preinfusion and 100% of infusion trials
(3c) skip-puff labels, infusion trials: 100% matched; preinfusion 95%; holds: True
     session 282 (also listed as sal_skip / kx_skip by the converter): smallest early response over puffs in preinfusion trials, min over trials 0.421
     session 316 (also listed as sal_skip / kx_skip by the converter): smallest early response over puffs in preinfusion trials, min over trials 0.122
link holds: True

Result: the link holds. Every id the scripts list is a sub-mouse file and every file is listed (printed above), no session is shorter than its script reads, and the 20-trial sessions are those of the pressure and muscimol scripts. The data agree with the labels they can show: all ten ketamine and PCP sessions drop and the saline median change is near zero, all K/X infusion values are below 0.1, and the skip-puff positions match the code's SKIP_PUFFS in the infusion trials, all printed above. Ids 1020 to 1027 serve both as ketamine 35 mg/kg (40 trials) and as 28 PSI (first 20 trials), and 282 to 286 both as saline and as the female group. The mouse behind each session, its sex, the day and the dose are not in the files and rest on the lists.

fig1k-mouse-late-eye-closure-ketamine¶

In mice, late eye closure normalised by early eye closure on the first puff of each trial is lower with ketamine (50 mg/kg) than with saline (-0.07 against 0.26).

M-EYE on the first puff (fig1_drugs_first_puff.py): each trial minus its first 2 s, per mouse mean late (0.3 to 0.8 s) over mean early (0.1 to 0.2 s), infusion trials of the saline session against those of the ketamine session, sessions paired by list position. Open I: (a) the code's trials 6 to 20 and 23 to 37; (b) trials 3 to 20 and 23 to 40 (18 trials, the fig. S2B and S2C legends), since the files carry no trial times for the 3-min rule. The late/early ratio does not depend on the scale factor; absolute values are shown under the code's scale (one) and the Methods' (1/(1 - raw baseline of the first two trials)).

In [11]:
READINGS = {"a (code, 6-20 / 23-37)": (me.PRE_CODE, me.POST_CODE), "b (3-20 / 23-40)": (me.PRE_B, me.POST_B)}
FP = {}
for rname, (sp, sq) in READINGS.items():
    for scale in ("code", "methods"):
        FP[(rname, scale)] = {d: me.condition_values(DATA, E6[d], 40, sp, sq, 0, scale) for d in ("sal", "ket50", "pcp20")}
for rname in READINGS:
    R = FP[(rname, "code")]
    t = st.ttest_rel(R["sal"]["post_ratio"], R["ket50"]["post_ratio"])
    print(f"reading {rname}")
    for d in ("sal", "ket50"):
        print(f"   {d:6s} per mouse {R[d]['post_ratio'].round(3)}  mean {R[d]['post_ratio'].mean():.3f} "
              f"s.e.m. {st.sem(R[d]['post_ratio']):.3f}")
    print(f"   paired t = {t.statistic:.3f}, p = {t.pvalue:.4f}")
exp = expected("fig1k-mouse-late-eye-closure-ketamine")
R = FP[(list(READINGS)[0], "code")]
fig1k_vals = [R["sal"]["post_ratio"].mean(), R["ket50"]["post_ratio"].mean()]
for (q, v, tol), x in zip(exp, fig1k_vals):
    print(f"expected {q}: {v} +/- {tol}; measured {x:.3f}; within: {within(x, v, tol)}")
reading a (code, 6-20 / 23-37)
   sal    per mouse [0.225 0.217 0.144 0.208 0.515]  mean 0.262 s.e.m. 0.065
   ket50  per mouse [ 0.122 -0.161 -0.191 -0.088 -0.019]  mean -0.068 s.e.m. 0.056
   paired t = 4.737, p = 0.0091
reading b (3-20 / 23-40)
   sal    per mouse [0.205 0.234 0.161 0.22  0.539]  mean 0.272 s.e.m. 0.068
   ket50  per mouse [ 0.087 -0.163 -0.172 -0.061 -0.011]  mean -0.064 s.e.m. 0.049
   paired t = 4.751, p = 0.0090
expected late/early first puff, saline: 0.26 +/- 0.06; measured 0.262; within: True
expected late/early first puff, ketamine: -0.07 +/- 0.06; measured -0.068; within: True
In [12]:
R = FP[(list(READINGS)[0], "code")]
t = np.arange(me.FRAMES_PER_TRIAL) / me.FPS - me.PRE_S
fig, ax = plt.subplots(1, 2, figsize=(8, 2.6), sharey=True)
rng_txt = []
for a, d in zip(ax, ("sal", "ket50")):
    pre = np.mean([me.mean_trace(DATA, e, 40, me.PRE_CODE) for e in E6[d]], 0)
    post = np.mean([me.mean_trace(DATA, e, 40, me.POST_CODE) for e in E6[d]], 0)
    a.plot(t, pre, lw=0.7, label="preinfusion"); a.plot(t, post, lw=0.7, label="infusion")
    a.set_xlim(-1, 30); a.set_title(d); a.set_xlabel("time from first puff (s)")
    rng_txt.append(f"{d}: pre {pre.min():.2f} to {pre.max():.2f}, infusion {post.min():.2f} to {post.max():.2f}")
ax[0].set_ylabel("eye closure (baseline subtracted)"); ax[0].legend(frameon=False, fontsize=7)
plt.tight_layout(); plt.show()
print("plotted ranges over the whole trial:", "; ".join(rng_txt))
No description has been provided for this image
plotted ranges over the whole trial: sal: pre -0.02 to 0.91, infusion -0.03 to 0.89; ket50: pre -0.02 to 0.86, infusion -0.04 to 0.64

Result: reproduced. Under reading (a) the saline and ketamine means are those printed above, both within tolerance, with paired t = 4.737 and p = 0.009 across five mice, the values of Table S3; reading (b) gives the same conclusion (printed above).

fig1l-mouse-dissociatives-reduce-late¶

Ketamine (50 mg/kg) and PCP (20 mg/kg) reduce late/early eye closure on the first puff relative to preinfusion, while saline does not.

Preinfusion against infusion within each session, paired t-tests across mice, Benjamini-Hochberg over the three drugs (fig1_drugs.py, tpuff = 0). Both Open I readings.

In [13]:
exp = expected("fig1l-mouse-dissociatives-reduce-late")
for rname in READINGS:
    R = FP[(rname, "code")]
    tests = [st.ttest_rel(R[d]["pre_ratio"], R[d]["post_ratio"]) for d in ("sal", "ket50", "pcp20")]
    p = bh([x.pvalue for x in tests])
    print(f"reading {rname}")
    for d, x, pc in zip(("sal", "ket50", "pcp20"), tests, p):
        print(f"   {d:6s} pre {R[d]['pre_ratio'].mean():.3f}  infusion {R[d]['post_ratio'].mean():.3f}  "
              f"t = {x.statistic:.3f}  BH p = {pc:.4f}  mice lower on infusion: {(R[d]['post_ratio'] < R[d]['pre_ratio']).sum()}/5")
R = FP[(list(READINGS)[0], "code")]
fig1l_vals = [R["sal"]["pre_ratio"].mean(), R["sal"]["post_ratio"].mean(), R["ket50"]["pre_ratio"].mean(),
              R["ket50"]["post_ratio"].mean(), R["pcp20"]["pre_ratio"].mean(), R["pcp20"]["post_ratio"].mean()]
for (q, v, tol), x in zip(exp, fig1l_vals):
    print(f"expected {q}: {v} +/- {tol}; measured {x:.3f}; within: {within(x, v, tol)}")
reading a (code, 6-20 / 23-37)
   sal    pre 0.286  infusion 0.262  t = 0.733  BH p = 0.5041  mice lower on infusion: 3/5
   ket50  pre 0.371  infusion -0.068  t = 9.525  BH p = 0.0020  mice lower on infusion: 5/5
   pcp20  pre 0.349  infusion -0.033  t = 3.409  BH p = 0.0406  mice lower on infusion: 5/5
reading b (3-20 / 23-40)
   sal    pre 0.293  infusion 0.272  t = 0.527  BH p = 0.6259  mice lower on infusion: 2/5
   ket50  pre 0.390  infusion -0.064  t = 8.440  BH p = 0.0032  mice lower on infusion: 5/5
   pcp20  pre 0.372  infusion -0.035  t = 3.532  BH p = 0.0363  mice lower on infusion: 5/5
expected pre-saline: 0.29 +/- 0.08; measured 0.286; within: True
expected saline: 0.26 +/- 0.06; measured 0.262; within: True
expected pre-ketamine: 0.37 +/- 0.06; measured 0.371; within: True
expected ketamine: -0.07 +/- 0.06; measured -0.068; within: True
expected pre-PCP: 0.35 +/- 0.1; measured 0.349; within: True
expected PCP: -0.03 +/- 0.05; measured -0.033; within: True

Result: reproduced. Under reading (a) ketamine (t = 9.53, corrected p = 0.002) and PCP (t = 3.41, corrected p = 0.041) fall on infusion, saline does not (corrected p = 0.504), and all six means are within tolerance (printed above). Reading (b) gives the same pattern of tests.

figs2bc-mouse-ketamine-early-present-late-abolished¶

With ketamine, early eye closure on the first puff decreases but remains (0.78 to 0.45), while late eye closure is abolished (0.34 to -0.02), compared with saline.

As fig1k, for early and late separately (fig1_drugs_first_puff.py prints both). Absolute values depend on the scale factor, so both the code's scale and the Methods' are shown, each under both Open I readings.

In [14]:
exp = expected("figs2bc-mouse-ketamine-early-present-late-abolished")
s2bc = {}
for (rname, scale), R in FP.items():
    te = st.ttest_rel(R["sal"]["post_early"], R["ket50"]["post_early"])
    tl = st.ttest_rel(R["sal"]["post_late"], R["ket50"]["post_late"])
    vals = [R["sal"]["post_early"].mean(), R["ket50"]["post_early"].mean(), R["sal"]["post_late"].mean(), R["ket50"]["post_late"].mean()]
    s2bc[(rname, scale)] = vals
    print(f"reading {rname}, scale {scale}: early saline {vals[0]:.3f} ketamine {vals[1]:.3f} (t = {te.statistic:.3f}, "
          f"p = {te.pvalue:.4f}); late saline {vals[2]:.3f} ketamine {vals[3]:.3f} (t = {tl.statistic:.3f}, p = {tl.pvalue:.4f})")
R = FP[(list(READINGS)[0], "code")]
print("late, saline infusion trials, per mouse:", R["sal"]["post_late"].round(3), "s.e.m.", round(st.sem(R["sal"]["post_late"]), 3),
      "median", round(np.median(R["sal"]["post_late"]), 3))
print("late, saline preinfusion trials of the same sessions, mean:", round(R["sal"]["pre_late"].mean(), 3))
print("ketamine early above zero in every mouse:", bool((R["ket50"]["post_early"] > 0).all()))
for (q, v, tol), x in zip(exp, s2bc[(list(READINGS)[0], "code")]):
    print(f"expected {q}: {v} +/- {tol}; measured {x:.3f}; within: {within(x, v, tol)}")
reading a (code, 6-20 / 23-37), scale code: early saline 0.778 ketamine 0.451 (t = 6.854, p = 0.0024); late saline 0.203 ketamine -0.025 (t = 4.951, p = 0.0078)
reading a (code, 6-20 / 23-37), scale methods: early saline 0.722 ketamine 0.463 (t = 2.873, p = 0.0453); late saline 0.186 ketamine -0.020 (t = 5.155, p = 0.0067)
reading b (3-20 / 23-40), scale code: early saline 0.770 ketamine 0.430 (t = 6.065, p = 0.0037); late saline 0.208 ketamine -0.024 (t = 4.908, p = 0.0080)
reading b (3-20 / 23-40), scale methods: early saline 0.715 ketamine 0.441 (t = 2.993, p = 0.0402); late saline 0.191 ketamine -0.020 (t = 5.164, p = 0.0067)
late, saline infusion trials, per mouse: [0.191 0.17  0.089 0.19  0.374] s.e.m. 0.047 median 0.19
late, saline preinfusion trials of the same sessions, mean: 0.24
ketamine early above zero in every mouse: True
expected early, saline: 0.78 +/- 0.05; measured 0.778; within: True
expected early, ketamine: 0.45 +/- 0.06; measured 0.451; within: True
expected late, saline: 0.34 +/- 0.07; measured 0.203; within: False
expected late, ketamine: -0.02 +/- 0.02; measured -0.025; within: True

Result: partially reproduced. Early eye closure falls with ketamine and stays above zero in every mouse, late eye closure falls, and both paired tests match Table S3 (t = 6.854, p = 0.002; t = 4.951, p = 0.008). Three of the four means are within tolerance; the saline late mean is 0.203 (code scale, reading a) against the 0.34 of Table S3, outside its tolerance under every reading and scale printed above. Because the test statistics match the paper's to three decimals, the per-mouse values are those the paper used, and the 0.34 is not consistent with them.

fig1m-mouse-late-closure-accumulates-and-persists¶

In mice, late eye closure accumulates across a sequence of eight puffs and persists after the last puff, with a Kaplan-Meier median survival of 15.16 s.

fig1_persistence.py: raw eye closure of the saline sessions; the eye counts as closed while above 1.2 times the trial's mean over its first 5 s; the time runs from the start frame to the first open frame, censored at 50 s if it never reopens; lifelines KaplanMeierFitter on the pooled trials. The cell first prints the lines of the script that choose the trials and the start frame. The criterion's main reading: the 20 preinfusion trials per mouse (Table S3: 100 trials), with two readings of the start, (a) the code's frame, 3.3 s after the eighth puff, and (b) 0.3 s after the eighth puff. The code's own trial choice, all 40 trials of each session (200 trials, the last 20 after the saline injection), is shown next to them for both starts; it does not decide the outcome. Accumulation: mean late eye closure (M-EYE) on the eighth puff against the first.

In [15]:
src_p = zf.read(me.SCRIPTS + "fig1_persistence.py").decode()
print("fig1_persistence.py, the lines that choose the trials and the start frame:")
print(me.grep_lines(src_p, ["ntrials =", "fps =", "aff_w =", "load_files_nwb(", "corr.reshape", "for exp in experiments",
                            "last_puff_ind =", "baseline_vals =", "b = m >", "first_inds = b", "no_first =", "first_inds[no_first]"]))
i0 = int(200 * (6 + 8 * 3 + 0.3))
print(f"the code's start frame: {i0} = {i0 / 200:.1f} s into the 81-s window, {i0 / 200 - 6:.1f} s after the first puff, "
      f"{i0 / 200 - 6 - 21:.1f} s after the eighth (at 21 s); the trials: all ntrials = 40 rows of each saline session\n")
sal = LISTS["fig1_persistence"]["experiments"]["sal"]
km = {}
for ntr, tlab in ((20, "20 preinfusion trials"), (40, "all 40 trials (code)")):
    for start, slab in ((30.3, "a: 3.3 s after 8th puff (code)"), (27.3, "b: 0.3 s after 8th puff")):
        T, Ev, per = [], [], []
        for e in sal:
            tt, ev = me.survival_times(me.trials(DATA[e], 40)[:ntr], start)
            T += list(tt); Ev += list(ev)
            per.append(KaplanMeierFitter().fit(tt, ev).median_survival_time_)
        k = KaplanMeierFitter().fit(T, Ev)
        km[(ntr, slab)] = k.median_survival_time_
        print(f"{tlab:22s} {slab:32s} n = {len(T)} trials, {int(np.sum(Ev))} reopen; pooled median {k.median_survival_time_:.2f} s; "
              f"per mouse {np.round(per, 2)}")
for th in (1.1, 1.2, 1.3):
    T, Ev = [], []
    for e in sal:
        tt, ev = me.survival_times(me.trials(DATA[e], 40)[:20], 27.3, thresh=th)
        T += list(tt); Ev += list(ev)
    print(f"threshold {th} x baseline, 20 trials, start b: median {KaplanMeierFitter().fit(T, Ev).median_survival_time_:.2f} s")
R = me.condition_values(DATA, sal, 40, np.arange(0, 20), np.arange(0, 20), 0)
R8 = me.condition_values(DATA, sal, 40, np.arange(0, 20), np.arange(0, 20), 21)
print("late eye closure, trials 1-20, per mouse: first puff", R["pre_late"].round(3), "eighth puff", R8["pre_late"].round(3))
print(f"mean first {R['pre_late'].mean():.3f}, eighth {R8['pre_late'].mean():.3f}; higher on eighth in "
      f"{(R8['pre_late'] > R['pre_late']).sum()}/5 mice")
exp = expected("fig1m-mouse-late-closure-accumulates-and-persists")
print(f"\nexpected median {exp[0][1]} +/- {exp[0][2]} s")
for ntr, tlab in ((20, "20 preinfusion (criterion)"), (40, "all 40 (code, not the criterion)")):
    for slab in ("a: 3.3 s after 8th puff (code)", "b: 0.3 s after 8th puff"):
        x = km[(ntr, slab)]
        print(f"   {tlab:33s} {slab:32s} median {x:.2f} s, minus 15.16: {x - exp[0][1]:+.2f}; within: {within(x, exp[0][1], exp[0][2])}")
fig1_persistence.py, the lines that choose the trials and the start frame:
   50      ntrials = 40
   51      fps = 200
   54      aff_w = [0.3, 0.8]
   59      corrs = etu.load_files_nwb(experiment_ids, datapath, ntrials)
   63          by_trial[key] = corr.reshape(ntrials, -1)
   68      for exp in experiments['sal']:
   70          last_puff_ind = int(fps * (6 + 8 * 3 + aff_w[0]))
   71          baseline_vals = m[:, 0:fps * 5].mean(1)
   73          b = m > 1.2 * bmat
   74          first_inds = b[:, last_puff_ind:].argmin(1)
   75          no_first = np.where(b[:, last_puff_ind:].min(1) == 1)[0]
   76          first_inds[no_first] = fps * 50
the code's start frame: 6060 = 30.3 s into the 81-s window, 24.3 s after the first puff, 3.3 s after the eighth (at 21 s); the trials: all ntrials = 40 rows of each saline session

20 preinfusion trials  a: 3.3 s after 8th puff (code)   n = 100 trials, 86 reopen; pooled median 13.05 s; per mouse [22.54  4.3   5.33 23.33 31.43]
20 preinfusion trials  b: 0.3 s after 8th puff          n = 100 trials, 86 reopen; pooled median 15.45 s; per mouse [25.54  7.3   5.34 26.33 34.43]
all 40 trials (code)   a: 3.3 s after 8th puff (code)   n = 200 trials, 166 reopen; pooled median 15.16 s; per mouse [22.54  5.44  5.33 24.24 33.37]
all 40 trials (code)   b: 0.3 s after 8th puff          n = 200 trials, 166 reopen; pooled median 18.16 s; per mouse [25.54  8.44  5.34 27.24 36.37]
threshold 1.1 x baseline, 20 trials, start b: median 14.92 s
threshold 1.2 x baseline, 20 trials, start b: median 15.45 s
threshold 1.3 x baseline, 20 trials, start b: median 16.84 s
late eye closure, trials 1-20, per mouse: first puff [0.321 0.208 0.072 0.249 0.369] eighth puff [0.449 0.463 0.379 0.578 0.716]
mean first 0.244, eighth 0.517; higher on eighth in 5/5 mice

expected median 15.16 +/- 1.52 s
   20 preinfusion (criterion)        a: 3.3 s after 8th puff (code)   median 13.05 s, minus 15.16: -2.11; within: False
   20 preinfusion (criterion)        b: 0.3 s after 8th puff          median 15.45 s, minus 15.16: +0.29; within: True
   all 40 (code, not the criterion)  a: 3.3 s after 8th puff (code)   median 15.16 s, minus 15.16: +0.00; within: True
   all 40 (code, not the criterion)  b: 0.3 s after 8th puff          median 18.16 s, minus 15.16: +3.00; within: False

Result: reproduced under reading (b). On the 20 preinfusion trials per mouse that the criterion sets, the pooled median is 15.45 s from 0.3 s after the eighth puff (reading b, within tolerance) and 13.05 s from the code's start 3.3 s after it (reading a, outside); late eye closure is higher on the eighth puff (mean 0.517) than on the first (0.244) in 5 of 5 mice. The printed lines of fig1_persistence.py set ntrials = 40 and start at frame 6060, 3.3 s after the eighth puff; with all 40 trials (200 trials) and that start the median is 15.16 s, the paper's value, while the same 200 trials from 0.3 s after the eighth puff give 18.16 s. The paper's number thus comes from the code's trial choice and start frame, not from the 100 trials of Table S3; the pass under reading (b) is not the paper's computation. The median also moves with the 1.2 threshold (14.92 to 16.84 s, printed above).

figs2g-mouse-scaling-with-puff-number-not-pressure¶

Normalised late eye closure rises from the first to the eighth puff at 8, 18 and 28 PSI, but does not differ between pressures at the eighth puff.

fig1_psi.py: first 20 trials of each session, trials 3 to 17, late/early per puff (plot_puff_comparison_bar_trials with mean_ref=True), paired t-tests (sessions paired by list position), one Benjamini-Hochberg correction over the six tests. The 28 PSI sessions are the preinfusion halves of the ketamine 35 mg/kg sessions (same ids).

In [16]:
P = LISTS["fig1_psi"]["experiments"]
psi = {}
for ps in ("8 PSI", "18 PSI", "28 PSI"):
    psi[ps] = np.array([[me.per_mouse(me.prox_norm(me.trials(DATA[e], 20))[me.TRIALS_20], 3 * k)["ratio"] for k in range(8)]
                        for e in P[ps]])
    print(f"{ps}: mean late/early by puff {psi[ps].mean(0).round(3)}")
tests, labels = [], []
for ps in ("8 PSI", "18 PSI", "28 PSI"):
    tests.append(st.ttest_rel(psi[ps][:, 0], psi[ps][:, 7])); labels.append(f"{ps} puff 1 vs 8")
for a, b in (("8 PSI", "18 PSI"), ("8 PSI", "28 PSI"), ("18 PSI", "28 PSI")):
    tests.append(st.ttest_rel(psi[a][:, 7], psi[b][:, 7])); labels.append(f"puff 8, {a} vs {b}")
pc = bh([x.pvalue for x in tests])
for l, x, p in zip(labels, tests, pc):
    print(f"   {l:26s} t = {x.statistic:7.3f}  BH p = {p:.4f}")
exp = expected("figs2g-mouse-scaling-with-puff-number-not-pressure")
s2g_vals = [psi["8 PSI"][:, 0].mean(), psi["8 PSI"][:, 7].mean(), psi["18 PSI"][:, 0].mean(),
            psi["18 PSI"][:, 7].mean(), psi["28 PSI"][:, 0].mean(), psi["28 PSI"][:, 7].mean()]
for (q, v, tol), x in zip(exp, s2g_vals):
    print(f"expected {q}: {v} +/- {tol}; measured {x:.3f}; within: {within(x, v, tol)}")
8 PSI: mean late/early by puff [0.256 0.327 0.423 0.491 0.518 0.517 0.525 0.517]
18 PSI: mean late/early by puff [0.239 0.33  0.408 0.453 0.488 0.476 0.513 0.515]
28 PSI: mean late/early by puff [0.286 0.318 0.384 0.419 0.444 0.444 0.451 0.447]
   8 PSI puff 1 vs 8          t =  -4.834  BH p = 0.0038
   18 PSI puff 1 vs 8         t =  -9.243  BH p = 0.0001
   28 PSI puff 1 vs 8         t = -11.633  BH p = 0.0000
   puff 8, 8 PSI vs 18 PSI    t =   0.024  BH p = 0.9815
   puff 8, 8 PSI vs 28 PSI    t =   1.106  BH p = 0.4578
   puff 8, 18 PSI vs 28 PSI   t =   0.718  BH p = 0.5955
expected 8 PSI puff 1: 0.256 +/- 0.067; measured 0.256; within: True
expected 8 PSI puff 8: 0.517 +/- 0.058; measured 0.517; within: True
expected 18 PSI puff 1: 0.239 +/- 0.032; measured 0.239; within: True
expected 18 PSI puff 8: 0.515 +/- 0.037; measured 0.515; within: True
expected 28 PSI puff 1: 0.286 +/- 0.068; measured 0.286; within: True
expected 28 PSI puff 8: 0.447 +/- 0.061; measured 0.447; within: True

Result: reproduced. The three puff 1 against puff 8 tests give corrected p < 0.01, the three pressure comparisons at the eighth puff give corrected p of 0.98, 0.46 and 0.60, and the six means are within tolerance (printed above); the statistics equal those of Table S3.

figs2h-mouse-dose-threshold¶

The dissociative effect on normalised late eye closure (eighth puff) is significant from about 35 mg/kg ketamine and 10 mg/kg PCP, and not at lower doses.

fig1_dose.py: late/early on the eighth puff, preinfusion against infusion per dose, paired t-tests, Benjamini-Hochberg over the ten conditions; both Open I readings.

In [17]:
DOSE = LISTS["fig1_dose"]["experiments"]
od = ["sal", "ket50", "ket35", "ket25", "ket13", "ket6", "pcp20", "pcp10", "pcp5", "pcp1"]
should = {"ket50", "ket35", "pcp20", "pcp10"}
dose_out = {}
for rname, (sp, sq) in READINGS.items():
    R = {d: me.condition_values(DATA, DOSE[d], 40, sp, sq, 21) for d in od}
    tests = [st.ttest_rel(R[d]["pre_ratio"], R[d]["post_ratio"]) for d in od]
    pc = bh([x.pvalue for x in tests])
    agree = 0
    print(f"reading {rname}")
    for d, x, p in zip(od, tests, pc):
        sig = (p < 0.05) and R[d]["post_ratio"].mean() < R[d]["pre_ratio"].mean()
        ok = sig == (d in should); agree += ok
        print(f"   {d:6s} n={len(DOSE[d])} pre {R[d]['pre_ratio'].mean():.3f} infusion {R[d]['post_ratio'].mean():.3f} "
              f"t = {x.statistic:6.3f} BH p = {p:.4f} significant drop: {sig} as claimed: {ok}")
    dose_out[rname] = agree
    print(f"   conditions agreeing with the claim: {agree}/10")
reading a (code, 6-20 / 23-37)
   sal    n=5 pre 0.657 infusion 0.705 t = -0.980 BH p = 0.4252 significant drop: False as claimed: True
   ket50  n=5 pre 0.659 infusion -0.036 t =  8.488 BH p = 0.0106 significant drop: True as claimed: True
   ket35  n=8 pre 0.447 infusion 0.169 t =  3.843 BH p = 0.0159 significant drop: True as claimed: True
   ket25  n=5 pre 0.614 infusion 0.439 t =  1.232 BH p = 0.3566 significant drop: False as claimed: True
   ket13  n=5 pre 0.573 infusion 1.000 t = -1.270 BH p = 0.3566 significant drop: False as claimed: True
   ket6   n=5 pre 0.702 infusion 0.678 t =  0.329 BH p = 0.7585 significant drop: False as claimed: True
   pcp20  n=5 pre 0.709 infusion -0.006 t =  6.080 BH p = 0.0159 significant drop: True as claimed: True
   pcp10  n=5 pre 0.719 infusion 0.205 t =  5.404 BH p = 0.0159 significant drop: True as claimed: True
   pcp5   n=5 pre 0.644 infusion 0.505 t =  2.682 BH p = 0.1103 significant drop: False as claimed: True
   pcp1   n=5 pre 0.610 infusion 0.395 t =  2.064 BH p = 0.1799 significant drop: False as claimed: True
   conditions agreeing with the claim: 10/10
reading b (3-20 / 23-40)
   sal    n=5 pre 0.651 infusion 0.696 t = -0.807 BH p = 0.5164 significant drop: False as claimed: True
   ket50  n=5 pre 0.655 infusion -0.025 t =  8.807 BH p = 0.0092 significant drop: True as claimed: True
   ket35  n=8 pre 0.454 infusion 0.175 t =  3.735 BH p = 0.0192 significant drop: True as claimed: True
   ket25  n=5 pre 0.598 infusion 0.445 t =  1.049 BH p = 0.4416 significant drop: False as claimed: True
   ket13  n=5 pre 0.570 infusion 1.122 t = -1.234 BH p = 0.4066 significant drop: False as claimed: True
   ket6   n=5 pre 0.682 infusion 0.665 t =  0.261 BH p = 0.8068 significant drop: False as claimed: True
   pcp20  n=5 pre 0.698 infusion -0.007 t =  5.992 BH p = 0.0192 significant drop: True as claimed: True
   pcp10  n=5 pre 0.712 infusion 0.219 t =  4.964 BH p = 0.0192 significant drop: True as claimed: True
   pcp5   n=5 pre 0.641 infusion 0.497 t =  2.530 BH p = 0.1253 significant drop: False as claimed: True
   pcp1   n=5 pre 0.606 infusion 0.399 t =  2.390 BH p = 0.1253 significant drop: False as claimed: True
   conditions agreeing with the claim: 10/10

Result: reproduced. Under reading (a) ketamine 50 and 35 mg/kg and PCP 20 and 10 mg/kg fall with corrected p < 0.05 and the other six conditions do not, 10 of 10 as claimed, with the statistics of Table S3; reading (b) is printed above.

figs5c-mouse-other-drugs-no-disconnection¶

Buprenorphine and diazepam do not reduce normalised late eye closure, whereas ketamine and PCP do.

fig1_drugs.py: late/early on the eighth puff, preinfusion against infusion, paired t-tests, Benjamini-Hochberg over the five drugs (K/X excluded, ordering_nokx).

In [18]:
o5 = ["sal", "ket50", "pcp20", "bup", "dzp"]
EIGHTH = {}
for rname, (sp, sq) in READINGS.items():
    EIGHTH[rname] = {d: me.condition_values(DATA, E6[d], 40, sp, sq, 21) for d in E6}
    R = EIGHTH[rname]
    tests = [st.ttest_rel(R[d]["pre_ratio"], R[d]["post_ratio"]) for d in o5]
    pc = bh([x.pvalue for x in tests])
    print(f"reading {rname}")
    for d, x, p in zip(o5, tests, pc):
        print(f"   {d:6s} pre {R[d]['pre_ratio'].mean():.3f} infusion {R[d]['post_ratio'].mean():.3f} "
              f"t = {x.statistic:7.4f} BH p = {p:.4f} mean change {np.mean(R[d]['post_ratio'] - R[d]['pre_ratio']):+.3f}")
reading a (code, 6-20 / 23-37)
   sal    pre 0.657 infusion 0.705 t = -0.9797 BH p = 0.4784 mean change +0.047
   ket50  pre 0.659 infusion -0.036 t =  8.4882 BH p = 0.0053 mean change -0.695
   pcp20  pre 0.709 infusion -0.006 t =  6.0804 BH p = 0.0092 mean change -0.716
   bup    pre 0.647 infusion 0.639 t =  0.0878 BH p = 0.9342 mean change -0.008
   dzp    pre 0.748 infusion 0.616 t =  1.6120 BH p = 0.3038 mean change -0.132
reading b (3-20 / 23-40)
   sal    pre 0.651 infusion 0.696 t = -0.8073 BH p = 0.5810 mean change +0.045
   ket50  pre 0.655 infusion -0.025 t =  8.8073 BH p = 0.0046 mean change -0.680
   pcp20  pre 0.698 infusion -0.007 t =  5.9920 BH p = 0.0098 mean change -0.705
   bup    pre 0.647 infusion 0.644 t =  0.0372 BH p = 0.9721 mean change -0.003
   dzp    pre 0.749 infusion 0.593 t =  2.1353 BH p = 0.1660 mean change -0.156

Result: reproduced. Under reading (a) ketamine (corrected p = 0.0053) and PCP (0.0092) fall, buprenorphine (0.93) and diazepam (0.30) do not, with the statistics of Table S3 (printed above).

fig1p-mouse-anesthesia-abolishes-both¶

An anaesthetic dose of ketamine with xylazine abolishes both reflexive and affective eye closure.

fig1_drugs.py: early and late on the eighth puff, preinfusion against infusion, paired t-tests, Benjamini-Hochberg over the six drugs. Absolute values under both scales.

In [19]:
o6 = ["sal", "ket50", "pcp20", "bup", "dzp", "kx"]
exp = expected("fig1p-mouse-anesthesia-abolishes-both")
fig1p_vals = {}
for rname, (sp, sq) in READINGS.items():
    for scale in ("code", "methods"):
        R = {d: me.condition_values(DATA, E6[d], 40, sp, sq, 21, scale) for d in o6}
        out = []
        for m in ("early", "late"):
            tests = [st.ttest_rel(R[d]["pre_" + m], R[d]["post_" + m]) for d in o6]
            pc = bh([x.pvalue for x in tests])
            out.append(f"{m}: K/X pre {R['kx']['pre_' + m].mean():.3f} infusion {R['kx']['post_' + m].mean():.3f} "
                       f"t = {tests[-1].statistic:.4f} BH p = {pc[-1]:.4f}")
        fig1p_vals[(rname, scale)] = [R["kx"]["post_early"].mean(), R["kx"]["post_late"].mean()]
        print(f"reading {rname}, scale {scale}: " + "; ".join(out))
for (q, v, tol), x in zip(exp, fig1p_vals[(list(READINGS)[0], "code")]):
    print(f"expected {q}: {v} +/- {tol}; measured {x:.3f}; within: {within(x, v, tol)}")
reading a (code, 6-20 / 23-37), scale code: early: K/X pre 0.773 infusion 0.025 t = 8.6017 BH p = 0.0060; late: K/X pre 0.470 infusion 0.024 t = 12.0602 BH p = 0.0016
reading a (code, 6-20 / 23-37), scale methods: early: K/X pre 0.777 infusion 0.023 t = 11.9189 BH p = 0.0017; late: K/X pre 0.479 infusion 0.025 t = 12.9131 BH p = 0.0006
reading b (3-20 / 23-40), scale code: early: K/X pre 0.783 infusion 0.025 t = 8.8342 BH p = 0.0030; late: K/X pre 0.466 infusion 0.026 t = 11.1774 BH p = 0.0022
reading b (3-20 / 23-40), scale methods: early: K/X pre 0.787 infusion 0.023 t = 12.6979 BH p = 0.0013; late: K/X pre 0.475 infusion 0.027 t = 11.7544 BH p = 0.0009
expected early eye closure, K/X: 0.03 +/- 0.02; measured 0.025; within: True
expected late eye closure, K/X: 0.02 +/- 0.01; measured 0.024; within: True

Result: reproduced. Under reading (a) and the code's scale, early (t = 8.6017, corrected p = 0.006) and late (t = 12.0602, corrected p = 0.0016) eye closure fall under K/X to means of 0.025 and 0.024, both within tolerance; the other readings and the Methods' scale are printed above.

fig1no-mouse-dissociatives-no-accumulation¶

Under ketamine and PCP, reflexive blinks remain but affective eye closure does not accumulate or persist; its decay after the last puff is faster (rate 11.89 and 13.20 against 3.53 and 3.04).

fig1_drugs.py with fit_exponential_decay: aexp(-bx) + c fit to each mouse's trial-mean trace from 21.2 to 23.2 s; b is the decay rate; paired t-tests, Benjamini-Hochberg over five drugs. The legend's 0.2 to 2 s window (21.2 to 23.0 s) is shown as a variant. Accumulation: late on the eighth puff against the first in infusion trials; early on the first puff in infusion trials. Effect sizes: the eighth-minus-first difference with its paired 95% interval for saline, ketamine and PCP, and each drug's difference against saline's.

In [20]:
exp = expected("fig1no-mouse-dissociatives-no-accumulation")
for rname, (sp, sq) in READINGS.items():
    for t_end, wlab in ((23.2, "21.2-23.2 s (code)"), (23.0, "21.2-23.0 s (legend)")):
        bp = {d: np.array([me.fit_decay(me.mean_trace(DATA, e, 40, sp), 21.2, t_end) for e in E6[d]]) for d in o5}
        bq = {d: np.array([me.fit_decay(me.mean_trace(DATA, e, 40, sq), 21.2, t_end) for e in E6[d]]) for d in o5}
        fails = sum(int(np.isnan(bp[d]).sum() + np.isnan(bq[d]).sum()) for d in o5)
        tests = [st.ttest_rel(bp[d], bq[d]) for d in o5]
        pc = bh([x.pvalue for x in tests])
        print(f"reading {rname}, window {wlab}: failed fits {fails}")
        for d, x, p in zip(o5, tests, pc):
            print(f"   {d:6s} pre {bp[d].mean():6.2f} infusion {bq[d].mean():6.2f} t = {x.statistic:7.4f} BH p = {p:.4f}")
        if rname == list(READINGS)[0] and t_end == 23.2:
            fig1no_vals = [bp["ket50"].mean(), bq["ket50"].mean(), bp["pcp20"].mean(), bq["pcp20"].mean()]
            print("   per mouse, ketamine infusion:", bq["ket50"].round(2), " PCP infusion:", bq["pcp20"].round(2))
for (q, v, tol), x in zip(exp, fig1no_vals):
    print(f"expected {q}: {v} +/- {tol}; measured {x:.2f}; within: {within(x, v, tol)}")
R = EIGHTH[list(READINGS)[0]]
R1 = {d: me.condition_values(DATA, E6[d], 40, me.PRE_CODE, me.POST_CODE, 0) for d in ("sal", "ket50", "pcp20")}
for d in ("sal", "ket50", "pcp20"):
    print(f"{d}: infusion late, first puff {R1[d]['post_late'].mean():.3f}, eighth puff {R[d]['post_late'].mean():.3f} "
          f"(eighth above first in {(R[d]['post_late'] > R1[d]['post_late']).sum()}/5 mice, paired t = "
          f"{st.ttest_rel(R[d]['post_late'], R1[d]['post_late']).statistic:.2f}, p = {st.ttest_rel(R[d]['post_late'], R1[d]['post_late']).pvalue:.3f}); "
          f"infusion early on first puff {R1[d]['post_early'].mean():.3f}")
# Effect sizes: infusion late eye closure, eighth minus first puff, mean and paired 95% CI per condition;
# and each drug's rise minus the saline rise (Welch 95% CI; the sessions are treated as independent,
# an assumption, since the files carry no mouse label).
rise = {}
for d in ("sal", "ket50", "pcp20"):
    x = st.ttest_rel(R[d]["post_late"], R1[d]["post_late"]); ci = x.confidence_interval()
    rise[d] = R[d]["post_late"] - R1[d]["post_late"]
    print(f"{d}: infusion late, eighth minus first puff {rise[d].mean():+.3f}, 95% CI [{ci.low:+.3f}, {ci.high:+.3f}] "
          f"(paired t = {x.statistic:.2f}, p = {x.pvalue:.3f}, n = {len(rise[d])})")
for d in ("ket50", "pcp20"):
    x = st.ttest_ind(rise[d], rise["sal"], equal_var=False); ci = x.confidence_interval()
    print(f"{d} rise minus saline rise {rise[d].mean() - rise['sal'].mean():+.3f}, 95% CI [{ci.low:+.3f}, {ci.high:+.3f}] "
          f"(Welch t = {x.statistic:.2f}, p = {x.pvalue:.4f})")
reading a (code, 6-20 / 23-37), window 21.2-23.2 s (code): failed fits 0
   sal    pre   3.33 infusion   3.96 t = -0.8204 BH p = 0.7634
   ket50  pre   3.53 infusion  11.89 t = -4.5198 BH p = 0.0266
   pcp20  pre   3.04 infusion  13.20 t = -5.3534 BH p = 0.0266
   bup    pre   4.15 infusion   3.60 t =  0.4065 BH p = 0.8243
   dzp    pre   3.29 infusion   3.03 t =  0.2370 BH p = 0.8243
   per mouse, ketamine infusion: [ 7.42 14.77 17.    9.78 10.47]  PCP infusion: [ 7.33 14.39 13.26 14.08 16.93]
reading a (code, 6-20 / 23-37), window 21.2-23.0 s (legend): failed fits 0
   sal    pre   3.36 infusion   4.04 t = -0.8772 BH p = 0.7165
   ket50  pre   3.64 infusion  11.82 t = -4.5097 BH p = 0.0269
   pcp20  pre   3.10 infusion  13.15 t = -5.5518 BH p = 0.0258
   bup    pre   4.16 infusion   3.72 t =  0.3332 BH p = 0.8668
   dzp    pre   3.29 infusion   3.09 t =  0.1788 BH p = 0.8668
reading b (3-20 / 23-40), window 21.2-23.2 s (code): failed fits 0
   sal    pre   3.43 infusion   3.74 t = -0.4731 BH p = 0.8260
   ket50  pre   3.35 infusion  11.77 t = -4.9200 BH p = 0.0198
   pcp20  pre   3.23 infusion  13.29 t = -5.5511 BH p = 0.0198
   bup    pre   4.49 infusion   3.45 t =  0.6955 BH p = 0.8260
   dzp    pre   3.23 infusion   3.09 t =  0.1465 BH p = 0.8906
reading b (3-20 / 23-40), window 21.2-23.0 s (legend): failed fits 0
   sal    pre   3.45 infusion   3.85 t = -0.5918 BH p = 0.7323
   ket50  pre   3.46 infusion  11.71 t = -4.9531 BH p = 0.0194
   pcp20  pre   3.34 infusion  13.25 t = -5.8062 BH p = 0.0194
   bup    pre   4.52 infusion   3.61 t =  0.6169 BH p = 0.7323
   dzp    pre   3.25 infusion   3.13 t =  0.1216 BH p = 0.9091
expected decay rate pre-ketamine: 3.53 +/- 0.28; measured 3.53; within: True
expected decay rate ketamine: 11.89 +/- 1.74; measured 11.89; within: True
expected decay rate pre-PCP: 3.04 +/- 0.46; measured 3.04; within: True
expected decay rate PCP: 13.2 +/- 1.59; measured 13.20; within: True
sal: infusion late, first puff 0.203, eighth puff 0.553 (eighth above first in 5/5 mice, paired t = 5.30, p = 0.006); infusion early on first puff 0.778
ket50: infusion late, first puff -0.025, eighth puff -0.007 (eighth above first in 4/5 mice, paired t = 1.65, p = 0.175); infusion early on first puff 0.451
pcp20: infusion late, first puff -0.010, eighth puff 0.000 (eighth above first in 4/5 mice, paired t = 0.93, p = 0.404); infusion early on first puff 0.427
sal: infusion late, eighth minus first puff +0.350, 95% CI [+0.167, +0.534] (paired t = 5.30, p = 0.006, n = 5)
ket50: infusion late, eighth minus first puff +0.017, 95% CI [-0.012, +0.046] (paired t = 1.65, p = 0.175, n = 5)
pcp20: infusion late, eighth minus first puff +0.010, 95% CI [-0.019, +0.039] (paired t = 0.93, p = 0.404, n = 5)
ket50 rise minus saline rise -0.333, 95% CI [-0.516, -0.151] (Welch t = -4.98, p = 0.0067)
pcp20 rise minus saline rise -0.341, 95% CI [-0.523, -0.158] (Welch t = -5.09, p = 0.0062)

Result: reproduced, on a test reading of "not above" (an assumption); read as point values the last part fails. The decay rates match Table S3 (ketamine 3.53 to 11.89, PCP 3.04 to 13.20, corrected p = 0.0266 for both, the four means within tolerance; saline, buprenorphine and diazepam not significant), and early eye closure on the first puff remains under both drugs (0.451 and 0.427). On the criterion's last part, the infusion late eye closure on the eighth puff is slightly above that on the first under ketamine (-0.007 against -0.025, difference +0.017, 95% CI -0.012 to +0.046, paired p = 0.175, higher in 4 of 5 mice) and under PCP (0.000 against -0.010, difference +0.010, 95% CI -0.019 to +0.039, p = 0.404, 4 of 5 mice). Under saline the same difference is +0.350 (95% CI +0.167 to +0.534, p = 0.006, 5 of 5 mice), and the ketamine and PCP differences are below saline's by 0.333 and 0.341 (Welch p = 0.0067 and 0.0062). Read as point values the part fails, since both differences are above zero; read as a test, neither difference is significant and both intervals include zero, so the part holds.

figs2l-mouse-repeatable-across-days¶

Preinfusion eye closure (reflexive, affective, normalised) on days 2 to 5 does not differ from day 1.

fig1_days.py: preinfusion trials (6 to 20) of five saline sessions per mouse, eighth puff, day 1 against each later day by paired t-test (get_drug_comparison_pvals, do_paired=True), Benjamini-Hochberg over the four per measure. An absence claim judged on the paper's test.

In [21]:
DAYS = LISTS["fig1_days"]["experiments"]
days = ["day1", "day2", "day3", "day4", "day5"]
for rname, (sp, sq) in READINGS.items():
    R = {d: me.condition_values(DATA, DAYS[d], 40, sp, sq, 21) for d in days}
    n_sig = 0
    print(f"reading {rname}")
    for m, lab in (("ratio", "normalised"), ("early", "reflexive"), ("late", "affective")):
        tests = [st.ttest_rel(R["day1"]["pre_" + m], R[d]["pre_" + m]) for d in days[1:]]
        pc = bh([x.pvalue for x in tests]); n_sig += int((pc < 0.05).sum())
        print(f"   {lab:10s} means {[round(R[d]['pre_' + m].mean(), 3) for d in days]} |t| {[round(abs(x.statistic), 2) for x in tests]} BH p {pc.round(4)}")
    print(f"   tests with corrected p < 0.05: {n_sig}/12")
reading a (code, 6-20 / 23-37)
   normalised means [np.float64(0.422), np.float64(0.466), np.float64(0.305), np.float64(0.502), np.float64(0.478)] |t| [np.float64(0.74), np.float64(1.38), np.float64(1.24), np.float64(0.48)] BH p [0.657 0.564 0.564 0.657]
   reflexive  means [np.float64(0.392), np.float64(0.359), np.float64(0.41), np.float64(0.677), np.float64(0.428)] |t| [np.float64(0.91), np.float64(0.35), np.float64(3.31), np.float64(0.63)] BH p [0.743 0.743 0.119 0.743]
   affective  means [np.float64(0.166), np.float64(0.166), np.float64(0.124), np.float64(0.341), np.float64(0.208)] |t| [np.float64(0.02), np.float64(1.13), np.float64(3.19), np.float64(0.63)] BH p [0.988 0.642 0.132 0.748]
   tests with corrected p < 0.05: 0/12
reading b (3-20 / 23-40)
   normalised means [np.float64(0.427), np.float64(0.466), np.float64(0.307), np.float64(0.484), np.float64(0.48)] |t| [np.float64(0.65), np.float64(1.37), np.float64(0.86), np.float64(0.47)] BH p [0.663 0.663 0.663 0.663]
   reflexive  means [np.float64(0.4), np.float64(0.368), np.float64(0.416), np.float64(0.681), np.float64(0.442)] |t| [np.float64(0.85), np.float64(0.33), np.float64(3.32), np.float64(0.72)] BH p [0.679 0.758 0.118 0.679]
   affective  means [np.float64(0.171), np.float64(0.171), np.float64(0.128), np.float64(0.333), np.float64(0.214)] |t| [np.float64(0.02), np.float64(1.12), np.float64(2.82), np.float64(0.66)] BH p [0.985 0.65  0.191 0.724]
   tests with corrected p < 0.05: 0/12

Result: reproduced. None of the twelve tests gives corrected p < 0.05 under reading (a); the statistics are those of Table S3 (printed above).

figs2n-mouse-no-sex-difference¶

Male and female mice do not differ in normalised late eye closure, and saline does not change it in either sex.

fig1_sex.py and get_sex_comparison_pvals: eighth puff, late/early; independent t-tests between sexes (preinfusion, saline) and paired t-tests within sex, Benjamini-Hochberg over the four. The female group is the saline sessions of the drug experiment (282 to 286).

In [22]:
SEX = LISTS["fig1_sex"]["experiments"]
for rname, (sp, sq) in READINGS.items():
    R = {s: me.condition_values(DATA, SEX[s], 40, sp, sq, 21) for s in ("male", "female")}
    tests = [st.ttest_ind(R["male"]["pre_ratio"], R["female"]["pre_ratio"]),
             st.ttest_ind(R["male"]["post_ratio"], R["female"]["post_ratio"]),
             st.ttest_rel(R["male"]["pre_ratio"], R["male"]["post_ratio"]),
             st.ttest_rel(R["female"]["pre_ratio"], R["female"]["post_ratio"])]
    pc = bh([x.pvalue for x in tests])
    print(f"reading {rname}: means male pre {R['male']['pre_ratio'].mean():.3f} saline {R['male']['post_ratio'].mean():.3f}; "
          f"female pre {R['female']['pre_ratio'].mean():.3f} saline {R['female']['post_ratio'].mean():.3f}")
    for l, x, p in zip(["pre male vs female", "saline male vs female", "male pre vs saline", "female pre vs saline"], tests, pc):
        print(f"   {l:24s} t = {x.statistic:6.3f} BH p = {p:.4f}")
reading a (code, 6-20 / 23-37): means male pre 0.445 saline 0.567; female pre 0.657 saline 0.705
   pre male vs female       t = -2.079 BH p = 0.2851
   saline male vs female    t = -0.983 BH p = 0.3827
   male pre vs saline       t = -1.080 BH p = 0.3827
   female pre vs saline     t = -0.980 BH p = 0.3827
reading b (3-20 / 23-40): means male pre 0.463 saline 0.557; female pre 0.651 saline 0.696
   pre male vs female       t = -1.945 BH p = 0.3506
   saline male vs female    t = -1.084 BH p = 0.4648
   male pre vs saline       t = -0.927 BH p = 0.4648
   female pre vs saline     t = -0.807 BH p = 0.4648

Result: reproduced. None of the four tests gives corrected p < 0.05 (smallest 0.285 under reading a), with the statistics of Table S3. Sex is not in the files; it comes from the code's lists.

figs2o-mouse-no-anticipation¶

When a puff in the sequence is skipped, mice do not blink at its expected time, while late eye closure stays elevated.

fig1_skippuff.py: the ketamine skip-puff sessions 411, 412, 413, 420 and 421; SKIP_PUFFS (read from the script) gives, for each of the 20 preinfusion trials, the skipped puff (index 5 or 6, the sixth or seventh puff); 10 trials per mouse each. Criterion: in the preinfusion trials, mean eye closure 0.1 to 0.2 s after the skipped puff's time does not rise above its value in the 0.1 s before by more than the across-mouse s.e.m., and stays above the trial baseline (zero after the baseline subtraction). The same rise at the puff before the skipped one is the reference.

In [23]:
SK = LISTS["fig1_skippuff"]["experiments"]["ket50"]
skip_ok = []
for k in (5, 6):
    rows = []
    for e in SK:
        m = me.prox_norm(me.trials(DATA[e], 40))[np.where(skip == k)[0]].mean(0)
        ts = 3 * k
        rows.append(dict(mouse=e, n_trials=int((skip == k).sum()),
                         rise_at_skip=m[me.window(ts, (0.1, 0.2))].mean() - m[me.window(ts, (-0.1, 0.0))].mean(),
                         level_at_skip=m[me.window(ts, (0.1, 0.2))].mean(),
                         late_at_skip=m[me.window(ts, (0.3, 0.8))].mean(),
                         rise_prev_puff=m[me.window(3 * (k - 1), (0.1, 0.2))].mean() - m[me.window(3 * (k - 1), (-0.1, 0.0))].mean()))
    df = pd.DataFrame(rows)
    print(f"skipped puff {k + 1}:")
    print(df.round(3).to_string(index=False))
    rise, sem = df.rise_at_skip.mean(), st.sem(df.rise_at_skip)
    ok = (rise <= sem) and (df.level_at_skip.mean() > 0)
    skip_ok.append(ok)
    print(f"   mean rise {rise:.4f}, s.e.m. {sem:.4f}; mean level {df.level_at_skip.mean():.3f}; "
          f"rise at the previous (delivered) puff {df.rise_prev_puff.mean():.3f}; criterion met: {ok}")
print("both skipped positions meet the criterion:", all(skip_ok))
skipped puff 6:
 mouse  n_trials  rise_at_skip  level_at_skip  late_at_skip  rise_prev_puff
   411        10        -0.016          0.374         0.379           0.570
   412        10        -0.005          0.301         0.299           0.336
   413        10         0.008          0.057         0.054           0.223
   420        10         0.028          0.116         0.091           0.160
   421        10        -0.011          0.171         0.159           0.558
   mean rise 0.0008, s.e.m. 0.0079; mean level 0.204; rise at the previous (delivered) puff 0.369; criterion met: True
skipped puff 7:
 mouse  n_trials  rise_at_skip  level_at_skip  late_at_skip  rise_prev_puff
   411        10        -0.004          0.297         0.297           0.516
   412        10        -0.004          0.253         0.246           0.397
   413        10         0.013          0.047         0.043           0.222
   420        10         0.003          0.106         0.099           0.177
   421        10        -0.005          0.207         0.212           0.555
   mean rise 0.0004, s.e.m. 0.0035; mean level 0.182; rise at the previous (delivered) puff 0.373; criterion met: True
both skipped positions meet the criterion: True

Result: reproduced. At the skipped sixth and seventh puffs the mean rise is about zero and within one s.e.m., against a rise near 0.37 at the delivered puff before, and eye closure stays above baseline (levels printed above).

figs4-mouse-reward-seeking-reduced (blocked)¶

Eyepuff sequences cause a persistent reduction in licking for a liquid reward that recovers slowly after the last puff.

The lick data are not in the dataset. The check below lists every asset of 001347 at the pinned version and every acquisition name in the mouse files read above; none holds licks. The code record has no lick script either.

In [24]:
assets = dandi_io.list_assets()
print("assets with 'lick' or 'reward' in the path:", [p for p in assets if "lick" in p.lower() or "reward" in p.lower()])
print("acquisitions in a sub-mouse file:", ["eye_closure"])
print("acquisitions in the Neuropixels-rig files:", sorted(k for k in b0 if k != "subject_id"))
print("code record files mentioning lick:", [n for n in zf.namelist() if n.endswith(".py") and b"lick" in zf.read(n).lower()])
print("sub-mouse sessions not used by any listed condition:", unlisted)
assets with 'lick' or 'reward' in the path: []
acquisitions in a sub-mouse file: ['eye_closure']
acquisitions in the Neuropixels-rig files: ['aff_w', 'events-off', 'events-on', 'eyeclosure', 'eyeclosure_by_puff', 'eyeclosure_by_trial', 'eyesize', 'eyesize_by_puff', 'eyesize_by_trial', 'fps', 'ket_puff', 'ket_trial', 'led', 'led_delay', 'npuffs_per_trial', 'p_post', 'p_pre', 'puff_aff', 'puff_ref', 'ref_w', 't_eyeclosure', 't_trial_eyeclosure', 'trial_range_ket', 'trial_range_preket', 'w_post', 'w_pre']
code record files mentioning lick: []
sub-mouse sessions not used by any listed condition: []

figs6-mouse-vpm-inhibition-reduces-affective¶

Muscimol inhibition of VPM thalamus reduces normalised late eye closure (0.60 to 0.11) without changing the fast early eye closure.

fig1_muscimol.py: 20-trial sessions, trials 3 to 17, late/early on the eighth puff; paired t-tests saline against no injection, saline against muscimol and no injection against muscimol, Benjamini-Hochberg over the three. Fast early eye closure is the mean 0 to 40 ms after the first puff; the script tests it with independent t-tests (get_drug_comparison_pvals without do_paired), Table S3 calls them paired; both are shown.

In [25]:
MU = LISTS["fig1_muscimol"]["experiments"]
R = {d: me.condition_values(DATA, MU[d], 20, me.TRIALS_20, me.TRIALS_20, 21) for d in MU}
tests = [st.ttest_rel(R["sal"]["pre_ratio"], R["intact"]["pre_ratio"]),
         st.ttest_rel(R["sal"]["pre_ratio"], R["vpm_musc"]["pre_ratio"]),
         st.ttest_rel(R["intact"]["pre_ratio"], R["vpm_musc"]["pre_ratio"])]
pc = bh([x.pvalue for x in tests])
for l, x, p in zip(["saline vs no injection", "saline vs muscimol", "no injection vs muscimol"], tests, pc):
    print(f"late/early {l:26s} t = {x.statistic:6.3f} BH p = {p:.4f}")
print({d: (R[d]["pre_ratio"].round(3), round(R[d]["pre_ratio"].mean(), 3)) for d in R})
F = {d: me.condition_values(DATA, MU[d], 20, me.TRIALS_20, me.TRIALS_20, 0, w_early=(0.0, 0.04)) for d in MU}
for test in (st.ttest_ind, st.ttest_rel):
    tt = [test(F["sal"]["pre_early"], F["intact"]["pre_early"]), test(F["sal"]["pre_early"], F["vpm_musc"]["pre_early"]),
          test(F["vpm_musc"]["pre_early"], F["intact"]["pre_early"])]
    pp = bh([x.pvalue for x in tt])
    print(f"fast early 0-40 ms ({test.__name__}): means {[round(F[d]['pre_early'].mean(), 4) for d in ('intact', 'sal', 'vpm_musc')]} "
          f"saline vs muscimol t = {tt[1].statistic:.3f} BH p = {pp[1]:.4f}; all BH p {pp.round(4)}")
exp = expected("figs6-mouse-vpm-inhibition-reduces-affective")
s6_vals = [R["sal"]["pre_ratio"].mean(), R["vpm_musc"]["pre_ratio"].mean()]
for (q, v, tol), x in zip(exp, s6_vals):
    print(f"expected {q}: {v} +/- {tol}; measured {x:.3f}; within: {within(x, v, tol)}")
late/early saline vs no injection     t = -0.455 BH p = 0.6727
late/early saline vs muscimol         t =  3.845 BH p = 0.0289
late/early no injection vs muscimol   t =  3.789 BH p = 0.0289
{'intact': (array([0.613, 0.749, 0.542, 0.657, 0.603]), np.float64(0.633)), 'sal': (array([0.724, 0.586, 0.618, 0.434, 0.649]), np.float64(0.602)), 'vpm_musc': (array([ 0.339, -0.284, -0.078,  0.303,  0.247]), np.float64(0.105))}
fast early 0-40 ms (ttest_ind): means [np.float64(0.0801), np.float64(0.0755), np.float64(0.0887)] saline vs muscimol t = -0.367 BH p = 0.8898; all BH p [0.89 0.89 0.89]
fast early 0-40 ms (ttest_rel): means [np.float64(0.0801), np.float64(0.0755), np.float64(0.0887)] saline vs muscimol t = -0.379 BH p = 0.9013; all BH p [0.901 0.901 0.901]
expected late/early, saline injection: 0.6 +/- 0.05; measured 0.602; within: True
expected late/early, muscimol: 0.11 +/- 0.12; measured 0.105; within: True

Result: reproduced. Muscimol lowers late/early against saline and against no injection (corrected p = 0.0289 each), saline does not differ from no injection (0.67), the fast early response does not differ under either test, and the saline and muscimol means are within tolerance (printed above).

fig4b-mouse-recorded-affective-reduced¶

Mice recorded with Neuropixels also show affective eye closure that ketamine reduces.

Main reading, the criterion's: the Neuropixels-rig files with their stored trial ranges (trial_range_preket, trials 1 to 20; trial_range_ket, trials 23 to 30) and stored late window (aff_w, 0.3 to 0.8 s); the eye trace around the first puff of each series (5 s after each onset), minus the mean of the 1 s before the puff, averaged over trials within session, with no normalisation (Table S3, Fig. 4B: "Traces are baseline subtracted with a 1s window before puffs ... with trials averaged within session and variability taken across sessions"). Across sessions, the mean and 1.96 s.e.m. (ddof 1; the code's ddof 0 is also printed). Accumulation (Fig. 5B): late after the eighth puff minus late after the first, per session, the baseline being the 1 s before the first puff (analysis.py).

The first cell prints the code. fig1_npx_behavior.py, the script the criterion names, calls plot_npx_eyeclosure, which plots each session's median over the stored trial ranges with no baseline and no normalisation, one session per figure. The across-session trace is made by make_puffseries_behavior_plot in analysis.py (Zenodo 10.5281/zenodo.14876694), which divides each session's mean trace by its own maximum and uses ketamine trials 23 to 32.

Variants: max-normalised (analysis.py); ketamine trials 23 to 32 (analysis.py and the 10 ketamine trials of Table S3); the median over trials (plot_npx_eyeclosure). The dataset holds 10 of the 13 sessions (10 mice) of Table S3. The stored per-puff values puff_aff and puff_ref are shown as a cross-check.

In [26]:
src_an = me.fetch_analysis_py()
ROOT = me.SCRIPTS.split("mouse_eyepuff/")[0]
src_npx = zf.read(me.SCRIPTS + "fig1_npx_behavior.py").decode()
src_etu = zf.read(ROOT + "ketmouse/eye_trace_utils.py").decode()
print("fig1_npx_behavior.py (the script the criterion names), main():")
print(me.show_lines(src_npx, *me.def_lines(src_npx, "main")))
print("\nketmouse/eye_trace_utils.py, plot_npx_eyeclosure, which main() calls (whole function):")
print(me.show_lines(src_etu, *me.def_lines(src_etu, "plot_npx_eyeclosure")))
print("\nanalysis.py (Zenodo 14876694, md5 checked), make_puffseries_behavior_plot, the lines that cut, baseline and normalise:")
print(me.def_grep(src_an, "make_puffseries_behavior_plot",
                  ["trial_events =", "pertrial.append", "peranimal.append", "preket_behavior.append",
                   "postket_behavior.append", "ste ="]))
MAXKEYS = (".max()", "nanmax", "np.max(", "amax")
for name, s in (("fig1_npx_behavior.py main + plot_npx_eyeclosure",
                 me.def_source(src_npx, "main") + me.def_source(src_etu, "plot_npx_eyeclosure")),
                ("analysis.py make_puffseries_behavior_plot", me.def_source(src_an, "make_puffseries_behavior_plot"))):
    print(f"{name}: divides by a maximum: {any(k in s for k in MAXKEYS)}")
fig1_npx_behavior.py (the script the criterion names), main():
   18  def main():
   19  
   20      for expid in expids:
   21          datadir, plotdir = io.get_dirs(pt=expid)
   22          savepath = os.path.join(datadir, f'sub-{expid}_ses-puff_behavior.nwb')
   23          nwb_loaded = etu.npx_load_nwb(expid, savepath)
   24  
   25          etu.plot_npx_eyeclosure(expid, nwb_loaded, plotdir)
   26          plt.show()

ketmouse/eye_trace_utils.py, plot_npx_eyeclosure, which main() calls (whole function):
  130  def plot_npx_eyeclosure(expid, data, savedir=None):
  131      by_trial_closure = data['eyeclosure_by_trial']
  132      trial_range_preket = data['trial_range_preket']
  133      trial_range_ket = data['trial_range_ket']
  134      ket_trial = data['ket_trial']
  135      w_pre = data['w_pre']
  136      w_post = data['w_post']
  137      led_delay = data['led_delay']
  138      t_trial = data['t_trial_eyeclosure']
  139  
  140      fontsize = 8
  141      preket_closure = np.nanmedian(by_trial_closure[trial_range_preket, :],
  142                                    axis=0)
  143      ket_closure = np.nanmedian(by_trial_closure[trial_range_ket, :],
  144                                 axis=0)
  145      plt.figure(figsize=(3.4, 1.75))
  146      plt.subplot(1, 2, 1)
  147      plt.imshow(by_trial_closure[:, :], aspect='auto',
  148                 interpolation='nearest',
  149                 extent=[-w_pre - led_delay, w_post - led_delay,
  150                         by_trial_closure.shape[0], 0],
  151                 clim=[0, 1])
  152  
  153      plt.axvline((0), linestyle='--', color='k', linewidth=0.5)
  154      plt.axhline(ket_trial, linestyle='--', color='k', linewidth=0.5)
  155      # plt.title(f'{expid}, eye closure')
  156      plt.ylabel('Trial #', fontsize=fontsize)
  157      plt.xlabel('Time (s) ', fontsize=fontsize)
  158  
  159      plt.subplot(1, 2, 2)
  160      plt.plot(t_trial, preket_closure, '#6AADD6', linewidth=1)
  161      plt.plot(t_trial, ket_closure, '#CD1D58', linewidth=1)
  162      plt.axvline((0), linestyle='--', color='k', linewidth=0.5)
  163      plt.ylabel('Eye closure', fontsize=fontsize)
  164      plt.xlabel('Time (s)', fontsize=fontsize)
  165      plt.xticks([0, 25, 50], fontsize=fontsize)
  166      # plt.legend(['Pre', 'Ket.'], frameon=False, handlelength=0.5, handletextpad=0.3, loc='right')
  167      # plt.title('Average across trials')
  168      simpleaxis(plt.gca())
  169      plt.tight_layout(rect=(0, 0, 1, 0.97), w_pad=2)
  170      # plt.suptitle(f'{expid.split("_")[-2]}-{expid.split("_")[-1]}',
  171      #              fontsize=fontsize)
  172      plt.suptitle(f'{expid}',
  173                   fontsize=fontsize)
  174      if savedir is not None:
  175          plt.savefig(os.path.join(savedir, f'{expid}_eyeclosure.png'),
  176                      dpi=300)
  177      plt.show()

analysis.py (Zenodo 14876694, md5 checked), make_puffseries_behavior_plot, the lines that cut, baseline and normalise:
   38  def make_puffseries_behavior_plot(save_path, all_data, do_save=True, save_dpi=300, display_dpi=100, first_only=False, plot_late=True):
   55              trial_events = b['events']['on'] + 5*b['fps']
   68                              pertrial.append(b['eyeclosure'][p-int(1*b['fps']): p+int(2*b['fps'])+extra])
   72                          pertrial.append(b['eyeclosure'][p-int(1*b['fps']): p+int(2*b['fps'])+extra])
   73                  peranimal.append(np.concatenate(pertrial) - np.mean(pertrial[0][:100]))
   78              preket_behavior.append(np.nanmean(peranimal[:20,:],0).flatten()/np.nanmean(peranimal[:20,:],0).flatten().max())
   80              postket_behavior.append(np.nanmean(peranimal[22:32,:],0).flatten()/np.nanmean(peranimal[22:32,:],0).flatten().max())
  103      ste = np.std(np.stack(preket_behavior),0)/np.sqrt(np.stack(preket_behavior).shape[0])
  108      ste = np.std(np.stack(postket_behavior),0)/np.sqrt(np.stack(postket_behavior).shape[0])
  114          ste = np.std(np.stack(lateket_behavior),0)/np.sqrt(np.stack(lateket_behavior).shape[0])
fig1_npx_behavior.py main + plot_npx_eyeclosure: divides by a maximum: False
analysis.py make_puffseries_behavior_plot: divides by a maximum: True
In [27]:
def ci95(x, ddof=1):
    return 1.96 * np.std(x, ddof=ddof) / np.sqrt(len(x))


def fig4b_table(max_norm, ket_kind, agg):
    # Per session: first-puff late (stored aff_w) pre and ketamine, and accumulation (8th - 1st).
    rows = []
    for b in NPX.values():
        pre = b["trial_range_preket"]
        ket = b["trial_range_ket"] if ket_kind == "stored" else slice(22, 32)
        aw = b["aff_w"]
        t, mp, mk, per = me.npx_series(b, first_only=True, max_norm=max_norm, pre=pre, ket=ket, agg=agg)
        w = (t >= aw[0]) & (t < aw[1])
        t2, sp, sk, _ = me.npx_series(b, first_only=False, max_norm=max_norm, pre=pre, ket=ket, agg=agg)
        w1 = (t2 >= aw[0]) & (t2 < aw[1]); w8 = (t2 >= 21 + aw[0]) & (t2 < 21 + aw[1])
        rows.append(dict(session=b["subject_id"], late_pre=np.nanmean(mp[w]), late_ket=np.nanmean(mk[w]),
                         acc_pre=np.nanmean(sp[w8]) - np.nanmean(sp[w1]), acc_ket=np.nanmean(sk[w8]) - np.nanmean(sk[w1]),
                         nan_baseline_trials=int(np.isnan(per[:, :int(b["fps"][0])]).any(1).sum())))
    return pd.DataFrame(rows)


fig4b_res = {}
MAIN4B = ("not normalised (criterion)", "stored 23-30", "mean")
for mx, mlab in ((False, "not normalised (criterion)"), (True, "max-normalised (analysis.py)")):
    for kr, klab in (("stored", "stored 23-30"), ("code", "23-32 (analysis.py, Table S3)")):
        for agg in ("mean", "median"):
            df = fig4b_table(mx, kr, agg)
            n = len(df)
            lp, lk = df.late_pre.mean(), df.late_ket.mean()
            r = dict(late_pre=lp, late_ket=lk, acc_pre=df.acc_pre.mean(), acc_ket=df.acc_ket.mean(), n=n,
                     n_lower=int((df.late_ket < df.late_pre).sum()), n_acc_lower=int((df.acc_ket < df.acc_pre).sum()))
            for dd in (1, 0):
                r[f"ci_pre_{dd}"], r[f"ci_ket_{dd}"] = ci95(df.late_pre, dd), ci95(df.late_ket, dd)
                r[f"sep_{dd}"] = bool((lp - r[f"ci_pre_{dd}"]) > (lk + r[f"ci_ket_{dd}"]))
            dci = st.ttest_rel(df.late_pre, df.late_ket).confidence_interval()
            r["diff"], r["diff_ci"] = lp - lk, (dci.low, dci.high)
            fig4b_res[(mlab, klab, agg)] = r
            tag = "  <- main reading" if (mlab, klab, agg) == MAIN4B else ""
            print(f"{mlab}; ketamine trials {klab}; trials aggregated by {agg}; n = {n} sessions{tag}")
            print(f"   late, first puff: pre {lp:.3f} +/- {r['ci_pre_1']:.3f}, ketamine {lk:.3f} +/- {r['ci_ket_1']:.3f} "
                  f"(1.96 s.e.m.); intervals separate: {r['sep_1']}. With ddof 0 (analysis.py): +/- {r['ci_pre_0']:.3f}, "
                  f"+/- {r['ci_ket_0']:.3f}, separate: {r['sep_0']}")
            print(f"   pre minus ketamine {r['diff']:.3f}, paired 95% CI [{dci.low:.3f}, {dci.high:.3f}] (effect size; the paper "
                  f"reports no test); lower on ketamine in {r['n_lower']}/{n} sessions")
            print(f"   accumulation (late after 8th minus after 1st): pre {r['acc_pre']:.3f} +/- {ci95(df.acc_pre):.3f}, "
                  f"ketamine {r['acc_ket']:.3f} +/- {ci95(df.acc_ket):.3f}; lower on ketamine: {r['acc_ket'] < r['acc_pre']}, "
                  f"in {r['n_acc_lower']}/{n} sessions")
            if (mlab, klab, agg) == MAIN4B:
                print(df.round(3).to_string(index=False))
                print(f"   trials (of 40 per session) with a NaN frame in the baseline second, which np.mean in analysis.py "
                      f"would drop: {df.nan_baseline_trials.sum()} in total")
not normalised (criterion); ketamine trials stored 23-30; trials aggregated by mean; n = 10 sessions  <- main reading
   late, first puff: pre 0.174 +/- 0.061, ketamine 0.147 +/- 0.034 (1.96 s.e.m.); intervals separate: False. With ddof 0 (analysis.py): +/- 0.058, +/- 0.032, separate: False
   pre minus ketamine 0.027, paired 95% CI [-0.039, 0.094] (effect size; the paper reports no test); lower on ketamine in 6/10 sessions
   accumulation (late after 8th minus after 1st): pre 0.058 +/- 0.028, ketamine 0.046 +/- 0.031; lower on ketamine: True, in 5/10 sessions
    session  late_pre  late_ket  acc_pre  acc_ket  nan_baseline_trials
 m11208-959     0.117     0.169    0.076    0.010                    1
 m11208-962     0.074     0.170    0.044    0.004                    0
 m11210-960     0.142     0.053    0.065    0.017                    0
m11219-1108     0.206     0.155    0.080    0.081                    0
m11221-1119     0.119     0.189    0.031    0.041                    0
m11223-1125     0.146     0.093    0.029    0.027                    0
m11225-1131     0.127     0.116    0.001    0.006                    0
m11227-1132     0.204     0.096    0.001    0.013                    1
   m304-876     0.430     0.224    0.121    0.131                    0
    m94-549     0.175     0.203    0.132    0.132                    0
   trials (of 40 per session) with a NaN frame in the baseline second, which np.mean in analysis.py would drop: 2 in total
not normalised (criterion); ketamine trials stored 23-30; trials aggregated by median; n = 10 sessions
   late, first puff: pre 0.169 +/- 0.063, ketamine 0.144 +/- 0.034 (1.96 s.e.m.); intervals separate: False. With ddof 0 (analysis.py): +/- 0.060, +/- 0.032, separate: False
   pre minus ketamine 0.026, paired 95% CI [-0.045, 0.096] (effect size; the paper reports no test); lower on ketamine in 6/10 sessions
   accumulation (late after 8th minus after 1st): pre 0.050 +/- 0.025, ketamine 0.043 +/- 0.025; lower on ketamine: True, in 5/10 sessions
not normalised (criterion); ketamine trials 23-32 (analysis.py, Table S3); trials aggregated by mean; n = 10 sessions
   late, first puff: pre 0.174 +/- 0.061, ketamine 0.148 +/- 0.037 (1.96 s.e.m.); intervals separate: False. With ddof 0 (analysis.py): +/- 0.058, +/- 0.035, separate: False
   pre minus ketamine 0.026, paired 95% CI [-0.042, 0.094] (effect size; the paper reports no test); lower on ketamine in 6/10 sessions
   accumulation (late after 8th minus after 1st): pre 0.058 +/- 0.028, ketamine 0.040 +/- 0.029; lower on ketamine: True, in 8/10 sessions
not normalised (criterion); ketamine trials 23-32 (analysis.py, Table S3); trials aggregated by median; n = 10 sessions
   late, first puff: pre 0.169 +/- 0.063, ketamine 0.143 +/- 0.037 (1.96 s.e.m.); intervals separate: False. With ddof 0 (analysis.py): +/- 0.060, +/- 0.035, separate: False
   pre minus ketamine 0.026, paired 95% CI [-0.048, 0.101] (effect size; the paper reports no test); lower on ketamine in 5/10 sessions
   accumulation (late after 8th minus after 1st): pre 0.050 +/- 0.025, ketamine 0.037 +/- 0.028; lower on ketamine: True, in 6/10 sessions
max-normalised (analysis.py); ketamine trials stored 23-30; trials aggregated by mean; n = 10 sessions
   late, first puff: pre 0.630 +/- 0.080, ketamine 0.318 +/- 0.061 (1.96 s.e.m.); intervals separate: True. With ddof 0 (analysis.py): +/- 0.076, +/- 0.058, separate: True
   pre minus ketamine 0.312, paired 95% CI [0.228, 0.396] (effect size; the paper reports no test); lower on ketamine in 10/10 sessions
   accumulation (late after 8th minus after 1st): pre 0.200 +/- 0.083, ketamine 0.087 +/- 0.045; lower on ketamine: True, in 8/10 sessions
max-normalised (analysis.py); ketamine trials stored 23-30; trials aggregated by median; n = 10 sessions
   late, first puff: pre 0.656 +/- 0.080, ketamine 0.312 +/- 0.064 (1.96 s.e.m.); intervals separate: True. With ddof 0 (analysis.py): +/- 0.076, +/- 0.061, separate: True
   pre minus ketamine 0.343, paired 95% CI [0.224, 0.463] (effect size; the paper reports no test); lower on ketamine in 10/10 sessions
   accumulation (late after 8th minus after 1st): pre 0.169 +/- 0.055, ketamine 0.086 +/- 0.040; lower on ketamine: True, in 8/10 sessions
max-normalised (analysis.py); ketamine trials 23-32 (analysis.py, Table S3); trials aggregated by mean; n = 10 sessions
   late, first puff: pre 0.630 +/- 0.080, ketamine 0.322 +/- 0.054 (1.96 s.e.m.); intervals separate: True. With ddof 0 (analysis.py): +/- 0.076, +/- 0.051, separate: True
   pre minus ketamine 0.308, paired 95% CI [0.229, 0.387] (effect size; the paper reports no test); lower on ketamine in 10/10 sessions
   accumulation (late after 8th minus after 1st): pre 0.200 +/- 0.083, ketamine 0.074 +/- 0.044; lower on ketamine: True, in 9/10 sessions
max-normalised (analysis.py); ketamine trials 23-32 (analysis.py, Table S3); trials aggregated by median; n = 10 sessions
   late, first puff: pre 0.656 +/- 0.080, ketamine 0.314 +/- 0.057 (1.96 s.e.m.); intervals separate: True. With ddof 0 (analysis.py): +/- 0.076, +/- 0.054, separate: True
   pre minus ketamine 0.342, paired 95% CI [0.231, 0.453] (effect size; the paper reports no test); lower on ketamine in 10/10 sessions
   accumulation (late after 8th minus after 1st): pre 0.169 +/- 0.055, ketamine 0.073 +/- 0.045; lower on ketamine: True, in 9/10 sessions
In [28]:
# Stored per-puff values: puff_aff (late, aff_w) and puff_ref (early, ref_w) on the first puff of each
# series, averaged over the stored trial ranges per session; across sessions mean and 1.96 s.e.m.
rows = []
for b in NPX.values():
    pa, pr = b["puff_aff"].reshape(-1, 8), b["puff_ref"].reshape(-1, 8)
    pre, ket = b["trial_range_preket"], b["trial_range_ket"]
    bp, tb = b["eyeclosure_by_puff"], b["t_eyeclosure"]
    raw_late = np.nanmean(bp[:, (tb >= 0.3 - 1e-9) & (tb < 0.8 - 1e-9)], 1)
    raw_early = np.nanmean(bp[:, (tb >= -1e-9) & (tb < 0.3 - 1e-9)], 1)
    rows.append(dict(session=b["subject_id"], aff_pre=np.nanmean(pa[pre, 0]), aff_ket=np.nanmean(pa[ket, 0]),
                     ref_pre=np.nanmean(pr[pre, 0]), ref_ket=np.nanmean(pr[ket, 0]),
                     ratio_pre=np.nanmean(pa[pre, 0] / pr[pre, 0]), ratio_ket=np.nanmean(pa[ket, 0] / pr[ket, 0]),
                     base_pre=np.nanmean(bp.reshape(-1, 8, bp.shape[1])[pre, 0][:, (tb >= -1) & (tb < 0)]),
                     base_ket=np.nanmean(bp.reshape(-1, 8, bp.shape[1])[ket, 0][:, (tb >= -1) & (tb < 0)]),
                     d_aff=np.nanmax(np.abs(raw_late - b["puff_aff"])), d_ref=np.nanmax(np.abs(raw_early - b["puff_ref"]))))
sdf = pd.DataFrame(rows)
print(sdf.drop(columns=["d_aff", "d_ref"]).round(3).to_string(index=False))
print(f"stored values against the raw (not baseline-subtracted) mean of eyeclosure_by_puff: puff_ref over 0-0.3 s, max "
      f"difference {sdf.d_ref.max():.4f}; puff_aff over 0.3-0.8 s, max difference {sdf.d_aff.max():.4f}")
for q, lab in (("aff", "puff_aff"), ("ref", "puff_ref"), ("ratio", "puff_aff/puff_ref"), ("base", "raw 1 s before the first puff")):
    a, k = sdf[f"{q}_pre"], sdf[f"{q}_ket"]
    print(f"{lab:30s} pre {a.mean():.3f} +/- {ci95(a):.3f}, ketamine {k.mean():.3f} +/- {ci95(k):.3f} (1.96 s.e.m.); "
          f"intervals separate: {(a.mean() - ci95(a)) > (k.mean() + ci95(k))}; lower on ketamine in {(k < a).sum()}/{len(a)}")
    session  aff_pre  aff_ket  ref_pre  ref_ket  ratio_pre  ratio_ket  base_pre  base_ket
 m11208-959    0.616    0.303    0.684    0.528      0.914      0.575     0.499     0.129
 m11208-962    0.830    0.339    0.851    0.634      0.973      0.519     0.756     0.163
 m11210-960    0.733    0.336    0.851    0.438      0.862      0.769     0.588     0.282
m11219-1108    0.704    0.341    0.726    0.546      0.966      0.631     0.497     0.186
m11221-1119    0.799    0.414    0.735    0.472      1.099      0.881     0.679     0.223
m11223-1125    0.787    0.283    0.850    0.439      0.925      0.654     0.639     0.186
m11225-1131    0.762    0.429    0.825    0.714      0.924      0.602     0.634     0.307
m11227-1132    0.638    0.283    0.661    0.508      0.965      0.571     0.436     0.182
   m304-876    0.754    0.440    0.764    0.723      0.985      0.609     0.324     0.210
    m94-549    0.749    0.413    0.866    0.782      0.865      0.532     0.581     0.222
stored values against the raw (not baseline-subtracted) mean of eyeclosure_by_puff: puff_ref over 0-0.3 s, max difference 0.0000; puff_aff over 0.3-0.8 s, max difference 0.0164
puff_aff                       pre 0.737 +/- 0.042, ketamine 0.358 +/- 0.038 (1.96 s.e.m.); intervals separate: True; lower on ketamine in 10/10
puff_ref                       pre 0.781 +/- 0.047, ketamine 0.578 +/- 0.078 (1.96 s.e.m.); intervals separate: True; lower on ketamine in 10/10
puff_aff/puff_ref              pre 0.948 +/- 0.042, ketamine 0.634 +/- 0.069 (1.96 s.e.m.); intervals separate: True; lower on ketamine in 10/10
raw 1 s before the first puff  pre 0.563 +/- 0.079, ketamine 0.209 +/- 0.033 (1.96 s.e.m.); intervals separate: True; lower on ketamine in 10/10
In [29]:
fig, ax = plt.subplots(1, 2, figsize=(8, 2.6))
rng = []
for a, mx in zip(ax, (False, True)):
    P_, K_ = [], []
    for b in NPX.values():
        t, mp, mk, _ = me.npx_series(b, first_only=True, max_norm=mx, pre=b["trial_range_preket"], ket=b["trial_range_ket"])
        P_.append(mp); K_.append(mk)
    P_, K_ = np.array(P_), np.array(K_)
    for X, lab in ((P_, "preinfusion"), (K_, "ketamine")):
        m, h = np.nanmean(X, 0), 1.96 * np.nanstd(X, 0, ddof=1) / np.sqrt(len(X))
        a.plot(t, m, label=lab); a.fill_between(t, m - h, m + h, alpha=0.25, linewidth=0)
        rng.append(f"{'max-normalised' if mx else 'not normalised'} {lab}: {np.nanmin(m - h):.2f} to {np.nanmax(m + h):.2f}")
    a.axvspan(0.3, 0.8, color="0.9", zorder=0)
    a.set_title("max-normalised (analysis.py)" if mx else "not normalised (criterion)"); a.set_xlabel("time from first puff (s)")
ax[0].legend(frameon=False, fontsize=7); ax[0].set_ylabel("eye closure")
plt.tight_layout(); plt.show()
print("plotted ranges (mean +/- 1.96 s.e.m.), all inside the axes:", "; ".join(rng))
No description has been provided for this image
plotted ranges (mean +/- 1.96 s.e.m.), all inside the axes: not normalised preinfusion: -0.01 to 0.33; not normalised ketamine: -0.00 to 0.55; max-normalised preinfusion: -0.07 to 1.05; max-normalised ketamine: -0.01 to 1.00

Result: not reproduced under the criterion's method, on 10 of 13 sessions. With the baseline subtracted and no normalisation (main reading), first-puff late eye closure is 0.174 +/- 0.061 preinfusion and 0.147 +/- 0.034 on ketamine (1.96 s.e.m.), so the intervals overlap; the difference is 0.027 (paired 95% CI -0.039 to 0.094) and ketamine is lower in 6 of 10 sessions. Accumulation is lower on ketamine (0.046 against 0.058, lower in 5 of 10 sessions), so only the second part holds. The intervals also overlap with ddof 0, with ketamine trials 23 to 32, and with the median over trials. The printed code shows that fig1_npx_behavior.py and the plot_npx_eyeclosure it calls take a median over the stored trial ranges with no baseline and no normalisation, while make_puffseries_behavior_plot in analysis.py divides each session's trace by its maximum (lines 78 and 80). With that normalisation the intervals separate (0.630 +/- 0.080 against 0.318 +/- 0.061, lower in 10 of 10 sessions; with trials 23 to 32, 0.630 against 0.322) and accumulation is lower on ketamine (0.200 against 0.087). The stored puff_aff and puff_ref are raw window means without baseline subtraction (puff_ref equal to the raw 0 to 0.3 s mean, puff_aff within 0.016 of the raw 0.3 to 0.8 s mean); on the first puff puff_aff falls from 0.737 +/- 0.042 to 0.358 +/- 0.038 and puff_aff/puff_ref from 0.948 to 0.634, in 10 of 10 sessions, but the raw eye closure in the second before the first puff also falls, from 0.563 to 0.209, so the stored values mostly carry the change in baseline that the criterion's method removes.

Summary¶

Claim Outcome Key numbers
mouse-session-labels-link holds all listed ids present, lengths as read, ketamine/PCP drops, K/X near zero, skip labels match
fig1k-mouse-late-eye-closure-ketamine reproduced saline 0.262, ketamine -0.068, t = 4.737, p = 0.009
fig1l-mouse-dissociatives-reduce-late reproduced t = 0.73 / 9.53 / 3.41, BH p = 0.504 / 0.002 / 0.041
figs2bc-mouse-ketamine-early-present-late-abolished partially reproduced tests as Table S3; saline late 0.203 against 0.34
fig1m-mouse-late-closure-accumulates-and-persists reproduced (reading b) 20 trials: median 15.45 s (b), 13.05 s (a); the code's 40 trials and start frame give 15.16 s, the paper's value
figs2g-mouse-scaling-with-puff-number-not-pressure reproduced statistics as Table S3
figs2h-mouse-dose-threshold reproduced 10 of 10 conditions as claimed
figs5c-mouse-other-drugs-no-disconnection reproduced ketamine 0.0053, PCP 0.0092, bup 0.93, dzp 0.30
fig1p-mouse-anesthesia-abolishes-both reproduced K/X early 0.025, late 0.024
fig1no-mouse-dissociatives-no-accumulation reproduced (assumed, test reading of "not above") decay rates as Table S3; late on 8th minus 1st puff +0.017 [-0.012, +0.046] (ketamine), +0.010 [-0.019, +0.039] (PCP), saline +0.350 [+0.167, +0.534]
figs2l-mouse-repeatable-across-days reproduced 0 of 12 tests significant
figs2n-mouse-no-sex-difference reproduced 0 of 4 tests significant
figs2o-mouse-no-anticipation reproduced no rise at the skipped puff, closure above baseline
figs4-mouse-reward-seeking-reduced blocked (data missing) no lick data in the dataset
figs6-mouse-vpm-inhibition-reduces-affective reproduced saline 0.602, muscimol 0.105
fig4b-mouse-recorded-affective-reduced not reproduced (10 of 13 sessions) criterion's method: 0.174 +/- 0.061 against 0.147 +/- 0.034, intervals overlap; separate only with analysis.py's max normalisation (0.630 against 0.318)

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.

Differences from the paper's method. The processing reimplements the paper's code, not the Methods' text where they differ: the scale factor is one (the code's) for the main values, with the Methods' 1/(1 - baseline) shown for absolute values; the infusion trials are the code's 23 to 37 (Open I a), with trials 23 to 40 as reading (b), since the sub-mouse files carry no trial times for the 3-min rule. Persistence uses 20 preinfusion trials per mouse as Table S3 says, where the code uses all 40. The Neuropixels summary uses 10 of the 13 sessions, follows the criterion and Table S3 (baseline subtraction, no normalisation) as its main reading, and shows the max normalisation of analysis.py as a variant.

What was found about the dataset. The 142 sub-mouse files hold one eye-closure trace each and no label of condition, dose, mouse, sex, day or pressure (sex "O", age "P8W/" placeholders); the labels exist only in the paper's scripts. No recording is deposited twice. The Neuropixels-rig behaviour of 3 of the 13 Table S3 sessions (dandiset 001326 sessions of 2023-02-25 and of mouse m11217 on 2023-07-06 and 2023-08-22, named in the commented extra list of the converter, printed in the data check) is not in the dataset. Lick data (fig. S4) are not in the dataset.

In [30]:
print(f"runtime {(time.time() - T0) / 60:.1f} min")
runtime 2.5 min