Kauvar, Richman, Liu et al. (2025) on dandiset 001326¶

  • Paper: Kauvar, Richman, Liu et al., "Conserved brain-wide emergence of emotional response from sensory experience in humans and mice", Science 388, eadt3971 (2025), doi:10.1126/science.adt3971.
  • Dataset: DANDI:001326, "Brain-wide mouse electrophysiology during aversive stimuli and ketamine", version 0.250528.1957 (doi:10.48324/dandi.001326/0.250528.1957), streamed from the archive.
  • Version. The paper cites the dandiset without a version. 0.250528.1957 is its only published version; the draft holds the same 13 assets (same asset ids), so the most recent published version is pinned.
  • Claims file: claims/001326/10.1126-science.adt3971.yaml, written from the accepted author manuscript (Europe PMC, PMC12286656) with its supplementary materials, and the paper's code (analysis.py, Zenodo doi:10.5281/zenodo.14876694; brainwide-npix at commit 97cdf58).
  • Outside input: the Allen CCFv3 2017 structure tree (structure_tree_safe_2017.csv, the file brainwide-npix loads), from github.com/cortex-lab/allenCCF at commit e5a57fe7, used only to place the dataset's region acronyms in the Allen hierarchy.

The dandiset holds the mouse Neuropixels recordings (13 sessions, 10 mice). The human data, the behavior-only mouse experiments and the eye closure of the recorded mice are in DANDI:001347; the claims on them that the data allow are tested in reproduce_kauvar_2025_behavior.ipynb and, for the human iEEG, reproduce_kauvar_2025_ieeg.ipynb.

Revised after the first verification round: the clustering is re-run over ten Leiden seeds; the regional timing shows the weight of clusters without a value and a Pre set of Ketamine's size; the fig5d and figs32c checks use the interval wording of the criterion; figs33 scores the Ketamine series only; the timescale fits are shown bounded, with the mean removed and per-series medians; the coupling and timescale are shown on windows free of tone and light onsets; figs30 is shown with the file's infusion time; fig6d is shown with weights from half the Pre series.

Revised after the second verification round: the float32 PCA before the clustering rounded differently with the number of BLAS threads, so the partition, and every clustering and regional-timing number, changed between machines. BLAS is now limited to one thread for the clustering, the conclusions of the clustering and regional-timing sections are rewritten from this run's outputs, and the stability section adds the partition with the PCA in float64 and with 4 BLAS threads. The data check adds the epochs against the infusion time and the tone and light onsets inside the analysis windows.

Claim In this notebook
fig1e-human-ketamine-late-closure tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
figs1b-human-early-closure-unchanged tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
fig1f-human-reports not tested (see the attempt record)
fig2d-cadss-ketamine not tested (see the attempt record)
fig2g-six-factors tested in reproduce_kauvar_2025_ieeg.ipynb (DANDI:001347)
fig2i-factors-1-2-ketamine tested in reproduce_kauvar_2025_ieeg.ipynb (DANDI:001347)
fig3b-cluster-test-networks tested in reproduce_kauvar_2025_ieeg.ipynb (DANDI:001347)
figs12-erp-not-blink not tested (see the attempt record)
figs14g-cadss-correlation not tested (see the attempt record)
fig6c-human-persistent not tested (see the attempt record)
fig6e-human-fast-unchanged not tested (see the attempt record)
fig6g-human-timescale not tested (see the attempt record)
fig6i-human-phase-locking not tested (see the attempt record)
fig1k-mouse-ketamine-late-closure tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
fig1l-dissociatives-preinfusion tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
figs2bc-mouse-early-late tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
fig1m-accumulation-persistence tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
figs2g-scaling-puff-number tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
figs2h-dose-response tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
fig1p-anesthesia-abolishes-both tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
figs5-other-drugs tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
figs6-vpm-muscimol tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
figs4-reward-licking not tested (see the attempt record)
fig5hi-behavior-model-doses tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
fig4-recordings-scale tested here
fig4b-recorded-eye-closure-ketamine tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
fig5b-recorded-accumulation tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
fig4de-response-clusters tested here
figs31b-cluster-overrepresentation tested here
fig4g-ketamine-alters-slow-clusters tested here
fig4i-rise-times-unchanged tested here
fig4h-decay-shortened tested here
fig4h-decay-anatomical-progression tested here
fig5d-emotion-dimension-accumulates tested here
fig5e-late-reduced tested here
fig5e-rapid-unchanged tested here
fig5g-tau-persist-model tested here
figs33-alternative-models tested here
figs32c-regional-emotion-dimension tested here
fig6b-persistent-dimension-distributed tested here
fig6d-mouse-persistent tested here
fig6f-mouse-fast-unchanged tested here
fig6h-mouse-timescale tested here
fig6j-mouse-coupling tested here
figs34c-tone-no-eye-closure tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
figs34d-tone-fast-unchanged tested here
figs34e-tone-persistent tested here
figs34f-tone-shorter-than-puff tested here
figs34g-tone-timescale tested here
figs34h-tone-coupling tested here
figs30-correlation-structure tested here
figs29-rsp-oscillations tested here
link-001347-condition-labels tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)
link-001347-recorded-sessions tested in reproduce_kauvar_2025_behavior.ipynb (DANDI:001347)

Loading¶

Each file is streamed and reduced as it loads (kauvar.process_session): rates in 10 ms bins with a causal 50 ms moving average, z-scored per unit, the puff-modulated units of analysis.py selective, and every per-session quantity the claims need. Nothing is written to disk.

In [1]:
import time
import warnings
import numpy as np
import pandas as pd
import scipy
import scipy.cluster
import scipy.spatial
import scipy.stats
import kauvar as K

warnings.filterwarnings("ignore", category=RuntimeWarning)
print(f"dandiset {K.DANDISET}, version {K.VERSION}")
urls = K.asset_urls()
paths = K.allen_tree()
res, meta = {}, {}
t0 = time.time()
for p, u in urls:
    s = K.load_session(p, u)
    meta[s.name] = dict(subject=s.subject, units=len(s.spikes), unsorted_units=s.n_unsorted_units, spikes=s.n_spikes_total,
                        n_spikes_column=int(s.n_spikes.sum()), snr_min=s.snr.min(), n_spikes_min=int(s.n_spikes.min()),
                        quality=sorted(set(s.quality)), duration_s=s.maxt, stim=s.stim.stimulus.value_counts().to_dict(),
                        stim_dur=s.stim.groupby("stimulus").apply(lambda g: (g.stop_time - g.start_time).agg(["min", "max"]).round(4).tolist()).to_dict(),
                        puff_isi=np.round(np.percentile(np.diff(s.onsets("puff").reshape(-1, 8), axis=1), [0, 100]), 3).tolist(),
                        series_gap=np.round(np.percentile(np.diff(s.onsets("puff").reshape(-1, 8)[:, 0]), [0, 100]), 1).tolist(),
                        **s.meta)
    res[s.name] = K.process_session(s, paths)
    del s
names = list(res)
print(f"{len(names)} sessions loaded and reduced in {time.time() - t0:.0f} s")
dandiset 001326, version 0.250528.1957
13 sessions loaded and reduced in 200 s

Data check¶

What the files hold, checked against the paper's description (Methods, "Neuropixels recording and processing"; claims file, opening comment). The clock check compares the mean z-scored population response 0 to 200 ms after each stimulus onset of the table (minus the 1 s before) with the same measure at onsets shifted by 1.5 s, between stimuli: a response at the table's onsets and none at the shifted ones shows that spike times and stimulus table share a clock. The second cell (added in revision) places the puff series against the file's infusion time and counts tone and light onsets inside the windows the analyses use.

In [2]:
m = pd.DataFrame(meta).T
print("Units table: every unit is labelled", sorted({q for v in m.quality for q in v}), "; smallest SNR",
      round(m.snr_min.min(), 2), "(paper: SNR above 2); smallest spike count", m.n_spikes_min.min(), "(paper: more than 500)")
print("n_spikes column equals the stored spike times in every file:", bool((m.spikes == m.n_spikes_column).all()))
print("units whose spike times are not in ascending order:", int(m.unsorted_units.sum()), "of", int(m.units.sum()))
print(m[["subject", "units", "spikes", "duration_s", "devices", "age", "sex"]].to_string())
print("\nStimulus presentations per session (count) and duration range (s):")
print(pd.DataFrame({k: v for k, v in m.stim.items()}).T.fillna(0).astype(int).to_string())
print(pd.Series(m.stim_dur).to_string())
print("\nPuff onset intervals within a series (s, min and max over sessions):", [min(v[0] for v in m.puff_isi), max(v[1] for v in m.puff_isi)])
print("Intervals between series starts (s, min and max):", [min(v[0] for v in m.series_gap), max(v[1] for v in m.series_gap)])
rows = []
for k in names:
    o = res[k]
    rows.append({"session": k, "subject": o["subject"], "puff series": o["n_series"], "series of 8": set(o["series_sizes"]) == {8},
                 "code ketamine onset (s)": round(o["ket_start_code"], 1), "infusion in file (s)": round(o["ket_infusion_nwb"], 1),
                 "tone series": len(o.get("tone_series_sizes", [])), "regions (finest, no NA)": len(set(o["regions_all"]) - {"NA"}),
                 "units": o["n_units"], "puff-modulated": o["n_mod"],
                 "clock puff true": round(o["clock_puff"]["true"], 3), "clock puff shifted": round(o["clock_puff"]["shifted 1.5 s"], 3),
                 "clock tone true": round(o["clock_tone"]["true"], 3) if "clock_tone" in o else np.nan,
                 "clock tone shifted": round(o["clock_tone"]["shifted 1.5 s"], 3) if "clock_tone" in o else np.nan})
sess = pd.DataFrame(rows).set_index("session")
print(sess.to_string())
print(f"\nsessions {len(names)}, mice {sess.subject.nunique()}, units {sess.units.sum()}, puff-modulated {sess['puff-modulated'].sum()}")
print("every session has at least 40 puff series:", bool((sess["puff series"] >= 40).all()),
      "; the infusion in the file falls after series 20 in every session:", bool((sess["infusion in file (s)"] > sess["code ketamine onset (s)"] - 30).all()))
print("rates: z-scored per unit; z values of the stored windows range",
      round(min(res[k]["pre_avg"].min() for k in names), 2), "to", round(max(res[k]["pre_avg"].max() for k in names), 2), "(Pre averages)")
Units table: every unit is labelled [np.str_('good')] ; smallest SNR 2.0 (paper: SNR above 2); smallest spike count 517 (paper: more than 500)
n_spikes column equals the stored spike times in every file: True
units whose spike times are not in ascending order: 9554 of 9554
                   subject units    spikes   duration_s                                                                               devices       age sex
sub-0_ses-20230901       0   942  30275497  7860.708509  [Neuropixels_Probe_0, Neuropixels_Probe_1, Neuropixels_Probe_2, Neuropixels_Probe_3]  P8W/P12W   F
sub-1_ses-20230218       1   583  17014893  5881.223584  [Neuropixels_Probe_0, Neuropixels_Probe_1, Neuropixels_Probe_2, Neuropixels_Probe_3]  P8W/P12W   F
sub-1_ses-20230226       1  1020  45462064  8090.584986  [Neuropixels_Probe_0, Neuropixels_Probe_1, Neuropixels_Probe_2, Neuropixels_Probe_3]  P8W/P12W   F
sub-2_ses-20230902       2   708  16019860  7920.803845  [Neuropixels_Probe_0, Neuropixels_Probe_1, Neuropixels_Probe_2, Neuropixels_Probe_3]  P8W/P12W   F
sub-3_ses-20230218       3   431  10144317  5881.294849  [Neuropixels_Probe_0, Neuropixels_Probe_1, Neuropixels_Probe_2, Neuropixels_Probe_3]  P8W/P12W   F
sub-3_ses-20230225       3   917  35381600  7947.431481  [Neuropixels_Probe_0, Neuropixels_Probe_1, Neuropixels_Probe_2, Neuropixels_Probe_3]  P8W/P12W   F
sub-4_ses-20230907       4   885  29845185  8103.080661  [Neuropixels_Probe_0, Neuropixels_Probe_1, Neuropixels_Probe_2, Neuropixels_Probe_3]  P8W/P12W   F
sub-5_ses-20230706       5   154   2753026  8101.440109                                            [Neuropixels_Probe_0, Neuropixels_Probe_3]  P8W/P12W   F
sub-5_ses-20230822       5   664  16205211   7921.21314  [Neuropixels_Probe_0, Neuropixels_Probe_1, Neuropixels_Probe_2, Neuropixels_Probe_3]  P8W/P12W   F
sub-6_ses-20230823       6   591  17459504  7920.848412  [Neuropixels_Probe_0, Neuropixels_Probe_1, Neuropixels_Probe_2, Neuropixels_Probe_3]  P8W/P12W   F
sub-7_ses-20220831       7   928  37991946  7537.026668  [Neuropixels_Probe_0, Neuropixels_Probe_1, Neuropixels_Probe_2, Neuropixels_Probe_3]  P8W/P12W   F
sub-8_ses-20220228       8   851  42459218  9174.911337  [Neuropixels_Probe_0, Neuropixels_Probe_1, Neuropixels_Probe_2, Neuropixels_Probe_3]  P8W/P12W   F
sub-9_ses-20230908       9   880  23843952   8402.29841  [Neuropixels_Probe_0, Neuropixels_Probe_1, Neuropixels_Probe_2, Neuropixels_Probe_3]  P8W/P12W   F

Stimulus presentations per session (count) and duration range (s):
                    puff  light  tone
sub-0_ses-20230901   480    240   232
sub-1_ses-20230218   400      0     0
sub-1_ses-20230226   488    248   240
sub-2_ses-20230902   480    240   232
sub-3_ses-20230218   400      0     0
sub-3_ses-20230225   496    248   248
sub-4_ses-20230907   480    240   240
sub-5_ses-20230706   480    240   240
sub-5_ses-20230822   480    240   232
sub-6_ses-20230823   480    240   232
sub-7_ses-20220831   416      0     0
sub-8_ses-20220228   320      0     0
sub-9_ses-20230908   480    240   240
sub-0_ses-20230901    {'light': ['0.25', '0.25'], 'puff': ['0.25', '...
sub-1_ses-20230218                           {'puff': ['0.25', '0.25']}
sub-1_ses-20230226    {'light': ['0.25', '0.25'], 'puff': ['0.25', '...
sub-2_ses-20230902    {'light': ['0.25', '0.25'], 'puff': ['0.25', '...
sub-3_ses-20230218                           {'puff': ['0.25', '0.25']}
sub-3_ses-20230225    {'light': ['0.25', '0.25'], 'puff': ['0.25', '...
sub-4_ses-20230907    {'light': ['0.25', '0.25'], 'puff': ['0.25', '...
sub-5_ses-20230706    {'light': ['0.25', '0.25'], 'puff': ['0.25', '...
sub-5_ses-20230822    {'light': ['0.25', '0.25'], 'puff': ['0.25', '...
sub-6_ses-20230823    {'light': ['0.25', '0.25'], 'puff': ['0.25', '...
sub-7_ses-20220831                           {'puff': ['0.25', '0.25']}
sub-8_ses-20220228                           {'puff': ['0.25', '0.25']}
sub-9_ses-20230908    {'light': ['0.25', '0.25'], 'puff': ['0.25', '...

Puff onset intervals within a series (s, min and max over sessions): [2.63, 3.38]
Intervals between series starts (s, min and max): [106.4, 475.7]
                   subject  puff series  series of 8  code ketamine onset (s)  infusion in file (s)  tone series  regions (finest, no NA)  units  puff-modulated  clock puff true  clock puff shifted  clock tone true  clock tone shifted
session                                                                                                                                                                                                                                   
sub-0_ses-20230901       0           60         True                   2747.6                2841.6           29                       31    942             767            0.171              -0.015            0.128              -0.015
sub-1_ses-20230218       1           50         True                   2401.4                2500.2            0                       32    583             408            0.141              -0.003              NaN                 NaN
sub-1_ses-20230226       1           61         True                   2645.0                3023.1           30                       33   1020             641            0.092              -0.012            0.098              -0.002
sub-2_ses-20230902       2           60         True                   2748.4                2846.4           29                       33    708             461            0.063               0.003            0.050              -0.019
sub-3_ses-20230218       3           50         True                   2403.4                2528.2            0                       24    431             316            0.075              -0.027              NaN                 NaN
sub-3_ses-20230225       3           62         True                   2636.8                2764.4           31                       33    917             589            0.057              -0.018            0.043              -0.005
sub-4_ses-20230907       4           60         True                   2751.0                2844.9           30                       33    885             661            0.120              -0.016            0.057               0.004
sub-5_ses-20230706       5           60         True                   2747.3                2861.7           30                       11    154              71            0.033              -0.013            0.015              -0.003
sub-5_ses-20230822       5           60         True                   2751.7                2886.6           29                       20    664             405            0.068              -0.016            0.041              -0.020
sub-6_ses-20230823       6           60         True                   2750.7                2860.6           29                       22    591             429            0.129               0.002            0.156              -0.004
sub-7_ses-20220831       7           52         True                   2283.8                2703.4            0                       29    928             645            0.089              -0.002              NaN                 NaN
sub-8_ses-20220228       8           40         True                   2285.6                2445.9            0                       37    851             675            0.194              -0.025              NaN                 NaN
sub-9_ses-20230908       9           60         True                   2747.5                2989.5           30                       35    880             630            0.104              -0.010            0.051              -0.011

sessions 13, mice 10, units 9554, puff-modulated 6698
every session has at least 40 puff series: True ; the infusion in the file falls after series 20 in every session: True
rates: z-scored per unit; z values of the stored windows range -1.83 to 18.91 (Pre averages)
In [3]:
# Added in revision. Epochs against the file's infusion time: the first puff of series 20 to 24 relative to
# the "Ketamine infusion start" of each file (negative: before it). Ketamine is series 23 to 32 (code), the
# Methods-literal reading 22 to 32.
ep = pd.DataFrame({k: {f"series {s + 1}": res[k]["series_rel_infusion"][s, 0] for s in range(19, 24)} for k in names}).T
ep["code onset - infusion"] = [res[k]["ket_start_code"] - res[k]["ket_infusion_nwb"] for k in names]
print(ep.round(1).to_string())
print("sessions where series 23 starts before the file's infusion time:", list(ep.index[ep["series 23"] < 0]))
print("sessions where series 22 starts before the file's infusion time:", list(ep.index[ep["series 22"] < 0]))
# tone and light onsets inside the windows of the analyses: the 30 s before puff series (fig6h, fig6j, Pre 1-20
# and Ketamine 23-32) and the 50 s after each puff series (fig5d, fig5g, figs33)
rows = []
for k in names:
    for kind in ("tone", "light"):
        w = res[k]["stim_in_windows"][kind]
        for wn, v in w.items():
            used = np.r_[0:20, 22:32]
            rows.append({"session": k, "stimulus": kind, "window": wn, "series with an onset inside (of 30 used)": int((v[used] > 0).sum()),
                         "onsets inside": int(v[used].sum())})
sw = pd.DataFrame(rows)
print("\ntone and light onsets inside the windows of Pre series 1-20 and Ketamine series 23-32:")
print(sw.pivot_table(index="session", columns=["stimulus", "window"], values="series with an onset inside (of 30 used)").to_string())
print("total onsets inside, by stimulus and window:", sw.groupby(["stimulus", "window"])["onsets inside"].sum().to_dict())
# added in revision 2: series with a tone or a light onset in the 30 s before them, per session
either = {k: int(((res[k]["stim_in_windows"]["tone"]["30 s before series"][used] > 0)
                  | (res[k]["stim_in_windows"]["light"]["30 s before series"][used] > 0)).sum()) for k in names}
print("series (of 30 used) with a tone or light onset in the 30 s before them:", either)
pattern = {kind: sorted({tuple(int(x) + 1 for x in used if res[k]["stim_in_windows"][kind]["30 s before series"][x] > 0) for k in names} - {()})
           for kind in ("tone", "light")}
print("series numbers with an onset in the 30 s before them, in the sessions that have any:", pattern)
                    series 20  series 21  series 22  series 23  series 24  code onset - infusion
sub-0_ses-20230901     -145.0        5.0      134.5      265.6      390.3                  -94.0
sub-1_ses-20230218     -149.8        5.0      119.4      235.5      345.1                  -98.8
sub-1_ses-20230226     -429.1        5.0      129.5      260.6      380.2                 -378.1
sub-2_ses-20230902     -149.0        5.0      134.5      265.6      390.3                  -98.0
sub-3_ses-20230218     -175.8        5.0      119.4      235.5      345.1                 -124.8
sub-3_ses-20230225     -178.6        5.0      129.5      260.6      380.2                 -127.6
sub-4_ses-20230907     -144.9        5.0      134.5      265.7      390.3                  -93.9
sub-5_ses-20230706     -165.4        5.0      134.5      265.6      390.3                 -114.4
sub-5_ses-20230822     -185.9        5.0      134.5      265.6      390.3                 -134.9
sub-6_ses-20230823     -160.9        5.0      134.5      265.6      390.2                 -109.9
sub-7_ses-20220831     -470.7        5.0      119.5      235.7      345.3                 -419.6
sub-8_ses-20220228     -211.4        5.0      119.5      235.7      345.3                 -160.3
sub-9_ses-20230908     -293.0        5.0      134.5      265.6      390.3                 -242.0
sessions where series 23 starts before the file's infusion time: []
sessions where series 22 starts before the file's infusion time: []

tone and light onsets inside the windows of Pre series 1-20 and Ketamine series 23-32:
stimulus                        light                                 tone                  
window             30 s before series 50 s after series 30 s before series 50 s after series
session                                                                                     
sub-0_ses-20230901               10.0               0.0                4.0               0.0
sub-1_ses-20230218                0.0               0.0                0.0               0.0
sub-1_ses-20230226               10.0              15.0                4.0               0.0
sub-2_ses-20230902               10.0               0.0                4.0               0.0
sub-3_ses-20230218                0.0               0.0                0.0               0.0
sub-3_ses-20230225               10.0              15.0                4.0               0.0
sub-4_ses-20230907               10.0               0.0                4.0               0.0
sub-5_ses-20230706               10.0               0.0                4.0               0.0
sub-5_ses-20230822               10.0               0.0                4.0               0.0
sub-6_ses-20230823               10.0               0.0                4.0               0.0
sub-7_ses-20220831                0.0               0.0                0.0               0.0
sub-8_ses-20220228                0.0               0.0                0.0               0.0
sub-9_ses-20230908               10.0               0.0                4.0               0.0
total onsets inside, by stimulus and window: {('light', '30 s before series'): 108, ('light', '50 s after series'): 30, ('tone', '30 s before series'): 45, ('tone', '50 s after series'): 0}
series (of 30 used) with a tone or light onset in the 30 s before them: {'sub-0_ses-20230901': 14, 'sub-1_ses-20230218': 0, 'sub-1_ses-20230226': 14, 'sub-2_ses-20230902': 14, 'sub-3_ses-20230218': 0, 'sub-3_ses-20230225': 14, 'sub-4_ses-20230907': 14, 'sub-5_ses-20230706': 14, 'sub-5_ses-20230822': 14, 'sub-6_ses-20230823': 14, 'sub-7_ses-20220831': 0, 'sub-8_ses-20220228': 0, 'sub-9_ses-20230908': 14}
series numbers with an onset in the 30 s before them, in the sessions that have any: {'tone': [(5, 7, 9, 29)], 'light': [(4, 6, 8, 14, 16, 20, 24, 28, 30, 32)]}

fig4-recordings-scale¶

Many thousands of single units were recorded across 13 sessions from 10 mice, with dozens of regions recorded simultaneously.

(Figure 4C; fig. S29A and S29B; table S2; table S3, Fig. 4E)

Counts from the files. The units in the files are the curated units (all labelled good, SNR at least 2, more than 500 spikes, see the data check); "puff-modulated" is analysis.py selective with has_min_spikes on the summed Hz rate (claims file, opening comment). Regions are the finest Allen acronyms in brain_region, without the unassigned label NA.

In [4]:
n_sessions, n_mice = len(names), sess.subject.nunique()
n_mod = int(sess["puff-modulated"].sum())
print(f"sessions: {n_sessions} (expected 13); mice: {n_mice} (expected 10)")
print(f"units after the quality criteria (all units in the files): {sess.units.sum()} (range per session {sess.units.min()}-{sess.units.max()})")
print(f"puff-modulated units: {n_mod} (expected 6969, tolerance 697); with the spike-count reading of the 250 minimum: "
      f"{sum(res[k]['sel_count_reading'] for k in names)}")
print("per test (units passing, summed over sessions):", pd.DataFrame({k: res[k]["n_test"] for k in names}).sum(1).to_dict())
reg = sess["regions (finest, no NA)"]
print(f"regions per session at the finest Allen level: median {reg.median():.0f}, range {reg.min()}-{reg.max()}; "
      f"sessions with at least 24: {(reg >= 24).sum()} of {len(reg)}")
sessions: 13 (expected 13); mice: 10 (expected 10)
units after the quality criteria (all units in the files): 9554 (range per session 154-1020)
puff-modulated units: 6698 (expected 6969, tolerance 697); with the spike-count reading of the 250 minimum: 6491
per test (units passing, summed over sessions): {'pre vs 0-200 ms': 4931, 'pre vs 300-800 ms': 4533, 'pre vs 1-2 s': 1257, 'baseline first vs last puff': 310, 'response first vs last puff': 1345}
regions per session at the finest Allen level: median 32, range 11-37; sessions with at least 24: 10 of 13
/tmp/ipykernel_2286/626211601.py:7: Pandas4Warning: Starting with pandas version 4.0 all arguments of sum will be keyword-only.
  print("per test (units passing, summed over sessions):", pd.DataFrame({k: res[k]["n_test"] for k in names}).sum(1).to_dict())

Result: reproduced. The files hold 13 sessions from 10 mice and 9554 curated units; 6698 are puff-modulated, within 697 of the 6969 of table S3. The median session has units in 32 regions at the finest Allen level (range 11 to 37), above two dozen.

fig4de-response-clusters¶

Unsupervised clustering of peri-puff responses of puff-modulated units from all sessions gave twelve clusters spread over many regions, some responding rapidly and briefly and others persistently after rapid (clusters 0 and 1) or delayed (cluster 2) onset.

(Figure 4D and 4E; fig. S31A; table S3, Fig. 4E)

Method from the criterion: per unit the Pre average (160 puffs) of the z-scored rate from -1 to 2 s, minus its mean over -1 to 0 s; scanpy PCA (arpack), neighbors (30, 20 PCs), UMAP, Leiden with default resolution, np.random.seed(42) and scanpy's default seeds, as get_cell_clusters. Cluster means and the decay to 25% of the post-puff peak follow analysis.py (get_cell_clusters cluster averages; find_decay in plot_cluster_distribution, which takes absolute values and gives 2 s to a cluster whose peak has |z| below 1). "Persistent" is read as the mean absolute cluster trace over 1 to 2 s above 25% of the absolute peak (criterion); the minimum over 1 to 2 s is also printed.

In [5]:
import scanpy as sc

pre_avg = np.vstack([res[k]["pre_avg"] for k in names])
ket_avg = np.vstack([res[k]["ket_avg"] for k in names])
mod_regions = np.hstack([res[k]["regions"] for k in names])
mod_session = np.hstack([[k] * res[k]["n_mod"] for k in names])
X = np.nan_to_num(pre_avg - pre_avg[:, :100].mean(1, keepdims=True)).astype(np.float32)
# Added in revision 2. The float32 PCA rounds differently with the number of BLAS threads, and that is enough to
# change the neighbor graph and the partition (on the same data, 1, 2 and 4 threads gave three different
# partitions). BLAS is limited to one thread here, so the partition does not depend on the machine; this is a
# standard choice for a detail of computation, not a method choice. The PCA in float64 is shown in the stability
# section as a rounding-level perturbation.
from threadpoolctl import threadpool_limits
with threadpool_limits(1):
    np.random.seed(42)
    ann = sc.AnnData(X)
    sc.tl.pca(ann, svd_solver="arpack")
    sc.pp.neighbors(ann, n_neighbors=30, n_pcs=20)
    sc.tl.umap(ann)
    sc.tl.leiden(ann, flavor="leidenalg")
labels = ann.obs["leiden"].astype(int).to_numpy()
nclus = labels.max() + 1
sizes = np.bincount(labels)
expected_sizes = [1070, 832, 803, 663, 639, 597, 547, 508, 473, 344, 317, 176]
print(f"puff-modulated units clustered: {len(labels)}; clusters: {nclus} (expected 12, tolerance 1)")
srt = np.sort(sizes)[::-1]
rows = []
for i, e in enumerate(expected_sizes):
    got = srt[i] if i < len(srt) else 0
    rows.append({"rank": i, "size": got, "expected": e, "tolerance": round(0.1 * e), "within": abs(got - e) <= round(0.1 * e)})
print(pd.DataFrame(rows).to_string(index=False))

def cluster_means(avg, labels):
    """analysis.py get_cell_clusters: per cell z-scored over -1..2 s, averaged, minus the mean of -1..0 s."""
    out = []
    for c in range(labels.max() + 1):
        zz = np.nanmean(scipy.stats.zscore(avg[labels == c], axis=1), 0)
        out.append(zz - zz[:100].mean())
    return np.array(out)

def peak_decay(trace, frac=0.25):
    """analysis.py find_decay: |trace| normalized by its largest |value| after onset; the peak is its argmax and
    the decay the time from onset until it falls below frac after the peak; a peak whose z-score (over the
    trace) is below 1 in absolute value gets decay 2 s, as the code does. Returns peak value (signed),
    peak time, decay."""
    zt = scipy.stats.zscore(trace)
    a = np.abs(trace / np.nanmax(np.abs(trace[100:])))
    pk = int(np.argmax(a[100:]))
    below = np.where(a[100:][pk:] < frac)[0]
    decay = (pk + below[0]) * K.BIN if len(below) else np.nan
    if abs(zt[100:][pk]) < 1:
        decay = 2.0
    return trace[100 + pk], pk * K.BIN, decay

cm = cluster_means(X, labels)
# hierarchical reordering as get_cell_clusters (moving average of 20 bins, correlation, cosine, average linkage)
cp = np.array([K.moving_avg(c, 20) for c in cm])
link = scipy.cluster.hierarchy.linkage(scipy.spatial.distance.pdist(np.nan_to_num(np.corrcoef(cp)), metric="cosine"), method="average")
reordering = scipy.cluster.hierarchy.leaves_list(link)
disp = {c: i for i, c in enumerate(reordering)}       # figure number of each size-ranked cluster
shape = []
for c in range(nclus):
    pk, tpk, dec = peak_decay(cm[c])
    late = np.abs(cm[c][200:300]).mean()
    shape.append({"cluster (size rank)": c, "figure number": disp[c], "cells": sizes[c], "peak": round(pk, 2),
                  "peak time s": tpk, "decay to 25% s": dec, "mean |1-2 s| / |peak|": round(late / abs(pk), 2), "min |1-2 s| / |peak|": round(np.abs(cm[c][200:300]).min() / abs(pk), 2),
                  "rapid and brief": (dec <= 0.25) if not np.isnan(dec) else False,
                  "persistent": late > 0.25 * abs(pk), "sign": "up" if pk > 0 else "down"})
shape = pd.DataFrame(shape)
print(shape.to_string(index=False))
rb = shape["rapid and brief"].sum()
pers = shape[shape.persistent]
print(f"rapid and brief clusters: {rb}; persistent clusters: {len(pers)}, of which peak within 250 ms: "
      f"{(pers['peak time s'] < 0.25).sum()}, later: {(pers['peak time s'] >= 0.25).sum()}")
for numbering in ("cluster (size rank)", "figure number"):
    sub = shape.set_index(numbering).loc[[0, 1, 2]]
    print(f"clusters 0, 1, 2 by {numbering}: persistent {list(sub.persistent)}, peak times {list(sub['peak time s'])}")
print("cells per cluster by session:")
print(pd.crosstab(mod_session, labels).to_string())
/home/runner/work/_temp/executed/.venv/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
/tmp/ipykernel_2286/535742623.py:20: UserWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
  sc.tl.leiden(ann, flavor="leidenalg")
puff-modulated units clustered: 6698; clusters: 11 (expected 12, tolerance 1)
 rank  size  expected  tolerance  within
    0   963      1070        107    True
    1   772       832         83    True
    2   664       803         80   False
    3   657       663         66    True
    4   651       639         64    True
    5   604       597         60    True
    6   590       547         55    True
    7   579       508         51   False
    8   554       473         47   False
    9   491       344         34   False
   10   173       317         32   False
   11     0       176         18   False
 cluster (size rank)  figure number  cells  peak  peak time s  decay to 25% s  mean |1-2 s| / |peak|  min |1-2 s| / |peak|  rapid and brief  persistent sign
                   0              0    963 -1.06         0.11            1.49                   0.26                  0.07            False        True down
                   1              3    772  1.40         0.52            1.35                   0.22                  0.08            False       False   up
                   2              5    664  3.65         0.09            0.49                   0.03                  0.01            False       False   up
                   3              6    657  2.10         0.19            0.58                   0.09                  0.04            False       False   up
                   4              1    651 -1.90         0.35            1.16                   0.14                  0.00            False       False down
                   5              2    604  2.26         0.50            1.24                   0.18                  0.03            False       False   up
                   6              9    590  3.28         0.07            0.18                   0.02                  0.00             True       False   up
                   7              8    579  3.99         0.10            0.32                   0.01                  0.00            False       False   up
                   8              7    554  2.96         0.29            0.55                   0.02                  0.00            False       False   up
                   9             10    491  6.29         0.04            0.10                   0.01                  0.00             True       False   up
                  10              4    173  0.49         0.77            0.86                   0.42                  0.12            False        True   up
rapid and brief clusters: 2; persistent clusters: 2, of which peak within 250 ms: 1, later: 1
clusters 0, 1, 2 by cluster (size rank): persistent [True, False, False], peak times [0.11, 0.52, 0.09]
clusters 0, 1, 2 by figure number: persistent [True, False, False], peak times [0.11, 0.35000000000000003, 0.5]
cells per cluster by session:
col_0                0   1    2    3   4   5   6   7   8   9   10
row_0                                                            
sub-0_ses-20230901   78  62  116   48  91  53  67  93  78  65  16
sub-1_ses-20230218   50  30   42   43  37  21  56  54  26  42   7
sub-1_ses-20230226  106  74   28  102  33  15  67  76  56  60  24
sub-2_ses-20230902  104  61   19   58  36  23  48  27  40  30  15
sub-3_ses-20230218   39  65   10   41  13  57  21  24  28   9   9
sub-3_ses-20230225  116  76   34   53  70  74  38  43  40  32  13
sub-4_ses-20230907   74  87   61   50  74  54  60  69  70  47  15
sub-5_ses-20230706   13  13    9    3   8   4   5   2   3   5   6
sub-5_ses-20230822   69  49   24   48  35  53  29  32  32  13  21
sub-6_ses-20230823   49  25   78   20  67  26  45  35  36  37  11
sub-7_ses-20220831  120  98   67   58  64  80  52  33  33  26  14
sub-8_ses-20220228   75  58  125   52  59  90  49  46  45  68   8
sub-9_ses-20230908   70  74   51   81  64  54  53  45  67  57  14

Result: not reproduced. With scanpy's default seed, Leiden gives 11 clusters, within one of 12, but only 6 of the 12 expected sizes in descending order are within tolerance: the 3rd (664 against 803), the 8th to 11th (579, 554, 491 and 173 against 508, 473, 344 and 317) and the missing 12th are not. By the criterion's measures two clusters are rapid and brief (size ranks 9 and 6, decay 0.10 and 0.18 s) and two are persistent: cluster 0 (mean |1 to 2 s| at 0.26 of the peak, peak at 0.11 s) and cluster 10 (0.42 of the peak, peak at 0.77 s). The criterion asks for three persistent clusters, two with peaks in the first 250 ms, so the shape condition fails under both numberings. Clusters 1, 4 and 5 decay after more than 1 s (1.35, 1.16 and 1.24 s, peaks at 0.52, 0.35 and 0.50 s), but their mean over 1 to 2 s is 0.22, 0.14 and 0.18 of the peak, below the 25% the criterion asks. Over ten Leiden seeds (stability section below) the count is within 12 +- 1 in 6, at most 7 sizes are within tolerance (seeds 6 and 8), and the full condition holds in none. The partition also changes with a perturbation at the level of rounding: with the PCA in float64 Leiden gives 12 clusters with 7 sizes within tolerance, and the float32 PCA on 4 BLAS threads gives 11 clusters with 4 within (adjusted Rand index 0.71 against the main partition for both); the size condition fails in both.

figs31b-cluster-overrepresentation¶

Clusters overrepresented in the periaqueductal gray and VPM thalamus had fast, sharp responses, while clusters overrepresented in frontal cortex and limbic regions responded more slowly and persistently.

(Figure 4G; fig. S31A and S31B; table S3, fig. S31 B)

As analysis.py get_cluster_null_distribution: regions (finest acronym, NA left out) with more than 30 clustered cells; the null is 20,000 draws of uniform labels (fraction in label 0), two-sided against 1/n; Benjamini-Hochberg across regions per cluster; overrepresented means significant with a fraction above 1/n. Clusters are ranked by find_decay; with 11 clusters the faster half is the 5 fastest. Frontal and limbic regions are those under ORB, ACA, PL, ILA, MOs or AI in the Allen tree.

In [6]:
def cluster_trace(avg, mask):
    """Table S3, Fig. 4G: cells of the cluster pooled, baseline-subtracted (1 s before the puff), z-scored over -1..2 s."""
    m = np.nanmean(avg[mask] - avg[mask][:, :100].mean(1, keepdims=True), 0)
    return scipy.stats.zscore(m)

FRONTAL = ["ORB", "ACA", "PL", "ILA", "MOs", "AI"]
def is_frontal(r):
    return any(K.under(r, f, paths) for f in FRONTAL)

def overrep(labels, nclus, seed=2024):
    """analysis.py get_cluster_null_distribution: per region with more than 30 clustered cells, the fraction
    in each cluster against 20,000 uniformly random labelings (label 0 of the draws), two-sided; BH-FDR
    across regions. Returns the corrected p (regions x clusters), the fractions and the regions."""
    rng = np.random.default_rng(seed)
    valid = mod_regions != "NA"
    ureg, ucnt = np.unique(mod_regions[valid], return_counts=True)
    big = ureg[ucnt > 30]
    pvals, fracs = {}, {}
    for r in big:
        ncell = (mod_regions == r).sum()
        null = (rng.integers(nclus, size=(20000, ncell)) == 0).mean(1)
        obs = np.bincount(labels[mod_regions == r], minlength=nclus) / ncell
        pvals[r] = np.array([np.mean(np.abs(null - 1 / nclus) >= abs(o - 1 / nclus)) for o in obs])
        fracs[r] = obs
    P = pd.DataFrame(pvals).T
    F = pd.DataFrame(fracs).T
    Q = P.apply(lambda col: pd.Series(scipy.stats.false_discovery_control(np.maximum(col.values, 1 / 20000)), index=col.index))
    return Q, F, big

def figs31b_test(Q, F, big, nclus, dec, thr):
    """PAG and VPM in a faster-half cluster and a frontal/limbic region in a slower-half cluster, with clusters
    ranked by `dec` (decay to 25% of the peak). With an odd count the faster half is the nclus // 2 fastest."""
    fast_half = set(np.argsort(np.nan_to_num(dec, nan=99), kind="stable")[: nclus // 2])
    over = (Q < thr) & (F > 1 / nclus)
    pag = [int(c) for c in np.where(over.loc["PAG"])[0] if c in fast_half] if "PAG" in big else []
    vpm = [int(c) for c in np.where(over.loc["VPM"])[0] if c in fast_half] if "VPM" in big else []
    fr = [(r, int(c)) for r in big if is_frontal(r) for c in np.where(over.loc[r])[0] if c not in fast_half]
    return over, fast_half, pag, vpm, fr

Q, F, big = overrep(labels, nclus)
# two rankings of cluster speed: find_decay on the cluster mean of z-scored cells (get_cell_clusters, as
# fig4de) and on the pooled baseline-subtracted trace that table S3 gives for Fig. 4G (verification variant)
rankings = {"cluster mean (fig4de)": np.array([peak_decay(cm[c])[2] for c in range(nclus)]),
            "pooled trace (Fig. 4G)": np.array([peak_decay(cluster_trace(pre_avg, labels == c))[2] for c in range(nclus)])}
print(f"regions with more than 30 clustered cells: {len(big)}; chance fraction 1/{nclus} = {1 / nclus:.3f} (paper 1/12 = 0.083)")
for rk, dec in rankings.items():
    order = np.argsort(np.nan_to_num(dec, nan=99), kind="stable")
    print(f"\nranking by {rk}, fastest first:", [(int(c), round(float(dec[c]), 2)) for c in order])
f31 = []
for thr in (0.05, 0.01):
    over = (Q < thr) & (F > 1 / nclus)
    print(f"\nFDR threshold {thr}: significantly overrepresented (region: clusters)")
    for r in big:
        cl = [int(c) for c in np.where(over.loc[r])[0]]
        if cl:
            print(f"  {r}: {cl}" + ("  frontal/limbic" if is_frontal(r) else ""))
    for rk, dec in rankings.items():
        _, fast_half, pag, vpm, fr = figs31b_test(Q, F, big, nclus, dec, thr)
        print(f"  [{rk}] faster half {sorted(int(c) for c in fast_half)}: PAG in a faster-half cluster {pag}; VPM {vpm}; "
              f"frontal/limbic in a slower-half cluster {fr}")
        f31.append({"FDR": thr, "ranking": rk, "PAG fast": bool(pag), "VPM fast": bool(vpm), "frontal slow": bool(fr),
                    "holds": bool(pag) and bool(vpm) and bool(fr)})
print()
print(pd.DataFrame(f31).to_string(index=False))
regions with more than 30 clustered cells: 54; chance fraction 1/11 = 0.091 (paper 1/12 = 0.083)

ranking by cluster mean (fig4de), fastest first: [(9, 0.1), (6, 0.18), (7, 0.32), (2, 0.49), (8, 0.55), (3, 0.58), (10, 0.86), (4, 1.16), (5, 1.24), (1, 1.35), (0, 1.49)]

ranking by pooled trace (Fig. 4G), fastest first: [(9, 0.1), (6, 0.16), (10, 0.18), (1, 0.27), (7, 0.29), (0, 0.31), (2, 0.4), (3, 0.48), (8, 0.49), (4, 0.88), (5, 0.96)]

FDR threshold 0.05: significantly overrepresented (region: clusters)
  ACAd5: [2]  frontal/limbic
  ACAv1: [1]  frontal/limbic
  ACAv2/3: [1]  frontal/limbic
  ACAv5: [0]  frontal/limbic
  ACB: [2]
  AId5: [0]  frontal/limbic
  AON: [5]
  CA1: [2]
  CP: [0, 10]
  EPd: [0]
  FF: [9]
  HY: [4]
  ILA1: [5]  frontal/limbic
  LSr: [0, 4]
  LSv: [0]
  MB: [2, 9]
  MD: [5]
  MOp5: [0]
  MOp6a: [0]
  MOs6a: [0]  frontal/limbic
  MRN: [2]
  NPC: [9]
  OLF: [0]
  ORBl5: [0]  frontal/limbic
  ORBl6a: [1]  frontal/limbic
  ORBm5: [5]  frontal/limbic
  ORBvl2/3: [1, 5]  frontal/limbic
  ORBvl5: [3, 5]  frontal/limbic
  OT: [0]
  PAG: [9]
  PIR: [0, 1]
  PL1: [0, 5]  frontal/limbic
  PL2/3: [0]  frontal/limbic
  PL5: [2]  frontal/limbic
  PO: [3, 8]
  PoT: [0, 1]
  RSPv5: [0, 3]
  RSPv6a: [3]
  RT: [9]
  SI: [5]
  SUB: [3]
  VPM: [2, 8]
  VPMpc: [1, 3]
  ZI: [9]
  [cluster mean (fig4de)] faster half [2, 6, 7, 8, 9]: PAG in a faster-half cluster [9]; VPM [2, 8]; frontal/limbic in a slower-half cluster [(np.str_('ACAv1'), 1), (np.str_('ACAv2/3'), 1), (np.str_('ACAv5'), 0), (np.str_('AId5'), 0), (np.str_('ILA1'), 5), (np.str_('MOs6a'), 0), (np.str_('ORBl5'), 0), (np.str_('ORBl6a'), 1), (np.str_('ORBm5'), 5), (np.str_('ORBvl2/3'), 1), (np.str_('ORBvl2/3'), 5), (np.str_('ORBvl5'), 3), (np.str_('ORBvl5'), 5), (np.str_('PL1'), 0), (np.str_('PL1'), 5), (np.str_('PL2/3'), 0)]
  [pooled trace (Fig. 4G)] faster half [1, 6, 7, 9, 10]: PAG in a faster-half cluster [9]; VPM []; frontal/limbic in a slower-half cluster [(np.str_('ACAd5'), 2), (np.str_('ACAv5'), 0), (np.str_('AId5'), 0), (np.str_('ILA1'), 5), (np.str_('MOs6a'), 0), (np.str_('ORBl5'), 0), (np.str_('ORBm5'), 5), (np.str_('ORBvl2/3'), 5), (np.str_('ORBvl5'), 3), (np.str_('ORBvl5'), 5), (np.str_('PL1'), 0), (np.str_('PL1'), 5), (np.str_('PL2/3'), 0), (np.str_('PL5'), 2)]

FDR threshold 0.01: significantly overrepresented (region: clusters)
  ACAd5: [2]  frontal/limbic
  ACAv2/3: [1]  frontal/limbic
  ACAv5: [0]  frontal/limbic
  AId5: [0]  frontal/limbic
  AON: [5]
  CA1: [2]
  CP: [0]
  EPd: [0]
  ILA1: [5]  frontal/limbic
  LSr: [0, 4]
  LSv: [0]
  MB: [2, 9]
  MD: [5]
  MOp5: [0]
  MOp6a: [0]
  MRN: [2]
  NPC: [9]
  OLF: [0]
  ORBl5: [0]  frontal/limbic
  ORBm5: [5]  frontal/limbic
  ORBvl2/3: [1, 5]  frontal/limbic
  ORBvl5: [3, 5]  frontal/limbic
  OT: [0]
  PAG: [9]
  PIR: [0, 1]
  PL2/3: [0]  frontal/limbic
  PO: [3, 8]
  RSPv5: [0]
  RSPv6a: [3]
  RT: [9]
  SI: [5]
  SUB: [3]
  VPM: [8]
  VPMpc: [1, 3]
  ZI: [9]
  [cluster mean (fig4de)] faster half [2, 6, 7, 8, 9]: PAG in a faster-half cluster [9]; VPM [8]; frontal/limbic in a slower-half cluster [(np.str_('ACAv2/3'), 1), (np.str_('ACAv5'), 0), (np.str_('AId5'), 0), (np.str_('ILA1'), 5), (np.str_('ORBl5'), 0), (np.str_('ORBm5'), 5), (np.str_('ORBvl2/3'), 1), (np.str_('ORBvl2/3'), 5), (np.str_('ORBvl5'), 3), (np.str_('ORBvl5'), 5), (np.str_('PL2/3'), 0)]
  [pooled trace (Fig. 4G)] faster half [1, 6, 7, 9, 10]: PAG in a faster-half cluster [9]; VPM []; frontal/limbic in a slower-half cluster [(np.str_('ACAd5'), 2), (np.str_('ACAv5'), 0), (np.str_('AId5'), 0), (np.str_('ILA1'), 5), (np.str_('ORBl5'), 0), (np.str_('ORBm5'), 5), (np.str_('ORBvl2/3'), 5), (np.str_('ORBvl5'), 3), (np.str_('ORBvl5'), 5), (np.str_('PL2/3'), 0)]

 FDR                ranking  PAG fast  VPM fast  frontal slow  holds
0.05  cluster mean (fig4de)      True      True          True   True
0.05 pooled trace (Fig. 4G)      True     False          True  False
0.01  cluster mean (fig4de)      True      True          True   True
0.01 pooled trace (Fig. 4G)      True     False          True  False

Result: partially reproduced. With the default seed and the criterion's ranking (find_decay on the cluster mean; faster half 2, 6, 7, 8 and 9), the condition holds under both FDR thresholds: PAG is overrepresented in cluster 9, the fastest, VPM in clusters 2 and 8 at 0.05 and in cluster 8 at 0.01, and frontal or limbic regions in slower-half clusters (at 0.01, for example ACAv5, AId5, ORBl5 and PL2/3 in cluster 0, ORBvl2/3 in clusters 1 and 5, ORBvl5 in clusters 3 and 5). Some frontal regions are also overrepresented in the faster-half cluster 2 (ACAd5 at both thresholds, PL5 at 0.05). Under the ranking by the pooled trace of Fig. 4G (faster half 1, 6, 7, 9 and 10), VPM is in no faster-half cluster at either threshold, so the condition fails there. Over ten Leiden seeds (stability section below) the criterion's ranking holds in 8 seeds at 0.05 and 7 at 0.01, the pooled ranking in 3 and 2, and all four combinations in 2 (seeds 4 and 8); every failure is the VPM part. The outcome stays partially reproduced: the PAG and frontal parts hold under every seed and ranking, the VPM part holds under the criterion's ranking but not under the ranking of the paper's Fig. 4G cell, and it depends on the partition. The chance fraction is 1/11 here, against the paper's 1/12, as there are 11 clusters.

fig4g-ketamine-alters-slow-clusters¶

Ketamine substantially changed the responses of clusters with slow, persistent preinfusion dynamics but left fast clusters little changed.

(Figure 4G; fig. S31C; table S3, Fig 4G)

Readings from the criterion. Ketamine is series 23 to 32 (code). The Pre decay time of each cluster is find_decay on the same Pre cluster trace.

In [7]:
def fig4g_table(labels, nclus):
    rows = []
    for c in range(nclus):
        tp = cluster_trace(pre_avg, labels == c)
        tk = cluster_trace(ket_avg, labels == c)
        rows.append({"cluster": c, "cells": int((labels == c).sum()), "pre decay s": peak_decay(tp)[2],
                     "cosine distance": scipy.spatial.distance.cosine(tp, tk),
                     "|diff| 1-2 s": abs(tp[200:300].mean() - tk[200:300].mean())})
    g = pd.DataFrame(rows)
    rho = {meas: scipy.stats.spearmanr(g["pre decay s"], g[meas]) for meas in ("cosine distance", "|diff| 1-2 s")}
    return g, rho

g, rho = fig4g_table(labels, nclus)
print(g.round(3).to_string(index=False))
for meas, r in rho.items():
    print(f"{meas}: Spearman rho = {r.statistic:.3f}, p = {r.pvalue:.4f} (n = {nclus} clusters)")
 cluster  cells  pre decay s  cosine distance  |diff| 1-2 s
       0    963         0.31            1.171         0.464
       1    772         0.27            0.637         0.617
       2    664         0.40            0.060         0.089
       3    657         0.48            0.172         0.145
       4    651         0.88            0.622         0.363
       5    604         0.96            0.139         0.228
       6    590         0.16            0.583         0.420
       7    579         0.29            0.233         0.153
       8    554         0.49            0.024         0.084
       9    491         0.10            0.081         0.165
      10    173         0.18            0.754         0.831
cosine distance: Spearman rho = -0.273, p = 0.4171 (n = 11 clusters)
|diff| 1-2 s: Spearman rho = -0.409, p = 0.2115 (n = 11 clusters)

Result: not reproduced. Across the 11 clusters, the change from Pre to Ketamine does not correlate positively with the Pre decay time under either reading: Spearman rho = -0.273 (p = 0.42) for the cosine distance and rho = -0.409 (p = 0.21) for the difference over 1 to 2 s. The largest changes are in clusters 0 and 10 (cosine distance 1.171 and 0.754, Pre decay 0.31 and 0.18 s), while of the two slowest clusters 4 changes (0.622, decay 0.88 s) and 5 changes little (0.139, decay 0.96 s). No seed of the ten in the stability section gives a positive correlation with p < 0.05 (rho -0.297 to 0.224 for the cosine distance, -0.486 to 0.248 for the difference).

Stability of the clustering (fig4de, figs31b, fig4g)¶

Added in revision. The partition Leiden returns depends on its random seed, so the three clustering claims are re-run over ten seeds on the same neighbor graph. The main result is seed 0, scanpy's default, which the code uses as it calls sc.tl.leiden without a seed. Added in revision 2: the partition with the PCA in float64 and with the float32 PCA on 4 BLAS threads, two perturbations at the level of rounding, against the main partition (one BLAS thread).

In [8]:
# Stability of the clustering (added in revision). Leiden's partition depends on its random seed; scanpy's
# default (0) is the main result above, as the code calls sc.tl.leiden without a seed. The neighbor graph is
# the same for every seed. For each Leiden seed: the fig4de counts, the figs31b test under both rankings and
# both FDR thresholds, and the fig4g correlations.
stab = []
for seed in range(10):
    sc.tl.leiden(ann, flavor="leidenalg", random_state=seed, key_added=f"leiden_{seed}")
    lab = ann.obs[f"leiden_{seed}"].astype(int).to_numpy()
    nc = lab.max() + 1
    srt_s = np.sort(np.bincount(lab))[::-1]
    nwithin = sum(abs((srt_s[i] if i < len(srt_s) else 0) - e) <= round(0.1 * e) for i, e in enumerate(expected_sizes))
    cm_s = cluster_means(X, lab)
    pk_dec = [peak_decay(cm_s[c]) for c in range(nc)]
    late = [np.abs(cm_s[c][200:300]).mean() > 0.25 * abs(pk_dec[c][0]) for c in range(nc)]
    rapid = sum((d <= 0.25) for _, _, d in pk_dec if not np.isnan(d))
    pers_early = sum(l and t < 0.25 for l, (_, t, _) in zip(late, pk_dec))
    Qs, Fs, bigs = overrep(lab, nc)
    row = {"seed": seed, "clusters": nc, "sizes within (of 12)": nwithin, "rapid and brief": rapid,
           "persistent": sum(late), "persistent, early peak": pers_early,
           "fig4de holds": abs(nc - 12) <= 1 and nwithin == 12 and rapid >= 1 and sum(late) >= 3 and pers_early >= 2 and sum(late) - pers_early >= 1}
    for rk, dec in (("mean", np.array([d for _, _, d in pk_dec])),
                    ("pooled", np.array([peak_decay(cluster_trace(pre_avg, lab == c))[2] for c in range(nc)]))):
        for thr in (0.05, 0.01):
            _, _, pag, vpm, fr = figs31b_test(Qs, Fs, bigs, nc, dec, thr)
            miss = [n for n, v in (("PAG", pag), ("VPM", vpm), ("frontal", fr)) if not v]
            row[f"S31B {rk} {thr}"] = "holds" if not miss else "no " + "+".join(miss)
    _, rho_s = fig4g_table(lab, nc)
    row["4G rho cosine"] = rho_s["cosine distance"].statistic
    row["4G p cosine"] = rho_s["cosine distance"].pvalue
    row["4G rho |diff|"] = rho_s["|diff| 1-2 s"].statistic
    row["4G p |diff|"] = rho_s["|diff| 1-2 s"].pvalue
    stab.append(row)
stab = pd.DataFrame(stab)
print(stab.round(3).to_string(index=False))
print("\nseeds (of 10) where each test holds:")
print(f"fig4de: {int(stab['fig4de holds'].sum())}; cluster count within 12 +- 1: {int((abs(stab.clusters - 12) <= 1).sum())}")
for c in [c for c in stab.columns if c.startswith("S31B")]:
    print(f"{c}: {int((stab[c] == 'holds').sum())}")
print(f"fig4g: rho > 0 with p < 0.05 in {((stab['4G rho cosine'] > 0) & (stab['4G p cosine'] < 0.05)).sum()} seeds (cosine) and "
      f"{((stab['4G rho |diff|'] > 0) & (stab['4G p |diff|'] < 0.05)).sum()} (|diff|)")

# Added in revision 2: the same pipeline with the PCA run in float64, a change at the level of rounding, and
# the partition from 4 BLAS threads (what an unpinned run gave on a 4-core machine), against the main partition.
from sklearn.metrics import adjusted_rand_score
for lbl, dtype, nthr in (("float64 PCA, 1 thread", np.float64, 1), ("float32 PCA, 4 threads", np.float32, 4)):
    with threadpool_limits(nthr):
        a2 = sc.AnnData(X.astype(dtype))
        sc.tl.pca(a2, svd_solver="arpack")
        sc.pp.neighbors(a2, n_neighbors=30, n_pcs=20)
        sc.tl.leiden(a2, flavor="leidenalg")
    l2 = a2.obs["leiden"].astype(int).to_numpy()
    s2 = np.sort(np.bincount(l2))[::-1]
    nw2 = sum(abs((s2[i] if i < len(s2) else 0) - e) <= round(0.1 * e) for i, e in enumerate(expected_sizes))
    print(f"{lbl}: {l2.max() + 1} clusters, sizes {s2.tolist()}, {nw2} of 12 sizes within tolerance, "
          f"adjusted Rand index against the main partition {adjusted_rand_score(labels, l2):.3f}")
/tmp/ipykernel_2286/2756076075.py:7: UserWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
  sc.tl.leiden(ann, flavor="leidenalg", random_state=seed, key_added=f"leiden_{seed}")
/tmp/ipykernel_2286/2756076075.py:7: UserWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
  sc.tl.leiden(ann, flavor="leidenalg", random_state=seed, key_added=f"leiden_{seed}")
/tmp/ipykernel_2286/2756076075.py:7: UserWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
  sc.tl.leiden(ann, flavor="leidenalg", random_state=seed, key_added=f"leiden_{seed}")
/tmp/ipykernel_2286/2756076075.py:7: UserWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
  sc.tl.leiden(ann, flavor="leidenalg", random_state=seed, key_added=f"leiden_{seed}")
/tmp/ipykernel_2286/2756076075.py:7: UserWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
  sc.tl.leiden(ann, flavor="leidenalg", random_state=seed, key_added=f"leiden_{seed}")
/tmp/ipykernel_2286/2756076075.py:7: UserWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
  sc.tl.leiden(ann, flavor="leidenalg", random_state=seed, key_added=f"leiden_{seed}")
/tmp/ipykernel_2286/2756076075.py:7: UserWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
  sc.tl.leiden(ann, flavor="leidenalg", random_state=seed, key_added=f"leiden_{seed}")
/tmp/ipykernel_2286/2756076075.py:7: UserWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
  sc.tl.leiden(ann, flavor="leidenalg", random_state=seed, key_added=f"leiden_{seed}")
/tmp/ipykernel_2286/2756076075.py:7: UserWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
  sc.tl.leiden(ann, flavor="leidenalg", random_state=seed, key_added=f"leiden_{seed}")
/tmp/ipykernel_2286/2756076075.py:7: UserWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
  sc.tl.leiden(ann, flavor="leidenalg", random_state=seed, key_added=f"leiden_{seed}")
 seed  clusters  sizes within (of 12)  rapid and brief  persistent  persistent, early peak  fig4de holds S31B mean 0.05 S31B mean 0.01 S31B pooled 0.05 S31B pooled 0.01  4G rho cosine  4G p cosine  4G rho |diff|  4G p |diff|
    0        11                     6                2           2                       1         False          holds          holds           no VPM           no VPM         -0.273        0.417         -0.409        0.212
    1        10                     4                1           1                       0         False          holds         no VPM           no VPM           no VPM         -0.261        0.466         -0.486        0.154
    2        11                     6                1           2                       0         False          holds          holds            holds           no VPM          0.164        0.631          0.164        0.631
    3        10                     3                2           0                       0         False          holds          holds           no VPM           no VPM          0.134        0.713          0.164        0.650
    4        10                     2                1           3                       1         False          holds          holds            holds            holds          0.224        0.533          0.248        0.489
    5        12                     6                2           1                       0         False         no VPM         no VPM           no VPM           no VPM          0.035        0.914          0.049        0.880
    6        12                     7                1           1                       0         False         no VPM         no VPM           no VPM           no VPM         -0.007        0.983         -0.004        0.991
    7        10                     2                1           2                       1         False          holds          holds           no VPM           no VPM         -0.297        0.405          0.006        0.987
    8        11                     7                2           3                       1         False          holds          holds            holds            holds          0.173        0.611          0.091        0.790
    9        11                     2                1           3                       1         False          holds          holds           no VPM           no VPM         -0.045        0.894          0.018        0.958

seeds (of 10) where each test holds:
fig4de: 0; cluster count within 12 +- 1: 6
S31B mean 0.05: 8
S31B mean 0.01: 7
S31B pooled 0.05: 3
S31B pooled 0.01: 2
fig4g: rho > 0 with p < 0.05 in 0 seeds (cosine) and 0 (|diff|)
/tmp/ipykernel_2286/2756076075.py:50: UserWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
  sc.tl.leiden(a2, flavor="leidenalg")
float64 PCA, 1 thread: 12 clusters, sizes [910, 690, 660, 637, 611, 598, 576, 555, 464, 417, 392, 188], 7 of 12 sizes within tolerance, adjusted Rand index against the main partition 0.709
/tmp/ipykernel_2286/2756076075.py:50: UserWarning: The `igraph` implementation of leiden clustering is *orders of magnitude faster*. Set the flavor argument to (and install if needed) 'igraph' to use it.
  sc.tl.leiden(a2, flavor="leidenalg")
float32 PCA, 4 threads: 11 clusters, sizes [1019, 730, 681, 645, 636, 632, 629, 580, 560, 394, 192], 4 of 12 sizes within tolerance, adjusted Rand index against the main partition 0.710

fig4i-rise-times-unchanged¶

Most regions responded to the puff with fast rise times, and ketamine did not change regional rise times.

(Figure 4I; fig. S31D to S31F; table S3, Fig. 4H, I)

As analysis.py region_time_course: regions merged across layers (reduce_region), per region and cluster the first two puffs of each series averaged over cells (40 Pre replicates, 20 Ketamine), baseline-subtracted, d' against the 1 s before the puff, rise and decay at 25% of the normalized d' peak, peaks after 750 ms dropped, cluster values combined with the region's cell fractions (clusters with one cell weighted 0), 1000 bootstraps over puff replicates, the catch-all labels NA, STR, MB and HY dropped. The code skips cluster 11; this clustering has 11 clusters (0 to 10), so none is skipped. Bootstrap seed 0 (a standard choice). The d' threshold (1.3 Methods and code, 2 table S3), the region rule (more than 30, code; at least 30, Methods) and the Ketamine series (23 to 32, code; 22 to 32, Methods-literal) are all shown.

In [9]:
first2_pre = np.concatenate([res[k]["first2_pre"] for k in names])      # (cells, 20, 2, 300)
first2_ket = np.concatenate([res[k]["first2_ket"] for k in names])      # (cells, 10, 2, 300), series 23-32
first2_ket_lit = np.concatenate([res[k]["first2_ket_lit"] for k in names])  # series 22-32
red_regions = np.array([K.reduce_region(r) for r in mod_regions])

def dprime_abs(p):
    """region_time_course calculate_dprime on (..., trials, 300): |mean - baseline mean| / pooled sd."""
    bm = p[..., :100].mean((-1, -2))
    bs = np.nanstd(p[..., :100].reshape(p.shape[:-2] + (-1,)), -1)
    rm = p.mean(-2)
    rs = np.nanstd(p, -2)
    return np.abs(rm - bm[..., None]) / np.sqrt(0.5 * (bs[..., None] ** 2 + rs ** 2))

def rise_decay(p, thr=0.25):
    """region_time_course _find_rise_decay, vectorized over leading axes of p (..., trials, 300)."""
    d = dprime_abs(p)
    d = d - d[..., :100].mean(-1, keepdims=True)
    d = d / d.max(-1, keepdims=True)
    post = d[..., 100:]
    pk = post.argmax(-1)
    above = post > thr
    rise = np.where(above.any(-1), above.argmax(-1) * K.BIN, np.nan)
    idx = np.arange(post.shape[-1])
    after = (post < thr) & (idx >= pk[..., None])
    decay = np.where(after.any(-1), after.argmax(-1) * K.BIN, np.nan)
    bad = pk > 75
    rise[bad] = np.nan
    decay[bad] = np.nan
    return rise, decay

def region_times(first2_p, first2_k, min_cells=30, strict=True, dthr=1.3, nreps=1000, seed=0):
    rng = np.random.default_rng(seed)
    out = {}
    reg_u, reg_n = np.unique(red_regions[red_regions != "NA"], return_counts=True)
    for r, nr in zip(reg_u, reg_n):
        if (nr <= min_cells) if strict else (nr < min_cells):
            continue
        f = red_regions == r
        allp = first2_p[f].mean(0).reshape(-1, 300)
        allp = allp - allp[:, :100].mean(1, keepdims=True)
        peak = np.max(dprime_abs(allp[None])[0][100:])
        if peak < dthr:
            continue
        w = np.array([(f & (labels == c)).sum() for c in range(nclus)], float)
        w[w <= 1] = 0
        w /= w.sum()
        res_r = {"n": int(nr), "peak_dprime": peak}
        for cond, arr in (("pre", first2_p), ("ket", first2_k)):
            data = []
            for c in range(nclus):
                m = f & (labels == c)
                if w[c] == 0:
                    data.append(np.zeros((arr.shape[1] * 2, 300)))
                    continue
                d = np.nanmean(arr[m], 0).reshape(-1, 300)
                data.append(d - d[:, :100].mean(1, keepdims=True))
            data = np.stack(data)                                   # (clusters, trials, 300)
            nt = data.shape[1]
            ix = rng.integers(0, nt, size=(nreps, nt))
            ri = np.empty((nclus, nreps)); de = np.empty((nclus, nreps))
            for c in range(nclus):                                  # (reps, trials, 300) per cluster
                ri[c], de[c] = rise_decay(data[c][ix].astype(np.float32))
            res_r[cond + "_rise"] = np.nansum(w[:, None] * np.where(w[:, None] > 0, ri, 0), 0)
            res_r[cond + "_decay"] = np.nansum(w[:, None] * np.where(w[:, None] > 0, de, 0), 0)
            # verification variant: a cluster whose rise or decay is NaN (peak after 750 ms, or no fall below
            # 25%) adds 0 in the code's nansum; here the weights are renormalized over the clusters with a
            # value, and a replicate with none is NaN. Also the weighted fraction of NaN cluster values.
            for nm, v in (("rise", ri), ("decay", de)):
                ok = np.isfinite(v) & (w[:, None] > 0)
                wsum = (w[:, None] * ok).sum(0)
                res_r[f"{cond}_{nm}_rn"] = np.where(wsum > 0, (w[:, None] * np.where(ok, v, 0)).sum(0) / np.where(wsum > 0, wsum, 1), np.nan)
                res_r[f"{cond}_{nm}_nanfrac"] = 1 - wsum.mean()
        out[r] = res_r
    for catch in ("NA", "STR", "MB", "HY"):          # region_time_course drops these catch-all labels
        out.pop(catch, None)
    return out

# One pass per Ketamine reading at the widest settings (>= 30 cells, peak d' >= 1.3); the code's rule
# (> 30 cells) and the d' threshold of 2 (table S3) are subsets of it, applied below.
rt_all = {kname: region_times(first2_pre, karr, min_cells=30, strict=False, dthr=1.3)
          for kname, karr in (("ket 23-32", first2_ket), ("ket 22-32", first2_ket_lit))}
rt = {}
for dthr in (1.3, 2.0):
    for label, keep in (("code >30", lambda o: o["n"] > 30), ("Methods >=30", lambda o: o["n"] >= 30)):
        for kname, out in rt_all.items():
            rt[(dthr, label, kname)] = {r: o for r, o in out.items() if keep(o) and o["peak_dprime"] >= dthr}
# verification control: Pre has 20 series (40 replicate puffs) and Ketamine 10 (20); Pre from series 11 to 20
# only gives both 20, at the main settings
rt_sub = region_times(first2_pre[:, 10:20], first2_ket, min_cells=30, strict=True, dthr=1.3)
rows = []
for key, out in rt.items():
    med_change = np.array([np.nanmedian(o["ket_rise"] - o["pre_rise"]) for o in out.values()])
    pre_rise = np.array([np.nanmedian(o["pre_rise"]) for o in out.values()])
    rows.append({"d' threshold": key[0], "region rule": key[1], "ketamine series": key[2], "regions": len(out),
                 "median over regions of median rise change (s)": np.median(med_change),
                 "regions with Pre median rise < 0.1 s": int((pre_rise < 0.1).sum()),
                 "within one bin": abs(np.median(med_change)) <= 0.01 + 1e-9})
print(pd.DataFrame(rows).to_string(index=False))
main = rt[(1.3, "code >30", "ket 23-32")]
print(pd.DataFrame({r: {"n": o["n"], "peak d'": round(o["peak_dprime"], 2), "Pre rise": np.nanmedian(o["pre_rise"]),
                        "Ket rise": np.nanmedian(o["ket_rise"]), "Pre decay": np.nanmedian(o["pre_decay"]),
                        "Ket decay": np.nanmedian(o["ket_decay"])} for r, o in main.items()}).T.sort_values("Pre decay").to_string())
print("\nweighted fraction of NaN cluster values (cluster weight given 0 by nansum), main settings, over regions:")
nanf = pd.DataFrame({r: {f"{c} {m}": o[f"{c}_{m}_nanfrac"] for c in ("pre", "ket") for m in ("rise", "decay")} for r, o in main.items()}).T
print(nanf.describe().loc[["mean", "50%", "max"]].round(3).to_string())
print("regions where any NaN fraction exceeds 0.1:", list(nanf.index[(nanf > 0.1).any(axis=1)]))
rows = []
for lab, out, sfx in (("code nansum (main)", main, ""), ("renormalized weights", main, "_rn"), ("Pre series 11-20 only", rt_sub, ""), ("Pre series 11-20, renormalized", rt_sub, "_rn")):
    med_change = np.array([np.nanmedian(o["ket_rise" + sfx] - o["pre_rise" + sfx]) for o in out.values()])
    pre_rise = np.array([np.nanmedian(o["pre_rise" + sfx]) for o in out.values()])
    rows.append({"variant": lab, "regions": len(out), "median over regions of median rise change (s)": np.median(med_change),
                 "regions with Pre median rise < 0.1 s": int((pre_rise < 0.1).sum()), "within one bin": abs(np.median(med_change)) <= 0.01 + 1e-9})
print("\nrise-time variants (threshold 1.3, code rule, Ketamine 23-32):")
print(pd.DataFrame(rows).to_string(index=False))
 d' threshold  region rule ketamine series  regions  median over regions of median rise change (s)  regions with Pre median rise < 0.1 s  within one bin
          1.3     code >30       ket 23-32       35                                       0.000122                                    32            True
          1.3     code >30       ket 22-32       35                                      -0.003015                                    32            True
          1.3 Methods >=30       ket 23-32       35                                       0.000122                                    32            True
          1.3 Methods >=30       ket 22-32       35                                      -0.003015                                    32            True
          2.0     code >30       ket 23-32       31                                       0.000122                                    30            True
          2.0     code >30       ket 22-32       31                                      -0.003015                                    30            True
          2.0 Methods >=30       ket 23-32       31                                       0.000122                                    30            True
          2.0 Methods >=30       ket 22-32       31                                      -0.003015                                    30            True
           n  peak d'  Pre rise  Ket rise  Pre decay  Ket decay
LSv     44.0     1.34  0.032386  0.052273   0.202955   0.298636
RT      37.0     3.71  0.025588  0.025000   0.212941   0.149412
CA3     35.0     2.79  0.031471  0.010735   0.227647   0.143676
MOp     96.0     1.71  0.040691  0.143564   0.257926   0.460426
PAG    138.0     3.45  0.010882  0.008676   0.268529   0.145515
FF      51.0     3.03  0.059216  0.045294   0.281176   0.248431
NPC     84.0     3.50  0.010476  0.013571   0.284107   0.154524
TH      53.0     3.68  0.055577  0.030769   0.300865   0.227308
SCig    42.0     2.35  0.090714  0.051905   0.316905   0.194643
MRN     79.0     3.13  0.021795  0.009936   0.328910   0.195128
PoT     68.0     2.59  0.029104  0.028358   0.347612   0.236940
MOs     74.0     2.25  0.043425  0.052397   0.363151   0.263082
ZI     171.0     5.24  0.021023  0.017281   0.379971   0.174532
ILA     63.0     2.40  0.027581  0.029677   0.390081   0.162661
LP      82.0     2.54  0.082284  0.043272   0.393704   0.246173
CA1     99.0     3.76  0.030102  0.035204   0.401888   0.214745
ORBm    45.0     2.01  0.041136  0.050909   0.420795   0.233182
ACAv   154.0     3.29  0.051071  0.053279   0.433182   0.244383
SUB     47.0     2.64  0.059022  0.090870   0.437283   0.311413
DG-mo   57.0     2.97  0.038333  0.033333   0.458684   0.204035
CP     274.0     3.19  0.048175  0.077591   0.495109   0.460821
RSPv   297.0     3.18  0.049360  0.080067   0.512643   0.382626
AIv    132.0     2.10  0.062423  0.070577   0.513269   0.287269
PO     277.0     4.58  0.027762  0.028809   0.526264   0.376986
ACB    205.0     3.20  0.041317  0.041707   0.527439   0.341780
ORBl   390.0     3.92  0.053987  0.082038   0.532231   0.389192
ACAd   153.0     3.72  0.046732  0.038562   0.542353   0.261242
PL     193.0     3.38  0.034119  0.030570   0.549896   0.225648
VPM    131.0     3.20  0.062385  0.041538   0.557423   0.266962
MD     185.0     3.08  0.053777  0.038723   0.570815   0.246141
LSr    403.0     4.00  0.021216  0.032022   0.585695   0.220583
OLF    181.0     2.37  0.094006  0.089972   0.618011   0.387348
ORBvl  127.0     2.07  0.125952  0.054127   0.643016   0.286627
AON     62.0     1.48  0.123115  0.046885   0.646557   0.429836
PIR    154.0     1.76  0.147143  0.109610   0.751396   0.438766

weighted fraction of NaN cluster values (cluster weight given 0 by nansum), main settings, over regions:
      pre rise  pre decay  ket rise  ket decay
mean     0.089      0.087     0.246      0.246
50%      0.082      0.082     0.201      0.201
max      0.259      0.259     0.580      0.577
regions where any NaN fraction exceeds 0.1: [np.str_('ACAd'), np.str_('ACAv'), np.str_('ACB'), np.str_('AIv'), np.str_('AON'), np.str_('CA1'), np.str_('CA3'), np.str_('CP'), np.str_('DG-mo'), np.str_('FF'), np.str_('ILA'), np.str_('LP'), np.str_('LSr'), np.str_('LSv'), np.str_('MD'), np.str_('MOp'), np.str_('MOs'), np.str_('MRN'), np.str_('NPC'), np.str_('OLF'), np.str_('ORBl'), np.str_('ORBm'), np.str_('ORBvl'), np.str_('PAG'), np.str_('PL'), np.str_('RSPv'), np.str_('RT'), np.str_('SCig'), np.str_('SUB'), np.str_('TH'), np.str_('VPM'), np.str_('ZI')]

rise-time variants (threshold 1.3, code rule, Ketamine 23-32):
                       variant  regions  median over regions of median rise change (s)  regions with Pre median rise < 0.1 s  within one bin
            code nansum (main)       35                                       0.000122                                    32            True
          renormalized weights       35                                       0.006471                                    31            True
         Pre series 11-20 only       36                                      -0.003976                                    35            True
Pre series 11-20, renormalized       36                                       0.004043                                    31            True

Result: reproduced. Under every combination of d' threshold (1.3 or 2), region rule and Ketamine series, the median over regions of the bootstrap median change in rise time is +0.0001 s (Ketamine 23 to 32) or -0.0030 s (22 to 32), within one 10 ms bin of zero, and the Pre median rise is below 0.1 s in 32 of 35 regions (threshold 1.3) and 30 of 31 (threshold 2). In the code's weighting a cluster without a rise (peak after 750 ms) adds zero; that weight averages 0.089 of a region in Pre and 0.246 under ketamine. Renormalizing the weights over clusters with a value, taking Pre from series 11 to 20 only (20 replicate puffs, as Ketamine), or both, gives changes of +0.0065, -0.0040 and +0.0040 s, all within one bin, with the Pre rise below 0.1 s in 31 to 35 regions.

fig4h-decay-shortened¶

Ketamine shortened the time for regional puff responses to decay from their peak.

(Figure 4H; fig. S31G)

The bootstrapped decay times of fig4i-rise-times-unchanged.

In [10]:
rows = []
for key, out in rt.items():
    ch = np.concatenate([o["ket_decay"] - o["pre_decay"] for o in out.values()])
    shorter = sum(np.nanmedian(o["ket_decay"]) < np.nanmedian(o["pre_decay"]) for o in out.values())
    rows.append({"d' threshold": key[0], "region rule": key[1], "ketamine series": key[2], "regions": len(out),
                 "median of pooled bootstrap changes (s)": np.nanmedian(ch),
                 "regions with Ket median decay < Pre": shorter})
print(pd.DataFrame(rows).to_string(index=False))
rows = []
for lab, out, sfx in (("code nansum (main)", main, ""), ("renormalized weights", main, "_rn"), ("Pre series 11-20 only", rt_sub, ""), ("Pre series 11-20, renormalized", rt_sub, "_rn")):
    ch = np.concatenate([o["ket_decay" + sfx] - o["pre_decay" + sfx] for o in out.values()])
    shorter = sum(np.nanmedian(o["ket_decay" + sfx]) < np.nanmedian(o["pre_decay" + sfx]) for o in out.values())
    rows.append({"variant": lab, "regions": len(out), "median of pooled bootstrap changes (s)": np.nanmedian(ch),
                 "regions with Ket median decay < Pre": shorter})
print("\ndecay-time variants (threshold 1.3, code rule, Ketamine 23-32):")
print(pd.DataFrame(rows).to_string(index=False))
 d' threshold  region rule ketamine series  regions  median of pooled bootstrap changes (s)  regions with Ket median decay < Pre
          1.3     code >30       ket 23-32       35                               -0.162383                                   33
          1.3     code >30       ket 22-32       35                               -0.158051                                   33
          1.3 Methods >=30       ket 23-32       35                               -0.162383                                   33
          1.3 Methods >=30       ket 22-32       35                               -0.158051                                   33
          2.0     code >30       ket 23-32       31                               -0.165027                                   31
          2.0     code >30       ket 22-32       31                               -0.161015                                   31
          2.0 Methods >=30       ket 23-32       31                               -0.165027                                   31
          2.0 Methods >=30       ket 22-32       31                               -0.161015                                   31

decay-time variants (threshold 1.3, code rule, Ketamine 23-32):
                       variant  regions  median of pooled bootstrap changes (s)  regions with Ket median decay < Pre
            code nansum (main)       35                               -0.162383                                   33
          renormalized weights       35                               -0.124458                                   29
         Pre series 11-20 only       36                               -0.083488                                   31
Pre series 11-20, renormalized       36                               -0.058447                                   26

Result: reproduced. The median of the pooled bootstrapped changes in decay time is -0.158 to -0.165 s under every setting, and the Ketamine median decay is below the Pre median in 33 of 35 regions (threshold 1.3) and 31 of 31 (threshold 2). Part of this comes from the code's weighting, in which a cluster without a decay adds zero and which drops 0.246 of the weight under ketamine against 0.087 in Pre: renormalized weights give -0.124 s (29 of 35 regions shorter), Pre from series 11 to 20 only -0.083 s (31 of 36), and both -0.058 s (26 of 36). The decay still shortens under each control, by 0.36 to 0.77 of the main change (-0.162 s).

fig4h-decay-anatomical-progression¶

Before ketamine, regional decay times followed a progression from subcortical to frontal cortical regions, with long decays in frontal and limbic cortex and in VPM and MD thalamus.

(Figure 4H; Results, Brain-wide activity screen during emergence of affect in mice)

Pre decay medians of fig4i-rise-times-unchanged (code region rule, Ketamine 23 to 32). Frontal and limbic regions are those under ORB, ACA, PL, ILA, MOs, MOp or AI; midbrain those under MB, in the Allen tree.

In [11]:
FRONTAL_LIMBIC = ["ORB", "ACA", "PL", "ILA", "MOs", "MOp", "AI"]
rows = []
for key, out in rt.items():
    if key[1] != "code >30" or key[2] != "ket 23-32":
        continue
    dec = pd.Series({r: np.nanmedian(o["pre_decay"]) for r, o in out.items()})
    fl = [r for r in dec.index if any(K.under(r, f, paths) for f in FRONTAL_LIMBIC)]
    mb = [r for r in dec.index if K.under(r, "MB", paths)]
    med_all = dec.median()
    rows.append({"d' threshold": key[0], "frontal/limbic regions": ", ".join(fl), "median decay frontal/limbic": dec[fl].median(),
                 "midbrain regions": ", ".join(mb), "median decay midbrain": dec[mb].median(), "median all": med_all,
                 "VPM": dec.get("VPM", np.nan), "MD": dec.get("MD", np.nan)})
print(pd.DataFrame(rows).T.to_string())
                                                                                 0                                                 1
d' threshold                                                                   1.3                                               2.0
frontal/limbic regions       ACAd, ACAv, AIv, ILA, MOp, MOs, ORBl, ORBm, ORBvl, PL  ACAd, ACAv, AIv, ILA, MOs, ORBl, ORBm, ORBvl, PL
median decay frontal/limbic                                               0.473226                                          0.513269
midbrain regions                                               MRN, NPC, PAG, SCig                               MRN, NPC, PAG, SCig
median decay midbrain                                                     0.300506                                          0.300506
median all                                                                0.433182                                          0.433182
VPM                                                                       0.557423                                          0.557423
MD                                                                        0.570815                                          0.570815

Result: reproduced. The median Pre decay of the frontal and limbic regions (0.473 s at threshold 1.3, 0.513 s at 2) exceeds that of the midbrain regions MRN, NPC, PAG and SCig (0.301 s), and VPM (0.557 s) and MD (0.571 s) both lie above the median of all regions (0.433 s).

Session readings for Figures 5 and 6¶

"For cross-puff and population analyses, we excluded one animal that exhibited notable stress response (Harderian gland secretion) during the pre-ketamine epoch" (Methods, Neuropixels recording and processing). The animal is not named. Each mouse with two sessions (sub-1, sub-3, sub-5) is a reading: removing any of them leaves 11 sessions from 9 mice, as the paper reports. Only the removal of sub-5 also leaves 7 tone sessions from 7 mice, the tone counts of table S3 (fig. S34); the other two leave 8 tone sessions from 7 mice. Every session has at least 64 puff-modulated units, above the code's minimum of 20.

In [12]:
# Session readings for the cross-puff and population analyses (Figs. 5 and 6): the paper excludes one
# unnamed animal; each mouse with two sessions is a reading (fig5d-emotion-dimension-accumulates).
multi = sorted({res[k]["subject"] for k in names if sum(res[j]["subject"] == res[k]["subject"] for j in names) > 1})
READINGS = {f"without sub-{m}": [k for k in names if res[k]["subject"] != m and res[k]["n_mod"] >= 20] for m in multi}
for r, ks in READINGS.items():
    print(f"{r}: {len(ks)} sessions from {len({res[k]['subject'] for k in ks})} mice")
TONE_SESSIONS = [k for k in names if "tone" in res[k]]
for r, ks in READINGS.items():
    t = [k for k in ks if k in TONE_SESSIONS]
    print(f"  tone sessions {r}: {len(t)} sessions from {len({res[k]['subject'] for k in t})} mice")

def bh(p):
    return scipy.stats.false_discovery_control(np.asarray(p))

def ci95(x):
    x = np.asarray(x, float)
    m = x.mean(); h = 1.96 * x.std(ddof=0) / np.sqrt(len(x))
    return m, m - h, m + h
without sub-1: 11 sessions from 9 mice
without sub-3: 11 sessions from 9 mice
without sub-5: 11 sessions from 9 mice
  tone sessions without sub-1: 8 sessions from 7 mice
  tone sessions without sub-3: 8 sessions from 7 mice
  tone sessions without sub-5: 7 sessions from 7 mice

fig6d-mouse-persistent¶

In mice, activity along the persistent (0.5 to 1 s) coding dimension 1 to 2 s after the puff fell during ketamine (0.86 to 0.02) and partly recovered afterwards (0.52).

(Figure 6D; table S3, Fig. 6D)

Criterion: weights from the Pre series means over 0.5 to 1 s against the 1 s before the puff (analysis.py plot_coding_dimension_early_late averages the eight puffs of a series before computing the weights), per-unit variance (Methods) with the pooled variance of the code as a variant; projection of the first puff of each series averaged over series (code; all eight puffs a variant), z-scored over -1 to 2 s, minus its mean over -1 to 0 s, quantified over 1 to 2 s. Epochs: code (Ketamine 23 to 32, Post 35 to 40) and Methods-literal (22 to 32, 34 to 40). Paired t-tests, Benjamini-Hochberg over the two.

In [13]:
def zproj(trace, base="own"):
    z = scipy.stats.zscore(trace)
    return z - z[:100].mean()

rows = []
per_session = {}
for rd, ks in READINGS.items():
    for var in ("unit", "pooled"):
        for puffs in ("first", "all"):
            for ep in ("code", "Methods-literal"):
                kk = ("ket", "post") if ep == "code" else ("ket_lit", "post_lit")
                vals = {c: np.array([zproj(res[k]["cd_proj"][("persist", var, cc)][puffs])[200:300].mean() for k in ks])
                        for c, cc in (("Pre", "pre"), ("Ketamine", kk[0]), ("Post", kk[1]))}
                t1 = scipy.stats.ttest_rel(vals["Pre"], vals["Ketamine"])
                t2 = scipy.stats.ttest_rel(vals["Post"], vals["Ketamine"])
                q = bh([t1.pvalue, t2.pvalue])
                means = [vals[c].mean() for c in ("Pre", "Ketamine", "Post")]
                within = [abs(means[0] - 0.86) <= 0.1, abs(means[1] - 0.02) <= 0.25, abs(means[2] - 0.52) <= 0.41]
                rows.append({"reading": rd, "variance": var, "puffs": puffs, "epochs": ep, "Pre": means[0], "Ket": means[1], "Post": means[2],
                             "t Pre-Ket": t1.statistic, "q Pre-Ket": q[0], "t Post-Ket": t2.statistic, "q Post-Ket": q[1],
                             "means within": all(within), "tests hold": q[0] < 0.05 and q[1] < 0.05 and means[1] < min(means[0], means[2])})
                if var == "unit" and puffs == "first" and ep == "code":
                    per_session[rd] = pd.DataFrame(vals, index=ks)
f6d = pd.DataFrame(rows)
print(f6d.round(3).to_string(index=False))
print("\nper session, Methods variance, first puff, code epochs (reading without sub-5):")
print(per_session["without sub-5"].round(3).to_string())
# verification variant (leakage): weights from the odd Pre series only; Pre is then scored on the even Pre
# series, which the weights never saw (first puff, code epochs)
rows = []
for rd, ks in READINGS.items():
    for var in ("unit", "pooled"):
        vals = {c: np.array([zproj(res[k]["cd_proj_heldout"][(var, cc)])[200:300].mean() for k in ks])
                for c, cc in (("Pre (training series)", "pre_train"), ("Pre (held-out series)", "pre_test"), ("Ketamine", "ket"), ("Post", "post"))}
        t1 = scipy.stats.ttest_rel(vals["Pre (held-out series)"], vals["Ketamine"]); t2 = scipy.stats.ttest_rel(vals["Post"], vals["Ketamine"])
        q = bh([t1.pvalue, t2.pvalue])
        rows.append({"reading": rd, "variance": var, **{c: v.mean() for c, v in vals.items()}, "q held-out Pre-Ket": q[0], "q Post-Ket": q[1]})
print("\nheld-out check, first puff, code epochs:")
print(pd.DataFrame(rows).round(3).to_string(index=False))
      reading variance puffs          epochs   Pre   Ket  Post  t Pre-Ket  q Pre-Ket  t Post-Ket  q Post-Ket  means within  tests hold
without sub-1     unit first            code 0.827 0.055 0.501      5.301      0.001       2.213       0.051          True       False
without sub-1     unit first Methods-literal 0.827 0.156 0.453      5.266      0.001       1.436       0.182          True       False
without sub-1     unit   all            code 0.455 0.076 0.419      5.889      0.000       2.998       0.013         False        True
without sub-1     unit   all Methods-literal 0.455 0.126 0.368      5.090      0.001       2.460       0.034         False        True
without sub-1   pooled first            code 0.818 0.066 0.424      4.907      0.001       1.906       0.086          True       False
without sub-1   pooled first Methods-literal 0.818 0.185 0.394      4.686      0.002       1.207       0.255          True       False
without sub-1   pooled   all            code 0.445 0.089 0.352      4.961      0.001       2.021       0.071         False       False
without sub-1   pooled   all Methods-literal 0.445 0.155 0.319      4.831      0.001       1.628       0.134         False       False
without sub-3     unit first            code 0.813 0.000 0.514      5.320      0.001       3.045       0.012          True        True
without sub-3     unit first Methods-literal 0.813 0.091 0.488      5.413      0.001       2.636       0.025          True        True
without sub-3     unit   all            code 0.450 0.069 0.348      5.766      0.000       2.719       0.022         False        True
without sub-3     unit   all Methods-literal 0.450 0.114 0.304      5.605      0.000       2.124       0.060         False       False
without sub-3   pooled first            code 0.794 0.045 0.397      5.084      0.001       2.585       0.027          True        True
without sub-3   pooled first Methods-literal 0.794 0.144 0.383      4.985      0.001       2.174       0.055          True       False
without sub-3   pooled   all            code 0.435 0.066 0.241      5.383      0.001       1.431       0.183         False       False
without sub-3   pooled   all Methods-literal 0.435 0.124 0.216      6.065      0.000       0.966       0.357         False       False
without sub-5     unit first            code 0.867 0.007 0.511      5.527      0.001       2.490       0.032          True        True
without sub-5     unit first Methods-literal 0.867 0.141 0.423      5.218      0.001       1.419       0.186          True       False
without sub-5     unit   all            code 0.454 0.087 0.460      5.605      0.000       3.538       0.005         False        True
without sub-5     unit   all Methods-literal 0.454 0.156 0.413      4.780      0.001       2.601       0.026         False        True
without sub-5   pooled first            code 0.855 0.019 0.512      5.643      0.000       2.613       0.026          True        True
without sub-5   pooled first Methods-literal 0.855 0.172 0.423      5.095      0.001       1.337       0.211          True       False
without sub-5   pooled   all            code 0.441 0.052 0.394      5.891      0.000       2.737       0.021         False        True
without sub-5   pooled   all Methods-literal 0.441 0.143 0.347      5.063      0.001       1.840       0.096         False       False

per session, Methods variance, first puff, code epochs (reading without sub-5):
                      Pre  Ketamine   Post
sub-0_ses-20230901  1.206     0.554  1.331
sub-1_ses-20230218  0.909    -0.534  0.657
sub-1_ses-20230226  0.809    -0.002  0.979
sub-2_ses-20230902  0.608     0.274  0.457
sub-3_ses-20230218  1.071    -0.116  1.599
sub-3_ses-20230225  0.796     0.181 -0.101
sub-4_ses-20230907  0.777     0.471  1.031
sub-6_ses-20230823  0.734    -0.062  0.445
sub-7_ses-20220831  0.870     0.439  0.034
sub-8_ses-20220228  1.163    -0.854 -1.184
sub-9_ses-20230908  0.599    -0.269  0.378
held-out check, first puff, code epochs:
      reading variance  Pre (training series)  Pre (held-out series)  Ketamine  Post  q held-out Pre-Ket  q Post-Ket
without sub-1     unit                  0.797                  0.814     0.050 0.510               0.002       0.054
without sub-1   pooled                  0.795                  0.796     0.067 0.411               0.005       0.101
without sub-3     unit                  0.805                  0.753    -0.016 0.532               0.002       0.012
without sub-3   pooled                  0.810                  0.715     0.029 0.376               0.006       0.033
without sub-5     unit                  0.827                  0.847    -0.008 0.518               0.001       0.032
without sub-5   pooled                  0.833                  0.824     0.009 0.493               0.001       0.033

Result: reproduced, resting on the assumption that the excluded animal is sub-5. On the criterion's main reading (per-unit variance, first puff, code epochs) without sub-5, the projection is 0.867 (Pre), 0.007 (Ketamine) and 0.511 (Post), all within tolerance, with corrected p = 0.001 (Pre against Ketamine) and 0.032 (Post against Ketamine). Without sub-3 it also holds (q = 0.001 and 0.012); without sub-1 the Post comparison misses (q = 0.051), which would make it partially reproduced. With all eight puffs the Pre mean falls to 0.45, outside tolerance, and with the Methods-literal epochs the Post comparison fails in two of three readings. Weights from the odd Pre series give the even, unseen Pre series 0.847 against 0.827 on the training series (sub-5 excluded, per-unit variance), so scoring Pre on its own training series does not inflate it; Pre against Ketamine stays at q = 0.001 to 0.006.

fig6f-mouse-fast-unchanged¶

In mice, activity along the fast (0 to 70 ms) coding dimension was not changed by ketamine.

(Figure 6F; table S3, Fig. 6F)

Criterion: weights from each Pre puff, 0 to 70 ms against the 1 s before (Methods) or the 70 ms before (code); projection over all series and puffs, z-scored over -1 to 2 s, minus one mean over sessions of the -1 to 0 s bins (as the code), quantified over 0 to 70 ms.

In [14]:
rows = []
for rd, ks in READINGS.items():
    for var in ("unit", "pooled"):
        for ref in ("1s", "70ms"):
            tr = {c: np.array([scipy.stats.zscore(res[k]["cd_proj"][("fast", var, ref, cc)]["all"]) for k in ks])
                  for c, cc in (("Pre", "pre"), ("Ketamine", "ket"), ("Post", "post"))}
            vals = {}
            for c, a in tr.items():
                a = a - a[:, :100].mean()           # analysis.py subtracts one mean over sessions and baseline bins
                vals[c] = a[:, 100:107].mean(1)
            t1 = scipy.stats.ttest_rel(vals["Pre"], vals["Ketamine"])
            t2 = scipy.stats.ttest_rel(vals["Post"], vals["Ketamine"])
            q = bh([t1.pvalue, t2.pvalue])
            rows.append({"reading": rd, "variance": var, "reference": ref, "Pre": vals["Pre"].mean(), "Ket": vals["Ketamine"].mean(),
                         "Post": vals["Post"].mean(), "t Pre-Ket": t1.statistic, "q Pre-Ket": q[0], "t Post-Ket": t2.statistic,
                         "q Post-Ket": q[1], "Pre-Ket diff": (vals["Pre"] - vals["Ketamine"]).mean()})
print(pd.DataFrame(rows).round(4).to_string(index=False))
      reading variance reference    Pre    Ket   Post  t Pre-Ket  q Pre-Ket  t Post-Ket  q Post-Ket  Pre-Ket diff
without sub-1     unit        1s 4.0067 3.7224 3.5657     1.1732     0.2679     -1.3252      0.2679        0.2843
without sub-1     unit      70ms 3.9596 3.8488 3.5535     0.4379     0.6708     -2.0175      0.1426        0.1108
without sub-1   pooled        1s 4.2494 4.0557 3.7165     0.6466     0.5324     -2.5450      0.0582        0.1937
without sub-1   pooled      70ms 4.2026 4.0441 3.7140     0.5488     0.5952     -2.3094      0.0871        0.1585
without sub-3     unit        1s 4.0907 3.9166 3.7594     0.7513     0.4698     -1.4016      0.3826        0.1741
without sub-3     unit      70ms 4.0716 4.0552 3.7340     0.0701     0.9455     -2.1949      0.1058        0.0165
without sub-3   pooled        1s 4.3152 4.2962 3.9496     0.0729     0.9433     -2.5210      0.0607        0.0190
without sub-3   pooled      70ms 4.2855 4.2910 3.9382    -0.0220     0.9829     -2.4271      0.0713       -0.0055
without sub-5     unit        1s 4.2251 4.1753 3.9770     0.4045     0.6944     -2.0697      0.1306        0.0498
without sub-5     unit      70ms 4.1955 4.2598 3.9654    -0.4602     0.6552     -3.0308      0.0253       -0.0643
without sub-5   pooled        1s 4.4700 4.4988 4.1785    -0.1413     0.8904     -2.9660      0.0283       -0.0288
without sub-5   pooled      70ms 4.4334 4.4961 4.1614    -0.3263     0.7509     -3.1028      0.0224       -0.0627

Result: reproduced. The paper's test finds no Pre against Ketamine difference in any reading: corrected p from 0.27 to 0.98 (sub-5 excluded, per-unit variance, 1 s reference: 4.23 against 4.18, q = 0.69). The Pre means are 4.0 to 4.47, below the expected 4.48 +- 0.25 with the per-unit variance and within it with the pooled variance (4.43 to 4.47 without sub-5).

fig6b-persistent-dimension-distributed¶

The neurons carrying the persistent peri-puff dimension were not focally localized but distributed over much of the brain.

(Figure 6B; fig. S36A)

The top 10% of units by absolute persistent weight per session (fig6d-mouse-persistent weights), placed in the brainwide-npix top-level divisions through the Allen tree.

In [15]:
for var in ("unit", "pooled"):
    for rd, ks in READINGS.items():
        tops = np.hstack([res[k]["top_units_regions"][var] for k in ks])
        incl = np.hstack([res[k]["regions"] for k in ks])
        top_tl = pd.Series([K.top_level(r, paths) for r in tops]).value_counts()
        inc_tl = pd.Series([K.top_level(r, paths) for r in incl]).value_counts()
        tab = pd.DataFrame({"included units": inc_tl, "top-10% units": top_tl}).fillna(0).astype(int)
        tab["share of top units"] = (tab["top-10% units"] / tab["top-10% units"].sum()).round(3)
        div = [d for d in tab.index if d != "NA" and tab.loc[d, "included units"] >= 30]
        missing = [d for d in div if tab.loc[d, "top-10% units"] == 0]
        print(f"{var} variance, {rd}: divisions with >= 30 included units: {div}; without top units: {missing}; "
              f"largest share {tab.drop('NA', errors='ignore')['share of top units'].max():.3f}")
        if var == "unit" and rd == "without sub-5":
            print(tab.to_string())
unit variance, without sub-1: divisions with >= 30 included units: ['CTXsp', 'HPF', 'HY', 'Isocortex', 'MB', 'OLF', 'PAL', 'STR', 'TH']; without top units: []; largest share 0.254
unit variance, without sub-3: divisions with >= 30 included units: ['HPF', 'HY', 'Isocortex', 'MB', 'OLF', 'PAL', 'STR', 'TH']; without top units: []; largest share 0.241
unit variance, without sub-5: divisions with >= 30 included units: ['CTXsp', 'HPF', 'HY', 'Isocortex', 'MB', 'OLF', 'PAL', 'STR', 'TH']; without top units: []; largest share 0.209
           included units  top-10% units  share of top units
CTXsp                  96             15               0.024
HPF                   263             40               0.064
HY                    334             33               0.053
Isocortex            1778            131               0.209
MB                    711             47               0.075
NA                    507             34               0.054
OLF                   448             91               0.145
PAL                    85             13               0.021
STR                   975            110               0.175
TH                   1025            113               0.180
pooled variance, without sub-1: divisions with >= 30 included units: ['CTXsp', 'HPF', 'HY', 'Isocortex', 'MB', 'OLF', 'PAL', 'STR', 'TH']; without top units: []; largest share 0.318
pooled variance, without sub-3: divisions with >= 30 included units: ['HPF', 'HY', 'Isocortex', 'MB', 'OLF', 'PAL', 'STR', 'TH']; without top units: []; largest share 0.289
pooled variance, without sub-5: divisions with >= 30 included units: ['CTXsp', 'HPF', 'HY', 'Isocortex', 'MB', 'OLF', 'PAL', 'STR', 'TH']; without top units: []; largest share 0.266

Result: reproduced. In every reading each top-level division with at least 30 included units holds top-10% units, and no division holds more than 0.32 of them (0.21 for Isocortex with per-unit variance, sub-5 excluded).

fig6h-mouse-timescale¶

In mice, the intrinsic timescale of the persistent dimension in the 30 s before each series was shorter during ketamine (0.14 s) than before (0.45 s).

(Figure 6H; fig. S34B; table S3, Fig. 6H)

Criterion: the 30 s before the first puff of Pre series 11 to 20 and Ketamine series 23 to 32 projected on the persistent weights; analysis.py extract_timescale (autocorrelation from lag 10 ms, normalized to its maximum, fit over 2.5 s with initial a=1, b=0.2, c=0); (a) per series fits averaged within session, (b) one fit to the concatenated series. A fit that does not converge is left out of the session mean and counted.

In [16]:
FIT_VARIANTS = {"code": {}, "bounded b": {"bounded": True}, "demeaned": {"demean": True}, "demeaned, bounded b": {"demean": True, "bounded": True}}

def degenerate(b):
    """A fit whose timescale is not finite, not positive, or beyond the 2.5 s fit window."""
    b = np.asarray(b, float)
    return ~np.isfinite(b) | (b <= 0) | (b > 2.5)

def timescale_rows(get_sigs, ks, exp_pre, exp_ket, labels):
    """Per-series fits averaged (mean, as the Methods, and median) and one concatenated fit, under each fit
    variant; paired t-test across sessions. get_sigs(k) returns (Pre signals, Ketamine signals)."""
    rows = []
    for fv, kw in FIT_VARIANTS.items():
        a_pre, a_ket, m_pre, m_ket, b_pre, b_ket, ndeg, nfit = [], [], [], [], [], [], 0, 0
        for k in ks:
            ps, kt = get_sigs(k)
            tp = np.array([K.extract_timescale(s, **kw) for s in ps]); tk = np.array([K.extract_timescale(s, **kw) for s in kt])
            ndeg += degenerate(tp).sum() + degenerate(tk).sum(); nfit += len(tp) + len(tk)
            a_pre.append(np.nanmean(tp)); a_ket.append(np.nanmean(tk))
            m_pre.append(np.nanmedian(tp)); m_ket.append(np.nanmedian(tk))
            b_pre.append(K.extract_timescale(ps.reshape(-1), **kw)); b_ket.append(K.extract_timescale(kt.reshape(-1), **kw))
        for lab, p_, k_ in (("(a) per series, mean", a_pre, a_ket), ("(a') per series, median", m_pre, m_ket), ("(b) concatenated", b_pre, b_ket)):
            p_, k_ = np.array(p_), np.array(k_)
            t = scipy.stats.ttest_rel(p_, k_)
            rows.append(dict(labels, fit=fv, combine=lab, Pre=p_.mean(), Ket=k_.mean(), t=t.statistic, p=t.pvalue,
                             holds=bool(t.pvalue < 0.05 and k_.mean() < p_.mean()),
                             within=bool(abs(p_.mean() - exp_pre[0]) <= exp_pre[1] and abs(k_.mean() - exp_ket[0]) <= exp_ket[1]),
                             **{"degenerate per-series fits": f"{ndeg} of {nfit}" if lab.startswith("(a") else
                                f"{int(degenerate(p_).sum() + degenerate(k_).sum())} of {2 * len(p_)}"},
                             **{"Ket range": f"{k_.min():.3f}-{k_.max():.3f}"}))
    return rows

rows = []
for rd, ks in READINGS.items():
    for var in ("unit", "pooled"):
        get = lambda k, var=var: (res[k]["base_sigs"][var]["pre_sigs"][10:20], res[k]["base_sigs"][var]["ket_sigs"])
        rows += timescale_rows(get, ks, (0.45, 0.14), (0.14, 0.12), {"reading": rd, "variance": var})
f6h = pd.DataFrame(rows)
print("Pre series 11-20 against Ketamine 23-32. 'code' is analysis.py extract_timescale; the other fits are verification variants.")
print(f6h[f6h.fit == "code"].drop(columns="fit").round(4).to_string(index=False))
print("\nvariants: does the test hold (p < 0.05, Ketamine shorter), and are the means within tolerance?")
piv = f6h.assign(res=lambda d: d.holds.map({True: "holds", False: "fails"}) + d.within.map({True: "", False: " (out)"}) + " p=" + d.p.round(3).astype(str))
print(piv.pivot_table(index=["reading", "variance", "combine"], columns="fit", values="res", aggfunc="first").to_string())

# verification variant: only the series whose 30 s window holds no tone or light onset (cell above lists them)
rows = []
for rd, ks in READINGS.items():
    for var in ("unit", "pooled"):
        def get(k, var=var):
            w = res[k]["stim_in_windows"]
            okp = np.array([(w["tone"]["30 s before series"][s] + w["light"]["30 s before series"][s]) == 0 for s in range(10, 20)])
            okk = np.array([(w["tone"]["30 s before series"][s] + w["light"]["30 s before series"][s]) == 0 for s in range(22, 32)])
            return res[k]["base_sigs"][var]["pre_sigs"][10:20][okp], res[k]["base_sigs"][var]["ket_sigs"][okk]
        rows += [r for r in timescale_rows(get, ks, (0.45, 0.14), (0.14, 0.12), {"reading": rd, "variance": var}) if r["fit"] == "code"]
print("\nwindows without tone or light onsets only (code fit):")
print(pd.DataFrame(rows).drop(columns="fit").round(4).to_string(index=False))
Pre series 11-20 against Ketamine 23-32. 'code' is analysis.py extract_timescale; the other fits are verification variants.
      reading variance                 combine    Pre     Ket       t      p  holds  within degenerate per-series fits     Ket range
without sub-1     unit    (a) per series, mean 0.4515  0.3439  0.6040 0.5593  False   False                   4 of 220   0.019-1.710
without sub-1     unit (a') per series, median 0.2955  0.1267  2.2226 0.0505  False   False                   4 of 220   0.020-0.842
without sub-1     unit        (b) concatenated 0.3445  0.1202  3.0708 0.0118   True    True                    0 of 22   0.019-0.794
without sub-1   pooled    (a) per series, mean 0.5244 77.9188 -0.9978 0.3419  False   False                   5 of 220 0.031-853.339
without sub-1   pooled (a') per series, median 0.3012  0.2585  0.3284 0.7494  False   False                   5 of 220   0.028-1.024
without sub-1   pooled        (b) concatenated 0.3925  0.2083  1.8098 0.1004  False    True                    0 of 22   0.019-0.766
without sub-3     unit    (a) per series, mean 0.4614  0.3286  0.7413 0.4756  False   False                   4 of 220   0.019-1.710
without sub-3     unit (a') per series, median 0.3171  0.1268  2.3977 0.0375   True    True                   4 of 220   0.020-0.842
without sub-3     unit        (b) concatenated 0.3579  0.1207  3.1494 0.0103   True    True                    0 of 22   0.019-0.794
without sub-3   pooled    (a) per series, mean 0.5436 77.9087 -0.9974 0.3421  False   False                   5 of 220 0.035-853.339
without sub-3   pooled (a') per series, median 0.3352  0.2520  0.6012 0.5611  False    True                   5 of 220   0.029-1.024
without sub-3   pooled        (b) concatenated 0.4107  0.1876  2.1865 0.0536  False    True                    0 of 22   0.019-0.766
without sub-5     unit    (a) per series, mean 0.4837  0.2003  3.1881 0.0097   True    True                   3 of 220   0.026-1.049
without sub-5     unit (a') per series, median 0.3391  0.1253  2.7348 0.0210   True    True                   3 of 220   0.025-0.842
without sub-5     unit        (b) concatenated 0.3882  0.1188  3.6955 0.0041   True    True                    0 of 22   0.025-0.794
without sub-5   pooled    (a) per series, mean 0.5571  0.2213  2.9419 0.0147   True    True                   2 of 220   0.031-0.945
without sub-5   pooled (a') per series, median 0.3575  0.1455  2.4990 0.0315   True    True                   2 of 220   0.028-0.761
without sub-5   pooled        (b) concatenated 0.4170  0.1608  2.8857 0.0162   True    True                    0 of 22   0.030-0.766

variants: does the test hold (p < 0.05, Ketamine shorter), and are the means within tolerance?
fit                                                       bounded b                 code             demeaned  demeaned, bounded b
reading       variance combine                                                                                                    
without sub-1 pooled   (a') per series, median  fails (out) p=0.749  fails (out) p=0.749  fails (out) p=0.091  fails (out) p=0.091
                       (a) per series, mean     fails (out) p=0.542  fails (out) p=0.342  fails (out) p=0.342        fails p=0.126
                       (b) concatenated                 fails p=0.1          fails p=0.1        fails p=0.098        fails p=0.098
              unit     (a') per series, median   fails (out) p=0.05   fails (out) p=0.05  holds (out) p=0.047  holds (out) p=0.047
                       (a) per series, mean           fails p=0.077  fails (out) p=0.559  fails (out) p=0.425        fails p=0.078
                       (b) concatenated               holds p=0.012        holds p=0.012        holds p=0.012        holds p=0.012
without sub-3 pooled   (a') per series, median        fails p=0.561        fails p=0.561        fails p=0.061        fails p=0.061
                       (a) per series, mean     fails (out) p=0.437  fails (out) p=0.342  fails (out) p=0.342        fails p=0.096
                       (b) concatenated               fails p=0.054        fails p=0.054        fails p=0.054        fails p=0.054
              unit     (a') per series, median        holds p=0.037        holds p=0.037        holds p=0.028        holds p=0.028
                       (a) per series, mean            holds p=0.05  fails (out) p=0.476  fails (out) p=0.436         holds p=0.05
                       (b) concatenated                holds p=0.01         holds p=0.01        holds p=0.011        holds p=0.011
without sub-5 pooled   (a') per series, median        holds p=0.032        holds p=0.031        holds p=0.022        holds p=0.022
                       (a) per series, mean           holds p=0.029        holds p=0.015        holds p=0.012        holds p=0.024
                       (b) concatenated               holds p=0.016        holds p=0.016        holds p=0.015        holds p=0.015
              unit     (a') per series, median        holds p=0.021        holds p=0.021        holds p=0.019        holds p=0.019
                       (a) per series, mean           holds p=0.024         holds p=0.01        holds p=0.008        holds p=0.024
                       (b) concatenated               holds p=0.004        holds p=0.004        holds p=0.004        holds p=0.004
windows without tone or light onsets only (code fit):
      reading variance                 combine    Pre    Ket      t      p  holds  within degenerate per-series fits   Ket range
without sub-1     unit    (a) per series, mean 0.4707 0.1875 2.7550 0.0203   True    True                   3 of 156 0.019-1.049
without sub-1     unit (a') per series, median 0.3494 0.1247 2.0337 0.0694  False    True                   3 of 156 0.019-0.842
without sub-1     unit        (b) concatenated 0.3653 0.1200 3.0105 0.0131   True    True                    0 of 22 0.019-0.794
without sub-1   pooled    (a) per series, mean 0.5618 0.3059 1.4828 0.1689  False   False                   3 of 156 0.032-0.945
without sub-1   pooled (a') per series, median 0.3580 0.1614 1.5818 0.1448  False    True                   3 of 156 0.024-0.761
without sub-1   pooled        (b) concatenated 0.3960 0.1810 2.1246 0.0596  False    True                    0 of 22 0.020-0.766
without sub-3     unit    (a) per series, mean 0.4764 0.1720 3.0575 0.0121   True    True                   3 of 156 0.019-1.049
without sub-3     unit (a') per series, median 0.3591 0.1263 2.1070 0.0614  False    True                   3 of 156 0.019-0.842
without sub-3     unit        (b) concatenated 0.3704 0.1206 3.0589 0.0121   True    True                    0 of 22 0.019-0.794
without sub-3   pooled    (a) per series, mean 0.5771 0.2969 1.6200 0.1363  False   False                   3 of 156 0.032-0.945
without sub-3   pooled (a') per series, median 0.3825 0.1580 1.7602 0.1089  False    True                   3 of 156 0.024-0.761
without sub-3   pooled        (b) concatenated 0.4059 0.1604 2.4800 0.0325   True    True                    0 of 22 0.020-0.766
without sub-5     unit    (a) per series, mean 0.5106 0.1870 3.2207 0.0092   True    True                   3 of 164 0.026-1.049
without sub-5     unit (a') per series, median 0.3918 0.1260 2.4168 0.0363   True    True                   3 of 164 0.023-0.842
without sub-5     unit        (b) concatenated 0.4090 0.1204 3.6030 0.0048   True    True                    0 of 22 0.025-0.794
without sub-5   pooled    (a) per series, mean 0.6100 0.2075 2.9219 0.0153   True   False                   2 of 164 0.032-0.945
without sub-5   pooled (a') per series, median 0.4122 0.1407 2.1942 0.0530  False    True                   2 of 164 0.030-0.761
without sub-5   pooled        (b) concatenated 0.4438 0.1587 2.9232 0.0152   True    True                    0 of 22 0.032-0.766

Result: reproduced, resting on the assumption that the excluded animal is sub-5. Without sub-5 and with per-unit variance, per-series fits give 0.484 s (Pre) and 0.200 s (Ketamine), p = 0.0097, and one concatenated fit gives 0.388 and 0.119 s, p = 0.0041, all within tolerance. Without sub-5 the test holds under every fit variant (bounded timescale, mean removed before the autocorrelation, both) and with the per-series median (p = 0.004 to 0.032 with per-unit or pooled variance). With only the windows free of tone and light onsets it holds with per-unit weights (p = 0.005 to 0.036) and misses once with pooled weights (per-series median, p = 0.053). Without sub-1 or sub-3 the code's per-series reading fails (p = 0.56 and 0.48), as fits that run away enter the means (Ketamine up to 1.71 s with per-unit weights and 853 s with pooled weights; 4 and 5 of 220 per-series fits are degenerate). Bounding the fit does not rescue it without sub-1 (p = 0.077) and only just without sub-3 (p = 0.050); the concatenated reading holds (p = 0.012 and 0.010).

fig6j-mouse-coupling¶

In mice, correlations among the most persistent neurons in the 30 s before each series fell during ketamine to 0.49 of their preinfusion value.

(Figure 6J; table S3, Fig. 6J)

Criterion: (a) Methods, units in the top 10% of absolute persistent weight, Pearson r of z-scored rates over each 30 s window before the first puff, mean over edges, averaged over series (Pre 1 to 10, Ketamine 23 to 32); (b) code, units whose signed weight exceeds the 90th percentile of absolute weights, median absolute r over the concatenated windows. A session with no value (a silent unit leaves no defined correlation) is left out of the test and counted. Ketamine divided by Pre within session; paired t-test of Pre against Ketamine.

In [17]:
rows = []
for rd, ks in READINGS.items():
    for var in ("unit", "pooled"):
        for lab, sname, meas in (("(a) Methods", "methods", "per_series_mean_r"), ("(b) code", "code", "concat_median_abs_r")):
            pre_v, ket_v, nu = [], [], []
            for k in ks:
                c = res[k]["coupling"][(var, sname)]
                f = (lambda d: np.nanmean(d[meas])) if meas == "per_series_mean_r" else (lambda d: d[meas])
                pre_v.append(f(c["pre"])); ket_v.append(f(c["ket"])); nu.append(c["n_units"])
            pre_v, ket_v = np.array(pre_v), np.array(ket_v)
            ok = ~(np.isnan(pre_v) | np.isnan(ket_v))
            t = scipy.stats.ttest_rel(pre_v[ok], ket_v[ok])
            ratio = ket_v[ok] / pre_v[ok]
            rows.append({"reading": rd, "variance": var, "measure": lab, "Pre": pre_v.mean(), "Ket": ket_v.mean(),
                         "Ket/Pre mean": ratio.mean(), "ratio range": f"{ratio.min():.2f}-{ratio.max():.2f}", "t": t.statistic, "p": t.pvalue,
                         "neurons per session": f"{min(nu)}-{max(nu)}", "sessions without a value": int((~ok).sum())})
print(pd.DataFrame(rows).round(4).to_string(index=False))
# verification variant: reading (a) with only the series whose 30 s window holds no tone or light onset
def clean(k, series):
    w = res[k]["stim_in_windows"]
    return np.array([(w["tone"]["30 s before series"][s] + w["light"]["30 s before series"][s]) == 0 for s in series])

rows = []
for rd, ks in READINGS.items():
    for var in ("unit", "pooled"):
        pre_v, ket_v = [], []
        for k in ks:
            c = res[k]["coupling"][(var, "methods")]
            pre_v.append(np.nanmean(c["pre"]["per_series_mean_r"][clean(k, range(0, 10))]))
            ket_v.append(np.nanmean(c["ket"]["per_series_mean_r"][clean(k, range(22, 32))]))
        pre_v, ket_v = np.array(pre_v), np.array(ket_v)
        t = scipy.stats.ttest_rel(pre_v, ket_v)
        rows.append({"reading": rd, "variance": var, "measure": "(a) Methods, clean windows", "Ket/Pre mean": (ket_v / pre_v).mean(), "t": t.statistic, "p": t.pvalue})
print("\nwindows without tone or light onsets only (Pre series 1, 2, 3, 10 and Ketamine 23, 25, 26, 27, 31 in the sessions with those stimuli):")
print(pd.DataFrame(rows).round(4).to_string(index=False))
      reading variance     measure    Pre    Ket  Ket/Pre mean ratio range      t      p neurons per session  sessions without a value
without sub-1     unit (a) Methods 0.0236 0.0173        0.7457   0.12-1.69 2.3962 0.0376                8-77                         0
without sub-1     unit    (b) code 0.0349    NaN        0.7372   0.36-1.28 2.5557 0.0309                3-51                         1
without sub-1   pooled (a) Methods 0.0405 0.0202        0.5559  -0.06-1.40 3.2463 0.0088                8-77                         0
without sub-1   pooled    (b) code 0.0428    NaN        0.5711   0.32-0.89 4.3856 0.0018                7-65                         1
without sub-3     unit (a) Methods 0.0158 0.0115        0.7491   0.12-1.69 1.9056 0.0858                8-77                         0
without sub-3     unit    (b) code 0.0273    NaN        0.7564   0.36-1.28 2.3289 0.0448                3-51                         1
without sub-3   pooled (a) Methods 0.0310 0.0122        0.5115  -0.06-1.40 3.0119 0.0131                8-77                         0
without sub-3   pooled    (b) code 0.0369    NaN        0.5864   0.27-0.89 4.2114 0.0023                7-65                         1
without sub-5     unit (a) Methods 0.0236 0.0180        0.8170   0.35-1.69 2.1435 0.0577               32-77                         0
without sub-5     unit    (b) code 0.0382 0.0247        0.7422   0.36-1.28 2.7360 0.0210               17-51                         0
without sub-5   pooled (a) Methods 0.0352 0.0198        0.5861   0.14-1.40 3.7374 0.0039               32-77                         0
without sub-5   pooled    (b) code 0.0369 0.0188        0.5644   0.27-0.89 4.4947 0.0012               23-65                         0

windows without tone or light onsets only (Pre series 1, 2, 3, 10 and Ketamine 23, 25, 26, 27, 31 in the sessions with those stimuli):
      reading variance                    measure  Ket/Pre mean      t      p
without sub-1     unit (a) Methods, clean windows        0.7301 2.5771 0.0276
without sub-1   pooled (a) Methods, clean windows        0.5310 3.0504 0.0122
without sub-3     unit (a) Methods, clean windows        0.7184 2.2402 0.0490
without sub-3   pooled (a) Methods, clean windows        0.4806 2.8557 0.0171
without sub-5     unit (a) Methods, clean windows        0.7858 2.4353 0.0351
without sub-5   pooled (a) Methods, clean windows        0.5520 4.1778 0.0019

Result: partially reproduced. Without sub-5, the code's reading with the code's weights (pooled variance) gives Ketamine at 0.56 of Pre, within tolerance, p = 0.0012, and with per-unit weights 0.74, p = 0.021. The Methods reading with per-unit weights gives 0.82, p = 0.058, which misses; with pooled weights it gives 0.59, p = 0.0039. Coupling falls under every reading, but not significantly under the Methods reading with the Methods weights, so the criterion's "under both" is not met. Light or tone onsets fall in the 30 s before 6 of the Pre series 1 to 10 and 5 of the Ketamine series 23 to 32 in the 9 sessions with those stimuli; with only the clean windows, the Methods reading gives 0.79 (p = 0.035) with per-unit weights, which would pass.

fig5d-emotion-dimension-accumulates¶

Population activity along an emotion-like dimension (after the series against before it) rose and saturated across the puff series and decayed over tens of seconds afterwards; under ketamine it did not accumulate.

(Figure 5C and 5D; table S3, Fig. 5D)

Criterion: per session from the puff-modulated units; target the 1 s from 5 s after the last puff onset (Methods; the code's 3 to 4 s as a variant), reference the 750 ms ending 250 ms before the first puff (-1 to -0.25 s), weights normalized by per-unit variance (Methods; pooled, code variant), orthogonalized by analysis.py my_qr to the eye-reflex dimension of the last four puffs (200 ms after against 200 ms before), signed so that puff 8 exceeds puff 1. Projections are averaged over series, baseline-subtracted with the 250 ms before the first puff. The series windows are the eight -1 to 2 s puff windows (-1 to 23 s after the first puff); the post-series window starts 1 s before the last puff onset and lasts 50 s. Projections under the unit vector (Methods) and the sum of absolute values (code) are both shown. Intervals are mean +- 1.96 s.e.m. across sessions.

In [18]:
def emo_signal(o, cond, scale):
    """Projection over the series (8 puff windows, -1 to 23 s after the first puff) and the post-series window
    (from 1 s before the last puff, 50 s), baseline-subtracted with the 250 ms before the first puff."""
    f = o["l1_factor"] if scale == "l1" else 1.0
    s8 = o[cond]["s8"] * f
    post = o[cond]["post"] * f
    b = s8[75:100].mean()
    return s8 - b, post - b

def emo_checks(ks, group="all", tw="methods", var="unit", scale="unit"):
    d = {}
    for cond in ("pre", "ket"):
        s8 = []; post = []
        for k in ks:
            o = res[k]["emotion"].get((group, tw, var))
            if o is None:
                continue
            a, b = emo_signal(o, cond, scale)
            s8.append(a); post.append(b)
        d[cond] = (np.array(s8), np.array(post))
    s8p, postp = d["pre"]; s8k, postk = d["ket"]
    n = len(s8p)
    v8 = s8p[:, 7 * 300 + 75:7 * 300 + 100].mean(1)
    v2 = s8p[:, 300 + 75:300 + 100].mean(1)
    v10 = postp[:, 1100:1200].mean(1)          # 10 to 11 s after the last puff onset
    vend = postp[:, 4500:5000].mean(1)         # last 5 s of the 45 s after the series
    k8 = s8k[:, 7 * 300 + 75:7 * 300 + 100].mean(1)
    c8, c2, c10, cend, ck8 = ci95(v8), ci95(v2), ci95(v10), ci95(vend), ci95(k8)
    cd = ci95(v8 - v2)
    # "pre8>pre2" is the criterion's wording, the interval before puff 8 above the value before puff 2 (revised
    # after verification; the first version compared the means, kept as "pre8>pre2 (means)"); the paired
    # difference interval is a further variant. Only the criterion's wording enters "all hold".
    ok = {"pre8>0": c8[1] > 0, "pre8>pre2": c8[1] > c2[0], "10-11s>0": c10[1] > 0,
          "end includes 0": cend[1] <= 0 <= cend[2], "ket8 includes 0 or below pre": (ck8[1] <= 0 <= ck8[2]) or ck8[2] < c8[0]}
    extra = {"pre8>pre2 (means)": c8[0] > c2[0], "pre8-pre2 paired > 0": cd[1] > 0}
    return n, {"before puff 2": c2, "before puff 8": c8, "10-11 s after": c10, "last 5 s": cend, "ket before puff 8": ck8,
               "puff 8 minus puff 2": cd}, dict(ok, **extra)

CRITERION_CHECKS = ("pre8>0", "pre8>pre2", "10-11s>0", "end includes 0", "ket8 includes 0 or below pre")
rows = []
for rd, ks in READINGS.items():
    for tw in ("methods", "code"):
        for var in ("unit", "pooled"):
            for scale in ("unit", "l1"):
                n, c, ok = emo_checks(ks, "all", tw, var, scale)
                row = {"reading": rd, "target": tw, "variance": var, "scale": scale, "n": n}
                row.update({k: f"{v[0]:.3f} [{v[1]:.3f}, {v[2]:.3f}]" for k, v in c.items()})
                row.update(ok); row["all hold"] = all(v for k, v in ok.items() if k in CRITERION_CHECKS)
                rows.append(row)
f5d = pd.DataFrame(rows)
print(f5d.to_string(index=False))
print("\nchecks that hold, of", len(f5d), "readings:")
print(f5d[list(CRITERION_CHECKS) + ["pre8>pre2 (means)", "pre8-pre2 paired > 0", "all hold"]].sum().to_string())

# verification variant: without the two sessions whose 50 s post-series windows hold light onsets (sanity cell)
LIGHT_POST = [k for k in names if res[k]["stim_in_windows"]["light"]["50 s after series"][:20].sum() > 0]
rows = []
for rd, ks in READINGS.items():
    n, c, ok = emo_checks([k for k in ks if k not in LIGHT_POST], "all", "methods", "unit", "unit")
    rows.append({"reading": rd, "sessions": n, "last 5 s": f"{c['last 5 s'][0]:.3f} [{c['last 5 s'][1]:.3f}, {c['last 5 s'][2]:.3f}]",
                 **{k: v for k, v in ok.items() if k in CRITERION_CHECKS}})
print(f"\nwithout {LIGHT_POST} (Methods window, per-unit variance, unit vector):")
print(pd.DataFrame(rows).to_string(index=False))
      reading  target variance scale  n        before puff 2        before puff 8        10-11 s after             last 5 s      ket before puff 8  puff 8 minus puff 2  pre8>0  pre8>pre2  10-11s>0  end includes 0  ket8 includes 0 or below pre  pre8>pre2 (means)  pre8-pre2 paired > 0  all hold
without sub-1 methods     unit  unit 11 0.297 [0.080, 0.514] 0.928 [0.621, 1.235] 0.687 [0.470, 0.904] 0.251 [0.155, 0.347]  0.020 [-0.154, 0.194] 0.632 [0.427, 0.836]    True       True      True           False                          True               True                  True     False
without sub-1 methods     unit    l1 11 0.019 [0.005, 0.032] 0.059 [0.043, 0.075] 0.043 [0.031, 0.055] 0.016 [0.011, 0.021]  0.002 [-0.010, 0.014] 0.041 [0.030, 0.051]    True       True      True           False                          True               True                  True     False
without sub-1 methods   pooled  unit 11 0.461 [0.159, 0.763] 1.410 [0.974, 1.846] 1.057 [0.729, 1.385] 0.384 [0.240, 0.528]  0.040 [-0.074, 0.155] 0.949 [0.671, 1.227]    True       True      True           False                          True               True                  True     False
without sub-1 methods   pooled    l1 11 0.028 [0.009, 0.046] 0.086 [0.064, 0.108] 0.063 [0.046, 0.080] 0.023 [0.016, 0.030]  0.003 [-0.004, 0.010] 0.058 [0.044, 0.072]    True       True      True           False                          True               True                  True     False
without sub-1    code     unit  unit 11 0.525 [0.261, 0.789] 1.077 [0.737, 1.418] 0.638 [0.441, 0.835] 0.235 [0.137, 0.333] -0.054 [-0.198, 0.091] 0.553 [0.322, 0.783]    True       True      True           False                          True               True                  True     False
without sub-1    code     unit    l1 11 0.033 [0.018, 0.049] 0.068 [0.050, 0.086] 0.040 [0.029, 0.050] 0.014 [0.009, 0.020] -0.003 [-0.013, 0.007] 0.035 [0.022, 0.047]    True       True      True           False                          True               True                  True     False
without sub-1    code   pooled  unit 11 0.727 [0.386, 1.068] 1.570 [1.104, 2.035] 0.937 [0.656, 1.218] 0.346 [0.210, 0.482]  0.004 [-0.076, 0.084] 0.842 [0.558, 1.127]    True       True      True           False                          True               True                  True     False
without sub-1    code   pooled    l1 11 0.045 [0.025, 0.066] 0.098 [0.073, 0.123] 0.057 [0.042, 0.072] 0.020 [0.012, 0.028]  0.001 [-0.004, 0.007] 0.053 [0.037, 0.068]    True       True      True           False                          True               True                  True     False
without sub-3 methods     unit  unit 11 0.313 [0.106, 0.521] 1.054 [0.782, 1.326] 0.786 [0.589, 0.983] 0.302 [0.199, 0.405] -0.121 [-0.295, 0.052] 0.741 [0.520, 0.962]    True       True      True           False                          True               True                  True     False
without sub-3 methods     unit    l1 11 0.020 [0.007, 0.033] 0.067 [0.055, 0.080] 0.050 [0.040, 0.060] 0.020 [0.014, 0.025] -0.008 [-0.020, 0.004] 0.048 [0.036, 0.060]    True       True      True           False                          True               True                  True     False
without sub-3 methods   pooled  unit 11 0.465 [0.165, 0.764] 1.620 [1.270, 1.969] 1.208 [0.928, 1.488] 0.474 [0.331, 0.618] -0.078 [-0.225, 0.070] 1.155 [0.871, 1.439]    True       True      True           False                          True               True                  True     False
without sub-3 methods   pooled    l1 11 0.028 [0.010, 0.046] 0.098 [0.083, 0.113] 0.072 [0.058, 0.086] 0.028 [0.020, 0.036] -0.004 [-0.013, 0.005] 0.070 [0.057, 0.084]    True       True      True           False                          True               True                  True     False
without sub-3    code     unit  unit 11 0.517 [0.244, 0.789] 1.216 [0.940, 1.492] 0.709 [0.539, 0.878] 0.261 [0.163, 0.360] -0.112 [-0.242, 0.018] 0.699 [0.448, 0.950]    True       True      True           False                          True               True                  True     False
without sub-3    code     unit    l1 11 0.033 [0.017, 0.049] 0.078 [0.066, 0.090] 0.045 [0.036, 0.053] 0.017 [0.011, 0.022] -0.007 [-0.016, 0.002] 0.045 [0.031, 0.058]    True       True      True           False                          True               True                  True     False
without sub-3    code   pooled  unit 11 0.709 [0.349, 1.069] 1.783 [1.427, 2.138] 1.040 [0.801, 1.279] 0.386 [0.249, 0.523] -0.034 [-0.118, 0.049] 1.074 [0.744, 1.403]    True       True      True           False                          True               True                  True     False
without sub-3    code   pooled    l1 11 0.044 [0.023, 0.066] 0.111 [0.095, 0.127] 0.063 [0.051, 0.075] 0.023 [0.015, 0.031] -0.001 [-0.007, 0.005] 0.066 [0.050, 0.082]    True       True      True           False                          True               True                  True     False
without sub-5 methods     unit  unit 11 0.255 [0.037, 0.473] 1.033 [0.767, 1.300] 0.790 [0.609, 0.971] 0.303 [0.203, 0.403] -0.018 [-0.203, 0.167] 0.778 [0.580, 0.977]    True       True      True           False                          True               True                  True     False
without sub-5 methods     unit    l1 11 0.015 [0.002, 0.028] 0.063 [0.049, 0.077] 0.048 [0.039, 0.058] 0.019 [0.013, 0.025] -0.001 [-0.013, 0.011] 0.048 [0.036, 0.060]    True       True      True           False                          True               True                  True     False
without sub-5 methods   pooled  unit 11 0.368 [0.070, 0.666] 1.508 [1.115, 1.901] 1.178 [0.901, 1.456] 0.450 [0.296, 0.603] -0.040 [-0.201, 0.122] 1.140 [0.842, 1.439]    True       True      True           False                          True               True                  True     False
without sub-5 methods   pooled    l1 11 0.021 [0.004, 0.038] 0.087 [0.066, 0.108] 0.068 [0.053, 0.083] 0.026 [0.017, 0.035] -0.002 [-0.012, 0.008] 0.066 [0.049, 0.082]    True       True      True           False                          True               True                  True     False
without sub-5    code     unit  unit 11 0.444 [0.171, 0.717] 1.155 [0.856, 1.454] 0.695 [0.530, 0.861] 0.260 [0.160, 0.360] -0.041 [-0.160, 0.078] 0.711 [0.457, 0.966]    True       True      True           False                          True               True                  True     False
without sub-5    code     unit    l1 11 0.026 [0.011, 0.042] 0.069 [0.053, 0.085] 0.042 [0.033, 0.051] 0.016 [0.010, 0.022] -0.003 [-0.010, 0.005] 0.043 [0.028, 0.058]    True       True      True           False                          True               True                  True     False
without sub-5    code   pooled  unit 11 0.568 [0.223, 0.913] 1.616 [1.187, 2.045] 0.991 [0.748, 1.234] 0.368 [0.237, 0.500] -0.020 [-0.106, 0.066] 1.048 [0.693, 1.402]    True       True      True           False                          True               True                  True     False
without sub-5    code   pooled    l1 11 0.033 [0.013, 0.052] 0.093 [0.069, 0.116] 0.057 [0.044, 0.071] 0.021 [0.014, 0.029] -0.001 [-0.006, 0.004] 0.060 [0.041, 0.080]    True       True      True           False                          True               True                  True     False

checks that hold, of 24 readings:
pre8>0                          24
pre8>pre2                       24
10-11s>0                        24
end includes 0                   0
ket8 includes 0 or below pre    24
pre8>pre2 (means)               24
pre8-pre2 paired > 0            24
all hold                         0

without ['sub-1_ses-20230226', 'sub-3_ses-20230225'] (Methods window, per-unit variance, unit vector):
      reading  sessions             last 5 s  pre8>0  pre8>pre2  10-11s>0  end includes 0  ket8 includes 0 or below pre
without sub-1        10 0.265 [0.163, 0.367]    True       True      True           False                          True
without sub-3        10 0.305 [0.191, 0.419]    True       True      True           False                          True
without sub-5         9 0.327 [0.213, 0.441]    True       True      True           False                          True

Result: partially reproduced. In every reading the Pre projection before puff 8 is above zero and its interval lies above the value before puff 2 (Methods reading, sub-5 excluded: 1.033 [0.767, 1.300] against 0.255), it is still above zero 10 to 11 s after the last puff (0.790 [0.609, 0.971]), and under ketamine the value before puff 8 includes zero (-0.018 [-0.203, 0.167]). It does not return to baseline: the last 5 s of the 45 s after the series stay above zero in all 24 readings (0.303 [0.203, 0.403]). Two sessions (sub-1_ses-20230226 and sub-3_ses-20230225) have light onsets in every odd post-series window; without them the last 5 s are still above zero (0.265 to 0.327).

fig5e-late-reduced¶

On the first puff of a series, late emotion-like activity (2 to 3 s after onset) was reduced by ketamine (0.546 to 0.005).

(Figure 5E; table S3, Fig. 5E)

Criterion: per session the first-puff signal of analysis.py plot_affective_firstpuff_persist (series window from 0.25 s before the first puff, then the post-series window from 3 s after the last puff), minus its first 250 ms; early 0 to 0.25 s, late 2 to 3 s after the first puff; two-sided paired t-tests, Benjamini-Hochberg over the two windows.

In [19]:
def first_puff(ks, tw, var, scale):
    """analysis.py plot_affective_firstpuff_persist: signal from 0.25 s before the first puff, minus its first 250 ms."""
    out = {}
    for cond in ("pre", "ket"):
        e, l = [], []
        for k in ks:
            o = res[k]["emotion"][("all", tw, var)]
            f = o["l1_factor"] if scale == "l1" else 1.0
            sig = np.concatenate([o[cond]["s8"][75:], o[cond]["post"][400:5000]]) * f
            sig = sig - sig[:25].mean()
            e.append(sig[25:50].mean()); l.append(sig[225:325].mean())
        out[cond] = (np.array(e), np.array(l))
    return out

rows5e = []
for rd, ks in READINGS.items():
    for tw in ("methods", "code"):
        for var in ("unit", "pooled"):
            for scale in ("unit", "l1"):
                fp = first_puff(ks, tw, var, scale)
                te = scipy.stats.ttest_rel(fp["pre"][0], fp["ket"][0])
                tl = scipy.stats.ttest_rel(fp["pre"][1], fp["ket"][1])
                q = bh([te.pvalue, tl.pvalue])
                rows5e.append({"reading": rd, "target": tw, "variance": var, "scale": scale,
                               "early Pre": fp["pre"][0].mean(), "early Ket": fp["ket"][0].mean(), "t early": te.statistic, "q early": q[0],
                               "late Pre": fp["pre"][1].mean(), "late Ket": fp["ket"][1].mean(), "t late": tl.statistic, "q late": q[1],
                               "late within": abs(fp["pre"][1].mean() - 0.546) <= 0.34 and abs(fp["ket"][1].mean() - 0.005) <= 0.084,
                               "early within": abs(fp["pre"][0].mean() - 0.601) <= 0.55 and abs(fp["ket"][0].mean() - 0.332) <= 0.27})
f5e = pd.DataFrame(rows5e)
print(f5e.round(4).to_string(index=False))
      reading  target variance scale  early Pre  early Ket  t early  q early  late Pre  late Ket  t late  q late  late within  early within
without sub-1 methods     unit  unit     0.8706     0.4406   1.9219   0.1094    0.3108    0.0403  1.7573  0.1094         True          True
without sub-1 methods     unit    l1     0.0624     0.0412   1.3841   0.1964    0.0206    0.0032  1.9896  0.1493        False         False
without sub-1 methods   pooled  unit     0.4869     0.3133   1.1119   0.2922    0.4886   -0.0011  2.7199  0.0431         True          True
without sub-1 methods   pooled    l1     0.0346     0.0189   1.3887   0.1951    0.0303   -0.0001  3.0130  0.0261        False         False
without sub-1    code     unit  unit     1.0307     0.4790   2.5120   0.0308    0.4948    0.0451  3.1728  0.0199         True          True
without sub-1    code     unit    l1     0.0669     0.0409   1.4784   0.1701    0.0323    0.0035  3.4743  0.0120        False         False
without sub-1    code   pooled  unit     0.7657     0.3199   2.1299   0.0590    0.7094    0.0124  3.9334  0.0056         True          True
without sub-1    code   pooled    l1     0.0491     0.0185   2.4361   0.0351    0.0454    0.0011  4.1750  0.0038        False         False
without sub-3 methods     unit  unit     0.7985     0.3627   1.7385   0.1128    0.3585    0.0406  2.1257  0.1128         True          True
without sub-3 methods     unit    l1     0.0581     0.0356   1.3079   0.2202    0.0235    0.0030  2.4141  0.0729        False         False
without sub-3 methods   pooled  unit     0.3669     0.2380   0.6086   0.5564    0.5365    0.0279  2.8493  0.0345         True          True
without sub-3 methods   pooled    l1     0.0271     0.0137   0.9449   0.3670    0.0329    0.0015  3.1318  0.0213        False         False
without sub-3    code     unit  unit     0.9851     0.4617   2.2504   0.0481    0.5072    0.0420  3.3253  0.0154         True          True
without sub-3    code     unit    l1     0.0639     0.0394   1.3536   0.2057    0.0334    0.0032  3.7195  0.0080        False         False
without sub-3    code   pooled  unit     0.7056     0.3154   1.6266   0.1349    0.7073    0.0431  3.3396  0.0150         True          True
without sub-3    code   pooled    l1     0.0457     0.0178   1.9467   0.0802    0.0457    0.0028  3.6568  0.0088        False         False
without sub-5 methods     unit  unit     0.8040     0.3293   1.9800   0.1049    0.3015    0.0345  1.7833  0.1049         True          True
without sub-5 methods     unit    l1     0.0487     0.0193   2.1116   0.0830    0.0185    0.0026  1.9262  0.0830        False         False
without sub-5 methods   pooled  unit     0.3275     0.2580   0.3370   0.7431    0.4284   -0.0088  2.4170  0.0725         True          True
without sub-5 methods   pooled    l1     0.0199     0.0148   0.4203   0.6832    0.0247   -0.0008  2.5209  0.0607        False         False
without sub-5    code     unit  unit     0.9589     0.3981   2.6898   0.0227    0.4107    0.0275  2.7859  0.0227         True          True
without sub-5    code     unit    l1     0.0560     0.0224   2.9820   0.0138    0.0243    0.0018  2.9915  0.0138        False         False
without sub-5    code   pooled  unit     0.6081     0.3078   1.3079   0.2202    0.5336    0.0052  2.7636  0.0400         True          True
without sub-5    code   pooled    l1     0.0347     0.0170   1.4005   0.1916    0.0305   -0.0000  2.8427  0.0349        False         False

Result: partially reproduced. On the criterion's main reading (Methods target window, per-unit variance) the late projection falls from 0.30 to 0.03 (sub-5 excluded; 0.31 to 0.04 and 0.36 to 0.04 under the other exclusions), both within tolerance under the unit-vector scaling, but the corrected p is 0.105, 0.109 and 0.113 under the three exclusions, above 0.05. The code's variants pass: its 3 to 4 s target window gives q = 0.015 to 0.023 with per-unit variance, and the pooled variance with the Methods window gives q = 0.034 to 0.073. The fall is present in every reading but significant only under the code's choices.

fig5e-rapid-unchanged¶

The rapid emotion-like response to the first puff (0 to 0.25 s) did not differ between preinfusion and ketamine.

(Figure 5E; table S3, Fig. 5E)

The early window of the computation above.

In [20]:
print(f5e[["reading", "target", "variance", "scale", "early Pre", "early Ket", "t early", "q early", "early within"]].round(4).to_string(index=False))
      reading  target variance scale  early Pre  early Ket  t early  q early  early within
without sub-1 methods     unit  unit     0.8706     0.4406   1.9219   0.1094          True
without sub-1 methods     unit    l1     0.0624     0.0412   1.3841   0.1964         False
without sub-1 methods   pooled  unit     0.4869     0.3133   1.1119   0.2922          True
without sub-1 methods   pooled    l1     0.0346     0.0189   1.3887   0.1951         False
without sub-1    code     unit  unit     1.0307     0.4790   2.5120   0.0308          True
without sub-1    code     unit    l1     0.0669     0.0409   1.4784   0.1701         False
without sub-1    code   pooled  unit     0.7657     0.3199   2.1299   0.0590          True
without sub-1    code   pooled    l1     0.0491     0.0185   2.4361   0.0351         False
without sub-3 methods     unit  unit     0.7985     0.3627   1.7385   0.1128          True
without sub-3 methods     unit    l1     0.0581     0.0356   1.3079   0.2202         False
without sub-3 methods   pooled  unit     0.3669     0.2380   0.6086   0.5564          True
without sub-3 methods   pooled    l1     0.0271     0.0137   0.9449   0.3670         False
without sub-3    code     unit  unit     0.9851     0.4617   2.2504   0.0481          True
without sub-3    code     unit    l1     0.0639     0.0394   1.3536   0.2057         False
without sub-3    code   pooled  unit     0.7056     0.3154   1.6266   0.1349          True
without sub-3    code   pooled    l1     0.0457     0.0178   1.9467   0.0802         False
without sub-5 methods     unit  unit     0.8040     0.3293   1.9800   0.1049          True
without sub-5 methods     unit    l1     0.0487     0.0193   2.1116   0.0830         False
without sub-5 methods   pooled  unit     0.3275     0.2580   0.3370   0.7431          True
without sub-5 methods   pooled    l1     0.0199     0.0148   0.4203   0.6832         False
without sub-5    code     unit  unit     0.9589     0.3981   2.6898   0.0227          True
without sub-5    code     unit    l1     0.0560     0.0224   2.9820   0.0138         False
without sub-5    code   pooled  unit     0.6081     0.3078   1.3079   0.2202          True
without sub-5    code   pooled    l1     0.0347     0.0170   1.4005   0.1916         False

Result: reproduced. On the main reading the early projection is 0.80 against 0.33 (sub-5 excluded), corrected p = 0.105 (0.109 and 0.113 under the other exclusions), so the paper's test finds no difference. Both means are within tolerance under the unit-vector scaling. Under the code's 3 to 4 s target window the early difference is significant with per-unit variance and unit-vector scaling (q = 0.023 to 0.048), but not with pooled variance (q = 0.059, 0.135 and 0.220); a claim of absence is judged on the paper's test as written, with the Methods window.

figs32c-regional-emotion-dimension¶

Emotion-like dimensions computed within each high-level brain area all showed accumulating persistent dynamics before ketamine that ketamine abolished.

(Figure S32C)

The emotion-like dimension recomputed within each brainwide-npix top-level division for every session with at least 20 puff-modulated units in it (Methods target window and per-unit variance; the code's window as a variant). Areas with at least two such sessions are judged on the puff-8 conditions of fig5d-emotion-dimension-accumulates.

In [21]:
rows_r = []
for rd, ks in READINGS.items():
    for tw in ("methods", "code"):
        for area in K.TOP_LEVEL:
            qual = [k for k in ks if (area, tw, "unit") in res[k]["emotion"]]
            if len(qual) < 2:
                continue
            n, c, ok = emo_checks(qual, area, tw, "unit", "unit")
            rows_r.append({"reading": rd, "target": tw, "area": area, "sessions": n,
                           "pre before puff 8": f"{c['before puff 8'][0]:.3f} [{c['before puff 8'][1]:.3f}, {c['before puff 8'][2]:.3f}]",
                           "pre before puff 2": f"{c['before puff 2'][0]:.3f}",
                           "ket before puff 8": f"{c['ket before puff 8'][0]:.3f} [{c['ket before puff 8'][1]:.3f}, {c['ket before puff 8'][2]:.3f}]",
                           "meets": ok["pre8>0"] and ok["pre8>pre2"] and ok["ket8 includes 0 or below pre"],
                           "meets (means)": ok["pre8>0"] and ok["pre8>pre2 (means)"] and ok["ket8 includes 0 or below pre"],
                           "meets (paired)": ok["pre8>0"] and ok["pre8-pre2 paired > 0"] and ok["ket8 includes 0 or below pre"]})
f32 = pd.DataFrame(rows_r)
print(f32.to_string(index=False))
print("\nareas meeting the conditions, under the criterion's wording (interval before puff 8 above the value before puff 2),")
print("comparing the means (the first version), and with the paired puff 8 minus puff 2 interval:")
print(f32.groupby(["reading", "target"])[["meets", "meets (means)", "meets (paired)"]].agg(lambda x: f"{x.sum()} of {len(x)}").to_string())
print("areas failing under the criterion's wording:", f32[~f32.meets].groupby("area").size().to_dict())
      reading  target      area  sessions    pre before puff 8 pre before puff 2      ket before puff 8  meets  meets (means)  meets (paired)
without sub-1 methods Isocortex        10 0.607 [0.400, 0.814]             0.211 -0.020 [-0.173, 0.133]   True           True            True
without sub-1 methods       OLF         8 0.189 [0.049, 0.330]             0.051 -0.077 [-0.219, 0.065]  False           True            True
without sub-1 methods       HPF         4 0.234 [0.016, 0.452]             0.119  0.093 [-0.026, 0.212]  False           True           False
without sub-1 methods       STR        10 0.226 [0.075, 0.377]            -0.007  0.042 [-0.027, 0.112]   True           True            True
without sub-1 methods        TH         6 0.735 [0.331, 1.139]             0.320  0.002 [-0.164, 0.169]   True           True            True
without sub-1 methods        HY         6 0.411 [0.153, 0.669]            -0.035 -0.004 [-0.078, 0.071]   True           True            True
without sub-1 methods        MB         5 0.444 [0.166, 0.723]             0.179  0.020 [-0.133, 0.172]  False           True            True
without sub-1    code Isocortex        10 0.720 [0.472, 0.967]             0.376 -0.103 [-0.241, 0.036]   True           True            True
without sub-1    code       OLF         8 0.204 [0.048, 0.360]             0.164 -0.081 [-0.246, 0.083]  False           True           False
without sub-1    code       HPF         4 0.251 [0.156, 0.347]             0.099  0.047 [-0.167, 0.261]   True           True           False
without sub-1    code       STR        10 0.287 [0.121, 0.454]             0.157 -0.018 [-0.100, 0.065]  False           True           False
without sub-1    code        TH         6 0.909 [0.512, 1.307]             0.439  0.052 [-0.122, 0.226]   True           True            True
without sub-1    code        HY         6 0.551 [0.273, 0.829]             0.122 -0.027 [-0.154, 0.101]   True           True            True
without sub-1    code        MB         5 0.500 [0.139, 0.861]             0.193  0.036 [-0.154, 0.227]  False           True            True
without sub-3 methods Isocortex        10 0.641 [0.437, 0.844]             0.268 -0.074 [-0.223, 0.074]   True           True            True
without sub-3 methods       OLF         6 0.262 [0.115, 0.409]             0.099 -0.036 [-0.214, 0.141]   True           True            True
without sub-3 methods       HPF         5 0.208 [0.028, 0.388]             0.077  0.049 [-0.073, 0.171]  False           True           False
without sub-3 methods       STR         9 0.280 [0.116, 0.443]            -0.009 -0.020 [-0.085, 0.046]   True           True            True
without sub-3 methods        TH         7 0.723 [0.368, 1.077]             0.334 -0.134 [-0.328, 0.060]   True           True            True
without sub-3 methods        HY         7 0.381 [0.147, 0.615]            -0.054 -0.014 [-0.081, 0.053]   True           True            True
without sub-3 methods        MB         6 0.527 [0.238, 0.816]             0.139  0.046 [-0.104, 0.196]   True           True            True
without sub-3    code Isocortex        10 0.747 [0.514, 0.981]             0.401 -0.137 [-0.273, 0.000]   True           True            True
without sub-3    code       OLF         6 0.290 [0.135, 0.444]             0.194 -0.129 [-0.331, 0.072]  False           True           False
without sub-3    code       HPF         5 0.241 [0.163, 0.320]             0.080  0.028 [-0.146, 0.202]   True           True           False
without sub-3    code       STR         9 0.333 [0.187, 0.479]             0.185 -0.028 [-0.110, 0.054]   True           True           False
without sub-3    code        TH         7 0.856 [0.507, 1.204]             0.344 -0.033 [-0.185, 0.118]   True           True            True
without sub-3    code        HY         7 0.502 [0.242, 0.762]             0.067  0.022 [-0.091, 0.135]   True           True            True
without sub-3    code        MB         6 0.629 [0.302, 0.957]             0.176  0.028 [-0.141, 0.197]   True           True            True
without sub-5 methods Isocortex        11 0.533 [0.333, 0.733]             0.180  0.008 [-0.096, 0.113]   True           True            True
without sub-5 methods       OLF         8 0.189 [0.049, 0.330]             0.051 -0.077 [-0.219, 0.065]  False           True            True
without sub-5 methods       HPF         5 0.208 [0.028, 0.388]             0.077  0.049 [-0.073, 0.171]  False           True           False
without sub-5 methods       STR        10 0.251 [0.096, 0.407]            -0.038  0.028 [-0.053, 0.109]   True           True            True
without sub-5 methods        TH         8 0.691 [0.375, 1.007]             0.292 -0.104 [-0.283, 0.074]   True           True            True
without sub-5 methods        HY         8 0.356 [0.146, 0.565]            -0.060 -0.022 [-0.082, 0.038]   True           True            True
without sub-5 methods        MB         7 0.503 [0.251, 0.754]             0.125  0.045 [-0.084, 0.174]   True           True            True
without sub-5    code Isocortex        11 0.608 [0.385, 0.831]             0.288 -0.071 [-0.169, 0.027]   True           True            True
without sub-5    code       OLF         8 0.204 [0.048, 0.360]             0.164 -0.081 [-0.246, 0.083]  False           True           False
without sub-5    code       HPF         5 0.241 [0.163, 0.320]             0.080  0.028 [-0.146, 0.202]   True           True           False
without sub-5    code       STR        10 0.282 [0.117, 0.447]             0.115 -0.008 [-0.088, 0.073]   True           True            True
without sub-5    code        TH         8 0.855 [0.550, 1.160]             0.317  0.002 [-0.145, 0.149]   True           True            True
without sub-5    code        HY         8 0.471 [0.237, 0.706]             0.069  0.001 [-0.105, 0.107]   True           True            True
without sub-5    code        MB         7 0.578 [0.283, 0.874]             0.156  0.037 [-0.109, 0.183]   True           True            True

areas meeting the conditions, under the criterion's wording (interval before puff 8 above the value before puff 2),
comparing the means (the first version), and with the paired puff 8 minus puff 2 interval:
                        meets meets (means) meets (paired)
reading       target                                      
without sub-1 code     4 of 7        7 of 7         4 of 7
              methods  4 of 7        7 of 7         6 of 7
without sub-3 code     6 of 7        7 of 7         4 of 7
              methods  6 of 7        7 of 7         6 of 7
without sub-5 code     6 of 7        7 of 7         5 of 7
              methods  5 of 7        7 of 7         6 of 7
areas failing under the criterion's wording: {'HPF': 3, 'MB': 2, 'OLF': 5, 'STR': 1}

Result: partially reproduced. Under the criterion's wording (the interval before puff 8 above zero and above the value before puff 2, Ketamine including zero or below the Pre mean), 5 of 7 areas meet the conditions without sub-5 with the Methods window (OLF fails narrowly, 0.189 [0.049, 0.330] against 0.051 before puff 2, and HPF fails), and 4 to 6 of 7 across all readings; OLF, HPF, MB and STR each fail in at least one. Comparing the means, as the first version did, all 7 areas meet them in every reading (Isocortex without sub-5, Methods window: 0.533 [0.333, 0.733] against 0.180, Ketamine 0.008 [-0.096, 0.113]). Most areas show the pattern, not every one.

fig5g-tau-persist-model¶

A two-phase first-order model in which only the persistence timescale differs between preinfusion and ketamine closely matched the emotion-like population activity.

(Figure 5F and 5G; table S3, Fig. 5G)

Criterion: analysis.py model_system and theoretical_persist (inputs of 250 ms every 3 s, the model output from -0.25 to 23 s and 25 to 70 s after the first puff), fitted with curve_fit (bounds [0, 100], factor per condition in [0, 1], null factor in [0.99999, 1]) to the across-session mean of the Pre and Ketamine projections over the same windows, concatenated. tauP Pre and tauP Ketamine are tauP times each condition's factor.

In [22]:
def model_data(ks, tw, var, scale):
    segs = []
    for cond in ("pre", "ket"):
        a = []
        for k in ks:
            s8, post = emo_signal(res[k]["emotion"][("all", tw, var)], cond, scale)
            a.append(np.concatenate([s8[75:2400], post[500:5000]]))     # -0.25 to 23 s and 25 to 70 s after the first puff
        segs.append(np.mean(a, 0))
    return np.concatenate(segs)

import time as _time
rows_m = []
fits = {}
t0 = _time.time()
for rd, ks in READINGS.items():
    for tw in ("methods", "code"):
        for var in ("unit", "pooled"):
            for scale in ("unit", "l1"):
                data = model_data(ks, tw, var, scale)
                r = {}
                for kind in ("persist", "saturation", "broadcast", "symmetric"):
                    try:
                        r[kind] = K.compare_model(kind, data)
                    except Exception as e:
                        r[kind] = {"varexp": np.nan, "fit": [np.nan] * 5, "pred": np.full_like(data, np.nan), "null": np.full_like(data, np.nan), "error": str(e)}
                fits[(rd, tw, var, scale)] = (data, r)
                pm = r["persist"]
                rows_m.append({"reading": rd, "target": tw, "variance": var, "scale": scale, "VE persist": pm["varexp"],
                               "S": pm["fit"][0], "tauB": pm["fit"][1], "tauP": pm["fit"][2], "factor Pre": pm["fit"][3], "factor Ket": pm["fit"][4],
                               "tauP Pre": pm["fit"][2] * pm["fit"][3], "tauP Ket": pm["fit"][2] * pm["fit"][4]})
print(f"{len(fits)} fits in {_time.time() - t0:.0f} s")
print(pd.DataFrame(rows_m).round(4).to_string(index=False))
24 fits in 18 s
      reading  target variance scale  VE persist      S   tauB    tauP  factor Pre  factor Ket  tauP Pre  tauP Ket
without sub-1 methods     unit  unit      0.8379 1.4593 0.7842 27.8453      1.0000      0.0124   27.8453    0.3459
without sub-1 methods     unit    l1      0.8125 0.0905 0.7225 27.3776      0.9998      0.0140   27.3719    0.3829
without sub-1 methods   pooled  unit      0.9196 2.6684 1.2785 28.5822      0.9935      0.0084   28.3964    0.2399
without sub-1 methods   pooled    l1      0.9062 0.1636 1.2891 27.5047      0.9999      0.0087   27.5031    0.2390
without sub-1    code     unit  unit      0.8663 1.4480 0.4002 20.8507      1.0000      0.0130   20.8507    0.2701
without sub-1    code     unit    l1      0.8502 0.0904 0.3755 20.4583      0.9999      0.0134   20.4554    0.2744
without sub-1    code   pooled  unit      0.9377 2.1250 0.5497 22.5010      1.0000      0.0084   22.5010    0.1891
without sub-1    code   pooled    l1      0.9253 0.1313 0.5296 21.5266      1.0000      0.0079   21.5266    0.1691
without sub-3 methods     unit  unit      0.8863 1.6526 0.9555 30.4425      1.0000      0.0065   30.4425    0.1964
without sub-3 methods     unit    l1      0.8695 0.1038 0.9138 30.1858      0.9994      0.0068   30.1684    0.2058
without sub-3 methods   pooled  unit      0.9399 3.1155 1.5449 31.6297      0.9992      0.0046   31.6060    0.1449
without sub-3 methods   pooled    l1      0.9303 0.1940 1.6131 30.7823      1.0000      0.0044   30.7816    0.1340
without sub-3    code     unit  unit      0.8997 1.5095 0.4799 23.6423      1.0000      0.0095   23.6423    0.2250
without sub-3    code     unit    l1      0.8891 0.0947 0.4572 23.6213      0.9999      0.0094   23.6187    0.2212
without sub-3    code   pooled  unit      0.9479 2.2766 0.6480 25.1294      1.0000      0.0060   25.1294    0.1507
without sub-3    code   pooled    l1      0.9392 0.1388 0.6124 24.3038      1.0000      0.0050   24.3038    0.1223
without sub-5 methods     unit  unit      0.8655 1.7350 1.0958 32.1677      1.0000      0.0078   32.1677    0.2517
without sub-5 methods     unit    l1      0.8629 0.1097 1.1733 32.3865      0.9992      0.0068   32.3591    0.2201
without sub-5 methods   pooled  unit      0.9286 3.5042 2.0460 33.4923      0.9985      0.0054   33.4408    0.1818
without sub-5 methods   pooled    l1      0.9285 0.2073 2.1135 33.6526      0.9995      0.0054   33.6370    0.1802
without sub-5    code     unit  unit      0.8860 1.4408 0.5075 25.7084      1.0000      0.0092   25.7084    0.2353
without sub-5    code     unit    l1      0.8813 0.0873 0.5383 26.0900      0.9997      0.0081   26.0813    0.2126
without sub-5    code   pooled  unit      0.9468 2.1496 0.7620 27.7854      1.0000      0.0067   27.7850    0.1869
without sub-5    code   pooled    l1      0.9460 0.1255 0.7829 27.8718      1.0000      0.0065   27.8710    0.1801

Result: reproduced. In all 24 readings the variance explained is positive (0.81 to 0.95) and the fitted Ketamine persistence timescale is far shorter than the Pre one (Methods reading, sub-5 excluded: 32.2 s against 0.25 s, variance explained 0.866).

figs33-alternative-models¶

Alternative models in which ketamine changes the input scale S or the broadcast rate, or a symmetric single-timescale model, fit the ketamine puff-series activity worse than the model in which ketamine changes the persistence timescale.

(Figure S33A to S33D; Results, Brain-wide activity screen during emergence of affect in mice)

The same fits with the factor on S (theoretical_saturation), on tauB (theoretical_broadcast) or a single timescale (theoretical_symmetric), and the variance explained on the Ketamine half of the concatenated data (each model against its own null fit).

In [23]:
# The fitted trace of each condition is the series (-0.25 to 23 s after the first puff, 2325 bins) followed by
# 25 to 70 s after it (4500 bins). Variance explained on the Ketamine puff-series segment (criterion) is taken
# over the Ketamine series bins only; the first version used the whole Ketamine half, shown as a variant.
N_SERIES_BINS = 2400 - 75
rows_a = []
for key, (data, r) in fits.items():
    half = len(data) // 2
    for seg, sl in (("Ketamine series", slice(half, half + N_SERIES_BINS)), ("Ketamine half (series and after)", slice(half, None))):
        ve = {}
        for kind, m in r.items():
            ve[kind] = 1 - np.mean((data[sl] - m["pred"][sl]) ** 2) / np.mean((data[sl] - m["null"][sl]) ** 2)
        best = max(ve, key=lambda k: -np.inf if np.isnan(ve[k]) else ve[k])
        srt_ve = sorted(v for v in ve.values() if np.isfinite(v))
        rows_a.append(dict(zip(("reading", "target", "variance", "scale"), key), segment=seg, **{f"VE {k}": v for k, v in ve.items()},
                           best=best, margin=srt_ve[-1] - srt_ve[-2]))
f33 = pd.DataFrame(rows_a)
for seg in ("Ketamine series", "Ketamine half (series and after)"):
    s = f33[f33.segment == seg]
    print(f"\n{seg}:")
    print(s.drop(columns="segment").round(4).to_string(index=False))
    print(f"tauP model best in {(s.best == 'persist').sum()} of {len(s)} readings; best otherwise: {s.loc[s.best != 'persist', 'best'].value_counts().to_dict()}")
Ketamine series:
      reading  target variance scale  VE persist  VE saturation  VE broadcast  VE symmetric       best  margin
without sub-1 methods     unit  unit      0.9030         0.8622        0.8586        0.7616    persist  0.0408
without sub-1 methods     unit    l1      0.8723         0.8207        0.8148        0.7356    persist  0.0516
without sub-1 methods   pooled  unit      0.9665         0.9495        0.9491        0.7696    persist  0.0170
without sub-1 methods   pooled    l1      0.9658         0.9504        0.9500        0.7772    persist  0.0154
without sub-1    code     unit  unit      0.9265         0.8706        0.8622        0.8206    persist  0.0559
without sub-1    code     unit    l1      0.9110         0.8492        0.8387        0.8059    persist  0.0618
without sub-1    code   pooled  unit      0.9635         0.9520        0.9506        0.8457    persist  0.0115
without sub-1    code   pooled    l1      0.9603         0.9582        0.9569        0.8563    persist  0.0021
without sub-3 methods     unit  unit      0.9196         0.8922        0.8914        0.7279    persist  0.0274
without sub-3 methods     unit    l1      0.8972         0.8613        0.8596        0.7122    persist  0.0359
without sub-3 methods   pooled  unit      0.9700         0.9669        0.9667        0.7396    persist  0.0032
without sub-3 methods   pooled    l1      0.9701         0.9692        0.9690        0.7447    persist  0.0009
without sub-3    code     unit  unit      0.9280         0.8862        0.8801        0.8076    persist  0.0418
without sub-3    code     unit    l1      0.9152         0.8703        0.8631        0.7972    persist  0.0449
without sub-3    code   pooled  unit      0.9644         0.9633        0.9618        0.8350    persist  0.0011
without sub-3    code   pooled    l1      0.9617         0.9697        0.9684        0.8437 saturation  0.0012
without sub-5 methods     unit  unit      0.9099         0.8759        0.8751        0.7125    persist  0.0339
without sub-5 methods     unit    l1      0.8992         0.8727        0.8722        0.6994    persist  0.0264
without sub-5 methods   pooled  unit      0.9651         0.9552        0.9545        0.7006    persist  0.0099
without sub-5 methods   pooled    l1      0.9639         0.9542        0.9530        0.6951    persist  0.0097
without sub-5    code     unit  unit      0.9259         0.8659        0.8620        0.7805    persist  0.0601
without sub-5    code     unit    l1      0.9180         0.8666        0.8639        0.7717    persist  0.0514
without sub-5    code   pooled  unit      0.9674         0.9469        0.9461        0.7946    persist  0.0205
without sub-5    code   pooled    l1      0.9672         0.9480        0.9477        0.7901    persist  0.0193
tauP model best in 23 of 24 readings; best otherwise: {'saturation': 1}

Ketamine half (series and after):
      reading  target variance scale  VE persist  VE saturation  VE broadcast  VE symmetric       best  margin
without sub-1 methods     unit  unit      0.8529         0.8385        0.8363        0.5077    persist  0.0144
without sub-1 methods     unit    l1      0.8228         0.8028        0.7993        0.4939    persist  0.0200
without sub-1 methods   pooled  unit      0.9542         0.9468        0.9466        0.5115    persist  0.0075
without sub-1 methods   pooled    l1      0.9515         0.9451        0.9449        0.5185    persist  0.0064
without sub-1    code     unit  unit      0.8769         0.8425        0.8366        0.5453    persist  0.0344
without sub-1    code     unit    l1      0.8595         0.8205        0.8132        0.5330    persist  0.0390
without sub-1    code   pooled  unit      0.9520         0.9453        0.9443        0.5729    persist  0.0068
without sub-1    code   pooled    l1      0.9492         0.9487        0.9478        0.5855    persist  0.0005
without sub-3 methods     unit  unit      0.8946         0.8802        0.8798        0.4493    persist  0.0144
without sub-3 methods     unit    l1      0.8753         0.8562        0.8555        0.4394    persist  0.0192
without sub-3 methods   pooled  unit      0.9655         0.9642        0.9638        0.4786    persist  0.0013
without sub-3 methods   pooled    l1      0.9650         0.9648        0.9644        0.4819    persist  0.0002
without sub-3    code     unit  unit      0.8955         0.8685        0.8649        0.5143    persist  0.0270
without sub-3    code     unit    l1      0.8836         0.8540        0.8499        0.5038    persist  0.0296
without sub-3    code   pooled  unit      0.9565         0.9573        0.9563        0.5638 saturation  0.0008
without sub-3    code   pooled    l1      0.9556         0.9619        0.9611        0.5723 saturation  0.0008
without sub-5 methods     unit  unit      0.8842         0.8686        0.8681        0.4557    persist  0.0156
without sub-5 methods     unit    l1      0.8759         0.8633        0.8630        0.4405    persist  0.0126
without sub-5 methods   pooled  unit      0.9620         0.9575        0.9551        0.4444    persist  0.0045
without sub-5 methods   pooled    l1      0.9608         0.9564        0.9532        0.4372    persist  0.0044
without sub-5    code     unit  unit      0.8883         0.8518        0.8496        0.4892    persist  0.0365
without sub-5    code     unit    l1      0.8798         0.8485        0.8473        0.4682    persist  0.0313
without sub-5    code   pooled  unit      0.9574         0.9457        0.9452        0.5104    persist  0.0117
without sub-5    code   pooled    l1      0.9572         0.9461        0.9457        0.5015    persist  0.0111
tauP model best in 22 of 24 readings; best otherwise: {'saturation': 2}

Result: reproduced. On the Ketamine puff-series segment (the 23 s of the series, as the criterion asks) the tauP model explains the most variance in 23 of 24 readings, including every reading with the Methods target window (sub-5 excluded, Methods, per-unit variance: 0.910 against 0.876 for S, 0.875 for tauB and 0.713 for the symmetric model). The one exception is the code's window with pooled variance and L1 scaling without sub-3, where the S model is best (0.9697 against 0.9617 for tauP). On the whole Ketamine half (series and 25 to 70 s after), as the first version scored it, the tauP model is best in 22 of 24.

figs34d-tone-fast-unchanged¶

Fast tone-related population activity (0 to 70 ms) was not changed by ketamine.

(Figure S34D; table S3, fig. S34 D)

Tone windows of the puff-modulated units. Splits: 'infusion' takes the 10 tone series before the infusion time in the file as Pre and counts Ketamine (series 2 to 6) and Post (8 to 10) from the infusion, as the criterion reads the code; 'code' splits at the code's ketamine onset (30 s after the last puff of series 20), which falls before the infusion, so that the code's Pre has 9 tone series and its first "post" series precedes the infusion. Otherwise as fig6f-mouse-fast-unchanged.

In [24]:
def tone_vals(ks, split, key, puffs, win, base="global"):
    out = {}
    for c in ("pre", "ket", "post"):
        a = np.array([scipy.stats.zscore(res[k]["tone"][split]["proj"][key + (c,)][puffs]) for k in ks])
        a = a - (a[:, :100].mean() if base == "global" else a[:, :100].mean(1, keepdims=True))
        out[c] = a[:, win].mean(1)
    return out

def tone_tests(v):
    t1 = scipy.stats.ttest_rel(v["pre"], v["ket"]); t2 = scipy.stats.ttest_rel(v["post"], v["ket"])
    q = bh([t1.pvalue, t2.pvalue])
    return t1.statistic, q[0], t2.statistic, q[1]

rows = []
for rd, ks0 in READINGS.items():
    ks = [k for k in ks0 if k in TONE_SESSIONS]
    for split in ("infusion", "code"):
        for var in ("unit", "pooled"):
            for ref in ("1s", "70ms"):
                v = tone_vals(ks, split, ("fast", var, ref), "all", slice(100, 107))
                t1, q1, t2, q2 = tone_tests(v)
                rows.append({"reading": rd, "sessions": len(ks), "split": split, "variance": var, "reference": ref,
                             "Pre": v["pre"].mean(), "Ket": v["ket"].mean(), "Post": v["post"].mean(),
                             "t Pre-Ket": t1, "q Pre-Ket": q1, "t Post-Ket": t2, "q Post-Ket": q2})
print(pd.DataFrame(rows).round(4).to_string(index=False))
      reading  sessions    split variance reference    Pre    Ket   Post  t Pre-Ket  q Pre-Ket  t Post-Ket  q Post-Ket
without sub-1         8 infusion     unit        1s 3.0267 0.9802 0.9053     3.2650     0.0275     -0.2734      0.7924
without sub-1         8 infusion     unit      70ms 2.5895 1.3429 0.9986     2.4572     0.0436     -2.4862      0.0436
without sub-1         8 infusion   pooled        1s 3.3142 1.3907 1.1113     3.0796     0.0357     -1.2871      0.2390
without sub-1         8 infusion   pooled      70ms 2.8318 1.4534 1.0376     2.5473     0.0383     -2.6085      0.0383
without sub-1         8     code     unit        1s 3.0599 1.0477 0.9762     2.8492     0.0494     -0.2563      0.8051
without sub-1         8     code     unit      70ms 2.5776 1.3931 1.0330     1.9813     0.0880     -2.0249      0.0880
without sub-1         8     code   pooled        1s 3.3372 1.4533 1.0795     2.8221     0.0514     -1.6569      0.1415
without sub-1         8     code   pooled      70ms 2.8356 1.5531 1.0433     2.2229     0.0616     -3.4461      0.0215
without sub-3         8 infusion     unit        1s 2.7692 1.2297 1.1575     3.1418     0.0327     -0.3115      0.7645
without sub-3         8 infusion     unit      70ms 2.3507 1.4457 1.1862     2.2142     0.1112     -1.8225      0.1112
without sub-3         8 infusion   pooled        1s 3.0290 1.5701 1.3056     2.6930     0.0619     -1.5260      0.1708
without sub-3         8 infusion   pooled      70ms 2.6166 1.5456 1.1968     2.1477     0.0688     -2.3386      0.0688
without sub-3         8     code     unit        1s 2.7998 1.3238 1.2250     2.5451     0.0768     -0.4103      0.6938
without sub-3         8     code     unit      70ms 2.3494 1.6219 1.2790     1.8468     0.1073     -2.4664      0.0861
without sub-3         8     code   pooled        1s 3.0485 1.7065 1.2724     2.5013     0.0409     -3.3438      0.0247
without sub-3         8     code   pooled      70ms 2.6253 1.7565 1.2938     1.9276     0.0953     -3.8361      0.0128
without sub-5         7 infusion     unit        1s 2.7134 1.3743 1.2160     2.2001     0.1402     -0.5634      0.5936
without sub-5         7 infusion     unit      70ms 2.2838 1.5737 1.2247     1.4766     0.1902     -2.2999      0.1222
without sub-5         7 infusion   pooled        1s 2.7808 1.6709 1.2970     1.9846     0.1564     -1.6199      0.1564
without sub-5         7 infusion   pooled      70ms 2.4107 1.7000 1.2713     1.6407     0.1520     -2.5288      0.0895
without sub-5         7     code     unit        1s 2.7725 1.5570 1.3635     1.8866     0.2163     -0.7203      0.4984
without sub-5         7     code     unit      70ms 2.2689 1.5125 1.3289     1.2128     0.3920     -0.9222      0.3920
without sub-5         7     code   pooled        1s 2.8121 1.6911 1.3852     1.7546     0.2597     -1.1365      0.2991
without sub-5         7     code   pooled      70ms 2.3935 1.6481 1.3056     1.3413     0.2284     -1.9034      0.2113

Result: reproduced, resting on the assumption that the excluded animal is sub-5. Without sub-5 (7 tone sessions from 7 mice, as table S3) the paper's test finds no Pre against Ketamine difference in any reading (corrected p 0.14 to 0.39; main reading 2.71 against 1.37, q = 0.14). With sub-1 or sub-3 excluded instead (8 sessions), the difference is significant in some readings (q = 0.028 and 0.033 on the main reading), which would not reproduce it.

figs34e-tone-persistent¶

Persistent tone-related activity (350 to 500 ms) fell during ketamine (1.531 to 0.392) and recovered (1.385).

(Figure S34E; table S3, fig. S34 E)

Weights from Pre tone series means over 150 to 350 ms against the 1 s before the tone; projection over all eight tones, z-scored over -1 to 2 s, minus its mean over -1 to 0 s, quantified over 350 to 500 ms. Paired t-tests, Benjamini-Hochberg over the two.

In [25]:
rows = []
for rd, ks0 in READINGS.items():
    ks = [k for k in ks0 if k in TONE_SESSIONS]
    for split in ("infusion", "code"):
        for var in ("unit", "pooled"):
            v = tone_vals(ks, split, ("persist", var), "all", slice(135, 150), base="own")
            t1, q1, t2, q2 = tone_tests(v)
            m = [v[c].mean() for c in ("pre", "ket", "post")]
            rows.append({"reading": rd, "sessions": len(ks), "split": split, "variance": var, "Pre": m[0], "Ket": m[1], "Post": m[2],
                         "t Pre-Ket": t1, "q Pre-Ket": q1, "t Post-Ket": t2, "q Post-Ket": q2,
                         "holds": q1 < 0.05 and q2 < 0.05 and m[1] < min(m[0], m[2]),
                         "within": abs(m[0] - 1.531) <= 0.16 and abs(m[1] - 0.392) <= 0.35 and abs(m[2] - 1.385) <= 0.66})
print(pd.DataFrame(rows).round(4).to_string(index=False))
      reading  sessions    split variance    Pre    Ket   Post  t Pre-Ket  q Pre-Ket  t Post-Ket  q Post-Ket  holds  within
without sub-1         8 infusion     unit 1.6784 0.3893 1.2816     5.6083     0.0016      2.6757      0.0317   True    True
without sub-1         8 infusion   pooled 1.5766 0.3501 1.1579     4.9560     0.0033      2.4550      0.0438   True    True
without sub-1         8     code     unit 1.6772 0.4851 1.2676     6.9246     0.0005      2.7975      0.0266   True    True
without sub-1         8     code   pooled 1.5867 0.5038 1.1784     6.2918     0.0008      2.3153      0.0538  False    True
without sub-3         8 infusion     unit 1.6389 0.3268 1.0882     5.7476     0.0014      2.7257      0.0295   True    True
without sub-3         8 infusion   pooled 1.5121 0.3131 0.9640     4.7916     0.0040      2.4170      0.0463   True    True
without sub-3         8     code     unit 1.6324 0.3554 1.0817     7.5289     0.0003      2.8879      0.0234   True    True
without sub-3         8     code   pooled 1.5201 0.3875 1.0052     6.7447     0.0005      2.3044      0.0546  False    True
without sub-5         7 infusion     unit 1.6388 0.4673 1.4996     7.8578     0.0004      2.8705      0.0284   True    True
without sub-5         7 infusion   pooled 1.5123 0.3565 1.3650     6.0215     0.0019      3.0380      0.0229   True    True
without sub-5         7     code     unit 1.6389 0.4919 1.4799     8.7250     0.0003      3.1089      0.0209   True    True
without sub-5         7     code   pooled 1.5221 0.4031 1.3760     7.1014     0.0008      3.5287      0.0124   True    True

Result: reproduced. On the main reading (infusion split, per-unit variance) without sub-5, the projection is 1.639 (Pre), 0.467 (Ketamine) and 1.500 (Post), all within tolerance, with corrected p = 0.0004 and 0.028. Every exclusion and split with per-unit variance gives the same; two pooled-variance readings miss on the Post comparison (q = 0.054 and 0.055).

figs34f-tone-shorter-than-puff¶

Persistent responses to the neutral tone were shorter than those to the eyepuff.

(Figure S34F; table S3, fig. S34 F)

analysis.py compare_tone_puff_late_proj: Pre projections on each stimulus's persistent dimension, averaged over the eight stimuli of a series, turned into d' (calculate_dprime, the series as trials) against the 1 s before onset. Half-peak fall is the time from onset until the across-session mean d' falls below half its peak after the peak.

In [26]:
def half_fall(tr):
    post = tr[100:]
    pk = int(np.argmax(post))
    below = np.where(post[pk:] < 0.5 * post[pk])[0]
    return (pk + below[0]) * K.BIN if len(below) else np.nan

rows = []
for rd, ks0 in READINGS.items():
    ks = [k for k in ks0 if k in TONE_SESSIONS]
    for split in ("infusion", "code"):
        for var in ("unit", "pooled"):
            dp = np.array([res[k]["tone"][split]["dprime_puff"][var] for k in ks])
            dt = np.array([res[k]["tone"][split]["dprime_tone"][var] for k in ks])
            cp, ct = ci95(dp[:, 200:300].mean(1)), ci95(dt[:, 200:300].mean(1))
            rows.append({"reading": rd, "sessions": len(ks), "split": split, "variance": var,
                         "puff half-peak fall (s)": half_fall(dp.mean(0)), "tone half-peak fall (s)": half_fall(dt.mean(0)),
                         "puff d' 1-2 s": f"{cp[0]:.2f} [{cp[1]:.2f}, {cp[2]:.2f}]", "tone d' 1-2 s": f"{ct[0]:.2f} [{ct[1]:.2f}, {ct[2]:.2f}]",
                         "holds": half_fall(dp.mean(0)) > half_fall(dt.mean(0)) and cp[1] > ct[2]})
print(pd.DataFrame(rows).to_string(index=False))
      reading  sessions    split variance  puff half-peak fall (s)  tone half-peak fall (s)     puff d' 1-2 s      tone d' 1-2 s  holds
without sub-1         8 infusion     unit                     0.95                     0.17 1.10 [0.76, 1.44] 0.10 [-0.05, 0.25]   True
without sub-1         8 infusion   pooled                     0.99                     0.17 1.06 [0.74, 1.38] 0.09 [-0.06, 0.24]   True
without sub-1         8     code     unit                     0.95                     0.17 1.10 [0.76, 1.44] 0.13 [-0.02, 0.27]   True
without sub-1         8     code   pooled                     0.99                     0.17 1.06 [0.74, 1.38] 0.12 [-0.02, 0.25]   True
without sub-3         8 infusion     unit                     0.92                     0.16 1.10 [0.76, 1.44]  0.17 [0.05, 0.30]   True
without sub-3         8 infusion   pooled                     0.98                     0.16 1.06 [0.74, 1.38]  0.19 [0.04, 0.33]   True
without sub-3         8     code     unit                     0.92                     0.16 1.10 [0.76, 1.44]  0.19 [0.06, 0.31]   True
without sub-3         8     code   pooled                     0.98                     0.16 1.06 [0.74, 1.38]  0.20 [0.06, 0.34]   True
without sub-5         7 infusion     unit                     0.90                     0.16 1.06 [0.73, 1.39]  0.17 [0.01, 0.34]   True
without sub-5         7 infusion   pooled                     0.97                     0.16 1.04 [0.73, 1.34] 0.19 [-0.00, 0.37]   True
without sub-5         7     code     unit                     0.90                     0.16 1.06 [0.73, 1.39]  0.19 [0.04, 0.35]   True
without sub-5         7     code   pooled                     0.97                     0.16 1.04 [0.73, 1.34]  0.21 [0.04, 0.38]   True

Result: reproduced. In every reading the mean puff d' falls to half its peak 0.90 to 0.99 s after onset against 0.16 to 0.17 s for the tone, and the puff d' over 1 to 2 s (1.04 to 1.10) lies above the tone's (0.09 to 0.21) with non-overlapping intervals.

figs34g-tone-timescale¶

The baseline intrinsic timescale of the persistent tone population was shorter during ketamine (0.089 s) than before (0.268 s).

(Figure S34G; table S3, fig. S34 G)

As fig6h-mouse-timescale with the tone dimension and the 30 s before the first tone of each series; Pre tone series 5 to 10 (code) or all 10 (table S3), Ketamine tone series 2 to 6.

In [27]:
rows = []
for rd, ks0 in READINGS.items():
    ks = [k for k in ks0 if k in TONE_SESSIONS]
    for split in ("infusion", "code"):
        for var in ("unit", "pooled"):
            for pre_lab, pre_sl in (("Pre 5-10 (code)", slice(4, 10)), ("Pre all 10 (table S3)", slice(0, 10))):
                get = lambda k, split=split, var=var, pre_sl=pre_sl: (res[k]["tone"][split]["base_sigs"][var]["pre_sigs"][pre_sl],
                                                                      res[k]["tone"][split]["base_sigs"][var]["post_sigs"][1:6])
                rows += timescale_rows(get, ks, (0.268, 0.073), (0.089, 0.024),
                                       {"reading": rd, "sessions": len(ks), "split": split, "variance": var, "pre series": pre_lab})
f34g = pd.DataFrame(rows)
print("Ketamine is tone series 2 to 6 after the split. 'code' is analysis.py extract_timescale; the other fits are verification variants.")
print(f34g[f34g.fit == "code"].drop(columns="fit").round(4).to_string(index=False))
print("\nvariants: does the test hold (p < 0.05, Ketamine shorter), and are the means within tolerance?")
piv = f34g.assign(res=lambda d: d.holds.map({True: "holds", False: "fails"}) + d.within.map({True: "", False: " (out)"}) + " p=" + d.p.round(3).astype(str))
print(piv.pivot_table(index=["reading", "split", "variance", "pre series", "combine"], columns="fit", values="res", aggfunc="first").to_string())
Ketamine is tone series 2 to 6 after the split. 'code' is analysis.py extract_timescale; the other fits are verification variants.
      reading  sessions    split variance            pre series                 combine    Pre    Ket       t      p  holds  within degenerate per-series fits   Ket range
without sub-1         8 infusion     unit       Pre 5-10 (code)    (a) per series, mean 0.2795 0.1862  1.2379 0.2557  False   False                    0 of 88 0.018-0.557
without sub-1         8 infusion     unit       Pre 5-10 (code) (a') per series, median 0.2342 0.0795  3.6226 0.0085   True    True                    0 of 88 0.019-0.169
without sub-1         8 infusion     unit       Pre 5-10 (code)        (b) concatenated 0.2543 0.1000  2.6439 0.0332   True    True                    0 of 16 0.018-0.279
without sub-1         8 infusion     unit Pre all 10 (table S3)    (a) per series, mean 0.2757 0.1862  1.1878 0.2737  False   False                   0 of 120 0.018-0.557
without sub-1         8 infusion     unit Pre all 10 (table S3) (a') per series, median 0.2181 0.0795  4.6358 0.0024   True    True                   0 of 120 0.019-0.169
without sub-1         8 infusion     unit Pre all 10 (table S3)        (b) concatenated 0.2195 0.1000  2.6828 0.0314   True    True                    0 of 16 0.018-0.279
without sub-1         8 infusion   pooled       Pre 5-10 (code)    (a) per series, mean 0.2813 0.2515  0.2358 0.8203  False   False                    1 of 88 0.023-1.047
without sub-1         8 infusion   pooled       Pre 5-10 (code) (a') per series, median 0.2301 0.1150  2.0933 0.0746  False   False                    1 of 88 0.022-0.373
without sub-1         8 infusion   pooled       Pre 5-10 (code)        (b) concatenated 0.2564 0.1248  1.8006 0.1148  False   False                    0 of 16 0.021-0.476
without sub-1         8 infusion   pooled Pre all 10 (table S3)    (a) per series, mean 0.3059 0.2515  0.4023 0.6995  False   False                   2 of 120 0.023-1.047
without sub-1         8 infusion   pooled Pre all 10 (table S3) (a') per series, median 0.2124 0.1150  2.1349 0.0702  False   False                   2 of 120 0.022-0.373
without sub-1         8 infusion   pooled Pre all 10 (table S3)        (b) concatenated 0.2166 0.1248  1.5829 0.1575  False   False                    0 of 16 0.021-0.476
without sub-1         8     code     unit       Pre 5-10 (code)    (a) per series, mean 0.2556 0.1827  1.2594 0.2483  False   False                    0 of 80 0.018-0.418
without sub-1         8     code     unit       Pre 5-10 (code) (a') per series, median 0.2090 0.0845  4.5937 0.0025   True    True                    0 of 80 0.019-0.170
without sub-1         8     code     unit       Pre 5-10 (code)        (b) concatenated 0.2358 0.0778  4.0015 0.0052   True    True                    0 of 16 0.019-0.134
without sub-1         8     code     unit Pre all 10 (table S3)    (a) per series, mean 0.2634 0.1827  1.2784 0.2418  False   False                   0 of 112 0.018-0.418
without sub-1         8     code     unit Pre all 10 (table S3) (a') per series, median 0.2023 0.0845  4.2730 0.0037   True    True                   0 of 112 0.019-0.170
without sub-1         8     code     unit Pre all 10 (table S3)        (b) concatenated 0.2096 0.0778  4.2591 0.0038   True    True                    0 of 16 0.019-0.134
without sub-1         8     code   pooled       Pre 5-10 (code)    (a) per series, mean 0.2582 0.2750 -0.1497 0.8852  False   False                    1 of 80 0.051-0.962
without sub-1         8     code   pooled       Pre 5-10 (code) (a') per series, median 0.2004 0.0838  4.1789 0.0041   True    True                    1 of 80 0.024-0.152
without sub-1         8     code   pooled       Pre 5-10 (code)        (b) concatenated 0.2392 0.1109  2.5844 0.0362   True    True                    0 of 16 0.021-0.321
without sub-1         8     code   pooled Pre all 10 (table S3)    (a) per series, mean 0.3026 0.2750  0.2172 0.8342  False   False                   2 of 112 0.051-0.962
without sub-1         8     code   pooled Pre all 10 (table S3) (a') per series, median 0.1991 0.0838  4.5898 0.0025   True    True                   2 of 112 0.024-0.152
without sub-1         8     code   pooled Pre all 10 (table S3)        (b) concatenated 0.2077 0.1109  2.4895 0.0416   True    True                    0 of 16 0.021-0.321
without sub-3         8 infusion     unit       Pre 5-10 (code)    (a) per series, mean 0.2659 0.1911  1.0651 0.3222  False   False                    0 of 88 0.018-0.557
without sub-3         8 infusion     unit       Pre 5-10 (code) (a') per series, median 0.2220 0.0841  3.3879 0.0116   True    True                    0 of 88 0.019-0.169
without sub-3         8 infusion     unit       Pre 5-10 (code)        (b) concatenated 0.2401 0.1052  2.4441 0.0445   True    True                    0 of 16 0.018-0.279
without sub-3         8 infusion     unit Pre all 10 (table S3)    (a) per series, mean 0.2707 0.1911  1.1082 0.3044  False   False                   0 of 120 0.018-0.557
without sub-3         8 infusion     unit Pre all 10 (table S3) (a') per series, median 0.2141 0.0841  4.8897 0.0018   True    True                   0 of 120 0.019-0.169
without sub-3         8 infusion     unit Pre all 10 (table S3)        (b) concatenated 0.2106 0.1052  2.6996 0.0307   True    True                    0 of 16 0.018-0.279
without sub-3         8 infusion   pooled       Pre 5-10 (code)    (a) per series, mean 0.2679 0.2541  0.1122 0.9138  False   False                    1 of 88 0.023-1.047
without sub-3         8 infusion   pooled       Pre 5-10 (code) (a') per series, median 0.2161 0.1169  1.8555 0.1059  False   False                    1 of 88 0.022-0.373
without sub-3         8 infusion   pooled       Pre 5-10 (code)        (b) concatenated 0.2441 0.1283  1.6229 0.1486  False   False                    0 of 16 0.021-0.476
without sub-3         8 infusion   pooled Pre all 10 (table S3)    (a) per series, mean 0.3063 0.2541  0.3869 0.7103  False   False                   2 of 120 0.023-1.047
without sub-3         8 infusion   pooled Pre all 10 (table S3) (a') per series, median 0.2082 0.1169  2.0670 0.0776  False   False                   2 of 120 0.022-0.373
without sub-3         8 infusion   pooled Pre all 10 (table S3)        (b) concatenated 0.2119 0.1283  1.5046 0.1761  False   False                    0 of 16 0.021-0.476
without sub-3         8     code     unit       Pre 5-10 (code)    (a) per series, mean 0.2425 0.1877  1.0945 0.3100  False   False                    0 of 80 0.018-0.418
without sub-3         8     code     unit       Pre 5-10 (code) (a') per series, median 0.2012 0.0893  4.4994 0.0028   True    True                    0 of 80 0.019-0.170
without sub-3         8     code     unit       Pre 5-10 (code)        (b) concatenated 0.2219 0.0828  3.9438 0.0056   True    True                    0 of 16 0.019-0.134
without sub-3         8     code     unit Pre all 10 (table S3)    (a) per series, mean 0.2603 0.1877  1.2223 0.2611  False   False                   0 of 112 0.018-0.418
without sub-3         8     code     unit Pre all 10 (table S3) (a') per series, median 0.2106 0.0893  4.1460 0.0043   True    True                   0 of 112 0.019-0.170
without sub-3         8     code     unit Pre all 10 (table S3)        (b) concatenated 0.2019 0.0828  4.9049 0.0017   True    True                    0 of 16 0.019-0.134
without sub-3         8     code   pooled       Pre 5-10 (code)    (a) per series, mean 0.2456 0.2780 -0.3002 0.7728  False   False                    1 of 80 0.075-0.962
without sub-3         8     code   pooled       Pre 5-10 (code) (a') per series, median 0.1918 0.0865  3.7987 0.0067   True   False                    1 of 80 0.024-0.152
without sub-3         8     code   pooled       Pre 5-10 (code)        (b) concatenated 0.2284 0.1146  2.4188 0.0462   True   False                    0 of 16 0.021-0.321
without sub-3         8     code   pooled Pre all 10 (table S3)    (a) per series, mean 0.3052 0.2780  0.2144 0.8364  False   False                   2 of 112 0.075-0.962
without sub-3         8     code   pooled Pre all 10 (table S3) (a') per series, median 0.2067 0.0865  4.4884 0.0028   True    True                   2 of 112 0.024-0.152
without sub-3         8     code   pooled Pre all 10 (table S3)        (b) concatenated 0.2053 0.1146  2.5138 0.0402   True   False                    0 of 16 0.021-0.321
without sub-5         7 infusion     unit       Pre 5-10 (code)    (a) per series, mean 0.3134 0.1404  3.8005 0.0090   True   False                    0 of 77 0.029-0.382
without sub-5         7 infusion     unit       Pre 5-10 (code) (a') per series, median 0.2537 0.0902  3.6170 0.0111   True    True                    0 of 77 0.029-0.169
without sub-5         7 infusion     unit       Pre 5-10 (code)        (b) concatenated 0.2834 0.0818  3.9344 0.0077   True    True                    0 of 14 0.028-0.167
without sub-5         7 infusion     unit Pre all 10 (table S3)    (a) per series, mean 0.3209 0.1404  3.8783 0.0082   True   False                   0 of 105 0.029-0.382
without sub-5         7 infusion     unit Pre all 10 (table S3) (a') per series, median 0.2463 0.0902  5.4483 0.0016   True    True                   0 of 105 0.029-0.169
without sub-5         7 infusion     unit Pre all 10 (table S3)        (b) concatenated 0.2523 0.0818  5.2857 0.0019   True    True                    0 of 14 0.028-0.167
without sub-5         7 infusion   pooled       Pre 5-10 (code)    (a) per series, mean 0.3075 0.1454  4.1451 0.0060   True   False                    0 of 77 0.055-0.399
without sub-5         7 infusion   pooled       Pre 5-10 (code) (a') per series, median 0.2410 0.0855  3.3078 0.0163   True    True                    0 of 77 0.058-0.128
without sub-5         7 infusion   pooled       Pre 5-10 (code)        (b) concatenated 0.2809 0.0826  3.7990 0.0090   True    True                    0 of 14 0.049-0.142
without sub-5         7 infusion   pooled Pre all 10 (table S3)    (a) per series, mean 0.3511 0.1454  3.2372 0.0177   True   False                   1 of 105 0.055-0.399
without sub-5         7 infusion   pooled Pre all 10 (table S3) (a') per series, median 0.2344 0.0855  5.2703 0.0019   True    True                   1 of 105 0.058-0.128
without sub-5         7 infusion   pooled Pre all 10 (table S3)        (b) concatenated 0.2457 0.0826  6.1216 0.0009   True    True                    0 of 14 0.049-0.142
without sub-5         7     code     unit       Pre 5-10 (code)    (a) per series, mean 0.2904 0.1565  3.1042 0.0210   True   False                    0 of 70 0.030-0.406
without sub-5         7     code     unit       Pre 5-10 (code) (a') per series, median 0.2234 0.0915  4.7699 0.0031   True    True                    0 of 70 0.030-0.170
without sub-5         7     code     unit       Pre 5-10 (code)        (b) concatenated 0.2626 0.0857  4.4812 0.0042   True    True                    0 of 14 0.031-0.134
without sub-5         7     code     unit Pre all 10 (table S3)    (a) per series, mean 0.3098 0.1565  2.7990 0.0312   True   False                    0 of 98 0.030-0.406
without sub-5         7     code     unit Pre all 10 (table S3) (a') per series, median 0.2488 0.0915  5.8721 0.0011   True    True                    0 of 98 0.030-0.170
without sub-5         7     code     unit Pre all 10 (table S3)        (b) concatenated 0.2436 0.0857  5.2846 0.0019   True    True                    0 of 14 0.031-0.134
without sub-5         7     code   pooled       Pre 5-10 (code)    (a) per series, mean 0.2857 0.1614  3.6355 0.0109   True   False                    0 of 70 0.051-0.373
without sub-5         7     code   pooled       Pre 5-10 (code) (a') per series, median 0.2085 0.0891  3.9462 0.0076   True    True                    0 of 70 0.051-0.152
without sub-5         7     code   pooled       Pre 5-10 (code)        (b) concatenated 0.2630 0.0885  4.5187 0.0040   True    True                    0 of 14 0.044-0.141
without sub-5         7     code   pooled Pre all 10 (table S3)    (a) per series, mean 0.3503 0.1614  2.6635 0.0373   True   False                    1 of 98 0.051-0.373
without sub-5         7     code   pooled Pre all 10 (table S3) (a') per series, median 0.2340 0.0891  5.6493 0.0013   True    True                    1 of 98 0.051-0.152
without sub-5         7     code   pooled Pre all 10 (table S3)        (b) concatenated 0.2387 0.0885  6.2473 0.0008   True    True                    0 of 14 0.044-0.141

variants: does the test hold (p < 0.05, Ketamine shorter), and are the means within tolerance?
fit                                                                                      bounded b                 code             demeaned  demeaned, bounded b
reading       split    variance pre series            combine                                                                                                    
without sub-1 code     pooled   Pre 5-10 (code)       (a') per series, median        holds p=0.004        holds p=0.004        holds p=0.006        holds p=0.006
                                                      (a) per series, mean     fails (out) p=0.539  fails (out) p=0.885  fails (out) p=0.779  fails (out) p=0.358
                                                      (b) concatenated               holds p=0.036        holds p=0.036        holds p=0.036        holds p=0.036
                                Pre all 10 (table S3) (a') per series, median        holds p=0.002        holds p=0.003        holds p=0.004        holds p=0.004
                                                      (a) per series, mean     fails (out) p=0.426  fails (out) p=0.834  fails (out) p=0.596  fails (out) p=0.268
                                                      (b) concatenated               holds p=0.042        holds p=0.042        holds p=0.039        holds p=0.039
                       unit     Pre 5-10 (code)       (a') per series, median        holds p=0.003        holds p=0.003        holds p=0.006        holds p=0.006
                                                      (a) per series, mean     fails (out) p=0.248  fails (out) p=0.248  fails (out) p=0.114  fails (out) p=0.114
                                                      (b) concatenated               holds p=0.005        holds p=0.005        holds p=0.006        holds p=0.006
                                Pre all 10 (table S3) (a') per series, median        holds p=0.004        holds p=0.004        holds p=0.006        holds p=0.006
                                                      (a) per series, mean     fails (out) p=0.242  fails (out) p=0.242  fails (out) p=0.088  fails (out) p=0.088
                                                      (b) concatenated               holds p=0.004        holds p=0.004        holds p=0.004        holds p=0.004
              infusion pooled   Pre 5-10 (code)       (a') per series, median  fails (out) p=0.075  fails (out) p=0.075        holds p=0.007        holds p=0.007
                                                      (a) per series, mean     fails (out) p=0.419   fails (out) p=0.82  fails (out) p=0.702  fails (out) p=0.282
                                                      (b) concatenated         fails (out) p=0.115  fails (out) p=0.115  fails (out) p=0.119  fails (out) p=0.119
                                Pre all 10 (table S3) (a') per series, median   fails (out) p=0.07   fails (out) p=0.07        holds p=0.002        holds p=0.002
                                                      (a) per series, mean     fails (out) p=0.399  fails (out) p=0.699  fails (out) p=0.609  fails (out) p=0.253
                                                      (b) concatenated         fails (out) p=0.157  fails (out) p=0.157  fails (out) p=0.163  fails (out) p=0.163
                       unit     Pre 5-10 (code)       (a') per series, median        holds p=0.008        holds p=0.008  holds (out) p=0.009  holds (out) p=0.009
                                                      (a) per series, mean     fails (out) p=0.256  fails (out) p=0.256  fails (out) p=0.165  fails (out) p=0.165
                                                      (b) concatenated               holds p=0.033        holds p=0.033        holds p=0.034        holds p=0.034
                                Pre all 10 (table S3) (a') per series, median        holds p=0.002        holds p=0.002  holds (out) p=0.003  holds (out) p=0.003
                                                      (a) per series, mean     fails (out) p=0.274  fails (out) p=0.274  fails (out) p=0.148  fails (out) p=0.148
                                                      (b) concatenated               holds p=0.031        holds p=0.031        holds p=0.033        holds p=0.033
without sub-3 code     pooled   Pre 5-10 (code)       (a') per series, median  holds (out) p=0.007  holds (out) p=0.007   holds (out) p=0.01   holds (out) p=0.01
                                                      (a) per series, mean     fails (out) p=0.659  fails (out) p=0.773  fails (out) p=0.871  fails (out) p=0.434
                                                      (b) concatenated         holds (out) p=0.046  holds (out) p=0.046        holds p=0.046        holds p=0.046
                                Pre all 10 (table S3) (a') per series, median        holds p=0.003        holds p=0.003        holds p=0.004        holds p=0.004
                                                      (a) per series, mean     fails (out) p=0.428  fails (out) p=0.836    fails (out) p=0.6   fails (out) p=0.27
                                                      (b) concatenated          holds (out) p=0.04   holds (out) p=0.04        holds p=0.038        holds p=0.038
                       unit     Pre 5-10 (code)       (a') per series, median        holds p=0.003        holds p=0.003  holds (out) p=0.008  holds (out) p=0.008
                                                      (a) per series, mean      fails (out) p=0.31   fails (out) p=0.31  fails (out) p=0.142  fails (out) p=0.142
                                                      (b) concatenated               holds p=0.006        holds p=0.006        holds p=0.006        holds p=0.006
                                Pre all 10 (table S3) (a') per series, median        holds p=0.004        holds p=0.004        holds p=0.006        holds p=0.006
                                                      (a) per series, mean     fails (out) p=0.261  fails (out) p=0.261  fails (out) p=0.093  fails (out) p=0.093
                                                      (b) concatenated               holds p=0.002        holds p=0.002        holds p=0.002        holds p=0.002
              infusion pooled   Pre 5-10 (code)       (a') per series, median  fails (out) p=0.106  fails (out) p=0.106        holds p=0.012        holds p=0.012
                                                      (a) per series, mean     fails (out) p=0.506  fails (out) p=0.914  fails (out) p=0.795  fails (out) p=0.349
                                                      (b) concatenated         fails (out) p=0.149  fails (out) p=0.149  fails (out) p=0.154  fails (out) p=0.154
                                Pre all 10 (table S3) (a') per series, median  fails (out) p=0.078  fails (out) p=0.078        holds p=0.002        holds p=0.002
                                                      (a) per series, mean     fails (out) p=0.409   fails (out) p=0.71  fails (out) p=0.624  fails (out) p=0.262
                                                      (b) concatenated         fails (out) p=0.176  fails (out) p=0.176  fails (out) p=0.183  fails (out) p=0.183
                       unit     Pre 5-10 (code)       (a') per series, median        holds p=0.012        holds p=0.012        holds p=0.013        holds p=0.013
                                                      (a) per series, mean     fails (out) p=0.322  fails (out) p=0.322   fails (out) p=0.21   fails (out) p=0.21
                                                      (b) concatenated               holds p=0.044        holds p=0.044        holds p=0.046        holds p=0.046
                                Pre all 10 (table S3) (a') per series, median        holds p=0.002        holds p=0.002        holds p=0.003        holds p=0.003
                                                      (a) per series, mean     fails (out) p=0.304  fails (out) p=0.304  fails (out) p=0.165  fails (out) p=0.165
                                                      (b) concatenated               holds p=0.031        holds p=0.031        holds p=0.033        holds p=0.033
without sub-5 code     pooled   Pre 5-10 (code)       (a') per series, median        holds p=0.008        holds p=0.008        holds p=0.011        holds p=0.011
                                                      (a) per series, mean     holds (out) p=0.011  holds (out) p=0.011  holds (out) p=0.008  holds (out) p=0.008
                                                      (b) concatenated               holds p=0.004        holds p=0.004        holds p=0.004        holds p=0.004
                                Pre all 10 (table S3) (a') per series, median        holds p=0.001        holds p=0.001        holds p=0.003        holds p=0.003
                                                      (a) per series, mean     holds (out) p=0.018  holds (out) p=0.037  holds (out) p=0.012  holds (out) p=0.005
                                                      (b) concatenated               holds p=0.001        holds p=0.001        holds p=0.001        holds p=0.001
                       unit     Pre 5-10 (code)       (a') per series, median        holds p=0.003        holds p=0.003        holds p=0.006        holds p=0.006
                                                      (a) per series, mean     holds (out) p=0.021  holds (out) p=0.021        holds p=0.006        holds p=0.006
                                                      (b) concatenated               holds p=0.004        holds p=0.004        holds p=0.004        holds p=0.004
                                Pre all 10 (table S3) (a') per series, median        holds p=0.001        holds p=0.001        holds p=0.002        holds p=0.002
                                                      (a) per series, mean     holds (out) p=0.031  holds (out) p=0.031        holds p=0.003        holds p=0.003
                                                      (b) concatenated               holds p=0.002        holds p=0.002        holds p=0.002        holds p=0.002
              infusion pooled   Pre 5-10 (code)       (a') per series, median        holds p=0.016        holds p=0.016        holds p=0.012        holds p=0.012
                                                      (a) per series, mean     holds (out) p=0.006  holds (out) p=0.006  holds (out) p=0.003  holds (out) p=0.003
                                                      (b) concatenated               holds p=0.009        holds p=0.009        holds p=0.008        holds p=0.008
                                Pre all 10 (table S3) (a') per series, median        holds p=0.002        holds p=0.002        holds p=0.002        holds p=0.002
                                                      (a) per series, mean     holds (out) p=0.008  holds (out) p=0.018  holds (out) p=0.005  holds (out) p=0.002
                                                      (b) concatenated               holds p=0.001        holds p=0.001        holds p=0.001        holds p=0.001
                       unit     Pre 5-10 (code)       (a') per series, median        holds p=0.011        holds p=0.011        holds p=0.012        holds p=0.012
                                                      (a) per series, mean     holds (out) p=0.009  holds (out) p=0.009        holds p=0.007        holds p=0.007
                                                      (b) concatenated               holds p=0.008        holds p=0.008        holds p=0.008        holds p=0.008
                                Pre all 10 (table S3) (a') per series, median        holds p=0.002        holds p=0.002        holds p=0.002        holds p=0.002
                                                      (a) per series, mean     holds (out) p=0.008  holds (out) p=0.008        holds p=0.001        holds p=0.001
                                                      (b) concatenated               holds p=0.002        holds p=0.002        holds p=0.002        holds p=0.002

Result: partially reproduced, resting on the assumption that the excluded animal is sub-5. Without sub-5 the Ketamine timescale is shorter in every reading and every fit variant (p = 0.0008 to 0.037 with the code's fit). The concatenated fits are within tolerance (main reading 0.283 against 0.082 s, p = 0.0077), but the code's per-series fits put Ketamine at 0.140 to 0.161 s, outside 0.089 +- 0.024. With the mean removed before the autocorrelation, the per-series means with per-unit weights are within tolerance too. With sub-1 or sub-3 excluded the per-series mean fails the test under every fit variant (p = 0.09 to 0.91), while the per-series median holds in most of those readings.

figs34h-tone-coupling¶

Correlations among the persistent tone neurons before the tone series fell during ketamine to 0.460 of their preinfusion value.

(Figure S34H; table S3, fig. S34 H)

As fig6j-mouse-coupling with the tone dimension and the 30 s before each tone series (Pre tone series, Ketamine 2 to 6). The code's own loop takes the windows before puff series 1 to 10 and 23 to 32 for any stimulus; that is shown as (b').

In [28]:
rows = []
for rd, ks0 in READINGS.items():
    ks = [k for k in ks0 if k in TONE_SESSIONS]
    for split in ("infusion", "code"):
        for var in ("unit", "pooled"):
            for lab, sname, meas, cp, ck in (("(a) Methods", "methods", "per_series_mean_r", "pre", "ket"),
                                             ("(b) code", "code", "concat_median_abs_r", "pre", "ket"),
                                             ("(b') code, puff windows", "code", "concat_median_abs_r", "pre_puffwin", "ket_puffwin")):
                f = (lambda d: np.nanmean(d[meas])) if meas == "per_series_mean_r" else (lambda d: d[meas])
                pv = np.array([f(res[k]["tone"][split]["coupling"][(var, sname)][cp]) for k in ks])
                kv = np.array([f(res[k]["tone"][split]["coupling"][(var, sname)][ck]) for k in ks])
                ok = ~(np.isnan(pv) | np.isnan(kv))
                t = scipy.stats.ttest_rel(pv[ok], kv[ok])
                ratio = kv[ok] / pv[ok]
                rows.append({"reading": rd, "split": split, "variance": var, "measure": lab, "sessions": int(ok.sum()),
                             "Ket/Pre mean": ratio.mean(), "t": t.statistic, "p": t.pvalue,
                             "holds": t.pvalue < 0.05 and kv[ok].mean() < pv[ok].mean(), "within": abs(ratio.mean() - 0.46) <= 0.16})
print(pd.DataFrame(rows).round(4).to_string(index=False))
      reading    split variance                 measure  sessions  Ket/Pre mean       t      p  holds  within
without sub-1 infusion     unit             (a) Methods         8        1.0055 -0.0895 0.9312  False   False
without sub-1 infusion     unit                (b) code         7        0.8315  1.4079 0.2088  False   False
without sub-1 infusion     unit (b') code, puff windows         7        0.6486  2.4483 0.0499   True   False
without sub-1 infusion   pooled             (a) Methods         8        0.7711  1.1668 0.2815  False   False
without sub-1 infusion   pooled                (b) code         8        0.6156  2.4817 0.0421   True    True
without sub-1 infusion   pooled (b') code, puff windows         8        0.4650  3.8310 0.0064   True    True
without sub-1     code     unit             (a) Methods         8        1.1107 -0.0226 0.9826  False   False
without sub-1     code     unit                (b) code         8        0.9332  0.7171 0.4965  False   False
without sub-1     code     unit (b') code, puff windows         8        0.6180  2.6320 0.0338   True    True
without sub-1     code   pooled             (a) Methods         8        0.8467  0.9236 0.3864  False   False
without sub-1     code   pooled                (b) code         8        0.8728  1.4690 0.1853  False   False
without sub-1     code   pooled (b') code, puff windows         8        0.4748  3.7229 0.0074   True    True
without sub-3 infusion     unit             (a) Methods         8        1.0455 -0.2623 0.8006  False   False
without sub-3 infusion     unit                (b) code         7        0.8762  1.1419 0.2970  False   False
without sub-3 infusion     unit (b') code, puff windows         7        0.7068  2.1008 0.0804  False   False
without sub-3 infusion   pooled             (a) Methods         8        0.7935  0.9408 0.3781  False   False
without sub-3 infusion   pooled                (b) code         8        0.6519  2.1845 0.0652  False   False
without sub-3 infusion   pooled (b') code, puff windows         8        0.5134  3.2941 0.0132   True    True
without sub-3     code     unit             (a) Methods         8        1.1659 -0.2286 0.8257  False   False
without sub-3     code     unit                (b) code         8        1.0031  0.2479 0.8113  False   False
without sub-3     code     unit (b') code, puff windows         8        0.6639  2.3376 0.0520  False   False
without sub-3     code   pooled             (a) Methods         8        0.8673  0.6720 0.5232  False   False
without sub-3     code   pooled                (b) code         8        0.9283  1.1010 0.3073  False   False
without sub-3     code   pooled (b') code, puff windows         8        0.5162  3.2648 0.0138   True    True
without sub-5 infusion     unit             (a) Methods         7        1.2282  0.1313 0.8998  False   False
without sub-5 infusion     unit                (b) code         7        0.8205  1.5155 0.1804  False   False
without sub-5 infusion     unit (b') code, puff windows         7        0.6699  2.4305 0.0511  False   False
without sub-5 infusion   pooled             (a) Methods         7        0.8733  0.9031 0.4013  False   False
without sub-5 infusion   pooled                (b) code         7        0.6605  2.3073 0.0605  False   False
without sub-5 infusion   pooled (b') code, puff windows         7        0.5335  3.1818 0.0190   True    True
without sub-5     code     unit             (a) Methods         7        1.2434 -0.0500 0.9617  False   False
without sub-5     code     unit                (b) code         7        1.0654  0.4303 0.6820  False   False
without sub-5     code     unit (b') code, puff windows         7        0.6470  2.6412 0.0385   True   False
without sub-5     code   pooled             (a) Methods         7        0.9114  0.7999 0.4543  False   False
without sub-5     code   pooled                (b) code         7        0.9976  0.9146 0.3957  False   False
without sub-5     code   pooled (b') code, puff windows         7        0.5362  3.1693 0.0193   True    True

Result: not reproduced. With the 30 s before each tone series, neither reading finds a significant fall without sub-5: the Methods reading gives Ketamine at 1.23 of Pre (p = 0.90) and the code's measure 0.82 (p = 0.18) with per-unit weights, and 0.87 (p = 0.40) and 0.66 (p = 0.061) with pooled weights. The code's own loop, which takes the windows before puff series 1 to 10 and 23 to 32, gives 0.53 (p = 0.019, pooled weights) and 0.67 (p = 0.051, per-unit weights), near the paper's 0.46.

figs30-correlation-structure¶

Ketamine changed the correlation structure of single units at rest, increasing correlations within regions such as retrosplenial/posteromedial and orbitofrontal cortex while reducing correlations between regions.

(Figure S30A; Results, Brain-wide activity screen during emergence of affect in mice)

Criterion: Pearson correlations of the smoothed rates of all units over the whole Pre period (from the start of the recording to the code's ketamine onset, 30 s after the last puff of series 20) and over the 10 minutes from that onset; per session the mean change (Ketamine minus Pre) over pairs in the same finest region and over pairs in different regions (NA left out). Pairs with a unit silent in a period have no correlation and are counted. RSP and ORB within-region changes: pairs in the same finest region under RSP or ORB; the change over all pairs within the RSP or ORB family is also shown. t-tests across the 13 sessions.

In [29]:
def corr_table(pre_key, ket_key):
    rows = []
    for k in names:
        reg = res[k]["regions_all"]
        ok = reg != "NA"
        d = (res[k][ket_key] - res[k][pre_key])[np.ix_(ok, ok)].astype(float)
        r = reg[ok]
        same = r[:, None] == r[None, :]
        iu = np.triu(np.ones_like(same), 1).astype(bool)
        fin = np.isfinite(d)
        row = {"session": k, "units": int(ok.sum()), "within change": np.nanmean(d[same & iu & fin]), "between change": np.nanmean(d[~same & iu & fin]),
               "pairs dropped (silent unit)": int((iu & ~fin).sum())}
        for fam in ("RSP", "ORB"):
            m = np.array([K.under(x, fam, paths) for x in r])
            row[f"{fam} units"] = int(m.sum())
            row[f"{fam} within change"] = np.nanmean(d[np.outer(m, m) & same & iu & fin]) if m.sum() > 1 else np.nan
            row[f"{fam} family change"] = np.nanmean(d[np.outer(m, m) & iu & fin]) if m.sum() > 1 else np.nan
        rows.append(row)
    return pd.DataFrame(rows).set_index("session")

def corr_summary(c30):
    tw = scipy.stats.ttest_1samp(c30["within change"], 0); tb = scipy.stats.ttest_1samp(c30["between change"], 0)
    tp = scipy.stats.ttest_rel(c30["within change"], c30["between change"])
    print(f"within-region change: mean {c30['within change'].mean():.4f}, t = {tw.statistic:.2f}, p = {tw.pvalue:.4f}")
    print(f"between-region change: mean {c30['between change'].mean():.4f}, t = {tb.statistic:.2f}, p = {tb.pvalue:.4f}")
    print(f"within against between (paired): t = {tp.statistic:.2f}, p = {tp.pvalue:.4f}")
    for fam in ("RSP", "ORB"):
        for col in ("within change", "family change"):
            v = c30[f"{fam} {col}"].dropna()
            print(f"{fam} {col}: positive in {(v > 0).sum()} of {len(v)} sessions recording it")

# criterion: Ketamine is the 10 min from the code's ketamine onset (30 s after the last puff of series 20)
c30 = corr_table("corr_pre", "corr_ket")
print("Ketamine window from the code's onset (criterion):")
print(c30.round(4).to_string())
corr_summary(c30)
# verification variant: the 10 min from the file's infusion time (Methods: "a 10-minute period following the
# onset of ketamine infusion"), Pre everything before the infusion; the code's onset is 94 to 420 s earlier
c30i = corr_table("corr_pre_inf", "corr_ket_inf")
print("\nvariant, Ketamine window from the file's infusion time:")
print(c30i[["within change", "between change", "RSP within change", "ORB within change"]].round(4).to_string())
corr_summary(c30i)
Ketamine window from the code's onset (criterion):
                    units  within change  between change  pairs dropped (silent unit)  RSP units  RSP within change  RSP family change  ORB units  ORB within change  ORB family change
session                                                                                                                                                                                
sub-0_ses-20230901    818         0.0019         -0.0042                         4075         23             0.0422             0.0418         83            -0.0041            -0.0031
sub-1_ses-20230218    495        -0.0014         -0.0038                         3924         16             0.0680             0.0658          0                NaN                NaN
sub-1_ses-20230226    984         0.0050          0.0028                         3926         56             0.0433             0.0469          0                NaN                NaN
sub-2_ses-20230902    677         0.0213          0.0013                         6715        115             0.0647             0.0415        128             0.0165             0.0196
sub-3_ses-20230218    388         0.0101          0.0008                          387          8             0.0284             0.0204        108             0.0156             0.0121
sub-3_ses-20230225    864        -0.0002         -0.0006                            0          3            -0.0446             0.0355         89            -0.0060            -0.0043
sub-4_ses-20230907    869         0.0312          0.0048                          868        109             0.1392             0.1176        177             0.0135             0.0092
sub-5_ses-20230706    139        -0.0089         -0.0078                         2570          0                NaN                NaN          0                NaN                NaN
sub-5_ses-20230822    499         0.0048         -0.0000                         1986          0                NaN                NaN          0                NaN                NaN
sub-6_ses-20230823    480        -0.0019         -0.0019                         1434          0                NaN                NaN          0                NaN                NaN
sub-7_ses-20220831    858         0.0019          0.0028                         3422          2             0.0018             0.0018        128             0.0098             0.0113
sub-8_ses-20220228    762         0.0061         -0.0014                         2280         69             0.0663             0.0103          0                NaN                NaN
sub-9_ses-20230908    854         0.0120          0.0054                         1705        105             0.0171             0.0159        123             0.0037             0.0038
within-region change: mean 0.0063, t = 2.16, p = 0.0522
between-region change: mean -0.0001, t = -0.14, p = 0.8934
within against between (paired): t = 2.82, p = 0.0155
RSP within change: positive in 9 of 10 sessions recording it
RSP family change: positive in 10 of 10 sessions recording it
ORB within change: positive in 5 of 7 sessions recording it
ORB family change: positive in 5 of 7 sessions recording it
variant, Ketamine window from the file's infusion time:
                    within change  between change  RSP within change  ORB within change
session                                                                                
sub-0_ses-20230901         0.0002         -0.0054             0.0510            -0.0071
sub-1_ses-20230218         0.0020         -0.0036             0.0855                NaN
sub-1_ses-20230226         0.0121          0.0055             0.1331                NaN
sub-2_ses-20230902         0.0282          0.0030             0.0820             0.0228
sub-3_ses-20230218         0.0091          0.0008             0.0429             0.0169
sub-3_ses-20230225         0.0002          0.0009            -0.1472            -0.0065
sub-4_ses-20230907         0.0353          0.0052             0.1603             0.0159
sub-5_ses-20230706        -0.0025         -0.0109                NaN                NaN
sub-5_ses-20230822         0.0043          0.0003                NaN                NaN
sub-6_ses-20230823        -0.0003         -0.0006                NaN                NaN
sub-7_ses-20220831         0.0112          0.0047            -0.0198             0.0004
sub-8_ses-20220228         0.0097          0.0008             0.0813                NaN
sub-9_ses-20230908         0.0165          0.0020             0.0321             0.0110
within-region change: mean 0.0097, t = 3.05, p = 0.0100
between-region change: mean 0.0002, t = 0.17, p = 0.8656
within against between (paired): t = 3.80, p = 0.0025
RSP within change: positive in 8 of 10 sessions recording it
RSP family change: positive in 8 of 10 sessions recording it
ORB within change: positive in 5 of 7 sessions recording it
ORB family change: positive in 4 of 7 sessions recording it

Result: partially reproduced. Within-region correlations rise in RSP in 9 of 10 sessions that record it and in ORB in 5 of 7. Across the 13 sessions the mean within-region change is +0.0063 (p = 0.052), short of significance, and the between-region change is -0.0001 (p = 0.89), not below zero. With the 10 minutes from the file's infusion time (the Methods' wording), the within-region change is +0.0097 (p = 0.010) and RSP rises in 8 of 10 sessions and ORB in 5 of 7, but the between-region change is still +0.0002 (p = 0.87). The within-region rise in RSP and ORB is present; the between-region fall is not.

figs29-rsp-oscillations¶

Ketamine brought low-frequency oscillations into the population activity, especially strong in retrosplenial cortex.

(Figure S29C and S29D; Results, Brain-wide activity screen during emergence of affect in mice)

Criterion: scipy.signal.welch (100 Hz, nperseg 2048) of each unit's z-scored rate over one minute, z-scored across frequency, peak below 7.5 Hz; the minute before the infusion time in the file against the minute from 5 min and from 10 min after it. Regions merged across layers (reduce_region); RSP is any region under RSP. Units silent in a window have no spectrum and are left out.

In [30]:
rows = []
for k in names:
    reg = np.array([K.reduce_region(x) for x in res[k]["regions_all"]])
    for after in ("after5", "after10"):
        ch = res[k]["spec_peak"][after] - res[k]["spec_peak"]["before"]
        for r in np.unique(reg):
            m = reg == r
            rows.append({"session": k, "after": after, "region": r, "rsp": K.under(r, "RSP", paths), "units": int(m.sum()), "change": np.nanmean(ch[m]),
                         "values": ch[m]})
sp = pd.DataFrame(rows)
sp = sp[sp.region != "NA"]
for after in ("after5", "after10"):
    s = sp[sp.after == after]
    rsp_sess = s[s.rsp].groupby("session").apply(lambda g: np.nanmean(np.concatenate(g["values"].to_list())))
    pooled = s.groupby("region").apply(lambda g: pd.Series({"units": g["units"].sum(), "change": np.nanmean(np.concatenate(g["values"].to_list()))}))
    pooled = pooled[pooled.units >= 30].sort_values("change", ascending=False)
    print(f"\n{after}: RSP units' mean change in z-scored low-frequency peak per session recording RSP:")
    print(rsp_sess.round(3).to_string())
    undefined = list(rsp_sess.index[rsp_sess.isna()])
    print(f"rises in {(rsp_sess > 0).sum()} of {rsp_sess.notna().sum()} sessions with a defined RSP value "
          f"({len(rsp_sess)} record RSP; no value in {undefined})")
    print("pooled regions with >= 30 units, largest rises first:")
    print(pooled.head(10).round(3).to_string())
    print(f"rank of the first RSP region: {[i for i, r in enumerate(pooled.index) if K.under(r, 'RSP', paths)][:1]} of {len(pooled)}")
print("frequency resolution (Hz):", res[names[0]]["spec_freqs"][1])
after5: RSP units' mean change in z-scored low-frequency peak per session recording RSP:
session
sub-0_ses-20230901    0.613
sub-1_ses-20230218   -3.178
sub-1_ses-20230226    4.090
sub-2_ses-20230902    1.645
sub-3_ses-20230218    0.276
sub-3_ses-20230225   -2.949
sub-4_ses-20230907    4.503
sub-7_ses-20220831      NaN
sub-8_ses-20220228    1.541
sub-9_ses-20230908    0.095
rises in 7 of 9 sessions with a defined RSP value (10 record RSP; no value in ['sub-7_ses-20220831'])
pooled regions with >= 30 units, largest rises first:
        units  change
region               
RSPv    430.0   2.128
TH       62.0   1.714
RSPagl   37.0   1.237
RSPd     39.0   0.826
NLOT2    32.0   0.688
SF       40.0   0.680
PO      345.0   0.569
SUB      67.0   0.505
DP       37.0   0.141
PoT      95.0   0.099
rank of the first RSP region: [0] of 52

after10: RSP units' mean change in z-scored low-frequency peak per session recording RSP:
session
sub-0_ses-20230901    2.557
sub-1_ses-20230218   -3.232
sub-1_ses-20230226    3.928
sub-2_ses-20230902    1.295
sub-3_ses-20230218    2.102
sub-3_ses-20230225   -7.901
sub-4_ses-20230907   -0.239
sub-7_ses-20220831      NaN
sub-8_ses-20220228   -0.321
sub-9_ses-20230908    0.471
rises in 5 of 9 sessions with a defined RSP value (10 record RSP; no value in ['sub-7_ses-20220831'])
pooled regions with >= 30 units, largest rises first:
        units  change
region               
RSPd     39.0   2.133
RSPv    430.0   0.740
TH       62.0   0.611
STR      59.0   0.516
SUB      67.0   0.453
AON      89.0   0.280
DP       37.0   0.157
BST      35.0   0.031
ORBvl   168.0   0.007
SF       40.0  -0.086
rank of the first RSP region: [0] of 52
frequency resolution (Hz): 0.048828125

Result: reproduced. RSPv has the largest rise of the z-scored low-frequency spectral peak of all regions with at least 30 units in the minute from 5 min after the infusion (2.13), and RSPd in the minute from 10 min (2.13). Of the 9 sessions with a defined RSP value (sub-7 has none: two RSP units, silent in a window), RSP units rise in 7 at 5 min and in 5 at 10 min, a majority under both readings. The first version counted sub-7 in the denominator (5 of 10).

Summary¶

Claim Outcome
fig4-recordings-scale reproduced
fig4de-response-clusters not reproduced
figs31b-cluster-overrepresentation partially reproduced
fig4g-ketamine-alters-slow-clusters not reproduced
fig4i-rise-times-unchanged reproduced
fig4h-decay-shortened reproduced
fig4h-decay-anatomical-progression reproduced
fig5d-emotion-dimension-accumulates partially reproduced
fig5e-late-reduced partially reproduced
fig5e-rapid-unchanged reproduced
figs32c-regional-emotion-dimension partially reproduced
fig5g-tau-persist-model reproduced
figs33-alternative-models reproduced (assumes the Methods target window and per-unit variance where the readings disagree)
fig6d-mouse-persistent reproduced (assumes sub-5 is the excluded animal)
fig6f-mouse-fast-unchanged reproduced
fig6b-persistent-dimension-distributed reproduced
fig6h-mouse-timescale reproduced (assumes sub-5 is the excluded animal)
fig6j-mouse-coupling partially reproduced (assumes sub-5 is the excluded animal)
figs34d-tone-fast-unchanged reproduced (assumes sub-5 is the excluded animal)
figs34e-tone-persistent reproduced
figs34f-tone-shorter-than-puff reproduced
figs34g-tone-timescale partially reproduced (assumes sub-5 is the excluded animal)
figs34h-tone-coupling not reproduced
figs30-correlation-structure partially reproduced
figs29-rsp-oscillations reproduced

The claims on DANDI:001347 are in reproduce_kauvar_2025_behavior.ipynb, reproduce_kauvar_2025_ieeg.ipynb or blocked; the attempt record has every outcome.

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

How the analysis differs from the paper's:

  • The excluded animal of Figures 5 and 6 is not named; every reading (without sub-1, sub-3 or sub-5) is run, and where they disagree sub-5 is assumed, the only reading that also gives the 7 tone sessions from 7 mice of table S3.
  • The clustering gives 11 clusters, not 12, so the code's skipping of cluster 11 in the regional timing does not apply. Leiden's partition changes with its seed and, through the rounding of the float32 PCA, with the number of BLAS threads; BLAS is limited to one thread so that the partition does not depend on the machine, the outcomes are judged on that partition with scanpy's default Leiden seed, and ten seeds and two rounding-level perturbations are shown.
  • Rates are binned from 0 to the last spike of the session, where brainwide-npix uses its own recording length; z-scoring is over that span.
  • The tone epochs are split at the infusion time of the file (criterion) and at the code's ketamine onset, which falls before the infusion, so the code's tone Pre has 9 series, not 10.
  • The driver of the paper's code is not released; the post-series window (from 1 s before the last puff) and other alignments follow the claims file's reading of the plotting code.

What was found about the dataset:

  • Spike times are stored in no particular order within every one of the 9554 units; sorted, they are consistent with the stimulus table (the clock check shows a response at the table's onsets in every session and none at onsets shifted by 1.5 s).
  • The file has tone series in 9 sessions from 8 mice, where table S3 reports 7 from 7, and light stimuli (240 to 248 per session in 9 sessions) that the paper does not describe.
  • The infusion time in the files ("approximate") falls 94 to 420 s after the code's ketamine onset (30 s after the last puff of series 20).
  • 867 units have the region label NA; sub-5_ses-20230706 has two probes and 154 units.
  • No eye closure is in the dandiset; it is in DANDI:001347.
  • In the 9 sessions with tones and lights, light or tone onsets fall in the 30 s before 14 of the 30 puff series (Pre 1 to 20 and Ketamine 23 to 32; tone before series 5, 7, 9 and 29, light before 4, 6, 8, 14, 16, 20, 24, 28, 30 and 32) that the timescale and coupling analyses use, and in two sessions light onsets fall in every odd post-series window. The first puff of series 21 comes 5.0 s after the file's infusion time in every session, and series 23 at least 235 s after it.