Reproducing Li, Namburi, Olson et al. (2022) from dandiset 000689ΒΆ

Paper. Li H, Namburi P, Olson JM, et al. (2022). Neurotensin orchestrates valence assignment in the amygdala. Nature 608, 586-592. doi:10.1038/s41586-022-04964-y. The claims file was written from the accepted author manuscript in PubMed Central (PMC9583860): main text, Methods with the per-panel statistics, and the legends of Figures 1 to 5 and Extended Data Figures 1 to 10.

Dandiset. DANDI:000689, version 0.240530.1923. The paper cites no DANDI version (it predates the dandiset, created in October 2023). There are two published versions, 0.240524.2012 and 0.240530.1923; the data check below shows that both list the same 85 assets with the same asset identifiers, so they do not differ in content. The most recent, 0.240530.1923, is pinned, as the protocol asks when the paper cites none.

This notebook tests the electrophysiology, behavior and NT sensor photometry claims that the files can support, and shows why the others are blocked.

What the dandiset holds. 31 sessions of BLA recordings during the three-cue discrimination task (one per mouse, spike times of 1902 units, trial events, DeepLabCut pose in 29 of them), two pose-only files for other sessions of two of those mice, 32 videos, and 20 photometry sessions of the GRAB-NTS1.0 NT sensor in the BLA (5 mice, early and late acquisition of tone-sucrose and tone-airpuff learning). No file says which mice are CRISPR and which are controls, nor which projection (NAc or CeA) was tagged in each mouse, the units are not the paper's sorted single units, and the pose has no timestamps, so its clock is fitted to the trial table (data check).

Claim Tested here Outcome basis
fig3k-nt-sensor-sucrose-increase yes NT sensor files
fig3l-nt-sensor-airpuff-decrease yes NT sensor files
fig3-nt-sensor-early-dip yes NT sensor files
fig4-recording-count yes units table
fig4c-photoidentification run, blocked, metadata_insufficient light events and units; no fake-block or projection labels
fig4d-seven-clusters run, blocked, methods_underspecified units table
fig5dh-behavior-clusters yes pose, on a clock fitted to the port entries
fig2c, ed8d, ed8fg, fig4e, ed8i, fig4fg (2), fig5a, fig5b, fig5c, ed8e, fig5fi, ed8os blocked, metadata_insufficient no group or projection labels
fig1c, ed1c, ed2, ed3, ed5f, fig1g, fig1h, fig2d, fig2g, fig2h, fig3bc, fig3e, fig3f, ed6, fig3h, ed9, ed9kl, ed10b, ed10 blocked, data_missing experiments not in the dandiset
InΒ [1]:
import time, json, warnings
from concurrent.futures import ThreadPoolExecutor
import numpy as np
import pandas as pd
import requests
import matplotlib.pyplot as plt
from scipy import stats
from scipy.cluster.hierarchy import linkage, fcluster
from scipy.spatial.distance import pdist
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE

import nt_bla as nb

warnings.filterwarnings("ignore", category=RuntimeWarning)
pd.set_option("display.width", 220); pd.set_option("display.max_columns", 40); pd.set_option("display.max_rows", 200)
started = time.time()
A = nb.assets()
print(f"dandiset {nb.DANDISET} version {nb.VERSION}: {len(A)} assets")
dandiset 000689 version 0.240530.1923: 85 assets

Data checkΒΆ

Each check of the metadata against the values is stated under its output.

InΒ [2]:
# The two published versions: same assets?
other = []
url = f"{nb.API}/dandisets/{nb.DANDISET}/versions/0.240524.2012/assets/?page_size=200"
while url:
    r = requests.get(url, timeout=60).json(); other += r["results"]; url = r.get("next")
a1 = {a["path"]: (a["asset_id"], a["size"]) for a in A}
a2 = {a["path"]: (a["asset_id"], a["size"]) for a in other}
print("assets in 0.240530.1923:", len(a1), " in 0.240524.2012:", len(a2),
      " same paths:", set(a1) == set(a2), " same asset ids and sizes:", a1 == a2)
kind = lambda p: ("photometry nwb" if "ophys" in p else "ecephys nwb" if "ecephys" in p and p.endswith(".nwb")
                  else "pose-only nwb" if p.endswith(".nwb") else "video")
inv = pd.DataFrame({"path": list(a1), "GB": [a1[p][1] / 1e9 for p in a1]})
inv["kind"] = inv.path.map(kind)
inv["subject"] = inv.path.str.split("/").str[0]
print(inv.groupby("kind").agg(files=("path", "size"), subjects=("subject", "nunique"), GB=("GB", "sum")).round(1).to_string())
assets in 0.240530.1923: 85  in 0.240524.2012: 85  same paths: True  same asset ids and sizes: True
                files  subjects     GB
kind                                  
ecephys nwb        31        31  534.0
photometry nwb     20         5    0.1
pose-only nwb       2         2    0.6
video              32        30   40.2

The two published versions hold the same assets, so the choice between them cannot change a result. The archive's summary counts 36 subjects: 31 electrophysiology mice and 5 photometry mice.

InΒ [3]:
# Electrophysiology: spike times and trial tables of the 31 sessions (raw voltage is not read).
EP = sorted(a["path"] for a in A if a["path"].endswith(".nwb") and "ecephys" in a["path"])
t = time.time()
with ThreadPoolExecutor(4) as ex:
    E = list(ex.map(nb.load_ephys, EP))
print(f"loaded {len(E)} sessions in {time.time() - t:.0f} s")
rows = []
for e in E:
    tr = e["trials"]
    r = {"subject": e["subject"], "session": e["session_id"][-22:], "genotype": e["genotype"],
         "units": len(e["spikes"]), "channels": e["n_channels"], "dur_s": round(e["duration"])}
    for k, v in nb.CS_TYPES.items():
        r[k] = len(tr[v][0]) if v in tr else 0
    r["sucrose_deliv"] = len(tr.get("reward_delivery", ([],))[0])
    r["shock_relay"] = len(tr.get("shock_relay", ([],))[0])
    r["light"] = len(tr.get("phototagging", ([],))[0])
    cs = np.concatenate([tr[v][0] for v in nb.CS_TYPES.values() if v in tr])
    r["task_end_s"] = round(cs.max()); r["light_start_s"] = round(tr["phototagging"][0].min())
    rows.append(r)
ses = pd.DataFrame(rows)
print(ses.to_string(index=False))
print("\nmice:", ses.subject.nunique(), " sessions:", len(ses), " units:", ses.units.sum(),
      " genotype values:", sorted(ses.genotype.unique()))
print("sessions without neutral CS trials:", list(ses.subject[ses.neutral == 0]))
print("CS counts (sucrose, neutral, shock) in the other sessions:",
      ses[ses.neutral > 0][["sucrose", "neutral", "shock"]].apply(tuple, axis=1).value_counts().to_dict())
loaded 31 sessions in 111 s
subject                session  genotype  units  channels  dur_s  sucrose  neutral  shock  sucrose_deliv  shock_relay  light  task_end_s  light_start_s
    H01 9-07-13-11-59-42-DiscS Wild type     54        32  11888       60       30     30             59           30    854       10916          11130
    H02 9-07-14-11-14-47-DiscS Wild type     64        32  12091       60       30     30             60           30    854       10934          11188
    H07 9-07-13-18-44-41-DiscS Wild type     64        32  12789       60       30     30             60           30    856       10934          12008
    H08 9-07-14-18-11-14-DiscS Wild type     60        32  12048       60       30     30             60           30    854       10951          11221
    H09 9-08-30-16-32-13-Disc4 Wild type     62        32  12224       75        0     45             72           45    854       10986          11089
    H10 9-08-30-16-32-20-Disc4 Wild type     64        32  12221       75        0     45             75           45    854       10980          11089
    H11 9-08-30-12-58-04-Disc4 Wild type     64        32  12271       75        0     45             75           45    854       10974          11498
    H12 9-08-30-20-05-12-Disc4 Wild type     64        32  11847       75        0     45             73           45    854       10982          11088
    H13 9-08-30-09-21-02-Disc4 Wild type     64        32  12261       75        0     45             63           45    854       10969          11391
    H14 9-08-30-09-21-05-Disc4 Wild type     64        32  12261       73        0     45             70           45    854       10966          11392
    H17 1-14-10-52-Disc5-20khz Wild type     64        32  12118       60       30     30             60           30    854       10989          11015
    H19 -17-44-38-Disc5-3,5khz Wild type     64        32  12434       60       30     30             60           30    854       11001          11154
    H22 9-12-27-13-58-44-Disc3 Wild type     64        32  12026       60       30     30             60           30    854       10978          11252
    H23 9-12-29-09-55-50-Disc4 Wild type     64        32  12287       60       30     30             27           31    854       10990          11251
    H24 9-12-27-10-25-11-Disc3 Wild type     46        32  12451       60       30     30             60           30    859       10986          11538
    H25 20-10-06-39-Disc4-3.5k Wild type     58        32  12307       60       30     30             56           31    854       11043          11358
    H27 -20-10-07-01-Disc4-20k Wild type     38        32  12300       60       30     30             60           31    854       11034          11353
    H28 -19-14-27-39-Disc3-20k Wild type     64        32  12114       60       30     30             60           30    854       11025          11238
    H29 -20-13-42-59-Disc4-20k Wild type     64        32  12831       60       30     30             60           30    854       11009          11435
    H31 0-04-30-09-56-31-Disc7 Wild type     64        32  12186       60       30     30             60           30    852       11032              3
    H32 0-04-23-13-59-47-Disc5 Wild type     64        32  11643       60       30     30             60           30    852       11035              3
    H33 0-04-15-12-44-39-Disc5 Wild type     64        32  11638       60       30     30             60           30    854       10988              3
    H34 0-04-15-12-44-32-Disc5 Wild type     64        32  11690       60       30     28             60           30    854       10995             13
    H35 09-57-13-Disc7-8,22,23 Wild type     58        32  12152       60       30     30             60           30    854       10991              2
    H36 0-04-24-12-47-33-Disc6 Wild type     64        32  11617       60       30     30             60           30    854       11001              2
    H37 29-10-04-51-Disc5-9-11 Wild type     58        32  11590       60       30     30             58           30    854       10994              2
    H38 0-04-29-10-04-41-Disc5 Wild type     64        32  11610       60       30     30             60           30    854       11006              2
    H39 0-04-29-13-33-40-Disc5 Wild type     64        32  11535       60       30     30             42           30    854       11012              2
    H40 0-04-29-13-33-54-Disc5 Wild type     64        32  11525       60       30     30             60           30    854       11000              2
    H41 04-29-16-55-26-Disc5-9 Wild type     62        32  11532       59       30     30             60           30    854       10992              2
    H42 04-29-16-55-04-Disc5-5 Wild type     62        32  11558       60       30     30             60           30    854       11016             29

mice: 31  sessions: 31  units: 1902  genotype values: ['Wild type']
sessions without neutral CS trials: ['H09', 'H10', 'H11', 'H12', 'H13', 'H14']
CS counts (sucrose, neutral, shock) in the other sessions: {(60, 30, 30): 23, (60, 30, 28): 1, (59, 30, 30): 1}

Checked against the paper. The session description says sucrose, neutral and shock CSs at 2:1:1, and 25 sessions hold 60, 30 and 30 CSs (some with one or two fewer), as the Methods give (120 trials). Six sessions (H09 to H14, recorded on 2019-08-30, "Disc4") hold 75 sucrose and 45 shock CSs and no neutral CS: the description does not fit them. Every file gives the genotype as "Wild type"; none says whether the mouse is in the CRISPR or the control group, nor whether Cav2-Cre was put in the NAc or the CeA, which the group comparisons and the phototagging need. There are 31 mice where the paper reports 30. H28 and H32 also have a pose-only file for a later discrimination day (Disc4, Disc6) than their recording (Disc3, Disc5), so their recorded session may not be the last day, which is the only day the paper analyses. Each session holds about 850 light events (the phototagging sessions); no "fake" session is labelled.

InΒ [4]:
# Are the units sorted single units? Refractory violations and spikes shared between units.
def coinc(a, b, w=1e-4):
    if len(a) == 0 or len(b) < 2: return 0.0
    j = np.clip(np.searchsorted(b, a), 1, len(b) - 1)
    return float(np.mean(np.minimum(np.abs(b[j] - a), np.abs(b[j - 1] - a)) < w))
def unit_quality(e):
    sp = e["spikes"]
    isi = [np.mean(np.diff(s) < 0.001) if len(s) > 1 else np.nan for s in sp]
    allsp = np.concatenate(sp)
    shared = 1 - len(np.unique(np.round(allsp * 30000).astype(np.int64))) / len(allsp)
    nn = min(len(sp), 16)
    pair = [coinc(sp[i], sp[j]) for i in range(nn) for j in range(nn) if i != j]
    return {"subject": e["subject"], "units": len(sp), "median_frac_isi_lt_1ms": np.nanmedian(isi),
            "units_with_isi_viol_gt_5pct": int(np.sum(np.array(isi) > 0.05)),
            "frac_spike_samples_shared": shared, "median_pair_coincidence_0.1ms": np.median(pair)}
with ThreadPoolExecutor(4) as ex:
    q = pd.DataFrame(list(ex.map(unit_quality, E)))
print(q.round(3).to_string(index=False))
print("\nover all units: median fraction of ISIs below 1 ms = %.3f; units with more than 5%% = %d of %d"
      % (q.median_frac_isi_lt_1ms.median(), q.units_with_isi_viol_gt_5pct.sum(), q.units.sum()))
print("units per session: %s" % q.units.value_counts().sort_index().to_dict())
subject  units  median_frac_isi_lt_1ms  units_with_isi_viol_gt_5pct  frac_spike_samples_shared  median_pair_coincidence_0.1ms
    H01     54                   0.265                           54                      0.201                          0.078
    H02     64                   0.190                           61                      0.101                          0.028
    H07     64                   0.168                           63                      0.136                          0.045
    H08     60                   0.217                           59                      0.168                          0.057
    H09     62                   0.241                           61                      0.202                          0.074
    H10     64                   0.351                           64                      0.267                          0.158
    H11     64                   0.255                           62                      0.178                          0.056
    H12     64                   0.289                           62                      0.194                          0.093
    H13     64                   0.315                           62                      0.216                          0.090
    H14     64                   0.243                           64                      0.408                          0.281
    H17     64                   0.048                           12                      0.652                          0.647
    H19     64                   0.046                           23                      0.166                          0.049
    H22     64                   0.048                           31                      0.102                          0.021
    H23     64                   0.045                           26                      0.102                          0.031
    H24     46                   0.031                           10                      0.191                          0.048
    H25     58                   0.030                            8                      0.257                          0.061
    H27     38                   0.046                           14                      0.449                          0.056
    H28     64                   0.061                           39                      0.076                          0.109
    H29     64                   0.039                           25                      0.141                          0.084
    H31     64                   0.046                           27                      0.130                          0.110
    H32     64                   0.032                           17                      0.114                          0.043
    H33     64                   0.037                           21                      0.238                          0.089
    H34     64                   0.074                           49                      0.133                          0.105
    H35     58                   0.058                           32                      0.137                          0.098
    H36     64                   0.026                           22                      0.105                          0.064
    H37     58                   0.025                           20                      0.057                          0.016
    H38     64                   0.033                           18                      0.116                          0.032
    H39     64                   0.036                           15                      0.267                          0.075
    H40     64                   0.023                           12                      0.081                          0.031
    H41     62                   0.032                            9                      0.479                          0.070
    H42     62                   0.028                           19                      0.081                          0.028

over all units: median fraction of ISIs below 1 ms = 0.046; units with more than 5% = 1061 of 1902
units per session: {38: 1, 46: 1, 54: 1, 58: 3, 60: 1, 62: 3, 64: 21}

Checked against the paper. The Methods say single units were sorted in Offline Sorter, and the paper analyses 685 neurons from 30 mice (about 23 per mouse). The files hold 1902 units, 64 in 21 of 31 sessions (two per channel of the 32-channel optrode). In 1061 units more than 5% of the inter-spike intervals are below 1 ms (a median of 17 to 35% per session in H01 to H14), which a well-isolated single neuron does not show, and in each session 6 to 65% of spike samples recur in another unit. These are not the paper's sorted single units: they look like unsorted or multi-unit threshold crossings. The claims that use units are tested on them as they are, and this is recorded as a finding.

InΒ [5]:
# Every text field of each recording file, the units and electrodes columns, and the archive's
# per-asset metadata: is there any group (CRISPR, control) or projection (NAc, CeA) label?
# Also every object under acquisition/ and processing/, with its type, and for image containers
# the description and the shape of each image.
import re
def contents(h, path):
    objs, images = [], []
    for top in ("acquisition", "processing"):
        for k in h[top]:
            o = h[top][k]
            nt = nb._s(o.attrs.get("neurodata_type", ""))
            objs.append(f"{top}/{k.split(':')[0]} ({nt})")
            if nt == "ProcessingModule":
                objs += [f"{top}/{k}/{j} ({nb._s(o[j].attrs.get('neurodata_type', ''))})" for j in o]
            if nt == "Images":
                images.append({"subject": path.split("/")[0][4:], "container": k,
                               "description": nb._s(o.attrs.get("description", ""))[:110],
                               "n_images": sum(hasattr(o[j], "shape") for j in o),
                               "first images": "; ".join(f"{j} {tuple(o[j].shape)}" for j in list(o)[:2] if hasattr(o[j], "shape"))})
    return objs, images

def text_fields(path):
    h = nb.open_h5(path)
    out = {}
    def visit(name, obj):
        if isinstance(obj, nb.h5py.Dataset) and obj.dtype.kind in "OSU" and obj.size <= 64 and not name.startswith(("units/", "intervals/", "acquisition/", "processing/")):
            v = obj[()]
            out[name] = " | ".join(str(nb._s(x)) for x in np.atleast_1d(v) if isinstance(nb._s(x), str))
    h["general"].visititems(lambda n, o: visit("general/" + n, o))
    out["session_description"] = nb._s(h["session_description"][()])
    out["units columns"] = ", ".join(sorted(h["units"].keys()))
    out["electrodes columns"] = ", ".join(sorted(h["general/extracellular_ephys/electrodes"].keys()))
    out["electrode locations"] = ", ".join(sorted(set(nb._s(x) for x in h["general/extracellular_ephys/electrodes/location"][:])))
    return out, contents(h, path)
with ThreadPoolExecutor(4) as ex:
    TF, CT = zip(*ex.map(text_fields, EP))
fields = sorted(set().union(*TF))
for f in fields:
    vals = sorted(set(t.get(f, "(absent)") for t in TF))
    shown = vals if f != "general/experiment_description" else [v[:80] + "..." for v in vals]
    print(f"{f}: {len(vals)} distinct value(s): {shown[:6]}{' ...' if len(vals) > 6 else ''}")
pat = re.compile(r"crispr|control|scrambl|sgrna|guide|nac|accumbens|cea|central amyg|cav2|projection|group", re.I)
hits = sorted({(f, m.group(0)) for t in TF for f, v in t.items() if f != "general/experiment_description" for m in pat.finditer(v)})
print("\nlabel words in the files' fields (experiment_description, the paper's abstract, left out):", hits)
def asset_subject(a):
    r = requests.get(f"{nb.API}/dandisets/{nb.DANDISET}/versions/{nb.VERSION}/assets/{a['asset_id']}/", timeout=60).json()
    return {k: str(v) for k, v in r.get("wasAttributedTo", [{}])[0].items() if k not in ("identifier",)}
with ThreadPoolExecutor(8) as ex:
    am = pd.DataFrame(list(ex.map(asset_subject, [a for a in A if a["path"] in EP])))
print("\narchive asset metadata, subject fields:", list(am.columns))
print({c: sorted(am[c].unique().tolist())[:4] for c in am.columns if c not in ("age",)})

counts = pd.Series([x for objs, _ in CT for x in objs]).value_counts()
print("\nobjects under acquisition/ and processing/ (number of the 31 files holding each):")
print(counts.to_string())
imgs = pd.DataFrame([r for _, im in CT for r in im])
print(f"\nimage containers: {len(imgs)} in {imgs.subject.nunique()} files; files without: {[e['subject'] for e, (_, im) in zip(E, CT) if not im]}")
print(imgs.to_string(index=False, max_colwidth=110))
print("image names in all containers:", sorted({re.sub(r"\d+", "#", n.split(" ")[0]) for s in imgs["first images"] for n in s.split("; ")}))
electrode locations: 1 distinct value(s): ['unknown']
electrodes columns: 1 distinct value(s): ['channel_name, gain_to_uV, group, group_name, id, location, offset_to_uV']
general/experiment_description: 1 distinct value(s): ['The ability to associate temporally segregated information and assign positive o...']
general/experimenter: 1 distinct value(s): ['Li, Hao']
general/extracellular_ephys/electrodes/channel_name: 1 distinct value(s): ['CH1 | CH2 | CH3 | CH4 | CH5 | CH6 | CH7 | CH8 | CH9 | CH10 | CH11 | CH12 | CH13 | CH14 | CH15 | CH16 | CH17 | CH18 | CH19 | CH20 | CH21 | CH22 | CH23 | CH24 | CH25 | CH26 | CH27 | CH28 | CH29 | CH30 | CH31 | CH32']
general/extracellular_ephys/electrodes/group: 1 distinct value(s): ['']
general/extracellular_ephys/electrodes/group_name: 1 distinct value(s): ['ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup | ElectrodeGroup']
general/extracellular_ephys/electrodes/location: 1 distinct value(s): ['unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown | unknown']
general/institution: 1 distinct value(s): ['Salk Institute for Biological Studies']
general/lab: 1 distinct value(s): ['Tye']
general/related_publications: 2 distinct value(s): ['doi: 10.1038/s41586-022-04964-y', 'https://doi.org/10.1038/s41586-022-04964-y']
general/session_id: 31 distinct value(s): ['1-2019-07-13-11-59-42-DiscS', '10-2019-08-30-16-32-20-Disc4', '11-2019-08-30-12-58-04-Disc4', '12-2019-08-30-20-05-12-Disc4', '13-2019-08-30-09-21-02-Disc4', '14-2019-08-30-09-21-05-Disc4'] ...
general/subject/age: 2 distinct value(s): ['P109D', 'P60D']
general/subject/genotype: 1 distinct value(s): ['Wild type']
general/subject/sex: 1 distinct value(s): ['M']
general/subject/species: 1 distinct value(s): ['Mus musculus']
general/subject/strain: 1 distinct value(s): ['C57BL/6J']
general/subject/subject_id: 31 distinct value(s): ['H01', 'H02', 'H07', 'H08', 'H09', 'H10'] ...
session_description: 2 distinct value(s): ['Auto-generated by neuroconv', 'Discrimination task in which three types of trials with distinct auditory tones predicting sucrose, shock or no outcome (sucrose, shock and neutral conditioned stimulus (CS), respectively) were presented randomly at a 2:1:1 ratio']
units columns: 1 distinct value(s): ['id, spike_times, spike_times_index, unit_name']

label words in the files' fields (experiment_description, the paper's abstract, left out): [('electrodes columns', 'group'), ('general/extracellular_ephys/electrodes/group_name', 'Group')]
archive asset metadata, subject fields: ['age', 'sex', 'species', 'genotype', 'schemaKey']
{'sex': ["{'name': 'Male', 'schemaKey': 'SexType', 'identifier': 'http://purl.obolibrary.org/obo/PATO_0000384'}"], 'species': ["{'name': 'Mus musculus - House mouse', 'schemaKey': 'SpeciesType', 'identifier': 'http://purl.obolibrary.org/obo/NCBITaxon_10090'}"], 'genotype': ['Wild type'], 'schemaKey': ['Participant']}

objects under acquisition/ and processing/ (number of the 31 files holding each):
acquisition/ElectricalSeries (ElectricalSeries)        31
acquisition/Video (ImageSeries)                        30
processing/behavior (ProcessingModule)                 29
processing/behavior/PoseEstimation (PoseEstimation)    29
acquisition/COFAImages (Images)                        27

image containers: 27 in 27 files; files without: ['H01', 'H02', 'H07', 'H08']
subject  container                                                                                                    description  n_images                                                              first images
    H09 COFAImages                        The confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H9_20x_PVT.oif.         8    GrayScaleImage1Depth1 (2420, 1992); GrayScaleImage1Depth2 (2420, 1992)
    H10 COFAImages                     The 0 confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H10_20x_PVT.oif.         8    GrayScaleImage1Depth1 (2418, 2440); GrayScaleImage1Depth2 (2418, 2440)
    H11 COFAImages                     The 1 confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H11_20x_PVT.oif.         8    GrayScaleImage1Depth1 (2866, 1988); GrayScaleImage1Depth2 (2866, 1988)
    H12 COFAImages The 2 confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H12_slidePVT3_slice3_zstack_40x_PVT.        12 GrayScaleImage1Composite (3764, 2448); GrayScaleImage1Depth1 (3764, 2448)
    H13 COFAImages The 3 confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H13_slidePVT4_slice6_zstack_40x_PVT.        18 GrayScaleImage1Composite (2418, 2900); GrayScaleImage1Depth1 (2418, 2900)
    H14 COFAImages The 4 confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H14_slidePVT4_slice6_zstack_40x_PVT.        12    GrayScaleImage1Depth1 (3764, 2896); GrayScaleImage1Depth2 (3764, 2896)
    H17 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H17PVT_40x.oif.        42 GrayScaleImage1Composite (3328, 3786); GrayScaleImage1Depth1 (3328, 3786)
    H19 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H19PVT_40x.oif.        27 GrayScaleImage1Composite (3772, 3348); GrayScaleImage1Depth1 (3772, 3348)
    H22 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H22PVT_40x.oif.        15 GrayScaleImage1Composite (3772, 3338); GrayScaleImage1Depth1 (3772, 3338)
    H23 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H23PVT_40x.oif.        18 GrayScaleImage1Composite (3764, 3338); GrayScaleImage1Depth1 (3764, 3338)
    H24 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H24PVT_40x.oif.        15 GrayScaleImage1Composite (3766, 3798); GrayScaleImage1Depth1 (3766, 3798)
    H25 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H25PVT_40x.oif.        27 GrayScaleImage1Composite (3772, 3784); GrayScaleImage1Depth1 (3772, 3784)
    H27 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H27PVT_40x.oif.        32 GrayScaleImage1Composite (4660, 4254); GrayScaleImage1Depth1 (4660, 4254)
    H28 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H28PVT_40x.oif.        28 GrayScaleImage1Composite (3774, 3782); GrayScaleImage1Depth1 (3774, 3782)
    H29 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H29PVT_40x.oif.        32 GrayScaleImage1Composite (3766, 3338); GrayScaleImage1Depth1 (3766, 3338)
    H31 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H31PVT_40x.oif.        28 GrayScaleImage1Composite (3316, 3340); GrayScaleImage1Depth1 (3316, 3340)
    H32 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H32PVT_40x.oif.        32 GrayScaleImage1Composite (3770, 2888); GrayScaleImage1Depth1 (3770, 2888)
    H33 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H33PVT_40x.oif.        40 GrayScaleImage1Composite (3778, 2894); GrayScaleImage1Depth1 (3778, 2894)
    H34 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H34PVT_40x.oif.        24 GrayScaleImage1Composite (3320, 3340); GrayScaleImage1Depth1 (3320, 3340)
    H35 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H35PVT_40x.oif.        32 GrayScaleImage1Composite (3318, 2890); GrayScaleImage1Depth1 (3318, 2890)
    H36 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H36PVT_40x.oif.        24 GrayScaleImage1Composite (3318, 2444); GrayScaleImage1Depth1 (3318, 2444)
    H37 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H37PVT_40x.oif.        32 GrayScaleImage1Composite (4224, 4224); GrayScaleImage1Depth1 (4224, 4224)
    H38 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H38PVT_40x.oif.        24 GrayScaleImage1Composite (3766, 3786); GrayScaleImage1Depth1 (3766, 3786)
    H39 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H39PVT_40x.oif.        24 GrayScaleImage1Composite (3766, 3790); GrayScaleImage1Depth1 (3766, 3790)
    H40 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H40PVT_40x.oif.        24 GrayScaleImage1Composite (3766, 3782); GrayScaleImage1Depth1 (3766, 3782)
    H41 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H41PVT_40x.oif.        28 GrayScaleImage1Composite (2880, 3330); GrayScaleImage1Depth1 (2880, 3330)
    H42 COFAImages                    The PVT confocal images from FLUOVIEW FV1000 version 1.2.6.0 extracted from H42PVT_40x.oif.        24 GrayScaleImage1Composite (3320, 3788); GrayScaleImage1Depth1 (3320, 3788)
image names in all containers: ['GrayScaleImage#Composite', 'GrayScaleImage#Depth#']

No field of the files, and no subject field of the archive's metadata, names a CRISPR or control group, a guide RNA, or the NAc or CeA (the words found, if any, are listed above with their field). The units table holds only spike times and a unit name; the electrodes table gives no location.

27 of the 31 files (all but H01, H02, H07 and H08) also hold a stack of confocal images (COFAImages, 8 to 42 images each), whose description names an Olympus FLUOVIEW file of the PVT (for example H17PVT_40x.oif). The images are named only by depth (GrayScaleImage1Depth#, and a composite), with no channel, probe or group label.

InΒ [6]:
# Clock check for the spikes and the trial table: CS-evoked change in population rate at the
# true CS onsets against the same onsets shifted 10 s earlier (inside the inter-trial interval).
def pop_resp(e, shift):
    on = np.concatenate([e["trials"][v][0] for v in nb.CS_TYPES.values() if v in e["trials"]]) + shift
    out = []
    for sp in e["spikes"]:
        p = nb.psth(sp, on, 1.0, 1.0, 0.1)
        b = p[:10].mean()
        out.append((p[10:13].mean() - b) / (p[:10].std(ddof=1) + 1e-9))
    return np.median(np.abs(out))
ck = pd.DataFrame([{"subject": e["subject"], "true_onsets": pop_resp(e, 0.0), "shifted_-10s": pop_resp(e, -10.0)} for e in E])
print("median |z| of the 0 to 0.3 s response across units, per session:")
print(ck.round(2).to_string(index=False))
print("sessions where the true onsets give the larger response: %d of %d" % ((ck.true_onsets > ck["shifted_-10s"]).sum(), len(ck)))
median |z| of the 0 to 0.3 s response across units, per session:
subject  true_onsets  shifted_-10s
    H01         8.25          0.41
    H02         2.92          0.50
    H07         1.94          0.50
    H08         1.16          0.49
    H09         3.75          1.39
    H10         7.76          0.58
    H11         1.99          0.93
    H12         5.99          0.44
    H13         2.12          0.55
    H14         1.01          0.50
    H17         0.00          0.00
    H19         4.70          0.42
    H22         0.50          0.65
    H23         1.66          0.67
    H24         1.05          0.48
    H25         0.71          0.60
    H27         0.98          1.02
    H28         0.63          0.58
    H29         0.96          0.60
    H31         2.17          0.34
    H32         3.53          0.47
    H33         1.89          0.51
    H34         0.41          0.46
    H35         4.36          0.63
    H36         2.28          0.51
    H37         1.18          0.52
    H38         1.86          0.42
    H39         0.48          0.51
    H40         2.08          0.42
    H41         1.16          0.70
    H42         2.16          0.99
sessions where the true onsets give the larger response: 26 of 31

The units respond more at the true CS onsets than at onsets shifted into the inter-trial interval in the sessions counted above, which is consistent with the spikes and the trial table sharing a clock (a check that supports, without proving, the alignment). It fails in H17, whose spike times end at 249 s of a 12118 s recording (next cell): its units cannot be aligned with its trials. In H22, H27, H34 and H39 the two values are close, so the alignment is not shown there.

InΒ [7]:
# Span of the spike times against the length of the recording, per session.
span = pd.DataFrame([{"subject": e["subject"], "first_spike_s": round(min(s[0] for s in e["spikes"] if len(s)), 1),
                      "last_spike_s": round(max(s[-1] for s in e["spikes"] if len(s)), 1), "recording_s": round(e["duration"], 1),
                      "task_start_s": round(min(e["trials"][v][0].min() for v in nb.CS_TYPES.values() if v in e["trials"]), 1),
                      "task_end_s": round(max(e["trials"][v][0].max() for v in nb.CS_TYPES.values() if v in e["trials"]), 1),
                      "light_before_task": int(np.sum(e["trials"]["phototagging"][0] < min(e["trials"][v][0].min() for v in nb.CS_TYPES.values() if v in e["trials"]))),
                      "light_after_task": int(np.sum(e["trials"]["phototagging"][0] > max(e["trials"][v][0].max() for v in nb.CS_TYPES.values() if v in e["trials"])))} for e in E])
print(span.to_string(index=False))
print("sessions whose spikes stop before 90% of the recording:", list(span.subject[span.last_spike_s < 0.9 * span.recording_s]))
print("sessions with light events before the task:", list(span.subject[span.light_before_task > 0]))
# Light events form two blocks per session: split at the largest gap between events.
for e in E[:3] + E[-3:]:
    x = e["trials"]["phototagging"][0]; g = np.argmax(np.diff(x))
    print(f'{e["subject"]}: blocks of {g + 1} and {len(x) - g - 1} light events, gap {np.diff(x)[g]:.0f} s')
subject  first_spike_s  last_spike_s  recording_s  task_start_s  task_end_s  light_before_task  light_after_task
    H01            0.0       11887.6      11887.8         216.4     10916.4                  0               854
    H02            0.0       12090.3      12090.6         233.8     10933.8                  0               854
    H07            0.0       12788.0      12788.5         233.8     10933.8                  0               856
    H08            0.0       12047.7      12048.2         251.1     10951.1                  0               854
    H09            0.0       12220.5      12223.7         285.6     10985.7                  0               854
    H10            0.0       12213.5      12220.9         279.5     10979.5                  0               854
    H11            0.0       12270.6      12270.7         273.5     10973.5                  0               854
    H12            0.0       11846.3      11846.5         282.4     10982.4                  0               854
    H13            0.0       12249.0      12261.5         269.0     10969.0                  0               854
    H14            0.0       12259.5      12261.4         266.1     10966.1                  0               854
    H17            0.0         249.2      12117.8         289.2     10989.3                  0               854
    H19            0.0       11969.4      12434.4         301.0     11001.1                  0               854
    H22            0.0       12026.1      12026.2         278.2     10978.3                  0               854
    H23            0.0       12285.7      12286.8         290.1     10990.2                  0               854
    H24            0.0       12451.0      12451.5         285.8     10985.8                  0               859
    H25            0.0       12255.5      12306.8         342.6     11042.6                  0               854
    H27            0.0       12249.9      12300.0         333.5     11033.5                  0               854
    H28            0.0       11993.3      12113.8         325.1     11025.1                  0               854
    H29            0.1       12468.7      12831.4         308.9     11009.0                  0               854
    H31            0.1       12165.5      12186.5         332.0     11032.0                386               426
    H32            0.0       11594.4      11643.5         335.3     11035.3                390               426
    H33            0.0       11624.3      11637.6         288.1     10988.1                353               426
    H34            0.0       11682.6      11690.5         294.9     10994.9                349               426
    H35            0.0       12126.4      12152.4         290.5     10990.5                356               426
    H36            0.0       11552.1      11616.6         300.5     11000.6                366               426
    H37            0.0       11585.3      11590.0         294.1     10994.2                360               426
    H38            0.0       11599.5      11609.6         306.0     11006.0                368               426
    H39            0.0       11431.7      11534.6         311.9     11012.0                368               426
    H40            0.0       11421.8      11524.5         299.7     10999.8                366               426
    H41            0.0       11510.6      11531.7         291.7     10991.7                357               426
    H42            0.0       11536.6      11558.0         315.5     11015.5                354               426
sessions whose spikes stop before 90% of the recording: ['H17']
sessions with light events before the task: ['H31', 'H32', 'H33', 'H34', 'H35', 'H36', 'H37', 'H38', 'H39', 'H40', 'H41', 'H42']
H01: blocks of 428 and 426 light events, gap 48 s
H02: blocks of 428 and 426 light events, gap 69 s
H07: blocks of 430 and 426 light events, gap 60 s
H40: blocks of 428 and 426 light events, gap 10714 s
H41: blocks of 428 and 426 light events, gap 10803 s
H42: blocks of 428 and 426 light events, gap 10801 s

H17's spike times all fall within the first 249 s of a recording of 12118 s, while its trial events cover the whole session: its spike times are not on the trial clock (a file error). Every session holds about 854 light events in two blocks of about 427, which fits the Methods' two light sessions (a "fake" one with the patch cord disconnected, then the real one), but no label says which block is the fake session. In 12 sessions (H31 to H42) one block comes before the task and one after it; the Methods put both after the task.

InΒ [8]:
# Does the pose share the trial clock? The pose series give only a start time (0 s) and a nominal
# 15 frames/s, no timestamps. At each port entry (beam break, trial table) the head should be at the
# port. (1) With the nominal clock, the fixed lag (pose time minus trial time) that minimises the
# median head-to-port distance at port entries, in each third of the session. (2) A linear map,
# pose time = T + offset + factor * (t - T), fitted to all port entries (nb.fit_pose_clock), and
# the best extra lag left in each third under it (0 when the map holds throughout).
def pose_clock(p):
    h = nb.open_h5(p)
    subj = p.split("/")[0][4:]
    if "processing/behavior/PoseEstimation" not in h:
        return subj, None
    pose = nb.load_pose(h); r, t0 = pose["rate"], pose["t0"]
    m = nb.pose_measures(pose)
    dist = m["distance_to_port"]
    tt = h["intervals/trials/trial_type"][:].astype(str); st = h["intervals/trials/start_time"][:]
    pe = np.sort(st[tt == "port_entry"])
    out = {"subject": subj, "pose_s": round(len(dist) / r), "port_entries": len(pe)}
    lags = np.arange(-200, 200.01, 0.2)
    for k, seg in enumerate(np.array_split(pe, 3)):
        med = nb._median_at(dist, r, t0, seg[None, :] + lags[:, None])
        out[f"lag{k + 1}_s"] = round(float(lags[np.nanargmin(med)]), 1)
    c = nb.fit_pose_clock(dist, r, t0, pe)
    out.update({"fit_offset_s": round(c["offset"], 1), "fit_factor": round(c["factor"], 4),
                "resid1_s": c["resid_lag_thirds"][0], "resid2_s": c["resid_lag_thirds"][1], "resid3_s": c["resid_lag_thirds"][2],
                "dist_fit": round(c["med_dist_fit"], 1), "dist_lag0": round(c["med_dist_lag0"], 1), "dist_all_frames": round(c["med_dist_all"], 1)})
    return subj, (out, {"measures": m, "rate": r, "t0": t0, "clock": c})
t = time.time()
with ThreadPoolExecutor(4) as ex:
    PC = dict(ex.map(pose_clock, EP))
pl = pd.DataFrame([v[0] for v in PC.values() if v is not None])
print(f"pose read and clocks fitted in {time.time() - t:.0f} s; sessions without pose: {[k for k, v in PC.items() if v is None]}")
print(pl.round(1).assign(fit_factor=pl.fit_factor.round(4)).to_string(index=False))
spread = pl[["lag1_s", "lag2_s", "lag3_s"]].max(axis=1) - pl[["lag1_s", "lag2_s", "lag3_s"]].min(axis=1)
resid = pl[["resid1_s", "resid2_s", "resid3_s"]].abs().max(axis=1)
pl["aligned"] = resid <= 1.0
print("\nnominal clock: median |lag| %.1f s; sessions whose best lag changes by more than 5 s within the session: %d of %d"
      % (np.median(np.abs(pl[["lag1_s", "lag2_s", "lag3_s"]].values)), int((spread > 5).sum()), len(pl)))
print("linear map: sessions with every third within 1 s of the map: %d of %d; not: %s"
      % (pl.aligned.sum(), len(pl), list(pl.subject[~pl.aligned])))
print("fitted factor in the aligned sessions: median %.4f, range %.4f to %.4f; median distance at port entries %.1f px under the map, %.1f px at lag 0, %.1f px over all frames"
      % (pl.fit_factor[pl.aligned].median(), pl.fit_factor[pl.aligned].min(), pl.fit_factor[pl.aligned].max(),
         pl.dist_fit[pl.aligned].median(), pl.dist_lag0[pl.aligned].median(), pl.dist_all_frames[pl.aligned].median()))
print("aligned sessions with a factor between 0.987 and 0.989: %d of %d" % (pl.fit_factor[pl.aligned].between(0.987, 0.989).sum(), pl.aligned.sum()))
pose read and clocks fitted in 157 s; sessions without pose: ['H28', 'H32']
subject  pose_s  port_entries  lag1_s  lag2_s  lag3_s  fit_offset_s  fit_factor  resid1_s  resid2_s  resid3_s  dist_fit  dist_lag0  dist_all_frames
    H01   11024           619    -6.0   -31.4   -77.0         -28.2      0.9876       0.2      -0.0      -0.2      28.3       51.6             63.9
    H02   11079           520   -27.8   -41.8  -122.8         -58.7      0.9875      -0.0      -0.0       0.4      39.3      100.3            105.1
    H07   11140           554    -8.8    33.6  -106.8         -55.8      0.9875      -0.2      -0.0       0.2      36.8       93.1             94.1
    H08   11092          1640   -37.6   -73.4  -114.4         -73.7      0.9877      -0.0      -0.0      -0.0      29.0       34.7             35.1
    H09   10982           885   -40.2   -83.8  -119.2         -73.7      0.9874      -0.0      -0.0       0.2      36.4       87.3             86.5
    H10   11042          1640   -30.2   -82.8  -105.4         -82.9      0.9875      -0.0      -0.0      -0.0      27.7       31.8             34.8
    H11   10947          2171   177.0  -192.2   148.6         243.3      1.0030       3.4      19.2      -0.0      37.6       39.1             39.2
    H12   11151          1597    47.0     4.2   -35.4           2.7      0.9877       0.2      -0.0      -0.2      24.6       36.2             39.0
    H13   11274           576     2.4   -65.4   -62.4         -56.4      0.9876      -0.0      -0.0      -0.0      31.1       53.7             62.4
    H14   11272           854   175.2   168.2   -80.2         292.7      0.9590       0.6      -2.2     -11.6      24.5       29.7             34.9
    H17   10891          1914   -32.8   -75.2  -104.6         -75.3      0.9876      -0.0       3.6      -0.0      32.2       38.7             40.0
    H19   11007          1456   -40.8   -79.6  -127.4         -83.9      0.9876      -0.4      -0.0      -0.0      36.1       67.2             89.6
    H22   10963          1328   -30.0   -61.6  -114.0         -62.8      0.9883      -0.0      -0.0      -0.2      43.7      105.7            114.2
    H23   11117           125    79.2   136.2   -80.8         169.8      0.9703      -1.6      -2.0     -11.6      37.8       41.9             49.3
    H24   11418          1558   186.8   -51.4  -102.0         -69.3      0.9881      -0.0      -0.0      -0.0      38.4       40.4             46.8
    H25   11345           708    33.4   -52.0    10.2         -49.7      1.0109     -14.8      -0.0       5.0      46.4       50.3             97.2
    H27   11345          1614   -15.4   -15.4   -15.4         -15.5      1.0000      -0.0      -0.0      -0.0      45.2       80.8             96.0
    H29   11327           714    84.8   127.4   -83.0         151.5      0.9655      -9.8      -0.0      -0.6     151.4      206.2            212.0
    H31   11586          1082   -88.2  -137.8  -158.4        -141.2      0.9884      -0.2      -0.0       0.2      40.7      136.7            187.4
    H33   11028           580   -37.2   -54.4  -192.4         -64.8      0.9884      -0.2      -0.0      -0.0      44.5      137.2            137.2
    H34   11027          1582   -47.0  -103.0  -111.2         -83.4      0.9885       0.4      -0.0       0.4      36.9      158.8            149.0
    H35   11586           646  -106.8   -58.2  -105.6         -71.3      0.9884      -0.0      -0.2      -0.0      41.9      232.6            181.3
    H36   10987          1394   -37.4   -71.8  -132.0         -81.3      0.9885      -0.0      -0.0       0.2      39.8      173.1            179.6
    H37   11062          1876   -41.4   -68.0  -112.6         -74.3      0.9884      -0.2      -0.0      -0.0      39.6      113.3            137.0
    H38   11060           899   -55.8   -87.6  -136.6         -93.6      0.9885       0.4      -0.0      -0.0      38.3      209.4            223.0
    H39   10909           606  -171.0   -89.2   -27.4        -129.7      0.9721      -0.6      -2.6     -14.0     154.9      226.4            220.9
    H40   10913          1226  -102.6   -79.4   186.0         207.1      0.9939      -0.2      -0.8       2.8     166.3      203.4            221.3
    H41   10946           686   -50.6  -106.2  -126.4         -92.8      0.9884      -0.2      -0.0      -0.2      39.7      204.9            205.9
    H42   10939          1065   -81.8  -104.4  -131.0        -110.4      0.9884      -0.0      -0.2      -0.0      35.8      198.5            180.9

nominal clock: median |lag| 80.2 s; sessions whose best lag changes by more than 5 s within the session: 28 of 29
linear map: sessions with every third within 1 s of the map: 21 of 29; not: ['H11', 'H14', 'H17', 'H23', 'H25', 'H29', 'H39', 'H40']
fitted factor in the aligned sessions: median 0.9883, range 0.9874 to 1.0000; median distance at port entries 38.3 px under the map, 100.3 px at lag 0, 105.1 px over all frames
aligned sessions with a factor between 0.987 and 0.989: 20 of 21
InΒ [9]:
# The video assets: frame count, frame rate and duration from each file's container header
# (streamed, no frame is decoded), against the pose series and the electrophysiology recording.
import av
def video_info(a):
    out = {"subject": a["path"].split("/")[0][4:], "file": a["path"].split("/")[1][:40], "container": a["path"][-3:]}
    try:
        rf = nb.RangeFile(f"{nb.API}/assets/{a['asset_id']}/download/")
        f = av.open(rf)
        v = f.streams.video[0]
        out.update({"frames": v.frames, "rate": float(v.average_rate), "duration_s": round(f.duration / 1e6, 1) if f.duration else np.nan,
                    "MB_read": round(rf.fetched / 1e6, 1)})
        f.close()
    except Exception as ex:
        out["error"] = str(ex)[:60]
    return out
VID = [a for a in A if not a["path"].endswith(".nwb")]
with ThreadPoolExecutor(8) as ex:
    vi = pd.DataFrame(list(ex.map(video_info, VID)))
rec = {e["subject"]: e["duration"] for e in E}
vi["recording_s"] = vi.subject.map(rec).round(1)
vi["pose_s"] = vi.subject.map(dict(zip(pl.subject, pl.pose_s)))
print(vi.sort_values("subject").to_string(index=False))
print("mkv files report no frame count in their header (0); their rate is the container's nominal one.")
subject                                     file container  frames  rate  duration_s  MB_read  recording_s  pose_s
    H01 sub-H01_ses-1-2019-07-13-11-59-42-DiscS_       mp4  165366  15.0     11024.4      2.0      11887.8 11024.0
    H02 sub-H02_ses-2-2019-07-14-11-14-47-DiscS_       mp4  166185  15.0     11079.0      1.9      12090.6 11079.0
    H07 sub-H07_ses-7-2019-07-13-18-44-41-DiscS_       mp4  179366  15.0     11957.7      2.1      12788.5 11140.0
    H08 sub-H08_ses-8-2019-07-14-18-11-14-DiscS_       mp4  166376  15.0     11091.7      2.0      12048.2 11092.0
    H09 sub-H09_ses-9-2019-08-30-16-32-13-Disc4_       mp4  187658  15.0     12510.5      2.0      12223.7 10982.0
    H10 sub-H10_ses-10-2019-08-30-16-32-20-Disc4       mp4  187638  15.0     12509.2      2.2      12220.9 11042.0
    H11 sub-H11_ses-11-2019-08-30-12-58-04-Disc4       mp4  170599  15.0     11373.3      1.8      12270.7 10947.0
    H13 sub-H13_ses-13-2019-08-30-09-21-02-Disc4       mp4  169115  15.0     11274.3      1.8      12261.5 11274.0
    H14 sub-H14_ses-14-2019-08-30-09-21-05-Disc4       mp4  169078  15.0     11271.9      2.0      12261.4 11272.0
    H17 sub-H17_ses-H17-2019-10-31-14-10-52-Disc       mp4  163360  15.0     10890.7      1.8      12117.8 10891.0
    H19 sub-H19_ses-H19-2019-10-31-17-44-38-Disc       mp4  165108  15.0     11007.2      1.9      12434.4 11007.0
    H22 sub-H22_ses-H22-2019-12-27-13-58-44-Disc       mp4  164449  15.0     10963.3      1.2      12026.2 10963.0
    H23 sub-H23_ses-H23-2019-12-29-09-55-50-Disc       mp4  166763  15.0     11117.5      1.4      12286.8 11117.0
    H24 sub-H24_ses-H24-2019-12-27-10-25-11-Disc       mp4  171278  15.0     11418.5      1.4      12451.5 11418.0
    H25 sub-H25_ses-H25-2020-02-20-10-06-39-Disc       mkv       0  30.0     11344.5      0.1      12306.8 11345.0
    H27 sub-H27_ses-H27-2020-02-20-10-07-01-Disc       mkv       0  30.0     11344.6      0.1      12300.0 11345.0
    H28 sub-H28_ses-H28-2020-02-19-14-27-39-Disc       mkv       0  30.0     11240.3      0.1      12113.8     NaN
    H28 sub-H28_ses-28-Disc4DLC-resnet50-Hao-Med       mkv       0  30.0     11327.0      0.1      12113.8     NaN
    H29 sub-H29_ses-H29-2020-02-20-13-42-59-Disc       mkv       0  30.0     11326.6      0.1      12831.4 11327.0
    H31 sub-H31_ses-H31-2020-04-30-09-56-31-Disc       mp4  173857  15.0     11590.5      1.3      12186.5 11586.0
    H32 sub-H32_ses-32-Disc6DLC-resnet50-Hao-Med       mp4  165802  15.0     11053.5      1.1      11643.5     NaN
    H32 sub-H32_ses-H32-2020-04-23-13-59-47-Disc       mp4  165880  15.0     11058.7      1.3      11643.5     NaN
    H33 sub-H33_ses-H33-2020-04-15-12-44-39-Disc       mp4  165486  15.0     11032.4      1.2      11637.6 11028.0
    H34 sub-H34_ses-H34-2020-04-15-12-44-32-Disc       mp4  165492  15.0     11032.8      1.2      11690.5 11027.0
    H35 sub-H35_ses-H35-2020-04-30-09-57-13-Disc       mp4  173882  15.0     11592.1      1.2      12152.4 11586.0
    H36 sub-H36_ses-H36-2020-04-24-12-47-33-Disc       mp4  164916  15.0     10994.4      1.1      11616.6 10987.0
    H37 sub-H37_ses-H37-2020-04-29-10-04-51-Disc       mp4  165971  15.0     11064.7      1.2      11590.0 11062.0
    H38 sub-H38_ses-H38-2020-04-29-10-04-41-Disc       mp4  165979  15.0     11065.3      1.2      11609.6 11060.0
    H39 sub-H39_ses-H39-2020-04-29-13-33-40-Disc       mp4  163737  15.0     10915.8      1.1      11534.6 10909.0
    H40 sub-H40_ses-H40-2020-04-29-13-33-54-Disc       mp4  163736  15.0     10915.7      1.4      11524.5 10913.0
    H41 sub-H41_ses-H41-2020-04-29-16-55-26-Disc       mp4  164227  15.0     10948.5      1.4      11531.7 10946.0
    H42 sub-H42_ses-H42-2020-04-29-16-55-04-Disc       mp4  164228  15.0     10948.5      1.1      11558.0 10939.0
mkv files report no frame count in their header (0); their rate is the container's nominal one.

With the nominal clock (15 frames per second from 0 s) the head is closest to the port up to 192 s away from the port entries (median 80 s), and the best lag drifts within the session in 28 of 29 sessions (columns lag1 to lag3). A linear map with an offset and a rate factor removes the drift in 21 of the 29 sessions: under it, the best extra lag in each third is within 1 s of zero, 20 of those 21 fitted factors lie between 0.9874 and 0.9885 (H27 is at 1.0000), and the median head-to-port distance at port entries is 38 pixels against 105 over all frames. A common factor of about 0.988 is what a video whose true frame rate is about 1.2% below the nominal 15 per second would give. In H11, H14, H17, H23, H25, H29, H39 and H40 the map leaves a lag of 2.8 to 19.2 s in some third, or the distance stays near that over all frames (H29, H39, H40).

The video headers (next output) give a constant 15 frames per second for the mp4 files and durations within 10 s of the pose series, except H07, H09 and H10, whose videos run 818 to 1528 s longer than their pose, and H11, 426 s longer; the mkv files give no frame count. So the drift is not visible in the files' own metadata; the original videos were .asf files (PoseEstimation original_videos), whose timing is lost. The pose can be put on the trial clock in the 21 aligned sessions, by a map estimated from the data, not given by the files. This is an assumption for the behavior claim, which uses only those sessions. The map is consistent with the data, not shown by them.

InΒ [10]:
# Photometry: the 20 NT sensor sessions.
PP = sorted(a["path"] for a in A if "ophys" in a["path"])
t = time.time()
with ThreadPoolExecutor(4) as ex:
    S = list(ex.map(nb.load_photometry, PP))
print(f"loaded {len(S)} sessions in {time.time() - t:.0f} s")
ph = []
for s in S:
    sid = s["session_id"]
    lick_rel = np.concatenate([s["licks"][(s["licks"] > c - 2) & (s["licks"] < c + 6)] - c for c in s["cs_on"]])
    ph.append({"subject": s["subject"], "task": "sucrose" if "Reward" in sid else "airpuff",
               "phase": "early" if "early" in sid else "late", "fluorophore": s["fluorophore"][:14],
               "fiber": s["fiber_location"], "nm": s["wavelengths"], "frames": s["n_frames"],
               "dt_ms": tuple(round(x * 1000, 1) for x in s["frame_dt"]),
               "phase_means": tuple(round(x, 3) for x in s["phase_means"]),
               "ph415": s["phase415"], "ph470": s["phase470"],
               "agree": round(min(s["phase_agree415"], s["phase_agree470"]), 3),
               "labels_moved": s["labels_moved415"] + s["labels_moved470"],
               "phase_means_after_irregular": tuple(round(x, 3) for x in s["phase_means_after_irregular"]),
               "cues": len(s["cs_on"]), "cue_s": round(float(np.median(s["cs_dur"])), 2),
               "min_gap_s": round(float(np.diff(s["cs_on"]).min()), 1),
               "lick_frames_-2_0": int(((lick_rel >= -2) & (lick_rel < 0)).sum()),
               "lick_frames_2_4": int(((lick_rel >= 2) & (lick_rel < 4)).sum())})
ph = pd.DataFrame(ph)
print(ph.to_string(index=False))
loaded 20 sessions in 45 s
subject    task phase    fluorophore fiber             nm  frames        dt_ms           phase_means  ph415  ph470  agree  labels_moved phase_means_after_irregular  cues  cue_s  min_gap_s  lick_frames_-2_0  lick_frames_2_4
    M-0 airpuff early neurotensin-fl   BLA [415.0, 470.0]  138159 (16.5, 16.8) (0.201, 0.012, 0.039)      1      2  1.000             0             (nan, nan, nan)    29    2.0        4.7                 8               13
    M-0 airpuff  late neurotensin-fl   BLA [415.0, 470.0]  251595 (16.5, 16.8) (0.202, 0.013, 0.039)      1      2  1.000             0             (nan, nan, nan)    60    2.0       29.7              2367             2338
    M-0 sucrose early neurotensin-fl   BLA [415.0, 470.0]  281228 (16.5, 16.8) (0.133, 0.015, 0.033)      1      2  1.000             0             (nan, nan, nan)    59    2.0       29.6               103              289
    M-0 sucrose  late neurotensin-fl   BLA [415.0, 470.0]  247903 (16.5, 16.8)  (0.198, 0.013, 0.04)      1      2  1.000             0             (nan, nan, nan)    59    2.0       29.7               139              489
    M-1 airpuff early neurotensin-fl   BLA [415.0, 470.0]  268252 (16.5, 16.8)   (0.2, 0.015, 0.042)      1      2  1.000             0             (nan, nan, nan)    60    2.0       29.7              2400             2373
    M-1 airpuff  late neurotensin-fl   BLA [415.0, 470.0]  257196 (16.5, 16.8)   (0.2, 0.015, 0.043)      1      2  1.000             0             (nan, nan, nan)    60    2.0       29.6                 0                0
    M-1 sucrose early neurotensin-fl   BLA [415.0, 470.0]  258206 (16.6, 16.8) (0.199, 0.015, 0.045)      1      2  1.000             0             (nan, nan, nan)    59    2.0       29.7                49              146
    M-1 sucrose  late neurotensin-fl   BLA [415.0, 470.0]  251391 (16.5, 16.8) (0.201, 0.017, 0.045)      1      2  1.000             0             (nan, nan, nan)    60    2.0       29.7               654             1405
    M-3 airpuff early neurotensin-fl   BLA [415.0, 470.0]  313891 (11.3, 16.8) (0.201, 0.019, 0.048)      1      2  1.000             0             (nan, nan, nan)    60    2.0       29.7                 7                0
    M-3 airpuff  late neurotensin-fl   BLA [415.0, 470.0]  251673 (16.5, 16.8)    (0.2, 0.015, 0.05)      1      2  1.000             0             (nan, nan, nan)    60    2.0       29.7                70              129
    M-3 sucrose early neurotensin-fl   BLA [415.0, 470.0]  245497 (16.5, 16.8) (0.196, 0.024, 0.082)      1      2  1.000             0             (nan, nan, nan)    59    2.0       29.7                 0              130
    M-3 sucrose  late neurotensin-fl   BLA [415.0, 470.0]  266708 (16.5, 16.8) (0.198, 0.026, 0.059)      1      2  1.000             0             (nan, nan, nan)    59    2.0       29.7               109             1417
    M-4 airpuff early neurotensin-fl   BLA [415.0, 470.0]  263618 (16.5, 16.8) (0.201, 0.014, 0.033)      1      2  1.000             0             (nan, nan, nan)    60    2.0       29.7                 4               86
    M-4 airpuff  late neurotensin-fl   BLA [415.0, 470.0]  273117 (11.4, 16.8)     (0.2, 0.01, 0.03)      1      2  1.000             0             (nan, nan, nan)    60    2.0       29.7               103              113
    M-4 sucrose early neurotensin-fl   BLA [415.0, 470.0]  249451 (16.5, 16.8) (0.133, 0.012, 0.027)      1      2  1.000             0             (nan, nan, nan)    59    2.0       29.5                76              564
    M-4 sucrose  late neurotensin-fl   BLA [415.0, 470.0]  256056 (16.5, 16.8)   (0.2, 0.013, 0.033)      1      2  1.000             0             (nan, nan, nan)    60    2.0       29.6                72              538
    M-5 airpuff early neurotensin-fl   BLA [415.0, 470.0]  263757 (16.5, 16.8) (0.201, 0.022, 0.053)      1      2  1.000             0             (nan, nan, nan)    60    2.0       29.7              1150             1243
    M-5 airpuff  late neurotensin-fl   BLA [415.0, 470.0]  248811 (16.5, 16.8) (0.202, 0.023, 0.054)      1      2  1.000             0             (nan, nan, nan)    60    2.0       29.7                 0                0
    M-5 sucrose early neurotensin-fl   BLA [415.0, 470.0]  291288 (16.5, 16.8) (0.131, 0.021, 0.048)      1      2  1.000             0             (nan, nan, nan)    61    2.0       29.5                52              560
    M-5 sucrose  late neurotensin-fl   BLA [415.0, 470.0]  250581  (2.3, 16.8)   (0.2, 0.019, 0.057)      1      2  0.989        165289         (0.2, 0.019, 0.057)    59    2.0       29.6                50              328

Checked against the paper. The files hold the GRAB-NTS1.0 sensor ("neurotensin-fluorescent sensor") in the BLA of five wild-type mice, with 415 and 470 nm excitation, which is the experiment of Figure 3j-l (N = 5; the Methods exclude one of six mice for fiber placement, and the subjects are Mouse1, 2, 4, 5 and 6). Frames come every 16.7 ms (60 Hz), not at the Methods' 40 Hz: one series interleaves three frame types. The LED labels mark every 415 nm frame and every 470 nm frame (a third of the frames each); these sit on a fixed phase of three ("agree" is the fraction of labelled frames on the most common phase). In Mouse6's late sucrose session (M-5) the labels move by one frame after a 2.3 ms frame interval ("labels_moved"), while the data keep their order: the mean of each phase after that interval ("phase_means_after_irregular") is the same as over the session, with the near-constant third frame type still on phase 0. Following the labels there would read that constant frame type as the 470 nm channel, so the channels are taken from the frame order, at the phase the labels give before the first irregular interval. The third frame type is a near-constant value around 0.13 to 0.20 that no metadata describes. Each session holds about 60 cues lasting 1.9 to 2 s, about 30 s apart. The exception is Mouse1's early airpuff session (M-0): it holds 29 cues, and its last three come 4.7 and 7.4 s apart, one of them a 4.7 s bout, which looks like a disrupted end of the session. These cues are among the "all trials" readings; the "first 16 trials" readings do not reach them. The Methods give a 4 s CS with the US 2 s after onset and about half of the CSs unpaired; the files mark one kind of cue, 2 s long, and no US. In sucrose sessions licking rises after the cue ends (frames with a lick 2 to 4 s after onset against the 2 s before), which places the sucrose at the end of the 2 s cue, as the Methods' US time of 2 s gives; no file marks whether a cue was paired or unpaired, nor the airpuff. All cues are therefore analysed as CS-US trials, an assumption recorded with the claims.

InΒ [11]:
# Per-trial sucrose check: in the late sucrose sessions, how many cues are followed by licking
# within 2 to 5 s of onset (a paired trial should be, an unpaired one need not)?
for s in S:
    if "Reward" in s["session_id"]:
        n = [np.sum((s["licks"] >= c + 2) & (s["licks"] < c + 5)) > 0 for c in s["cs_on"]]
        print(f'{s["session_id"]:34s} cues followed by a lick within 2-5 s: {sum(n)} of {len(n)}')
RewardLearning_earlyAcq_Mouse1     cues followed by a lick within 2-5 s: 50 of 59
RewardLearning_lateAcq_Mouse1      cues followed by a lick within 2-5 s: 57 of 59
RewardLearning_earlyAcq_Mouse2     cues followed by a lick within 2-5 s: 34 of 59
RewardLearning_lateAcq_Mouse2      cues followed by a lick within 2-5 s: 55 of 60
RewardLearning_earlyAcq_Mouse4     cues followed by a lick within 2-5 s: 17 of 59
RewardLearning_lateAcq_Mouse4      cues followed by a lick within 2-5 s: 58 of 59
RewardLearning_earlyAcq_Mouse5     cues followed by a lick within 2-5 s: 54 of 59
RewardLearning_lateAcq_Mouse5      cues followed by a lick within 2-5 s: 53 of 60
RewardLearning_earlyAcq_Mouse6     cues followed by a lick within 2-5 s: 24 of 61
RewardLearning_lateAcq_Mouse6      cues followed by a lick within 2-5 s: 49 of 59

In the late sucrose sessions most cues are followed by licking, so the files' cues are mostly, and perhaps all, paired with sucrose; the early sessions, before learning, cannot tell.

Photometry claims (Figure 3j-l)ΒΆ

Processing as the claims' criterion gives it (Methods, Fiber photometry, Data analysis): 415 and 470 nm channels median-filtered over 200 ms and averaged into 200 ms (5 Hz) bins from -5 to 10 s around each CS onset; the trial-averaged 470 nm trace regressed on the trial-averaged 415 nm trace over -5 to 0 s (the 415 nm fit subtracted from the 470 nm trace), the residuals z-scored against -5 to 0 s. Windows: CS 0 to 2 s, US 2 to 7 s (Figure 3 legend). Early and late are the files' early and late acquisition sessions. Readings run, as the criterion lists them: (a) first 16 trials or (b) all trials; (i) area under the z-scored curve or (ii) mean z. One more detail the Methods leave open is run both ways: the z-score uses each trial's own baseline ("trial") or the session's pooled baseline ("session"). The regression is fitted on the trial-averaged baseline, as the Methods say; the fitted slope, intercept and correlation of each session are printed, and a fit on the baseline bins of every trial pooled is run as a variant, since the averaged baseline may hold too little variance to fit.

InΒ [12]:
TRIALS, FITS = {}, []
for s in S:
    key = (s["subject"], "sucrose" if "Reward" in s["session_id"] else "airpuff", "early" if "early" in s["session_id"] else "late")
    for zm in ("trial", "session"):
        tb, Z, keep, fit = nb.photometry_trials(s, zmode=zm, return_fit=True)
        TRIALS[key + (zm,)] = (tb, Z)
    tb, Z, keep, fitp = nb.photometry_trials(s, zmode="trial", fit="pooled", return_fit=True)
    TRIALS[key + ("trial, pooled fit",)] = (tb, Z)
    FITS.append({"mouse": key[0], "task": key[1], "phase": key[2], "slope": fit[0], "intercept": fit[1], "r": fit[2],
                 "pooled_slope": fitp[0], "pooled_intercept": fitp[1], "pooled_r": fitp[2]})
print("470 nm on 415 nm regression over -5 to 0 s, per session (trial-averaged fit, and pooled-trials variant):")
print(pd.DataFrame(FITS).round(3).to_string(index=False))
print("trials kept per session (all cues have a full -5 to 10 s window):",
      {f"{k[0]} {k[1]} {k[2]}": v[1].shape[0] for k, v in TRIALS.items() if k[3] == "trial"})
mice = sorted({k[0] for k in TRIALS})

def measure(task, phase, zm, sub, meas, lo, hi):
    out = []
    for m in mice:
        tb, Z = TRIALS[(m, task, phase, zm)]
        Z = Z[:16] if sub == "first16" else Z
        out.append(nb.window_measure(tb, Z, lo, hi, meas).mean())
    return np.array(out)

def compare(task, zmodes=("trial", "session")):
    rows = []
    for zm in zmodes:
        for sub in ("first16", "all"):
            for meas in ("auc", "mean"):
                r = {"zscore": zm, "trials": sub, "measure": meas}
                for win, (lo, hi) in (("US", (2, 7)), ("CS", (0, 2))):
                    e, l = measure(task, "early", zm, sub, meas, lo, hi), measure(task, "late", zm, sub, meas, lo, hi)
                    tt = stats.ttest_rel(l, e)
                    d = l - e
                    ci = stats.t.interval(0.95, len(d) - 1, d.mean(), stats.sem(d))
                    r.update({f"{win}_late-early": d.mean(), f"{win}_ci_low": ci[0], f"{win}_ci_high": ci[1],
                              f"{win}_t": tt.statistic, f"{win}_p": tt.pvalue, f"{win}_mice_in_direction": None})
                    r[f"{win}_mice_up"] = int((d > 0).sum())
                rows.append(r)
    return pd.DataFrame(rows).drop(columns=["US_mice_in_direction", "CS_mice_in_direction"])
470 nm on 415 nm regression over -5 to 0 s, per session (trial-averaged fit, and pooled-trials variant):
mouse    task phase  slope  intercept     r  pooled_slope  pooled_intercept  pooled_r
  M-0 airpuff early  0.375      0.034 0.248         1.434             0.021     0.995
  M-0 airpuff  late  0.805      0.028 0.467         1.487             0.019     0.994
  M-0 sucrose early  0.928      0.019 0.563         1.249             0.014     0.990
  M-0 sucrose  late  1.054      0.026 0.745         1.433             0.021     0.996
  M-1 airpuff early  1.613      0.018 0.877         1.150             0.025     0.994
  M-1 airpuff  late  1.203      0.025 0.868         1.218             0.025     0.996
  M-1 sucrose early  0.939      0.030 0.936         1.670             0.020     0.991
  M-1 sucrose  late  0.737      0.032 0.480         1.432             0.020     0.997
  M-3 airpuff early  2.331      0.002 0.972         1.166             0.025     0.992
  M-3 airpuff  late  2.056      0.019 0.972         1.444             0.029     0.989
  M-3 sucrose early  1.458      0.047 0.614         2.058             0.033     0.990
  M-3 sucrose  late  1.857      0.012 0.966         1.689             0.016     0.993
  M-4 airpuff early  0.865      0.021 0.819         1.249             0.016     0.997
  M-4 airpuff  late  1.604      0.015 0.943         1.148             0.019     0.985
  M-4 sucrose early  1.559      0.008 0.821         1.099             0.014     0.998
  M-4 sucrose  late  2.018      0.008 0.935         1.229             0.018     0.958
  M-5 airpuff early  2.189      0.005 0.978         1.432             0.022     0.989
  M-5 airpuff  late  2.185      0.004 0.958         1.382             0.022     0.980
  M-5 sucrose early  1.712      0.012 0.903         1.615             0.014     0.986
  M-5 sucrose  late  2.168      0.017 0.988         1.246             0.034     0.938
trials kept per session (all cues have a full -5 to 10 s window): {'M-0 airpuff early': 29, 'M-0 airpuff late': 60, 'M-0 sucrose early': 59, 'M-0 sucrose late': 59, 'M-1 airpuff early': 60, 'M-1 airpuff late': 60, 'M-1 sucrose early': 59, 'M-1 sucrose late': 60, 'M-3 airpuff early': 60, 'M-3 airpuff late': 60, 'M-3 sucrose early': 59, 'M-3 sucrose late': 59, 'M-4 airpuff early': 60, 'M-4 airpuff late': 60, 'M-4 sucrose early': 59, 'M-4 sucrose late': 60, 'M-5 airpuff early': 60, 'M-5 airpuff late': 60, 'M-5 sucrose early': 61, 'M-5 sucrose late': 59}

fig3k-nt-sensor-sucrose-increaseΒΆ

The BLA NT sensor response to sucrose delivery increases after reward learning. (Paper: US window late against early, paired t-test t(4) = 8.709, P = 0.001, effect 4.14, CI 2.744 to 5.538; CS window t(4) = 1.788, P = 0.1483.) Reproduced when the US comparison has p < 0.05 with late higher under every reading.

InΒ [13]:
res_suc = compare("sucrose")
print(res_suc.round(4).to_string(index=False))
print("\nPer mouse, US window AUC (trial z-score, first 16 trials):")
print(pd.DataFrame({"mouse": mice, "early": measure("sucrose", "early", "trial", "first16", "auc", 2, 7),
                    "late": measure("sucrose", "late", "trial", "first16", "auc", 2, 7)}).round(2).to_string(index=False))
ok = (res_suc["US_p"] < 0.05) & (res_suc["US_late-early"] > 0)
print("\nReadings with US p < 0.05 and late higher: %d of %d; per-trial z-score %d of 4, session z-score %d of 4"
      % (ok.sum(), len(res_suc), ok[res_suc.zscore == "trial"].sum(), ok[res_suc.zscore == "session"].sum()))
print("\nVariant, regression fitted on the pooled baseline bins of every trial (per-trial z-score):")
print(compare("sucrose", ("trial, pooled fit",))[["zscore", "trials", "measure", "US_late-early", "US_ci_low", "US_ci_high", "US_t", "US_p", "US_mice_up"]].round(4).to_string(index=False))
 zscore  trials measure  US_late-early  US_ci_low  US_ci_high    US_t   US_p  US_mice_up  CS_late-early  CS_ci_low  CS_ci_high    CS_t   CS_p  CS_mice_up
  trial first16     auc         2.9428    -0.7733      6.6589  2.1987 0.0928           4         0.6614    -2.2730      3.5958  0.6258 0.5654           3
  trial first16    mean         0.5886    -0.1547      1.3318  2.1987 0.0928           4         0.3307    -1.1365      1.7979  0.6258 0.5654           3
  trial     all     auc         3.5294     1.8236      5.2352  5.7445 0.0046           5         0.5455    -3.0270      4.1180  0.4240 0.6934           3
  trial     all    mean         0.7059     0.3647      1.0470  5.7445 0.0046           5         0.2728    -1.5135      2.0590  0.4240 0.6934           3
session first16     auc        -2.9182    -9.5474      3.7111 -1.2222 0.2887           2        -1.2509    -3.8352      1.3334 -1.3439 0.2501           2
session first16    mean        -0.5836    -1.9095      0.7422 -1.2222 0.2887           2        -0.6255    -1.9176      0.6667 -1.3439 0.2501           2
session     all     auc         0.1574    -0.0985      0.4133  1.7074 0.1629           4        -0.0076    -0.2250      0.2098 -0.0967 0.9276           2
session     all    mean         0.0315    -0.0197      0.0827  1.7074 0.1629           4        -0.0038    -0.1125      0.1049 -0.0967 0.9276           2

Per mouse, US window AUC (trial z-score, first 16 trials):
mouse  early  late
  M-0  -7.85 -3.59
  M-1  -8.94 -6.50
  M-3   0.52  2.71
  M-4  -0.67  6.33
  M-5   1.44  0.27

Readings with US p < 0.05 and late higher: 2 of 8; per-trial z-score 2 of 4, session z-score 0 of 4

Variant, regression fitted on the pooled baseline bins of every trial (per-trial z-score):
           zscore  trials measure  US_late-early  US_ci_low  US_ci_high   US_t   US_p  US_mice_up
trial, pooled fit first16     auc         2.5313    -5.7652     10.8277 0.8471 0.4446           4
trial, pooled fit first16    mean         0.5063    -1.1530      2.1655 0.8471 0.4446           4
trial, pooled fit     all     auc         1.4543    -6.4857      9.3942 0.5085 0.6378           4
trial, pooled fit     all    mean         0.2909    -1.2971      1.8788 0.5085 0.6378           4
InΒ [14]:
fig, axes = plt.subplots(1, 2, figsize=(10, 3.2), sharey=True)
for ax, task in zip(axes, ("sucrose", "airpuff")):
    for phase, col in (("early", "tab:blue"), ("late", "tab:red")):
        tr = np.array([TRIALS[(m, task, phase, "trial")][1][:16].mean(0) for m in mice])
        tb = TRIALS[(mice[0], task, phase, "trial")][0] + 0.1
        ax.plot(tb, tr.mean(0), color=col, label=phase)
        ax.fill_between(tb, tr.mean(0) - stats.sem(tr), tr.mean(0) + stats.sem(tr), color=col, alpha=0.2)
        print(f"{task} {phase}: plotted mean z from {tr.mean(0).min():.2f} to {tr.mean(0).max():.2f}")
    ax.axvspan(2, 7, color="0.9"); ax.axvline(0, color="k", lw=0.5)
    ax.set_title(f"NT sensor, {task} (first 16 trials, mean of 5 mice)"); ax.set_xlabel("s from CS onset")
axes[0].set_ylabel("z"); axes[0].legend(); plt.tight_layout(); plt.show()
sucrose early: plotted mean z from -1.26 to 0.27
sucrose late: plotted mean z from -0.76 to 0.45
airpuff early: plotted mean z from -0.63 to 0.59
airpuff late: plotted mean z from -1.74 to 0.23
No description has been provided for this image

Result: not reproduced. Under the per-trial z-score the US response to sucrose is larger in the late session under all four readings of the criterion, but p < 0.05 holds only with all trials: AUC +3.53 (95% CI 1.82 to 5.24), t(4) = 5.74, p = 0.0046, 5 of 5 mice higher, inside the paper's CI of 2.744 to 5.538. With the first 16 trials it is AUC +2.94, p = 0.093, 4 of 5 mice higher. AUC and mean give the same t and p, so the two passing readings are one result, and the criterion's "p < 0.05 with late higher under every reading" holds in 2 of its 4 readings: the criterion is not met. The significant result also rests on the regression fitted on the trial-averaged baseline, whose correlation is 0.48 to 0.75 in four sucrose sessions (M-0 early and late, M-1 late, M-3 early); fitted on the pooled baseline bins of every trial (correlation at least 0.938) no reading reaches p < 0.05 (p >= 0.44), and under the session-pooled z-score none does either (p >= 0.16). The CS window does not change under any reading (p >= 0.25), as in the paper. The per-trial z-score is the reading adopted (an assumption: the Methods z-score "using data a baseline window of -5 s to 0 s", most simply read as each trial's own window). The criterion has no partial condition, so with p < 0.05 failing in half its readings, and in every variant of the processing, the claim is not reproduced; only the direction holds throughout.

fig3l-nt-sensor-airpuff-decreaseΒΆ

The BLA NT sensor response to airpuff decreases after punishment learning. (Paper: US window t(4) = 3.996, P = 0.0162, effect -3.657, CI -6.083 to -0.9583; CS t(4) = 2.137, P = 0.0994.) Reproduced when the US comparison has p < 0.05 with late lower under every reading.

InΒ [15]:
res_air = compare("airpuff")
print(res_air.round(4).to_string(index=False))
print("\nPer mouse, US window AUC (trial z-score, first 16 trials):")
print(pd.DataFrame({"mouse": mice, "early": measure("airpuff", "early", "trial", "first16", "auc", 2, 7),
                    "late": measure("airpuff", "late", "trial", "first16", "auc", 2, 7)}).round(2).to_string(index=False))
print("\nReadings with US p < 0.05 and late lower: %d of %d" % (((res_air["US_p"] < 0.05) & (res_air["US_late-early"] < 0)).sum(), len(res_air)))
print("\nVariant, regression fitted on the pooled baseline bins of every trial (per-trial z-score):")
print(compare("airpuff", ("trial, pooled fit",))[["zscore", "trials", "measure", "US_late-early", "US_ci_low", "US_ci_high", "US_t", "US_p", "US_mice_up"]].round(4).to_string(index=False))
 zscore  trials measure  US_late-early  US_ci_low  US_ci_high    US_t   US_p  US_mice_up  CS_late-early  CS_ci_low  CS_ci_high    CS_t   CS_p  CS_mice_up
  trial first16     auc        -5.4694   -12.3523      1.4135 -2.2062 0.0920           0        -0.8698    -2.3015      0.5619 -1.6868 0.1669           1
  trial first16    mean        -1.0939    -2.4705      0.2827 -2.2062 0.0920           0        -0.4349    -1.1508      0.2809 -1.6868 0.1669           1
  trial     all     auc        -4.6570   -10.0465      0.7325 -2.3991 0.0744           1        -0.7447    -1.6415      0.1521 -2.3055 0.0824           0
  trial     all    mean        -0.9314    -2.0093      0.1465 -2.3991 0.0744           1        -0.3724    -0.8208      0.0761 -2.3055 0.0824           0
session first16     auc        -1.1811   -11.1338      8.7715 -0.3295 0.7583           2        -0.3223    -4.4390      3.7945 -0.2173 0.8386           2
session first16    mean        -0.2362    -2.2268      1.7543 -0.3295 0.7583           2        -0.1611    -2.2195      1.8972 -0.2173 0.8386           2
session     all     auc        -0.6074    -1.3853      0.1705 -2.1679 0.0960           0        -0.1427    -0.3445      0.0591 -1.9637 0.1210           0
session     all    mean        -0.1215    -0.2771      0.0341 -2.1679 0.0960           0        -0.0714    -0.1722      0.0295 -1.9637 0.1210           0

Per mouse, US window AUC (trial z-score, first 16 trials):
mouse  early   late
  M-0  -1.36  -2.51
  M-1  -6.08 -18.11
  M-3  -0.33  -3.33
  M-4  10.77  -0.07
  M-5  -0.27  -0.61

Readings with US p < 0.05 and late lower: 0 of 8

Variant, regression fitted on the pooled baseline bins of every trial (per-trial z-score):
           zscore  trials measure  US_late-early  US_ci_low  US_ci_high    US_t   US_p  US_mice_up
trial, pooled fit first16     auc         0.2685    -3.6100      4.1470  0.1922 0.8569           3
trial, pooled fit first16    mean         0.0537    -0.7220      0.8294  0.1922 0.8569           3
trial, pooled fit     all     auc        -1.5799    -4.6941      1.5343 -1.4085 0.2318           2
trial, pooled fit     all    mean        -0.3160    -0.9388      0.3069 -1.4085 0.2318           2

Result: not reproduced. Under the per-trial z-score the US response to airpuff is lower in the late session under all four readings (AUC -5.47 with the first 16 trials, 5 of 5 mice lower, and -4.66 with all trials; the paper's effect is -3.657), but no reading reaches p < 0.05 (smallest p = 0.074, t(4) = -2.40). The session-pooled z-score gives p >= 0.096, and the pooled-baseline fit p >= 0.23. The CS window does not change significantly under any reading (p >= 0.08).

fig3-nt-sensor-early-dipΒΆ

Early in acquisition, the BLA NT sensor signal transiently decreases after both sucrose and airpuff. Reproduced when the mean z-scored signal over mice in the US window (2 to 7 s from CS onset) is below zero in early trials for both sucrose and airpuff.

InΒ [16]:
rows = []
for zm in ("trial", "session"):
    for sub in ("first16", "all"):
        for meas in ("mean", "auc"):
            r = {"zscore": zm, "trials": sub, "measure": meas}
            for task in ("sucrose", "airpuff"):
                v = measure(task, "early", zm, sub, meas, 2, 7)
                r[f"{task}_mean"] = v.mean(); r[f"{task}_mice_below_0"] = int((v < 0).sum())
                r[f"{task}_p_vs_0"] = stats.ttest_1samp(v, 0).pvalue
            rows.append(r)
dip = pd.DataFrame(rows)
print(dip.round(3).to_string(index=False))
print("\nReadings with both early means below zero: %d of %d" % (((dip.sucrose_mean < 0) & (dip.airpuff_mean < 0)).sum(), len(dip)))
print("\nEarly sessions, per mouse, mean z 2-7 s (trial z-score, first 16 trials):")
print(pd.DataFrame({"mouse": mice, "sucrose": measure("sucrose", "early", "trial", "first16", "mean", 2, 7),
                    "airpuff": measure("airpuff", "early", "trial", "first16", "mean", 2, 7)}).round(2).to_string(index=False))
# Variant for the word "transient" (asked of a reader): the lowest point, 2 to 7 s, of the trace
# averaged over trials and then over mice, and of each mouse's trial-averaged trace.
print("\nVariant: minimum of the early trace in 2 to 7 s (z), mean of 5 mice and per mouse:")
for zm in ("trial", "session"):
    for sub in ("first16", "all"):
        r = {}
        for task in ("sucrose", "airpuff"):
            tb = TRIALS[(mice[0], task, "early", zm)][0] + 0.1
            w = (tb > 2) & (tb < 7)
            tr = np.array([(TRIALS[(m, task, "early", zm)][1][:16] if sub == "first16" else TRIALS[(m, task, "early", zm)][1]).mean(0) for m in mice])
            r[task] = f"mean-trace min {tr.mean(0)[w].min():+.2f} at {tb[w][np.argmin(tr.mean(0)[w])]:.1f} s; per mouse {np.round(tr[:, w].min(1), 2).tolist()}"
        print(f"{zm:8s} {sub:8s} sucrose: {r['sucrose']}\n                  airpuff: {r['airpuff']}")
 zscore  trials measure  sucrose_mean  sucrose_mice_below_0  sucrose_p_vs_0  airpuff_mean  airpuff_mice_below_0  airpuff_p_vs_0
  trial first16    mean        -0.620                     3           0.231         0.109                     4           0.854
  trial first16     auc        -3.099                     3           0.231         0.544                     4           0.854
  trial     all    mean        -0.399                     4           0.351         0.192                     4           0.726
  trial     all     auc        -1.994                     4           0.351         0.960                     4           0.726
session first16    mean         0.344                     2           0.557        -0.397                     3           0.573
session first16     auc         1.719                     2           0.557        -1.987                     3           0.573
session     all    mean        -0.018                     4           0.655         0.014                     3           0.641
session     all     auc        -0.088                     4           0.655         0.068                     3           0.641

Readings with both early means below zero: 0 of 8

Early sessions, per mouse, mean z 2-7 s (trial z-score, first 16 trials):
mouse  sucrose  airpuff
  M-0    -1.57    -0.27
  M-1    -1.79    -1.22
  M-3     0.10    -0.07
  M-4    -0.13     2.15
  M-5     0.29    -0.05

Variant: minimum of the early trace in 2 to 7 s (z), mean of 5 mice and per mouse:
trial    first16  sucrose: mean-trace min -1.09 at 4.3 s; per mouse [-3.0, -2.44, -1.07, -0.82, -0.24]
                  airpuff: mean-trace min -0.63 at 3.7 s; per mouse [-3.81, -2.47, -0.77, -0.11, -0.27]
trial    all      sucrose: mean-trace min -0.82 at 4.3 s; per mouse [-2.64, -1.67, -1.59, -0.39, 0.65]
                  airpuff: mean-trace min -0.36 at 3.7 s; per mouse [-2.94, -1.48, -1.04, 0.15, -0.27]
session  first16  sucrose: mean-trace min +0.31 at 4.3 s; per mouse [0.7, 1.33, 1.23, -1.45, -0.43]
                  airpuff: mean-trace min -0.46 at 3.7 s; per mouse [0.47, -1.58, -1.46, 1.4, -1.43]
session  all      sucrose: mean-trace min -0.05 at 4.3 s; per mouse [-0.27, -0.07, -0.05, -0.03, 0.08]
                  airpuff: mean-trace min -0.03 at 3.7 s; per mouse [-0.23, -0.09, -0.03, -0.02, -0.03]

Result: not reproduced. In the early sessions the mean over mice of the US-window signal is below zero for sucrose under the per-trial z-score (-0.62 with the first 16 trials, -0.40 with all trials) but above zero for airpuff (+0.11 and +0.19; M-4 at +2.15), so the criterion, which needs both below zero, is not met under any per-trial reading. Under the session-pooled z-score the signs change with the trial subset (0 of 8 readings in all). No mean differs from zero (p >= 0.23). Read as a brief dip, the paper's word "transient", the mouse-averaged trace does go below zero within 2 to 7 s for both outcomes under the per-trial z-score (lowest -1.09 for sucrose and -0.63 for airpuff, first 16 trials); whether the criterion's window mean is the right measure for a transient decrease is asked of a reader.

Electrophysiology claims (Figure 4)ΒΆ

fig4-recording-countΒΆ

685 BLA neurons were recorded from 30 mice during the three-cue discrimination task. Units from the last discrimination-task session of each mouse, with baseline firing of at least 0.3 Hz (Methods). Whether 685 is counted before or after that exclusion is not said; both counts are shown and either may match (expected 685 +/- 68.5 neurons, 30 +/- 3 mice).

Baseline rate: the mean rate in the 1 s before each CS onset, over all CSs of the session (the PSTH baseline of the Methods); the whole-session rate is shown as a variant.

InΒ [17]:
base_rate, sess_rate, unit_rows = {}, {}, []
for e in E:
    on = np.sort(np.concatenate([e["trials"][v][0] for v in nb.CS_TYPES.values() if v in e["trials"]]))
    for u, sp in enumerate(e["spikes"]):
        b = np.mean(np.searchsorted(sp, on) - np.searchsorted(sp, on - 1.0))
        unit_rows.append({"subject": e["subject"], "unit": u, "baseline_hz": b, "session_hz": len(sp) / e["duration"]})
U = pd.DataFrame(unit_rows)
n_all, n_base, n_sess = len(U), int((U.baseline_hz >= 0.3).sum()), int((U.session_hz >= 0.3).sum())
print(f"units before exclusion: {n_all}; with baseline >= 0.3 Hz: {n_base}; with session rate >= 0.3 Hz: {n_sess}")
print(f"mice: {U.subject.nunique()}")
print("per mouse (all units / kept by baseline):")
print(U.groupby("subject").agg(units=("unit", "size"), kept=("baseline_hz", lambda x: int((x >= 0.3).sum()))).T.to_string())
print(f"\nexpected 685 +/- 68.5 neurons: before exclusion {abs(n_all - 685) <= 68.5}, after {abs(n_base - 685) <= 68.5}")
print(f"expected 30 +/- 3 mice: {abs(U.subject.nunique() - 30) <= 3}")
print("units per mouse: median %.0f (paper 685 / 30 = 22.8)" % U.groupby("subject").size().median())
ratio = ses[["sucrose", "neutral", "shock"]]
near = (ratio.neutral > 0) & ((ratio.sucrose - 2 * ratio.neutral).abs() <= 2) & ((ratio.shock - ratio.neutral).abs() <= 2)
print(f"sessions with sucrose, neutral and shock CSs near 2:1:1 (within 2 trials): {int(near.sum())} of {len(ses)}; not: {list(ses.subject[~near])}")
units before exclusion: 1902; with baseline >= 0.3 Hz: 1739; with session rate >= 0.3 Hz: 1884
mice: 31
per mouse (all units / kept by baseline):
subject  H01  H02  H07  H08  H09  H10  H11  H12  H13  H14  H17  H19  H22  H23  H24  H25  H27  H28  H29  H31  H32  H33  H34  H35  H36  H37  H38  H39  H40  H41  H42
units     54   64   64   60   62   64   64   64   64   64   64   64   64   64   46   58   38   64   64   64   64   64   64   58   64   58   64   64   64   62   62
kept      47   61   64   60   62   56   55   60   43   64    0   63   52   63   46   58   38   63   61   53   64   64   64   51   64   48   64   63   64   62   62

expected 685 +/- 68.5 neurons: before exclusion False, after False
expected 30 +/- 3 mice: True
units per mouse: median 64 (paper 685 / 30 = 22.8)
sessions with sucrose, neutral and shock CSs near 2:1:1 (within 2 trials): 25 of 31; not: ['H09', 'H10', 'H11', 'H12', 'H13', 'H14']

Result: partially reproduced. The number of mice (31) is within the tolerance of 30. The unit count is not: 1902 units, 1739 with a baseline rate of at least 0.3 Hz (1884 by whole-session rate), against the paper's 685; neither is within 68.5 of 685. The units are not sorted single units (data check), which fits a median of 64 per mouse instead of 23. The 2:1:1 ratio of the statement holds in 25 of 31 sessions; H09 to H14 have no neutral CS.

fig4d-seven-clustersΒΆ

Unsupervised hierarchical clustering of BLA responses to the sucrose, neutral and shock CSs gives seven functional clusters. Per neuron, 20 ms PSTHs from -1 to 1 s around CS onset, z-scored with the 1 s baseline, smoothed with a Gaussian-weighted moving average (smoothing factor 0.85), the three CS traces concatenated; Ward linkage with correlation distance cut at 35% of the maximum linkage distance; readings (a) Ward on the correlation distance matrix, (b) Ward with Euclidean distance on centred, unit-norm rows. Reproduced when one reading gives seven clusters.

Units with a baseline rate below 0.3 Hz are excluded (Methods). The six sessions without neutral CSs cannot give the concatenated trace and are left out; so are units whose baseline SD is zero. The "smoothing factor of 0.85" sets the window through a MATLAB heuristic the paper does not give, so the smoothing window is run over a range (in 20 ms bins, smoothing each CS trace separately).

InΒ [18]:
feats, owner = [], []
dropped = {"low_rate": 0, "no_neutral_session": 0, "zero_sd": 0}
for e in E:
    on = {k: e["trials"][v][0] for k, v in nb.CS_TYPES.items() if v in e["trials"]}
    allon = np.sort(np.concatenate(list(on.values())))
    for u, sp in enumerate(e["spikes"]):
        b = np.mean(np.searchsorted(sp, allon) - np.searchsorted(sp, allon - 1.0))
        if b < 0.3: dropped["low_rate"] += 1; continue
        if "neutral" not in on: dropped["no_neutral_session"] += 1; continue
        z = np.array([nb.zscore_psth(nb.psth(sp, on[k])) for k in ("sucrose", "neutral", "shock")])
        if np.isnan(z).any(): dropped["zero_sd"] += 1; continue
        feats.append(z); owner.append(e["subject"])
F = np.array(feats); owner = np.array(owner)
print("units clustered:", len(F), "from", len(set(owner)), "mice; dropped:", dropped)

def cluster(F, window):
    X = nb.gauss_smooth(F, window).reshape(len(F), -1)
    Za = linkage(pdist(X, "correlation"), "ward")
    ca = fcluster(Za, 0.35 * Za[:, 2].max(), "distance")
    Xc = X - X.mean(1, keepdims=True); Xc /= np.linalg.norm(Xc, axis=1, keepdims=True)
    Zb = linkage(Xc, "ward")
    cb = fcluster(Zb, 0.35 * Zb[:, 2].max(), "distance")
    return ca, cb

rows = []
CL = {}
for w in (1, 3, 5, 7, 9, 11, 15, 21, 31, 41, 51, 75, 101):
    ca, cb = cluster(F, w)
    CL[w] = (ca, cb)
    rows.append({"window_bins": w, "window_ms": w * 20, "clusters_a_corr_ward": ca.max(), "clusters_b_unitnorm_ward": cb.max(),
                 "sizes_a": sorted(np.bincount(ca)[1:].tolist(), reverse=True)})
cl = pd.DataFrame(rows)
print(cl.to_string(index=False))
print("\nwindows giving 7 clusters: reading (a)", list(cl.window_bins[cl.clusters_a_corr_ward == 7]),
      " reading (b)", list(cl.window_bins[cl.clusters_b_unitnorm_ward == 7]))
units clustered: 1399 from 24 mice; dropped: {'low_rate': 163, 'no_neutral_session': 340, 'zero_sd': 0}
 window_bins  window_ms  clusters_a_corr_ward  clusters_b_unitnorm_ward                                              sizes_a
           1         20                     9                        11            [338, 243, 192, 183, 162, 83, 78, 61, 59]
           3         60                    12                        14 [383, 292, 146, 105, 94, 84, 63, 57, 52, 44, 41, 38]
           5        100                    11                        13   [364, 303, 119, 108, 105, 100, 97, 69, 58, 40, 36]
           7        140                    12                        13  [381, 283, 125, 89, 88, 76, 73, 71, 67, 58, 48, 40]
           9        180                    11                        13    [320, 291, 130, 129, 113, 91, 86, 81, 60, 58, 40]
          11        220                    10                        12         [575, 191, 125, 107, 86, 83, 76, 64, 51, 41]
          15        300                     8                        12                [601, 227, 137, 118, 101, 88, 68, 59]
          21        420                     9                        11          [340, 238, 211, 153, 109, 106, 105, 77, 60]
          31        620                     6                         8                       [393, 249, 237, 208, 192, 120]
          41        820                     7                         9                  [385, 229, 188, 187, 178, 118, 114]
          51       1020                     6                         9                        [315, 268, 263, 263, 210, 80]
          75       1500                     6                         8                       [342, 285, 276, 201, 183, 112]
         101       2020                     8                         9             [250, 245, 175, 158, 153, 148, 138, 132]

windows giving 7 clusters: reading (a) [41]  reading (b) []
InΒ [19]:
# Cut-off sensitivity for one window (11 bins = 220 ms): clusters as a function of the cut fraction.
Xw = nb.gauss_smooth(F, 11).reshape(len(F), -1)
Za = linkage(pdist(Xw, "correlation"), "ward")
print("reading (a), window 11 bins: clusters by cut fraction of the maximum linkage distance")
print({f: int(fcluster(Za, f * Za[:, 2].max(), "distance").max()) for f in (0.25, 0.3, 0.35, 0.4, 0.45, 0.5)})
# Per mouse composition of the clusters, reading (a), window 11.
ca = CL[11][0]
print("\nunits per mouse in each cluster (reading (a), window 11):")
print(pd.crosstab(owner, ca).to_string())
fig, ax = plt.subplots(figsize=(7, 4))
order = np.argsort(ca, kind="stable")
im = ax.imshow(Xw[order], aspect="auto", cmap="RdBu_r", vmin=-5, vmax=5, interpolation="nearest")
ax.set_xticks([0, 50, 100, 150, 200, 250]); ax.set_xticklabels(["S -1", "S 0", "N -1", "N 0", "Sh -1", "Sh 0"])
ax.set_ylabel("unit (sorted by cluster)"); plt.colorbar(im, label="z (clipped at +/-5)"); plt.show()
print("plotted z from %.1f to %.1f; %.1f%% of values outside +/-5" % (Xw.min(), Xw.max(), 100 * np.mean(np.abs(Xw) > 5)))
# Clusters dominated by single mice, and the count without the sessions whose CS alignment is not
# shown (H22, H27, H34, H39; data check), for every window, reading (a).
for w in (11, 41):
    ct = pd.crosstab(owner, CL[w][0])
    share = (ct.max(axis=0) / ct.sum(axis=0)).round(2)
    print(f"window {w} bins, reading (a): largest share of one mouse in each cluster: {dict(zip(ct.columns, share))}; mouse: {dict(zip(ct.columns, ct.idxmax(axis=0)))}")
keep = ~np.isin(owner, ["H22", "H27", "H34", "H39"])
print("\nwithout H22, H27, H34 and H39 (%d units): clusters (a), (b) by window:" % keep.sum(),
      {w: tuple(int(c.max()) for c in cluster(F[keep], w)) for w in (1, 5, 11, 21, 31, 41, 51, 75, 101)})
reading (a), window 11 bins: clusters by cut fraction of the maximum linkage distance
{0.25: 14, 0.3: 13, 0.35: 10, 0.4: 7, 0.45: 7, 0.5: 5}

units per mouse in each cluster (reading (a), window 11):
col_0  1   2   3   4   5   6   7   8   9   10
row_0                                        
H01     0   0   0   3   0   0  38   6   0   0
H02     2   0   1  35   7   0   8   1   0   7
H07     0   0   0  14   0   5   9  14   0  22
H08     0   0   0   1   0  17  41   1   0   0
H19     0   0   0   0   1  33   4   2   0  23
H22     8   3  35   0   2   0   0   1   0   3
H23     2   0   7   0   0   7   7   4  29   7
H24     0   0   0   0   6   0  22   0  18   0
H25     1   0   0   5  47   0   0   5   0   0
H27     0  38   0   0   0   0   0   0   0   0
H28     2   0   2   0   2   0  54   0   0   3
H29    40   0   5   0   2   0   8   0   0   6
H31     2   0   0   1   3   0  42   0   0   5
H32     0   0   0   2   7   2  39   8   0   6
H33    58   0   0   1   1   0   0   4   0   0
H34     1   0   0   2   6   0  52   0   0   3
H35     0   0   0   4   1   0  46   0   0   0
H36     0   0   0   4  10  11  28   8   0   3
H37     6   0   7   5   6   0   8   2   0  14
H38     0   0   2   4  23   0  27   2   4   2
H39     0   0   1   0  29   0  33   0   0   0
H40     1   0   4   1  13   0  30  13   0   2
H41     0   0   0   0  18   0  44   0   0   0
H42     2   0   0   1   7   1  35  15   0   1
No description has been provided for this image
plotted z from -8.6 to 23.4; 0.7% of values outside +/-5
window 11 bins, reading (a): largest share of one mouse in each cluster: {1: 0.46, 2: 0.93, 3: 0.55, 4: 0.42, 5: 0.25, 6: 0.43, 7: 0.09, 8: 0.17, 9: 0.57, 10: 0.21}; mouse: {1: 'H33', 2: 'H27', 3: 'H22', 4: 'H02', 5: 'H25', 6: 'H19', 7: 'H28', 8: 'H42', 9: 'H23', 10: 'H19'}
window 41 bins, reading (a): largest share of one mouse in each cluster: {1: 0.23, 2: 0.18, 3: 0.11, 4: 0.25, 5: 0.29, 6: 0.2, 7: 0.26}; mouse: {1: 'H07', 2: 'H02', 3: 'H28', 4: 'H41', 5: 'H25', 6: 'H23', 7: 'H33'}
without H22, H27, H34 and H39 (1182 units): clusters (a), (b) by window: {1: (9, 11), 5: (11, 12), 11: (8, 12), 21: (7, 9), 31: (6, 9), 41: (6, 9), 51: (6, 7), 75: (6, 6), 101: (6, 7)}

Result: blocked (methods_underspecified). With the paper's rule (Ward linkage, correlation distance, cut at 35% of the maximum linkage distance) the 1399 units of 24 mice give 8 to 12 clusters under reading (a) and 11 to 14 under reading (b) for smoothing windows of 20 to 420 ms, and 6 to 8 under (a) for windows of 620 ms to 2 s; seven clusters come only under reading (a) with an 820 ms window. The count depends on the smoothing window, which the paper sets by MATLAB's "smoothing factor of 0.85" without the window it produced, so no reading can be preferred; the count also moves with the cut fraction (14 at 25%, 10 at 35%, 7 at 40%, 5 at 50% for a 220 ms window). Without H22, H27, H34 and H39, whose CS alignment is not shown, seven clusters come under (a) at 420 ms and under (b) at 1020 ms and 2 s. Some clusters are single sessions (at 220 ms one cluster is 93% H27), so a count here would not show functional types. The units are not the paper's sorted neurons.

fig4c-photoidentificationΒΆ

BLA-NAc and BLA-CeA neurons are identified by short-latency responses to light at the end of each session. The Methods' rule (1 ms PSTH 50 ms around light onset, z-scored against the 50 ms before; excitation above z = 1.98 within 6 ms, none 7 to 50 ms after with a 6 ms moving average, none in the fake session); readings (a) all pulse onsets of the trains and the onset of the long light, (b) the 1 Hz pulses only. Reproduced when the counts are within 10% of 39 BLA-NAc and 32 BLA-CeA neurons under one reading.

No file says which projection (NAc or CeA) carries the Cav2-Cre in each mouse, so the two counts cannot be split. Their sum can still be tested: both counts are within 10% only if the total is within 63.9 to 78.1 (0.9 and 1.1 times 71). The fake-session test needs to know which of the two light blocks is the fake one. The Methods put the fake session first ("a phototagging session was conducted after a 'fake' phototagging session in which the patch cord was disconnected", Methods, In vivo electrophysiology), so block 1 is taken as the fake one; the reverse is run as a reading, and for H31 to H42, where one block comes before the task, either order per session is allowed (the lowest total over those choices is printed). Light events of 5 and 10 ms are the pulses; the files' 100 ms events are the only longer light, and their onsets are added in reading (a).

InΒ [20]:
tag_rows = []
for e in E:
    on, off = e["trials"]["phototagging"]
    dur = off - on
    g = np.argmax(np.diff(on))
    block = [np.arange(len(on)) <= g, np.arange(len(on)) > g]
    def pulses(b, reading):
        p = on[block[b] & (dur <= 0.011)]
        if reading == "a":
            return np.sort(np.r_[p, on[block[b] & (np.abs(dur - 0.1) < 0.01)]])
        return p[(np.r_[np.inf, np.diff(p)] > 0.9) & (np.r_[np.diff(p), np.inf] > 0.9)]
    base = U[U.subject == e["subject"]].baseline_hz.values
    r = {"subject": e["subject"], "units": len(e["spikes"])}
    for rd in ("a", "b"):
        P = [pulses(0, rd), pulses(1, rd)]
        r[f"{rd}_pulses"] = len(P[1])
        # nb.photoresponsive(sp, real, fake) is "responsive to real and not to fake"; computed once per block.
        resp = [np.array([nb.photoresponsive(sp, P[b])[0] for sp in e["spikes"]]) for b in (0, 1)]
        for name, (fk, rl) in {"fake1": (0, 1), "fake2": (1, 0)}.items():
            ok = resp[rl] & ~resp[fk]
            r[f"{rd}_{name}"] = int(ok.sum()); r[f"{rd}_{name}_0.3Hz"] = int((ok & (base >= 0.3)).sum())
        r[f"{rd}_resp_block1"] = int(resp[0].sum()); r[f"{rd}_resp_block2"] = int(resp[1].sum())
    tag_rows.append(r)
tg = pd.DataFrame(tag_rows)
print(tg.to_string(index=False))
late = tg.subject.isin(list(span.subject[span.light_before_task > 0]))
print("\nlight event durations (s) and counts, first session:", dict(zip(*np.unique(np.round(E[0]["trials"]["phototagging"][1] - E[0]["trials"]["phototagging"][0], 3), return_counts=True))))
print("\ntagged units in total (paper 39 + 32 = 71; both counts within 10% needs 63.9 to 78.1):")
for rd in ("a", "b"):
    for sfx in ("", "_0.3Hz"):
        f1, f2 = tg[f"{rd}_fake1{sfx}"], tg[f"{rd}_fake2{sfx}"]
        lowest = int(f1[~late].sum() + np.minimum(f1[late], f2[late]).sum())
        print(f"reading ({rd}){' baseline >= 0.3 Hz' if sfx else ''}: block 1 fake {f1.sum()}, block 2 fake {f2.sum()}, "
              f"block 1 fake with either order in H31-H42 (lowest) {lowest}; "
              f"responsive without the fake test: block 1 {tg[f'{rd}_resp_block1'].sum()}, block 2 {tg[f'{rd}_resp_block2'].sum()}")
for rd in ("a", "b"):
    print(f"reading ({rd}): units responsive to block 1 {tg[f'{rd}_resp_block1'].sum()} of {tg.units.sum()}, to block 2 {tg[f'{rd}_resp_block2'].sum()} of {tg.units.sum()}")
print("units responsive to both blocks (reading a), share of those responsive to block 2: %.2f"
      % ((tg.a_resp_block2 - tg.a_fake1).sum() / tg.a_resp_block2.sum()))
print("mice with no tagged unit (reading a, block 1 fake):", list(tg.subject[tg.a_fake1 == 0]))
subject  units  a_pulses  a_fake1  a_fake1_0.3Hz  a_fake2  a_fake2_0.3Hz  a_resp_block1  a_resp_block2  b_pulses  b_fake1  b_fake1_0.3Hz  b_fake2  b_fake2_0.3Hz  b_resp_block1  b_resp_block2
    H01     54       426        0              0        1              1             53             52       150        4              4        5              5             48             47
    H02     64       426        4              4        2              2             45             47       150        8              7        4              4             47             51
    H07     64       426        0              0       10             10             63             53       150        1              1        5              5             61             57
    H08     60       426        0              0        4              4             60             56       150        0              0       16             16             60             44
    H09     62       426        2              2        1              1             59             60       150        0              0        5              5             61             56
    H10     64       426        0              0       11             11             64             53       150        0              0       20             19             64             44
    H11     64       426       13             12       16             16             47             44       150       17             14       13             13             30             34
    H12     64       426        1              1        2              2             63             62       150        3              3        6              6             60             57
    H13     64       426        4              4        6              6             38             36       150       11              9       11              6             33             33
    H14     64       426        0              0        3              3             33             30       150        0              0        0              0              0              0
    H17     64       426        0              0        0              0              0              0       150        0              0        0              0              0              0
    H19     64       426        0              0       41             41             64             23       150        0              0       38             38             62             24
    H22     64       426       17             13       23             18             29             23       150       20             16       19             18             28             29
    H23     64       426        3              3       32             31             34              5       150        9              9       30             30             45             24
    H24     46       428        8              8        0              0             32             40       151        0              0        0              0             46             46
    H25     58       426        0              0       10             10             51             41       150        1              1        1              1             56             56
    H27     38       426        4              4        1              1             27             30       150        3              3        0              0             35             38
    H28     64       426        2              2        6              6             38             34       150        0              0        8              7             56             48
    H29     64       426        2              1        3              3             44             43       150        0              0        6              6             61             55
    H31     64       426        4              2       15             14             46             35       150        0              0        0              0             64             64
    H32     64       426       23             23        1              1             38             60       150        1              1        0              0             63             64
    H33     64       426       10             10        2              2             42             50       150        4              4        0              0             60             64
    H34     64       426        9              9        9              9             42             42       150       12             12        2              2             46             56
    H35     58       426        0              0        6              5             52             46       150        5              4       10              7             45             40
    H36     64       426       10             10        4              4             41             47       150        2              2        5              5             53             50
    H37     58       426        8              4        4              4             31             35       150       14              9        1              1             36             49
    H38     64       426        2              2        9              9             48             41       150        6              6        3              3             52             55
    H39     64       426        4              3       17             17             60             47       150        0              0        0              0             64             64
    H40     64       426        4              4        6              6             34             32       150        5              5        6              6             40             39
    H41     62       426        7              7        8              8             45             44       150        0              0       10             10             62             52
    H42     62       426        5              5        3              3             30             32       150        7              7        9              9             36             34

light event durations (s) and counts, first session: {np.float64(0.005): np.int64(480), np.float64(0.01): np.int64(300), np.float64(0.1): np.int64(72), np.float64(0.228): np.int64(1), np.float64(0.279): np.int64(1)}

tagged units in total (paper 39 + 32 = 71; both counts within 10% needs 63.9 to 78.1):
reading (a): block 1 fake 146, block 2 fake 256, block 1 fake with either order in H31-H42 (lowest) 104; responsive without the fake test: block 1 1353, block 2 1243
reading (a) baseline >= 0.3 Hz: block 1 fake 133, block 2 fake 248, block 1 fake with either order in H31-H42 (lowest) 95; responsive without the fake test: block 1 1353, block 2 1243
reading (b): block 1 fake 133, block 2 fake 233, block 1 fake with either order in H31-H42 (lowest) 102; responsive without the fake test: block 1 1474, block 2 1374
reading (b) baseline >= 0.3 Hz: block 1 fake 117, block 2 fake 222, block 1 fake with either order in H31-H42 (lowest) 91; responsive without the fake test: block 1 1474, block 2 1374
reading (a): units responsive to block 1 1353 of 1902, to block 2 1243 of 1902
reading (b): units responsive to block 1 1474 of 1902, to block 2 1374 of 1902
units responsive to both blocks (reading a), share of those responsive to block 2: 0.88
mice with no tagged unit (reading a, block 1 fake): ['H01', 'H07', 'H08', 'H10', 'H14', 'H17', 'H19', 'H25', 'H35']

Result: blocked, metadata insufficient. With the fake-session test the rule tags 91 to 256 units in total, depending on the reading of the pulses, which block is the fake one, and the 0.3 Hz exclusion: the fewest are 91 (1 Hz pulses, either block order allowed in H31 to H42, baseline at least 0.3 Hz), and with the Methods' order (block 1 fake) 146 and 133 for readings (a) and (b). All are above 78.1. But the test cannot be applied as the Methods mean it. Block 1, taken as the fake session, evokes a short-latency response in 1353 of 1902 units (reading a), and block 2 in 1243; 88% of the units responsive to block 2 also respond to block 1. A session with the patch cord disconnected should evoke no light response, so either neither block is the fake session as the Methods describe it, or the responses are light artefacts on units that are not sorted. The 91 to 256 "tagged" units are then units that cross the threshold in one block and not in the other, and their count does not test the claim. Without labels for the fake and real blocks (and for the projection of each mouse), the claim cannot be tested.

Behavior claim (Figure 5d-h)ΒΆ

fig5dh-behavior-clustersΒΆ

Unsupervised trial-by-trial clustering of pose-derived behavior gives seven clusters for sucrose trials and five for shock trials. Eight measures for the 300 frames (20 s) after CS onset; tsfresh features (about 6000); PCA, t-SNE on the top 30 PCs, a kernel density over the t-SNE map (seaborn kdeplot), local maxima as watershed markers; sucrose and shock trials separately. t-SNE perplexity, kernel bandwidth and seed are not given: the defaults are used with several seeds. Reproduced when the default settings give 7 and 5 clusters for most seeds.

The trials come from the sessions where the fitted pose clock holds (data check), with the CS onsets of the trial table mapped onto the pose clock. Choices the paper does not make, each shown: tsfresh 0.16.0 does not install with this Python (3.13), so the current tsfresh with its default, comprehensive feature set is used (6264 features per trial, against the paper's "~6000"); features that are not finite or constant are dropped. Whether the features are scaled before PCA is not said; both are run. The z-scored features are the reading adopted, since on the raw features the PCA sees only the few features with the largest scales and the cluster counts move with changes of 1e-9 in the features (shown below). "The defaults of scikit-learn" are read two ways: the current defaults (PCA initialisation, automatic learning rate), as installed, and those of the scikit-learn releases contemporary with tsfresh 0.16.0 (2020: random initialisation, learning rate 200). The kernel density is seaborn's default (scipy gaussian_kde, Scott's bandwidth, 200 x 200 grid, 3 bandwidths beyond the data), and each local maximum of the grid is one watershed region.

InΒ [21]:
# Pose measures for the 300 frames after each sucrose and each shock CS, aligned sessions only.
BT = {"sucrose": [], "shock": []}; BW = {"sucrose": [], "shock": []}; lost = {"sucrose": 0, "shock": 0}
Ed = {e["subject"]: e for e in E}
for subj in pl.subject[pl.aligned]:
    d = PC[subj][1]
    for k in BT:
        on = Ed[subj]["trials"][nb.CS_TYPES[k]][0]
        X, kept = nb.behavior_trials(d["measures"], d["clock"], d["rate"], d["t0"], on)
        BT[k].append(X); BW[k] += [subj] * len(X); lost[k] += len(on) - len(kept)
BT = {k: np.concatenate(v) for k, v in BT.items()}; BW = {k: np.array(v) for k, v in BW.items()}
print({k: v.shape for k, v in BT.items()}, "trials dropped (frames beyond the pose):", lost)
print("trials per mouse:", pd.DataFrame({k: pd.Series(BW[k]).value_counts() for k in BW}).fillna(0).astype(int).T.to_string())
for k in BT:
    print(k, "measure ranges:", {m: (round(float(np.nanmin(BT[k][:, j])), 1), round(float(np.nanmax(BT[k][:, j])), 1)) for j, m in enumerate(nb.POSE_MEASURES)})
{'sucrose': (1319, 8, 300), 'shock': (688, 8, 300)} trials dropped (frames beyond the pose): {'sucrose': 0, 'shock': 0}
trials per mouse:          H01  H02  H07  H08  H09  H10  H12  H13  H19  H22  H24  H27  H31  H33  H34  H35  H36  H37  H38  H41  H42
sucrose   60   60   60   60   75   75   75   75   60   60   60   60   60   60   60   60   60   60   60   59   60
shock     30   30   30   30   45   45   45   45   30   30   30   30   30   30   28   30   30   30   30   30   30
sucrose measure ranges: {'velocity': (0.0, 10223.0), 'distance_to_port': (1.9, 596.8), 'direction': (-3.1, 3.1), 'rel_direction': (-180.0, 180.0), 'acceleration': (-151274.8, 153204.8), 'd_distance_to_port': (-8044.7, 8053.2), 'd_direction': (-47.1, 47.1), 'd_rel_direction': (-2699.8, 2699.2)}
shock measure ranges: {'velocity': (0.0, 9844.5), 'distance_to_port': (0.7, 603.2), 'direction': (-3.1, 3.1), 'rel_direction': (-180.0, 180.0), 'acceleration': (-146097.2, 147608.1), 'd_distance_to_port': (-7873.5, 8410.2), 'd_direction': (-47.1, 47.1), 'd_rel_direction': (-2699.2, 2699.9)}
InΒ [22]:
import logging
from tsfresh import extract_features
from tsfresh.feature_extraction import ComprehensiveFCParameters
logging.getLogger("tsfresh").setLevel(logging.ERROR)
FEAT = {}
for k, X in BT.items():
    t = time.time()
    df = pd.DataFrame({"id": np.repeat(np.arange(len(X)), X.shape[2]), "time": np.tile(np.arange(X.shape[2]), len(X))})
    for j, m in enumerate(nb.POSE_MEASURES):
        df[m] = X[:, j, :].ravel()
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        Fk = extract_features(df, column_id="id", column_sort="time", default_fc_parameters=ComprehensiveFCParameters(),
                              n_jobs=4, disable_progressbar=True)
    V = Fk.replace([np.inf, -np.inf], np.nan)
    V = V.loc[:, V.notna().all() & (V.std() > 0)]
    FEAT[k] = V
    print(f"{k}: {len(X)} trials, {Fk.shape[1]} features, {V.shape[1]} finite and not constant ({time.time() - t:.0f} s)")
sucrose: 1319 trials, 6264 features, 5931 finite and not constant (443 s)
shock: 688 trials, 6264 features, 5927 finite and not constant (232 s)
InΒ [23]:
from scipy import ndimage
from threadpoolctl import threadpool_info
def tsne_counts(PCs, seeds=range(5), store=None, **kw):
    """Density peaks of the t-SNE map per seed: all maxima, and maxima above 5% of the highest."""
    counts, counts5 = [], []
    for seed in seeds:
        Y = TSNE(2, random_state=seed, **kw).fit_transform(PCs)
        n, D = nb.density_peaks(Y); counts.append(n)
        counts5.append(int(((D == ndimage.maximum_filter(D, size=3, mode="constant", cval=-np.inf)) & (D > 0.05 * D.max())).sum()))
        if store is not None:
            store[seed] = Y
    return counts, counts5
target = {"sucrose": 7, "shock": 5}
rows = []
EMB = {}
for k, V in FEAT.items():
    for scaling in ("z-scored", "raw"):
        M = ((V - V.mean()) / V.std()).values if scaling == "z-scored" else V.values
        pca = PCA(30, random_state=0).fit(M)
        PCs = pca.transform(M)
        for defaults, kw in (("current", {}), ("2020", {"init": "random", "learning_rate": 200.0})):
            store = {}
            counts, counts5 = tsne_counts(PCs, store=store, **kw)
            EMB.update({(k, scaling, defaults, s): Y for s, Y in store.items()})
            rows.append({"trials": k, "features": scaling, "tsne_defaults": defaults, "pca_solver": pca._fit_svd_solver,
                         "var_30pcs": round(pca.explained_variance_ratio_.sum(), 3), "clusters_by_seed": counts,
                         "clusters_peaks_above_5pct": counts5, "target": target[k], "seeds_at_target": sum(c == target[k] for c in counts)})
bc = pd.DataFrame(rows)
print(bc.to_string(index=False))
both = {(f, d): (bc[(bc.trials == "sucrose") & (bc.features == f) & (bc.tsne_defaults == d)].seeds_at_target.iloc[0] >= 3,
                 bc[(bc.trials == "shock") & (bc.features == f) & (bc.tsne_defaults == d)].seeds_at_target.iloc[0] >= 3)
        for f in ("z-scored", "raw") for d in ("current", "2020")}
print("\n(sucrose 7 in most seeds, shock 5 in most seeds) per reading:", both)
print("readings with both:", [r for r, v in both.items() if all(v)])
for k in FEAT:
    raw_var = FEAT[k].var().sort_values(ascending=False)
    print(f"{k}, raw features: share of the total variance in the largest 1, 3 and 10 features: "
          f"{raw_var.iloc[0] / raw_var.sum():.4f}, {raw_var.iloc[:3].sum() / raw_var.sum():.4f}, {raw_var.iloc[:10].sum() / raw_var.sum():.4f}; largest: {raw_var.index[0]}")
fig, axes = plt.subplots(1, 2, figsize=(9, 4))
for ax, k in zip(axes, ("sucrose", "shock")):
    Y = EMB[(k, "z-scored", "current", 0)]
    ax.scatter(Y[:, 0], Y[:, 1], s=3, c=pd.factorize(BW[k])[0], cmap="tab20")
    ax.set_title(f"{k} trials, t-SNE (z-scored features, current defaults, seed 0)", fontsize=9); ax.set_xticks([]); ax.set_yticks([])
plt.tight_layout(); plt.show()
print("colours are mice; all points are plotted")

# Sensitivity to numerical detail that the seeds do not fix, with the current defaults: an exact PCA
# (svd_solver="full") in place of the randomized one PCA(30) chooses here, and each feature
# multiplied by (1 + 1e-9 * standard normal noise) before scaling. Seeds 0 to 2.
print("\nBLAS and OpenMP libraries (the PCA's arithmetic depends on them):",
      [(i.get("internal_api"), i.get("version"), i.get("architecture"), i.get("num_threads")) for i in threadpool_info()])
rng_b = np.random.default_rng(5)
for k, V in FEAT.items():
    noisy = V.values * (1 + 1e-9 * rng_b.standard_normal(V.shape))
    for scaling in ("z-scored", "raw"):
        for name, (Mv, solver) in {"exact PCA": (V.values, "full"), "features x (1 + 1e-9 noise)": (noisy, "auto")}.items():
            Mv = (Mv - Mv.mean(0)) / Mv.std(0, ddof=1) if scaling == "z-scored" else Mv
            counts, _ = tsne_counts(PCA(30, svd_solver=solver, random_state=0).fit_transform(Mv), seeds=range(3))
            print(f"{k}, {scaling} features, {name}: clusters by seed {counts}")

# Variant: the adopted reading (z-scored features, current defaults) without H08, H10 and H24,
# whose head-to-port distance under the fitted clock is least below that over all frames.
weak = ["H08", "H10", "H24"]
print("\nhead-to-port distance (px) under the map / over all frames:", {s: (float(pl.set_index("subject").dist_fit[s]), float(pl.set_index("subject").dist_all_frames[s])) for s in weak})
for k, V in FEAT.items():
    Vk = V[~np.isin(BW[k], weak)]
    Vk = Vk.loc[:, Vk.std() > 0]
    counts, _ = tsne_counts(PCA(30, random_state=0).fit_transform(((Vk - Vk.mean()) / Vk.std()).values))
    print(f"{k} without {weak}: {len(Vk)} trials, clusters by seed {counts}, at target {sum(c == target[k] for c in counts)} of 5")
print(f"\nnotebook ran in {(time.time() - started) / 60:.1f} min")
 trials features tsne_defaults pca_solver  var_30pcs clusters_by_seed clusters_peaks_above_5pct  target  seeds_at_target
sucrose z-scored       current randomized      0.421  [9, 9, 9, 9, 9]           [9, 9, 9, 9, 9]       7                0
sucrose z-scored          2020 randomized      0.421  [7, 7, 8, 8, 7]           [7, 7, 8, 8, 7]       7                3
sucrose      raw       current randomized      1.000  [7, 7, 7, 7, 7]           [7, 7, 7, 7, 7]       7                5
sucrose      raw          2020 randomized      1.000  [6, 4, 6, 6, 4]           [6, 4, 6, 6, 4]       7                0
  shock z-scored       current randomized      0.409  [4, 4, 4, 4, 4]           [4, 4, 4, 4, 4]       5                0
  shock z-scored          2020 randomized      0.409  [4, 4, 4, 5, 3]           [4, 4, 4, 5, 3]       5                1
  shock      raw       current randomized      1.000  [5, 5, 5, 5, 5]           [5, 5, 5, 5, 5]       5                5
  shock      raw          2020 randomized      1.000  [3, 4, 6, 4, 3]           [3, 4, 6, 4, 3]       5                0

(sucrose 7 in most seeds, shock 5 in most seeds) per reading: {('z-scored', 'current'): (np.False_, np.False_), ('z-scored', '2020'): (np.True_, np.False_), ('raw', 'current'): (np.True_, np.True_), ('raw', '2020'): (np.False_, np.False_)}
readings with both: [('raw', 'current')]
sucrose, raw features: share of the total variance in the largest 1, 3 and 10 features: 0.6309, 0.9530, 1.0000; largest: acceleration__time_reversal_asymmetry_statistic__lag_1
shock, raw features: share of the total variance in the largest 1, 3 and 10 features: 0.6414, 0.9753, 1.0000; largest: acceleration__time_reversal_asymmetry_statistic__lag_1
No description has been provided for this image
colours are mice; all points are plotted

BLAS and OpenMP libraries (the PCA's arithmetic depends on them): [('openmp', None, None, 4), ('openblas', '0.3.34.106.0', 'SkylakeX', 4), ('openblas', '0.3.31.dev', 'SkylakeX', 4)]
sucrose, z-scored features, exact PCA: clusters by seed [7, 7, 7]
sucrose, z-scored features, features x (1 + 1e-9 noise): clusters by seed [7, 7, 7]
sucrose, raw features, exact PCA: clusters by seed [9, 9, 9]
sucrose, raw features, features x (1 + 1e-9 noise): clusters by seed [8, 8, 8]
shock, z-scored features, exact PCA: clusters by seed [3, 3, 3]
shock, z-scored features, features x (1 + 1e-9 noise): clusters by seed [3, 3, 3]
shock, raw features, exact PCA: clusters by seed [5, 5, 5]
shock, raw features, features x (1 + 1e-9 noise): clusters by seed [5, 5, 5]

head-to-port distance (px) under the map / over all frames: {'H08': (29.0, 35.1), 'H10': (27.7, 34.8), 'H24': (38.4, 46.8)}
sucrose without ['H08', 'H10', 'H24']: 1124 trials, clusters by seed [5, 5, 5, 5, 5], at target 0 of 5
shock without ['H08', 'H10', 'H24']: 583 trials, clusters by seed [3, 3, 3, 3, 3], at target 0 of 5

notebook ran in 24.4 min

Result: partially reproduced. On 1319 sucrose and 688 shock trials from the 21 sessions with an aligned pose clock, no reading gives both counts: each of the four readings meets one count and not the other. With z-scored features, sucrose trials give 7 clusters for 5 of 5 seeds under the installed defaults (7, 7, 8, 7, 8 under the 2020 defaults, 3 of 5), and shock trials give 3 for 5 of 5 (4, 3, 4, 3, 3 under the 2020 defaults), never 5. With raw features, sucrose trials give 6 for 5 of 5 (7, 5, 7, 5, 6 under the 2020 defaults) and shock trials 5 for 5 of 5 (5, 5, 5, 6, 7 under the 2020 defaults, 3 of 5). The raw-feature counts are not stable: on the raw features the 3 largest features carry 95% (sucrose) and 98% (shock) of the variance and the 10 largest all of it, and with the installed defaults an exact PCA gives 8 and 5 clusters, and multiplying each feature by 1 + 1e-9 noise gives 7 and 7. With z-scored features the same changes give 7 and 4 clusters: the sucrose count stays at 7 and the shock count moves from 3 to 4, below 5 either way. The reading adopted is z-scored features with the installed defaults (7 and 3): the sucrose count holds and the shock count does not, as with the 2020 defaults; with raw features it is the reverse. Without H08, H10 and H24, whose pose clock is checked least well (head-to-port distance 29.0, 27.7 and 38.4 px under the map against 35.1, 34.8 and 46.8 px over all frames), the adopted reading gives 5 and 4. The trials rest on a pose clock fitted to the data, and the count of the sucrose trials also moves with the sessions included.

SummaryΒΆ

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.

Claim Outcome
fig3k-nt-sensor-sucrose-increase not reproduced (higher late in every reading, but p = 0.093 with the first 16 trials; p = 0.0046 with all trials only under the per-trial z-score and trial-averaged fit)
fig3l-nt-sensor-airpuff-decrease not reproduced (right direction, p = 0.074 at best)
fig3-nt-sensor-early-dip not reproduced (sucrose below zero, airpuff above)
fig4-recording-count partially reproduced (31 mice against 30; 1902 or 1739 units against 685)
fig4c-photoidentification blocked, metadata_insufficient (the "fake" block evokes responses in most units, and no block or projection labels)
fig4d-seven-clusters blocked, methods_underspecified (7 clusters only with an 820 ms smoothing window)
fig5dh-behavior-clusters partially reproduced (7 sucrose clusters, 3 shock clusters, adopted reading; no reading gives both)
13 claims needing group or projection labels blocked, metadata_insufficient
19 claims on experiments not in the dandiset blocked, data_missing

How the analysis differs from the paper's. The photometry cues are taken as CS-US trials with the US 2 s after onset, since the files mark neither the US nor unpaired cues; the photometry z-score is run with per-trial and session baselines; the clustering smoothing window is run over a range since MATLAB's smoothing factor cannot be reproduced; the units are the files' units, not sorted single units; the fake phototagging block is taken as the first one, as the Methods order them; the pose is put on the trial clock by a linear map fitted to the port entries, in 21 of 29 sessions; tsfresh is the current version, not 0.16.0, even under the 2020 t-SNE defaults, and the t-SNE defaults and feature scaling are run both ways.

What was found about the dataset. The files carry no CRISPR or control label and no tagged projection, which most of the paper's electrophysiology findings need; the units are not sorted single units (frequent sub-millisecond intervals, spikes shared between units, 64 units in most sessions); H17's spike times stop at 249 s of a 12118 s recording; the pose has no timestamps and on its nominal clock of 15 frames per second the head reaches the port up to 192 s (median 80 s) away from the port entries, a drift that a rate factor of about 0.988 removes in 21 sessions; there are 31 mice where the paper has 30, and two of them may not be recorded on their last day; six sessions have no neutral CS though the description says 2:1:1; the two light blocks are not labelled fake and real, and both evoke short-latency responses in most units (1353 and 1243 of 1902); 27 files hold confocal images of the PVT with no channel or group label; the photometry series interleaves three frame types at 60 Hz in one series, in one session the LED labels move by one frame against the data, the cue events last 2 s where the Methods give 4 s, and no event marks the US or unpaired cues. Only the NT sensor experiment of the photometry is deposited; the behavioral cohorts, the axon-terminal and somatic photometry, the anatomy and the slice physiology are not.