Reproducing Randi et al. (2023) from dandiset 001075¶

Paper. Randi F, Sharma AK, Dvali S, Leifer AM (2023). Neural signal propagation atlas of Caenorhabditis elegans. Nature 623, 406-414. doi:10.1038/s41586-023-06683-4. This attempt used the full text (open access, PubMed Central PMC10632145), its Methods, figure legends and the Source Data files of the article.

Dandiset. DANDI:001075, version 0.240930.1859. The paper cites no DANDI version (its data statement points to OSF). There are two published versions. They list the same 223 files; every file was uploaded again, the imaging files with the same sizes and the segmentation files with new content. The later version adds, to every segmentation file, the column target_pumpprobe_id (the manually located target of a stimulation) and NaN-interpolated copies of the traces. The stimulated neuron cannot be named without the target, so the later version, which is also the most recent, is pinned. The difference is shown in the data check.

What the dandiset holds. The 113 wild-type-background (WT) recordings of the paper: per animal, the raw volumes (about 40 GB) and, for 110 animals, a small segmentation file with the green (GCaMP6s) and red (tagRFP-T) intensity of every tracked neuron, NeuroPAL labels, and the table of optogenetic stimulations with their target positions. It does not hold the 18 unc-31 recordings, the spontaneous recordings of Fig. 6, or the unstimulated control recordings that the paper's q values compare against.

Outside inputs. The list of the 188 head neurons, from the paper's code (GitHub leiferlab/pumpprobe, commit 1dbc5e0). The paper's Source Data (Extended Data Figs. 3a, 4a, 4b), for the per-pair link claim and the q < 0.05 selection of one claim. The paper's exported recordings on OSF (doi 10.17605/OSF.IO/E2SYT, raw_extracted_data, file version 1), for the recording link claim, and its unc-31 export (exported_data_unc31.tar.gz) for fig4-rid-unc31.

Method. Traces are pre-processed as the Methods state; where the Methods leave a choice open the paper's code settles it (see the docstring of sigprop.py). The neuron stimulated in each event is the manually located ROI where the file gives one, otherwise the ROI nearest the target position. The photobleaching fit is checked against a full-resolution fit on recordings drawn with a fixed seed; nothing else is random.

Claim Data Tested
recordings-protocol: 113 WT animals, up to 40 min, a stimulation every 30 s dandiset yes
pairs-measured: 23,433 pairs, 186 of 188 neurons, up to 59 observations dandiset yes
fig1e-avjr-avdr: AVJR stimulation evokes AVDR activity dandiset yes
fig1f-aver-avar: AVER stimulation evokes AVAR activity dandiset yes
fig1g-saadl-ollr: SAADL stimulation inhibits OLLR dandiset yes
edfig2ij-spatial-specificity: neighbours of the target mostly do not respond dandiset yes
sourcedata-link: per-pair counts and means agree with the Source Data with inputs yes
fig2d-inhibitory-fraction: 11% of q < 0.05 connections are inhibitory with inputs yes
osf-link: recordings and stimulated neurons link to the OSF export with labels yes
fig2c-1310-connected, fig2c-bilateral, fig2efg-path-length dandiset blocked: data missing (control recordings)
fig3-anatomy-predictions dandiset blocked: data missing (model predictions)
edfig6-kernel-stereotypy dandiset blocked: not runnable
edfig7b-unc31-fewer, fig5-extrasynaptic-screen dandiset blocked: data missing
fig4-rid-wt: URX, ADL and AWB respond to RID stimulation dandiset yes
fig4-rid-unc31: these responses are reduced in unc-31 elsewhere (OSF) yes
fig6-spontaneous elsewhere (OSF) blocked: not runnable (needs the kernels of edfig6)
In [1]:
import hashlib
import io
import json
import os
import re
import time
import urllib.request
import warnings

os.environ.setdefault("OMP_NUM_THREADS", "1")
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")

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

import sigprop as sp

warnings.filterwarnings("ignore", category=RuntimeWarning)
pd.set_option("display.width", 200)
pd.set_option("display.max_columns", 30)
pd.set_option("display.max_rows", 200)

started = time.time()
assets = sp.asset_list()
seg = sorted([a for a in assets if "desc-segmentation" in a["path"]], key=lambda a: sp.session_key(a["path"])[0])
img = sorted([a for a in assets if "desc-imaging" in a["path"]], key=lambda a: sp.session_key(a["path"])[0])
print(f"{len(assets)} assets in dandiset {sp.DANDISET_ID} version {sp.DANDISET_VERSION}: "
      f"{len(img)} imaging, {len(seg)} segmentation files")
depth = sp.load_many(sp.load_imaging_depth, [sp.asset_url(a["asset"]) for a in img])
print(f"read frame depths of the imaging files in {time.time() - started:.0f} s")
res = sp.analyse_all([sp.asset_url(a["asset"]) for a in seg])
print(f"loaded and processed {len(res)} recordings in {time.time() - started:.0f} s")
head = sp.head_neurons()
print("head neurons in the paper's code:", len(head))
223 assets in dandiset 001075 version 0.240930.1859: 113 imaging, 110 segmentation files
read frame depths of the imaging files in 170 s
loaded and processed 110 recordings in 629 s
head neurons in the paper's code: 188

Data check¶

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

In [2]:
sub_img = {sp.session_key(a["path"])[0]: sp.session_key(a["path"])[1] for a in img}
sub_seg = {sp.session_key(a["path"])[0]: sp.session_key(a["path"])[1] for a in seg}
print("Subjects with an imaging file:", len(sub_img), "; with a segmentation file:", len(sub_seg))
print("Subjects with imaging only:", sorted(set(sub_img) - set(sub_seg)))
print("Every subject has one session:", all(sub_img[s] == sub_seg[s] for s in sub_seg))
sizes = pd.Series([a["size"] for a in seg]) / 1e6
print(f"Segmentation file sizes {sizes.min():.1f} to {sizes.max():.1f} MB; imaging files "
      f"{min(a['size'] for a in img) / 1e9:.1f} to {max(a['size'] for a in img) / 1e9:.1f} GB")

# The two published versions: which files differ, and how.
from dandi.dandiapi import DandiAPIClient
client = DandiAPIClient()
old = {a.path: a for a in client.get_dandiset(sp.DANDISET_ID, "0.240920.1434").get_assets()}
new = {a["path"]: a["asset"] for a in assets}
print("\nVersions 0.240920.1434 and 0.240930.1859 list the same paths:", set(old) == set(new))
same_blob = {p: old[p].blob == new[p].blob for p in new}
same_size = {p: old[p].size == new[p].size for p in new}
print("Imaging files: same blob %d, same size %d, of %d" % (sum(same_blob[a["path"]] for a in img), sum(same_size[a["path"]] for a in img), len(img)))
print("Segmentation files: same blob %d, same size %d, of %d" % (sum(same_blob[a["path"]] for a in seg), sum(same_size[a["path"]] for a in seg), len(seg)))
import h5py, remfile
def h5_names(asset):
    with h5py.File(remfile.File(sp.asset_url(asset)), "r") as f:
        names = []
        f.visit(lambda n: names.append(n) if "OptogeneticStimulusTarget" not in n and not n.startswith("specifications") else None)
        return set(names)
p0 = seg[0]["path"]
n_old, n_new = h5_names(old[p0]), h5_names(new[p0])
print(f"\n{p0}: only in 0.240920.1434:", sorted(n_old - n_new))
print(f"{p0}: only in 0.240930.1859:", sorted(n_new - n_old))
Subjects with an imaging file: 113 ; with a segmentation file: 110
Subjects with imaging only: [20, 23, 33]
Every subject has one session: True
Segmentation file sizes 1.9 to 42.1 MB; imaging files 4.4 to 60.5 GB
Versions 0.240920.1434 and 0.240930.1859 list the same paths: True
Imaging files: same blob 0, same size 113, of 113
Segmentation files: same blob 0, same size 0, of 110
sub-0/sub-0_ses-20210824-104000_desc-segmentation_ophys+ogen.nwb: only in 0.240920.1434: ['processing/ophys/GreenSignals/GreenSignal', 'processing/ophys/GreenSignals/GreenSignal/data', 'processing/ophys/GreenSignals/GreenSignal/table_region', 'processing/ophys/GreenSignals/GreenSignal/timestamps', 'processing/ophys/RedSignals/RedSignal', 'processing/ophys/RedSignals/RedSignal/data', 'processing/ophys/RedSignals/RedSignal/table_region', 'processing/ophys/RedSignals/RedSignal/timestamps']
sub-0/sub-0_ses-20210824-104000_desc-segmentation_ophys+ogen.nwb: only in 0.240930.1859: ['intervals/OptogeneticStimulusTable/target_pumpprobe_id', 'processing/ophys/GreenSignals/BaseGreenSignal', 'processing/ophys/GreenSignals/BaseGreenSignal/data', 'processing/ophys/GreenSignals/BaseGreenSignal/table_region', 'processing/ophys/GreenSignals/BaseGreenSignal/timestamps', 'processing/ophys/GreenSignals/InterpolatedGreenSignal', 'processing/ophys/GreenSignals/InterpolatedGreenSignal/data', 'processing/ophys/GreenSignals/InterpolatedGreenSignal/timestamps', 'processing/ophys/RedSignals/BaseRedSignal', 'processing/ophys/RedSignals/BaseRedSignal/data', 'processing/ophys/RedSignals/BaseRedSignal/table_region', 'processing/ophys/RedSignals/BaseRedSignal/timestamps', 'processing/ophys/RedSignals/InterpolatedRedSignal', 'processing/ophys/RedSignals/InterpolatedRedSignal/data', 'processing/ophys/RedSignals/InterpolatedRedSignal/timestamps']

The paper reports 113 WT-background animals; the dandiset has 113 subjects, one session each, but 3 subjects (listed above) have only the raw imaging file, so their extracted traces and stimulation tables are not available here. Between the two published versions every file was uploaded again; the imaging files kept their sizes, and in the segmentation files GreenSignal and RedSignal were renamed Base... and joined by NaN-interpolated copies, and the stimulation table gained target_pumpprobe_id. The analysis uses that column, so the earlier version could not support it in the same way.

In [3]:
meta = pd.DataFrame([{
    "subject": int(r["subject_id"]), "strain": r["strain"], "genotype": r["genotype"],
    "stage": r["growth_stage"], "age": r["age"], "start": r["session_start"][:19],
    "minutes": (r["t1"] - r["t0"]) / 60, "volumes": r["n_vol"], "rois": r["n_roi"],
    "stims": len(r["stim_start"]), "dt": r["dt"], "dt_max": r["dt_max"],
    "nan_frac": r["nan_frac"]} for r in res])
print(meta[["strain", "genotype", "stage", "age"]].value_counts().to_string())
print("\nSubjects with strain 'none':", meta[meta.strain == "none"].subject.tolist())
print("\nRecording length (min): min %.1f, median %.1f, max %.1f; longer than 40 min: %d, longer than 44 min: %d"
      % (meta.minutes.min(), meta.minutes.median(), meta.minutes.max(), (meta.minutes > 40).sum(), (meta.minutes > 44).sum()))
print("Volume interval (s): median over recordings %.4f (2 volumes/s in the Methods); largest gap %.2f s"
      % (meta.dt.median(), meta.dt_max.max()))
print("Recordings with a gap above 1 s:", meta[meta.dt_max > 1].subject.tolist())
print("Stimulations per recording: min %d, median %d, max %d, total %d"
      % (meta.stims.min(), meta.stims.median(), meta.stims.max(), meta.stims.sum()))
print("Recordings with fewer than 10 stimulations:", meta[meta.stims < 10][["subject", "stims"]].values.tolist())
dep = pd.DataFrame(depth)
dep["subject"] = [sp.session_key(a["path"])[0] for a in img]
print("\nImaging files: frame interval %s s; depth per frame %.1f to %.1f um; plane step median %.3f um (range %.3f to %.3f)"
      % (np.unique(dep.frame_dt.round(4)).tolist(), dep.depth_min.min(), dep.depth_max.max(), dep.plane_step_um.median(),
         dep.plane_step_um.min(), dep.plane_step_um.max()))
m = meta.merge(dep, on="subject")
print("Frames per volume (imaging frames / segmentation volumes): median %.1f"
      % (m.n_frames / m.volumes).median())
print("Indicator stated for the red channel:", sorted(set(dep.red_indicator)))
strain  genotype  stage  age
AML462  WT        L4     P1D    62
                  YA     P1D    46
none    WT        L4     P1D     1
                  YA     P1D     1

Subjects with strain 'none': [24, 25]

Recording length (min): min 3.3, median 31.7, max 48.7; longer than 40 min: 13, longer than 44 min: 3
Volume interval (s): median over recordings 0.5011 (2 volumes/s in the Methods); largest gap 8.41 s
Recordings with a gap above 1 s: [2, 46, 102]
Stimulations per recording: min 3, median 54, max 92, total 5614
Recordings with fewer than 10 stimulations: [[24, 3], [25, 3], [29, 5], [52, 7], [54, 6], [55, 8], [56, 4], [57, 5], [59, 4], [62, 6], [103, 8]]

Imaging files: frame interval [0.02] s; depth per frame 2.5 to 37.5 um; plane step median 1.219 um (range 1.216 to 1.423)
Frames per volume (imaging frames / segmentation volumes): median 25.0
Indicator stated for the red channel: ['GCaMP6s']

All recordings are strain AML462 (TWISP), genotype WT, except two whose strain is given as "none". The age field is P1D for every animal, whatever its stage (L4 or young adult). Volumes come every 0.501 s, the 2 volumes per second of the Methods, from about 25 frames each at 50 frames per second. Some recordings run longer than the "up to 40 min" of the paper. The red optical channel is described with the indicator GCaMP6s, while the red channel images tagRFP-T; this is a metadata error.

In [4]:
st = pd.DataFrame([{
    "subject": int(r["subject_id"]), "dur": d, "power": p, "manual": np.isfinite(m_),
    "tx": x, "ty": y, "tz": z}
    for r in res for d, p, m_, x, y, z in zip(r["stim_stop"] - r["stim_start"], r["stim_power"],
                                               r["stim_manual_roi"], r["target_x"], r["target_y"], r["target_z"])])
print("Stimulation durations (s):", np.unique(st.dur.round(3)).tolist(), "; powers (W):", np.unique(st.power).tolist())
isi = np.concatenate([np.diff(r["stim_start"]) for r in res])
print("Interval between stimulations (s): 5th %.1f, median %.2f, 95th %.1f percentile"
      % tuple(np.percentile(isi, [5, 50, 95])))
print("Stimulations with a manually located target ROI:", int(st.manual.sum()), "of", len(st))
zero = (st.tx == 0) & (st.ty == 0)
print("Stimulations with target position (0, 0):", int(zero.sum()), "; of these with a manual ROI:", int((zero & st.manual).sum()))
print("Target 'depth_in_um' range: %.3f to %.3f (imaging depths %.1f to %.1f um)"
      % (st.tz.min(), st.tz.max(), dep.depth_min.min(), dep.depth_max.max()))
print("Target x range %.0f to %.0f, y %.0f to %.0f pixels; ROI centroid x %.0f to %.0f, y %.0f to %.0f, plane index %.0f to %.0f"
      % (st.tx[~zero].min(), st.tx.max(), st.ty[~zero].min(), st.ty.max(),
         min(r["centroids"][:, 0].min() for r in res), max(r["centroids"][:, 0].max() for r in res),
         min(r["centroids"][:, 1].min() for r in res), max(r["centroids"][:, 1].max() for r in res),
         min(r["centroids"][:, 2].min() for r in res), max(r["centroids"][:, 2].max() for r in res)))
print("Green and red centroids identical in every file:", all(r["centroids_red_equal"] for r in res))
Stimulation durations (s): [0.5] ; powers (W): [0.0012]
Interval between stimulations (s): 5th 30.8, median 30.81, 95th 38.0 percentile
Stimulations with a manually located target ROI: 2815 of 5614
Stimulations with target position (0, 0): 122 ; of these with a manual ROI: 0
Target 'depth_in_um' range: -0.875 to 0.875 (imaging depths 2.5 to 37.5 um)
Target x range 122 to 459, y 118 to 376 pixels; ROI centroid x 0 to 510, y 14 to 504, plane index 0 to 24
Green and red centroids identical in every file: True

Every stimulation is stored as 0.5 s at 1.2 mW. The conversion code (leifer_lab_to_nwb) writes both as constants from the paper, so the files cannot show the 0.1 to 0.5 s pulses the Methods describe for the AFD-AIY stimulations. Stimulations come every 30.8 s at the median. Half of the stimulations carry a manually located target ROI. The column depth_in_um holds values from -0.875 to 0.875, which cannot be micrometres in an imaging volume 2.5 to 37.5 um deep; its unit is not stated (the acquisition code converts it as a tunable-lens setting). Targets at (0, 0) mark stimulations with no stored target position; none of them carries a manual ROI, so they are left unassigned.

In [5]:
units = sorted({r["green_unit"] for r in res})
print("Unit of the green and red series:", units)
neg = [(int(r["subject_id"]), round(r["green_range"][0])) for r in res if r["green_range"][0] < -100]
print("Recordings whose green trace goes below -100 (subject, minimum):", neg)
g = np.array([r["green_range"] for r in res]); rd = np.array([r["red_range"] for r in res])
print("Green (base) per recording: min %.1f, median of medians %.1f, max %.1f" % (g[:, 0].min(), np.median(g[:, 1]), g[:, 2].max()))
print("Red (base) per recording: min %.1f, median of medians %.1f, max %.1f" % (rd[:, 0].min(), np.median(rd[:, 1]), rd[:, 2].max()))
print("Fraction of missing (NaN) values in the base green traces: median %.3f, max %.3f" % (meta.nan_frac.median(), meta.nan_frac.max()))
print("Interpolated traces equal the base traces where the base is defined:", all(r["interp_matches_base"] for r in res))
print("All series of a file share one timestamp array:", all(r["same_timestamps"] for r in res))
print("Photobleaching fits that failed (trace left uncorrected): %d of %d traces"
      % (sum(r["photobleach_failed"] for r in res), sum(r["n_roi"] for r in res)))
pbs = pd.Series(np.concatenate([r["photobleach_status"] for r in res]))
print("  by kind:", pbs.value_counts().to_dict())
neg_recs = {k for k, r in enumerate(res) if r["green_range"][0] < -100}
pb_neg = sum(int(np.sum(res[k]["photobleach_status"] != "ok")) for k in neg_recs)
print("  of the failed fits, in the %d recordings with traces below -100: %d" % (len(neg_recs), pb_neg))
pbr = pd.Series({int(r["subject_id"]): int(r["photobleach_failed"]) for r in res})
print("  recordings with any failed fit: %d; most failures in one recording (subject: count):" % (pbr > 0).sum(),
      pbr.sort_values(ascending=False).head(12).to_dict())
Unit of the green and red series: ['n.a.']
Recordings whose green trace goes below -100 (subject, minimum): [(2, -196646), (3, -2549), (4, -1135), (34, -8622), (35, -4304), (36, -8201), (38, -562), (39, -965), (40, -1489)]
Green (base) per recording: min -196646.1, median of medians 15.0, max 31263.6
Red (base) per recording: min 0.0, median of medians 48.6, max 3074.5
Fraction of missing (NaN) values in the base green traces: median 0.420, max 0.682
Interpolated traces equal the base traces where the base is defined: True
All series of a file share one timestamp array: True
Photobleaching fits that failed (trace left uncorrected): 897 of 14039 traces
  by kind: {'ok': 13142, 'missing': 729, 'near zero': 138, 'failed': 28, 'nonpositive': 2}
  of the failed fits, in the 9 recordings with traces below -100: 4
  recordings with any failed fit: 76; most failures in one recording (subject: count): {88: 77, 92: 67, 73: 55, 79: 41, 45: 33, 75: 33, 85: 33, 69: 28, 70: 24, 93: 23, 48: 22, 89: 21}

The traces carry the unit "n.a." and are described only as "average baseline fluorescence"; the files do not say whether they are background-subtracted or corrected in any way. Median values of tens are consistent with raw intensities, but nine recordings hold values far below zero (down to -196,646), which an intensity cannot take; they are kept, and the 5-standard-deviation outlier step of the Methods removes only positive outliers. The interpolated copy agrees with the base trace wherever the base is defined. Traces whose photobleaching fit fails are left uncorrected, as in the code; a falling uncorrected trace gives a negative <dF/F0>_t, so the claims that use signs show the result without these traces as a variant, and likewise without rows whose F0 is not positive.

In [6]:
# Check of the shortened photobleaching fit: re-run 12 recordings, drawn with a fixed seed, with
# the fit on every volume and no limit on evaluations, as in the code, and compare the outputs.
rng_pb = np.random.default_rng(1075)
pick = sorted(rng_pb.choice(len(res), 12, replace=False).tolist())
full = sp.analyse_all([res[k]["url"] for k in pick], pb_points=None, pb_nfev=None)
rows = []
for k, f in zip(pick, full):
    r = res[k]
    ok = np.isfinite(r["dff"]) & np.isfinite(f["dff"])
    rows.append({"subject": int(r["subject_id"]), "traces": r["n_roi"],
                 "pb_not_ok_short": int(np.sum(r["photobleach_status"] != "ok")),
                 "pb_not_ok_full": int(np.sum(f["photobleach_status"] != "ok")),
                 "dff_pearson": np.corrcoef(r["dff"][ok], f["dff"][ok])[0, 1],
                 "dff_median_abs_diff": np.median(np.abs(r["dff"][ok] - f["dff"][ok])),
                 "sign_agree": np.mean(np.sign(r["dff"][ok]) == np.sign(f["dff"][ok])),
                 "auto_agree": np.mean(r["auto"] == f["auto"]),
                 "responder_calls_agree": np.mean([np.array_equal(np.isin(np.arange(r["n_roi"]), a), np.isin(np.arange(r["n_roi"]), b))
                                                   for a, b in zip(r["responders"], f["responders"])]),
                 "roi_calls_agree": np.mean(np.concatenate([np.isin(np.arange(r["n_roi"]), a) == np.isin(np.arange(r["n_roi"]), b)
                                                             for a, b in zip(r["responders"], f["responders"])])),
                 "responders_short": sum(len(a) for a in r["responders"]), "responders_full": sum(len(b) for b in f["responders"])})
pbc = pd.DataFrame(rows)
print(pbc.round(4).to_string(index=False))
print("Over the 12 recordings: failed fits %d (short) and %d (full); median Pearson of <dF/F0>_t %.4f; sign agreement %.4f; "
      "autoresponse agreement %.4f; classifier calls per (stimulation, ROI) agreeing %.4f; responses detected %d (short) and %d (full)"
      % (pbc.pb_not_ok_short.sum(), pbc.pb_not_ok_full.sum(), pbc.dff_pearson.median(),
         np.average(pbc.sign_agree, weights=pbc.traces), pbc.auto_agree.mean(), np.average(pbc.roi_calls_agree, weights=pbc.traces),
         pbc.responders_short.sum(), pbc.responders_full.sum()))
 subject  traces  pb_not_ok_short  pb_not_ok_full  dff_pearson  dff_median_abs_diff  sign_agree  auto_agree  responder_calls_agree  roi_calls_agree  responders_short  responders_full
      13      90                0               0       0.9958               0.0008      0.9826      1.0000                 0.6769           0.9952               835              825
      22     169                4               3       0.9846               0.0005      0.9852      1.0000                 0.6533           0.9962              1212             1206
      27      91                1               1       0.9963               0.0010      0.9868      0.9538                 0.5385           0.9902              1203             1183
      29      80                1               1       1.0000               0.0000      1.0000      1.0000                 1.0000           1.0000                98               98
      40     127                2               2       1.0000               0.0004      0.9931      1.0000                 0.7231           0.9973              1749             1747
      59      83                2               2       1.0000               0.0000      1.0000      1.0000                 1.0000           1.0000                48               48
      81     135                4               5       0.9979               0.0008      0.9900      1.0000                 0.4783           0.9950               877              876
      89     140               21              21       0.9969               0.0007      0.9881      1.0000                 0.3750           0.9938               629              630
      91     147                7               6       0.9215               0.0006      0.9909      0.9808                 0.5769           0.9945              1088             1100
      96     119                1               1       0.9932               0.0008      0.9845      1.0000                 0.4878           0.9947               445              427
     102     123                9               5       0.9992               0.0007      0.9887      1.0000                 0.2642           0.9903               896              869
     112     110                7               7       0.9993               0.0008      0.9907      1.0000                 0.3667           0.9911              1081             1094
Over the 12 recordings: failed fits 59 (short) and 54 (full); median Pearson of <dF/F0>_t 0.9974; sign agreement 0.9896; autoresponse agreement 0.9946; classifier calls per (stimulation, ROI) agreeing 0.9947; responses detected 10161 (short) and 10103 (full)

On the 12 recordings re-run with the photobleaching fit on every volume and no limit on evaluations, as in the code, the <dF/F0>_t values correlate with those of the shortened fit with a median Pearson coefficient of 0.997, their signs agree for 98.9% of values, the autoresponse decision agrees for 99.5% of stimulations, and 99.5% of the classifier's calls per stimulation and ROI agree (10,160 responses detected with the shortened fit, 10,103 with the full one). The shortened fit fails on 58 traces of these recordings and the full fit on 53. Over all recordings, 729 of the 897 traces left uncorrected have values missing even in the interpolated copy, 138 have a fit that comes within 0.01 of zero, 28 do not converge and 2 have no positive value. The shortened fit is kept.

In [7]:
lab = []
for r in res:
    names = [sp.atlas_name(x) for x in r["roi_label"]]
    named = [n for n in names if n]
    in_head = [n for n in named if n in head]
    dup = pd.Series(in_head).value_counts()
    lab.append({"subject": int(r["subject_id"]), "rois": r["n_roi"], "labelled": len(named), "head_labelled": len(in_head),
                "head_distinct": len(set(in_head)), "dup_labels": int((dup > 1).sum()),
                "other_ids": ",".join(r["neuropal_ids_other"])})
lab = pd.DataFrame(lab)
print("ROIs per recording: min %d, median %d, max %d" % (lab.rois.min(), lab.rois.median(), lab.rois.max()))
print("ROIs with a NeuroPAL label: median %d per recording; with a head-neuron label: median %d (%d distinct)"
      % (lab.labelled.median(), lab.head_labelled.median(), lab.head_distinct.median()))
print("Recordings in which a head label is used for more than one ROI:", int((lab.dup_labels > 0).sum()),
      "; such labels per recording, max", lab.dup_labels.max())
print("Head neurons labelled in at least one recording:", len({sp.atlas_name(x) for r in res for x in r["roi_label"]} & set(head)), "of", len(head))
print("Recordings whose neuropal_ids hold text other than a number or blank:", int((lab.other_ids != "").sum()),
      "; values:", sorted({v for s in lab.other_ids for v in s.split(",") if v}))
conf = np.concatenate([r["roi_conf"][np.array([bool(x) for x in r["roi_label"]])] for r in res])
print("Label confidences of labelled ROIs:", pd.Series(conf).round(2).value_counts().sort_index().to_dict())
print("Recordings with ROIs labelled 'target':", sum(any(x == "target" for x in r["roi_label"]) for r in res),
      "; such ROIs:", sum(int(np.sum(r["roi_label"] == "target")) for r in res))
never = sorted(set(head) - {sp.atlas_name(x) for r in res for x in r["roi_label"]})
print("Head neurons never labelled:", never)
print("ROIs labelled RID: %d, in %d recordings" % (sum(int(np.sum(r["roi_label"] == "RID")) for r in res),
                                               sum(any(x == "RID" for x in r["roi_label"]) for r in res)))
man_rid = sum(int(np.sum([r["roi_label"][int(m)] == "RID" for m in r["stim_manual_roi"] if np.isfinite(m)])) for r in res)
print("Manually located targets whose ROI is labelled RID:", man_rid)
non_head = pd.Series([sp.atlas_name(x) for r in res for x in r["roi_label"] if x and sp.atlas_name(x) not in head]).value_counts()
print("Most frequent labels that are not head-neuron names:", non_head.head(15).to_dict())
ROIs per recording: min 80, median 125, max 281
ROIs with a NeuroPAL label: median 67 per recording; with a head-neuron label: median 60 (60 distinct)
Recordings in which a head label is used for more than one ROI: 1 ; such labels per recording, max 1
Head neurons labelled in at least one recording: 186 of 188
Recordings whose neuropal_ids hold text other than a number or blank: 63 ; values: ['I4', 'RIAR', 'check', 'merge']
Label confidences of labelled ROIs: {-1.0: 356, 0.4: 2, 0.5: 16, 0.6: 121, 0.7: 566, 0.8: 1148, 0.9: 394, 1.0: 4849, 11.0: 1}
Recordings with ROIs labelled 'target': 21 ; such ROIs: 49
Head neurons never labelled: ['AIAR', 'I5']
ROIs labelled RID: 11, in 11 recordings
Manually located targets whose ROI is labelled RID: 5
Most frequent labels that are not head-neuron names: {'RIG': 51, 'target': 49, 'AMso': 34, 'RMDD': 30, 'IL1V': 26, 'CEPV': 25, 'SMDD': 21, 'SAAD': 19, 'IL2D': 18, 'AMSoL': 17, 'OLQV': 16, 'IL2V': 15, 'URX': 15, 'AMSoR': 15, 'SMB': 14}

Labels such as "RIG", "SAAD", "IL2V" or numbers name no single neuron; the paper's code drops them too (they do not match an atlas name), and so does this analysis. Some labels are given to two ROIs in one recording; both are kept, as in the code, which counts each ROI. The ROI-to-NeuroPAL column neuropal_ids is documented as a NeuroPAL ROI id or blank, but in most recordings it also holds the text "merge" or "check" (and in two recordings a neuron name), so those ROIs cannot be linked to a label here. One label confidence is 11, outside the 0 to 1 range of the others, and some ROIs carry the label "target", which names no neuron. The head neurons never labelled and the RID labels are printed above; RID is discussed in the summary.

In [8]:
tg = pd.DataFrame([{"subject": int(r["subject_id"]), "z_slope": r["z_slope"], "z_offset": r["z_offset"],
                    "med_dist_px": np.median(r["stim_dist_px"][r["stim_how"] == "nearest"]) if (r["stim_how"] == "nearest").any() else np.nan,
                    "nearest": int((r["stim_how"] == "nearest").sum()), "manual": int((r["stim_how"] == "manual").sum()),
                    "none": int((r["stim_how"] == "no target").sum()),
                    "xy_differs": int(np.sum((r["stim_roi"] != r["stim_roi_xy"]) & (r["stim_how"] == "nearest")))} for r in res])
print("Stimulated ROI assigned: manual %d, nearest centroid %d, no target %d"
      % (tg.manual.sum(), tg.nearest.sum(), tg.none.sum()))
d_all = np.concatenate([r["stim_dist_px"][r["stim_how"] == "nearest"] for r in res])
print("Distance from target to nearest centroid (pixels, 0.42 um each): median %.1f, 90th %.1f, 99th %.1f percentile"
      % tuple(np.percentile(d_all, [50, 90, 99])))
print("Nearest-centroid assignments that change when z is ignored: %d of %d" % (tg.xy_differs.sum(), tg.nearest.sum()))
print("Fitted z map slope (planes per unit): 5th %.1f, median %.1f, 95th %.1f percentile" % tuple(np.percentile(tg.z_slope, [5, 50, 95])))
man_check = []
for r in res:
    ok = (r["stim_how"] == "manual") & ((r["target_x"] > 0) | (r["target_y"] > 0))
    if ok.any():
        man_check.append(np.mean(r["stim_roi"][ok] == r["stim_roi_xy"][ok]))
print("Among manual targets with a stored position, the share whose nearest xy centroid is the manual ROI: median over recordings %.2f"
      % np.median(man_check))
Stimulated ROI assigned: manual 2815, nearest centroid 2677, no target 122
Distance from target to nearest centroid (pixels, 0.42 um each): median 4.1, 90th 8.2, 99th 19.1 percentile
Nearest-centroid assignments that change when z is ignored: 460 of 2677
Fitted z map slope (planes per unit): 5th -13.8, median 0.0, 95th 16.0 percentile
Among manual targets with a stored position, the share whose nearest xy centroid is the manual ROI: median over recordings 0.60

The nearest centroid lies a median of 4 pixels (1.7 um) from the target, within a nucleus. The fitted z map has slopes of either sign, as the scan direction differs between recordings. For manually located targets, the nearest centroid in x and y is the manually located ROI in the share printed above, which bounds how well the position alone identifies the stimulated neuron. The match is checked against the paper's own assignment in osf-link.

Clock check. If stimulation times and traces share a clock, the stimulated ROI should pass the response classifier much more often at the true stimulation times than at times shifted by 15 s, half the interval between stimulations.

In [9]:
clock = pd.DataFrame([{"subject": int(r["subject_id"]), "stims": len(r["auto"]),
                       "auto_true": r["auto"].mean(), "auto_shift15": r["auto_shift"].mean(),
                       "auto_xy": r["auto_xy"].mean()} for r in res])
a_true = np.concatenate([r["auto"] for r in res]); a_shift = np.concatenate([r["auto_shift"] for r in res])
print("Autoresponse rate, true stimulation times: %.3f; times shifted by +15 s: %.3f (of %d stimulations)"
      % (a_true.mean(), a_shift.mean(), len(a_true)))
print("Autoresponse rate with the nearest centroid in x and y only: %.3f" % np.concatenate([r["auto_xy"] for r in res]).mean())
diff = clock.auto_true - clock.auto_shift15
print("Recordings where the true rate exceeds the shifted rate: %d of %d" % ((diff > 0).sum(), len(clock)))
print("Recordings where it does not (clock not established):")
print(clock[diff <= 0].to_string(index=False))
dff_t = np.concatenate([r["dff"][np.arange(len(r["auto"])), np.clip(r["stim_roi"], 0, None)][r["stim_roi"] >= 0] for r in res])
dff_s = np.concatenate([r["dff_shift"][np.arange(len(r["auto"])), np.clip(r["stim_roi"], 0, None)][r["stim_roi"] >= 0] for r in res])
print("Stimulated ROI <dF/F0>_t: median %.3f at true times, %.3f at shifted times" % (np.nanmedian(dff_t), np.nanmedian(dff_s)))
Autoresponse rate, true stimulation times: 0.552; times shifted by +15 s: 0.103 (of 5614 stimulations)
Autoresponse rate with the nearest centroid in x and y only: 0.490
Recordings where the true rate exceeds the shifted rate: 110 of 110
Recordings where it does not (clock not established):
Empty DataFrame
Columns: [subject, stims, auto_true, auto_shift15, auto_xy]
Index: []
Stimulated ROI <dF/F0>_t: median 0.212 at true times, 0.035 at shifted times

The stimulated ROI responds in 55% of the stimulations at the true times and in 10% at shifted times, and the true rate is the higher in every recording, so the data are consistent with the stimulation table and the traces sharing a clock in all 110 recordings. The autoresponse rate is the paper's inclusion criterion; the paper does not report its own rate.

Recordings deposited twice. Compare frame counts, ROI counts, stimulation times relative to the start, and the first values of the traces between every pair of recordings.

In [10]:
fp = []
for k, r in enumerate(res):
    fp.append((r["n_vol"], r["n_roi"], tuple(np.round(r["stim_start"][:5] - r["t0"], 2)),
               tuple(np.round(r["centroids"][:3].ravel(), 1))))
pairs = [(int(res[a]["subject_id"]), int(res[b]["subject_id"]), what)
         for a in range(len(fp)) for b in range(a + 1, len(fp))
         for what in ["volumes and ROIs", "stimulation times", "centroids"]
         if (what == "volumes and ROIs" and fp[a][:2] == fp[b][:2])
         or (what == "stimulation times" and len(fp[a][2]) > 2 and fp[a][2] == fp[b][2])
         or (what == "centroids" and fp[a][3] == fp[b][3])]
print("Pairs of recordings sharing a fingerprint:", pairs if pairs else "none")
starts = pd.Series([r["session_start"] for r in res])
print("Session start times repeated:", starts[starts.duplicated()].tolist())
Pairs of recordings sharing a fingerprint: [(3, 109, 'volumes and ROIs')]
Session start times repeated: []

One pair of recordings shares its volume and ROI counts, but not its stimulation times or centroids, so no recording appears to be deposited twice. Each subject is one animal with one recording. Neurons are matched across animals by their NeuroPAL label, which is the paper's assumption; it is taken here as an assumption, and results are broken down by animal where they rest on few.

Observations¶

One row per stimulation with an assigned target ROI and per other ROI with a usable trace.

In [11]:
obs = sp.observations(res, head)
print("Observation rows:", len(obs), "from", obs.rec.nunique(), "recordings")
print("Stimulations with an assigned target ROI:", obs.groupby(["rec", "stim"]).ngroups)
st_auto = obs.groupby(["rec", "stim"]).agg(auto=("auto", "first"), j=("j", "first"), j_head=("j_head", "first"))
print("Of these with an autoresponse: %d; with a head-neuron label on the target: %d; both: %d"
      % (st_auto.auto.sum(), st_auto.j_head.sum(), (st_auto.auto & st_auto.j_head).sum()))
print("Rows with F0 <= 0: %d (%.4f); with F0 <= 0 among head pairs with an autoresponse: %d"
      % ((obs.f0 <= 0).sum(), (obs.f0 <= 0).mean(), ((obs.f0 <= 0) & obs.i_head & obs.j_head & obs.auto).sum()))
print("Rows from the %d recordings with traces below -100: %d; rows whose trace was not corrected for photobleaching: %d"
      % (len(neg_recs), obs.rec.isin(neg_recs).sum(), (~obs.pb_ok).sum()))
big = obs.dff.abs() > 2
print("Rows beyond +-2: %d; of these with F0 <= 0: %d, from the recordings with negative traces: %d, without photobleaching correction: %d"
      % (big.sum(), (big & (obs.f0 <= 0)).sum(), (big & obs.rec.isin(neg_recs)).sum(), (big & ~obs.pb_ok).sum()))
print("<dF/F0>_t of usable observed traces: 1st %.3f, median %.4f, 99th %.3f percentile; beyond +-2: %d rows"
      % (np.nanpercentile(obs.dff, 1), np.nanmedian(obs.dff), np.nanpercentile(obs.dff, 99), int((obs.dff.abs() > 2).sum())))
Observation rows: 433216 from 110 recordings
Stimulations with an assigned target ROI: 5492
Of these with an autoresponse: 3101; with a head-neuron label on the target: 3509; both: 2034
Rows with F0 <= 0: 3172 (0.0073); with F0 <= 0 among head pairs with an autoresponse: 761
Rows from the 9 recordings with traces below -100: 49753; rows whose trace was not corrected for photobleaching: 2039
Rows beyond +-2: 178; of these with F0 <= 0: 20, from the recordings with negative traces: 42, without photobleaching correction: 78
<dF/F0>_t of usable observed traces: 1st -0.304, median -0.0012, 99th 0.552 percentile; beyond +-2: 178 rows

recordings-protocol¶

Activity was recorded from 113 WT-background animals, each for up to 40 min, while a neuron was stimulated once every 30 s with a 0.5 s train of light pulses.

In [12]:
n_both = sum(1 for r in res if r["n_vol"] > 0 and len(r["stim_start"]) > 0)
print("Subjects in the dandiset:", len(sub_img), "; recordings with traces and a stimulation table:", n_both)
print("WT genotype among them:", int((meta.genotype == "WT").sum()))
print("Median interval between stimulations: %.2f s (criterion: within 10%% of 30 s, 27 to 33)" % np.median(isi))
share = (meta.minutes <= 44).mean()
print("Recordings lasting at most 44 min: %d of %d (%.3f; criterion at least 0.9)" % ((meta.minutes <= 44).sum(), len(meta), share))
print("Stored stimulus durations:", np.unique(st.dur.round(3)).tolist())
print("Recording length per animal (min):", meta.sort_values("minutes").minutes.round(1).tolist())
Subjects in the dandiset: 113 ; recordings with traces and a stimulation table: 110
WT genotype among them: 110
Median interval between stimulations: 30.81 s (criterion: within 10% of 30 s, 27 to 33)
Recordings lasting at most 44 min: 107 of 110 (0.973; criterion at least 0.9)
Stored stimulus durations: [0.5]
Recording length per animal (min): [3.3, 3.5, 5.6, 5.8, 6.3, 6.5, 6.9, 7.4, 7.6, 7.8, 9.8, 10.3, 11.1, 11.7, 13.3, 13.6, 15.0, 16.6, 20.9, 21.8, 22.5, 22.8, 22.9, 23.2, 23.8, 24.1, 24.3, 25.5, 25.5, 25.9, 26.1, 26.2, 26.2, 26.4, 26.5, 26.6, 27.0, 27.4, 27.6, 27.7, 27.7, 27.8, 27.9, 28.0, 28.0, 28.1, 28.4, 28.5, 29.9, 30.7, 30.8, 31.2, 31.5, 31.6, 31.7, 31.7, 31.7, 31.8, 31.9, 32.0, 32.1, 32.1, 32.2, 32.3, 32.9, 33.3, 33.4, 33.4, 33.6, 33.7, 33.7, 33.9, 34.0, 34.3, 34.5, 34.6, 34.7, 34.7, 34.8, 35.1, 35.1, 36.7, 36.7, 36.7, 36.8, 37.1, 37.1, 37.2, 37.2, 37.2, 37.3, 37.4, 37.7, 37.8, 38.5, 38.9, 39.4, 40.3, 41.5, 42.1, 42.1, 42.3, 42.4, 42.4, 42.5, 42.5, 43.1, 45.0, 45.2, 48.7]

Result: partially reproduced. The dandiset has the 113 subjects the paper reports, but traces and stimulation tables for 110 of them. The median interval between stimulations is 30.81 s, and 107 of the 110 recordings last no more than 44 min (the longest 48.7 min). Every stored stimulation lasts 0.5 s, a constant written by the conversion, not a measurement.

pairs-measured¶

Responses to stimulation were imaged for 23,433 pairs of neurons (66% of all possible pairs in the head), from 186 of the 188 head neurons, and some pairs were observed as many as 59 times.

In [13]:
def pair_counts(o, require_auto=True, diag=True):
    h = o[o.i_head & o.j_head]
    if require_auto:
        h = h[h.auto]
    if not diag:
        h = h[h.i != h.j]
    n = h.groupby(["i", "j"]).size()
    return n, h

rows = []
for name, ra, dg in [("(a) with i = j, autoresponse", True, True), ("(b) i != j, autoresponse", True, False),
                     ("(c) with i = j, no autoresponse required", False, True)]:
    n, h = pair_counts(obs, ra, dg)
    rows.append({"reading": name, "pairs": len(n), "share_of_188^2": len(n) / 188 ** 2,
                 "neurons_recorded": h[h.i != h.j].i.nunique(), "max_obs": int(n.max()),
                 "animals": h.rec.nunique()})
pm = pd.DataFrame(rows)
print(pm.to_string(index=False))
print("Expected: 23,433 pairs (tolerance 2,343), 186 neurons (18.6), 59 observations (5.9)")
n_a, h_a = pair_counts(obs)
print("\nMost observed pairs, reading (a):", n_a.sort_values(ascending=False).head(5).to_dict())
per_animal = h_a.groupby("rec").apply(lambda x: x.groupby(["i", "j"]).ngroups)
print("Pairs contributed per animal (reading a): min %d, median %d, max %d; animals contributing %d"
      % (per_animal.min(), per_animal.median(), per_animal.max(), len(per_animal)))
stim_n = h_a.groupby("j").apply(lambda x: x.groupby(["rec", "stim"]).ngroups)
print("Head neurons stimulated with an autoresponse: %d (Source Data Extended Data Fig. 3b lists 168 stimulated)" % len(stim_n))
                                 reading  pairs  share_of_188^2  neurons_recorded  max_obs  animals
            (a) with i = j, autoresponse  20587        0.582475               184       50      107
                (b) i != j, autoresponse  20422        0.577807               184       50      107
(c) with i = j, no autoresponse required  23813        0.673749               184       86      108
Expected: 23,433 pairs (tolerance 2,343), 186 neurons (18.6), 59 observations (5.9)

Most observed pairs, reading (a): {('M3R', np.str_('RMDVR')): 50, ('RMDVR', np.str_('RMDVR')): 49, ('AVEL', np.str_('AVEL')): 49, ('I1R', np.str_('RMDVR')): 48, ('M3R', np.str_('AVEL')): 48}
Pairs contributed per animal (reading a): min 22, median 696, max 2281; animals contributing 107
Head neurons stimulated with an autoresponse: 172 (Source Data Extended Data Fig. 3b lists 168 stimulated)

Result: partially reproduced. Under reading (a), 20,587 head pairs are observed (expected 23,433, tolerance 2,343), from 184 head neurons (186, tolerance 18.6), with at most 50 observations of one pair (59, tolerance 5.9), so only the neuron count is within tolerance. Under reading (c), without the autoresponse requirement, the 23,813 pairs and 184 neurons are within tolerance but the largest count, 86, is not. Reading (b) gives 20,422 pairs. No reading puts all three values within tolerance.

Source Data and the per-pair link¶

The paper's Source Data are read from the article's supplementary files; they supply reference values for sourcedata-link and the q < 0.05 selection of fig2d-inhibitory-fraction.

In [14]:
SOURCE = "https://static-content.springer.com/esm/art%3A10.1038%2Fs41586-023-06683-4/MediaObjects/"
def source_sheet(fname, sheet):
    with urllib.request.urlopen(SOURCE + fname, timeout=120) as r:
        raw = r.read()
    print(f"{fname}: {len(raw)} bytes, md5 {hashlib.md5(raw).hexdigest()}")
    x = pd.read_excel(io.BytesIO(raw), sheet_name=sheet, header=None)
    m = x.iloc[1:, 1:].astype(float)
    m.index = x.iloc[1:, 0].astype(str).values
    m.columns = x.iloc[0, 1:].astype(str).values
    return m.stack(future_stack=True).rename_axis(["i", "j"])

sd_obs = source_sheet("41586_2023_6683_MOESM12_ESM.xlsx", "EDFig4a")
sd_q = source_sheet("41586_2023_6683_MOESM12_ESM.xlsx", "EDFig4b")
sd_dff = source_sheet("41586_2023_6683_MOESM11_ESM.xlsx", "EDFig3a")
sd = pd.DataFrame({"n_paper": sd_obs, "q": sd_q, "dff_paper": sd_dff})
sd = sd[sd.n_paper > 0]
print("Source Data: pairs observed %d, with q %d, q < 0.05 %d, largest count %d"
      % (len(sd), sd.q.notna().sum(), (sd.q < 0.05).sum(), sd.n_paper.max()))

def link_stats(here, label):
    j = sd.join(here, how="left")
    found = j.n.notna()
    both = j[found]
    rho_n = stats.spearmanr(both.n_paper, both.n).statistic
    ok = both.dff_paper.notna() & both["mean"].notna()
    r_dff = stats.pearsonr(both.dff_paper[ok], both["mean"][ok]).statistic
    rho_dff = stats.spearmanr(both.dff_paper[ok], both["mean"][ok]).statistic
    extra = len(here.index.difference(sd.index))
    return {"reading": label, "paper_pairs_found": found.mean(), "pairs_here_not_in_paper": extra,
            "spearman_counts": rho_n, "pearson_dff": r_dff, "spearman_dff": rho_dff,
            "median_count_ratio": np.median(both.n / both.n_paper), "pairs_compared": int(ok.sum())}
41586_2023_6683_MOESM12_ESM.xlsx: 378622 bytes, md5 8d55d8113826b809c9610e46accdf093
41586_2023_6683_MOESM12_ESM.xlsx: 378622 bytes, md5 8d55d8113826b809c9610e46accdf093
41586_2023_6683_MOESM11_ESM.xlsx: 282522 bytes, md5 02ca4ec308dc5e398f059c4f9a2c63d1
Source Data: pairs observed 23433, with q 23433, q < 0.05 1310, largest count 59

osf-link¶

The dandiset's recordings can be linked to the recordings the paper exported to its OSF repository, and the stimulated neuron found here from the target position agrees with the one the paper's pipeline assigned.

In [15]:
# The paper's export on OSF (doi 10.17605/OSF.IO/E2SYT), file exported_data.tar.gz (OSF guid 9mecf,
# version 1, md5 7ae518fea414f664e2493238afe209bb). It is streamed once and only its small per-recording
# files are kept: the folder name, the stimulated ROI of every stimulation and the ROI labels.
import tarfile
OSF_URL, OSF_MD5 = "https://osf.io/download/9mecf/", "7ae518fea414f664e2493238afe209bb"

class _Hashed:
    def __init__(self, f):
        self.f, self.md5, self.n = f, hashlib.md5(), 0
    def read(self, n=-1):
        b = self.f.read(n)
        self.md5.update(b)
        self.n += len(b)
        return b

import zlib
texts, big_txt = {}, {}
def _has_rid(k):
    if (k, "labels") not in texts or (k, "stim_neurons") not in texts:
        return None
    le = [x.strip() for x in texts[(k, "labels")].split("\n")]
    return any(0 <= int(s) < len(le) and le[int(s)] == "RID" for s in texts[(k, "stim_neurons")].split())
with urllib.request.urlopen(OSF_URL, timeout=600) as r:
    hr = _Hashed(r)
    with tarfile.open(fileobj=hr, mode="r|gz") as tf:
        for m in tf:
            mm = re.search(r"/(\d+)_(ds_name|stim_neurons|labels|gcamp|t|stim_volume_i)\.txt$", m.name)
            if mm:
                k, what = int(mm.group(1)), mm.group(2)
                if what in ("gcamp", "t", "stim_volume_i"):
                    # the traces are kept (compressed) only for recordings that stimulate an ROI labelled RID
                    if _has_rid(k) is not False:
                        big_txt[(k, what)] = zlib.compress(tf.extractfile(m).read())
                else:
                    texts[(k, what)] = tf.extractfile(m).read().decode()
                    if _has_rid(k) is False:
                        for w in ("gcamp", "t", "stim_volume_i"):
                            big_txt.pop((k, w), None)
    while hr.read(1 << 20):
        pass
print("exported_data.tar.gz: %d bytes, md5 %s, matches the OSF record: %s" % (hr.n, hr.md5.hexdigest(), hr.md5.hexdigest() == OSF_MD5))
n_exp = len({k for k, _ in texts})
exp = []
for k in range(n_exp):
    folder = texts[(k, "ds_name")].strip()
    m = re.search(r"pumpprobe_(\d{8})_(\d{6})", folder)
    exp.append({"k": k, "key": f"{m.group(1)}-{m.group(2)}",
                "stim": np.array([int(x) for x in texts[(k, "stim_neurons")].split()]),
                "labels": texts[(k, "labels")].split("\n")})
exp = {e["key"]: e for e in exp}
print("Exported recordings with distinct session keys:", len(exp))
rid_k = sorted(k for k in range(n_exp) if _has_rid(k))
print("Exported WT recordings that stimulate an ROI labelled RID:", len(rid_k), "; traces kept for", len({k for k, _ in big_txt}))
osf_roi = []
for r in res:
    key = r["session_start"][:10].replace("-", "") + "-" + r["session_start"][11:19].replace(":", "")
    e = exp.get(key)
    osf_roi.append(e["stim"] if e is not None and len(e["stim"]) == len(r["stim_start"]) else None)
excluded = {(k, e) for k, roi in enumerate(osf_roi) if roi is not None for e in np.where(roi == -2)[0]}
print("Stimulations the export marks excluded (-2), over the recordings here:", len(excluded))
exported_data.tar.gz: 523093816 bytes, md5 7ae518fea414f664e2493238afe209bb, matches the OSF record: True
Exported recordings with distinct session keys: 113
Exported WT recordings that stimulate an ROI labelled RID: 7 ; traces kept for 7
Stimulations the export marks excluded (-2), over the recordings here: 950
In [16]:
rows, assign = [], []
for r in res:
    key = r["session_start"][:10].replace("-", "") + "-" + r["session_start"][11:19].replace(":", "")
    e = exp.get(key)
    row = {"subject": int(r["subject_id"]), "key": key, "matched": e is not None}
    if e is not None:
        row["same_stims"] = len(e["stim"]) == len(r["stim_start"])
        labs_e = e["labels"][:-1] if e["labels"] and e["labels"][-1] == "" else e["labels"]
        n_lab = len(labs_e)
        row["rois_here"] = r["n_roi"]
        row["labels_exported"] = n_lab
        ok = row["same_stims"]
        if ok:
            both = (e["stim"] >= 0) & (r["stim_roi"] >= 0)
            row["both_assigned"] = int(both.sum())
            row["agree"] = int(np.sum(e["stim"][both] == r["stim_roi"][both]))
            row["agree_nearest"] = int(np.sum((e["stim"] == r["stim_roi"])[both & (r["stim_how"] == "nearest")]))
            row["n_nearest"] = int(np.sum(both & (r["stim_how"] == "nearest")))
            row["agree_manual"] = int(np.sum((e["stim"] == r["stim_roi"])[both & (r["stim_how"] == "manual")]))
            row["n_manual"] = int(np.sum(both & (r["stim_how"] == "manual")))
            row["exported_excluded"] = int(np.sum(e["stim"] == -2))
            row["exported_unassigned"] = int(np.sum(e["stim"] == -1))
            row["exported_minus3"] = int(np.sum(e["stim"] == -3))
            labs = labs_e[:r["n_roi"]]
            same = [a.strip() == b.strip() for a, b in zip(labs, r["roi_label"])]
            row["labels_equal"] = float(np.mean(same))
            named = [(a.strip(), b.strip()) for a, b in zip(labs, r["roi_label"]) if a.strip() not in ("", "merge") or b.strip()]
            row["labels_equal_named"] = float(np.mean([a == b for a, b in named])) if named else np.nan
            row["export_merge"] = sum(a.strip() == "merge" for a in labs)
    rows.append(row)
ol = pd.DataFrame(rows)
mt = ol[ol.matched & ol.same_stims.fillna(False).astype(bool)]
print("Recordings with traces: %d; matched to an export by session key: %d; with the same number of stimulations: %d"
      % (len(ol), ol.matched.sum(), len(mt)))
print("Exported recordings with no recording here:", sorted(set(exp) - set(ol.key)))
print("Session ids of the subjects with an imaging file only:", {s_: sub_img[s_] for s_ in sorted(set(sub_img) - set(sub_seg))})
print("ROI count equal to the exported label count in %d of %d matched recordings; differing:"
      % ((mt.rois_here == mt.labels_exported).sum(), len(mt)), mt[mt.rois_here != mt.labels_exported][["subject", "rois_here", "labels_exported"]].values.tolist())
print("Stimulations assigned by both: %d; same ROI: %d (%.3f)" % (mt.both_assigned.sum(), mt.agree.sum(), mt.agree.sum() / mt.both_assigned.sum()))
print("  manual targets: %d of %d agree; nearest-centroid targets: %d of %d agree (%.3f)"
      % (mt.agree_manual.sum(), mt.n_manual.sum(), mt.agree_nearest.sum(), mt.n_nearest.sum(), mt.agree_nearest.sum() / mt.n_nearest.sum()))
print("Exported stimulations marked excluded (-2): %d, unassigned (-1): %d, labelled outside the reference volume (-3): %d"
      % (mt.exported_excluded.sum(), mt.exported_unassigned.sum(), mt.exported_minus3.sum()))
print("Share of ROI labels equal to the exported labels: median %.3f; recordings below 0.9: %d"
      % (mt.labels_equal.median(), (mt.labels_equal < 0.9).sum()))
print("Exported labels 'merge' (blank here): %d ROIs in %d recordings" % (mt.export_merge.sum(), (mt.export_merge > 0).sum()))
print("Over ROIs named on either side, leaving out 'merge': median agreement %.3f; recordings below 0.9 (subject, agreement):"
      % mt.labels_equal_named.median(), mt[mt.labels_equal_named < 0.9][["subject", "labels_equal_named"]].round(3).values.tolist())
per = (mt.agree / mt.both_assigned)
print("Agreement per recording: min %.2f, median %.2f; recordings below 0.6: %s"
      % (per.min(), per.median(), mt[per < 0.6].subject.tolist()))
low_recs = set(mt[per < 0.6].subject.tolist())
mt = mt.merge(tg[["subject", "z_slope", "z_offset"]], on="subject", how="left")
mt["pos_agree"] = mt.agree_nearest / mt.n_nearest
print("\nPosition-found agreement per recording against the fitted z slope:")
print("  z slope exactly 0: %d recordings, median agreement %.3f; nonzero: %d, median %.3f"
      % ((mt.z_slope == 0).sum(), mt.pos_agree[mt.z_slope == 0].median(), (mt.z_slope != 0).sum(), mt.pos_agree[mt.z_slope != 0].median()))
print("  Spearman of agreement with |slope|: %.3f" % stats.spearmanr(mt.pos_agree, mt.z_slope.abs(), nan_policy="omit").statistic)
print("  Recordings with overall agreement below 0.6:")
print(mt[mt.subject.isin(low_recs)][["subject", "both_assigned", "agree", "n_nearest", "agree_nearest", "pos_agree", "z_slope", "z_offset"]].round(3).to_string(index=False))
# Recordings whose labels disagree with the export: the first named ROIs side by side.
for s_ in mt[mt.labels_equal_named < 0.9].subject:
    r = next(r for r in res if int(r["subject_id"]) == s_)
    key = r["session_start"][:10].replace("-", "") + "-" + r["session_start"][11:19].replace(":", "")
    le = [x.strip() for x in exp[key]["labels"]]
    le = le[:-1] if le and le[-1] == "" else le   # trailing empty entry, as above
    lf = [x.strip() for x in r["roi_label"]]
    pairs_ = [(k, le[k] if k < len(le) else "-", lf[k]) for k in range(len(lf)) if (k < len(le) and le[k] not in ("", "merge")) or lf[k]][:8]
    se, sf = {x for x in le if x not in ("", "merge")}, {x for x in lf if x}
    print("\nsubject %d: ROIs %d here, %d exported; distinct names exported %d, here %d, shared %d" % (s_, len(lf), len(le), len(se), len(sf), len(se & sf)))
    print("  first named ROIs (index, exported, here):", pairs_)
# RID among the exported labels and stimulated ROIs.
rid_lab = rid_stim = 0
for r in res:
    key = r["session_start"][:10].replace("-", "") + "-" + r["session_start"][11:19].replace(":", "")
    e = exp.get(key)
    if e is None:
        continue
    le = [x.strip() for x in e["labels"]]
    rid_lab += sum(x == "RID" for x in le)
    rid_stim += sum(1 for s in e["stim"] if 0 <= s < len(le) and le[s] == "RID")
print("\nExported ROI labels RID: %d; exported stimulations whose ROI is labelled RID: %d" % (rid_lab, rid_stim))
Recordings with traces: 110; matched to an export by session key: 110; with the same number of stimulations: 110
Exported recordings with no recording here: ['20210917-132736', '20211008-115929', '20220330-110253']
Session ids of the subjects with an imaging file only: {20: '20210917-132736', 23: '20211008-115929', 33: '20220330-110253'}
ROI count equal to the exported label count in 109 of 110 matched recordings; differing: [[11, 161, 200]]
Stimulations assigned by both: 4479; same ROI: 3913 (0.874)
  manual targets: 2812 of 2812 agree; nearest-centroid targets: 1101 of 1667 agree (0.660)
Exported stimulations marked excluded (-2): 950, unassigned (-1): 156, labelled outside the reference volume (-3): 28
Share of ROI labels equal to the exported labels: median 0.975; recordings below 0.9: 23
Exported labels 'merge' (blank here): 732 ROIs in 63 recordings
Over ROIs named on either side, leaving out 'merge': median agreement 1.000; recordings below 0.9 (subject, agreement): [[11.0, 0.0], [64.0, 0.0], [76.0, 0.0], [77.0, 0.0], [86.0, 0.0], [91.0, 0.0]]
Agreement per recording: min 0.24, median 0.97; recordings below 0.6: [27, 28, 30, 35, 42, 50, 51, 71, 86]

Position-found agreement per recording against the fitted z slope:
  z slope exactly 0: 3 recordings, median agreement 0.579; nonzero: 107, median 0.739
  Spearman of agreement with |slope|: 0.478
  Recordings with overall agreement below 0.6:
 subject  both_assigned  agree  n_nearest  agree_nearest  pos_agree  z_slope  z_offset
      27             27     13         17              3      0.176     -2.0      13.5
      28             33      8         31              6      0.194      2.5      13.0
      30             47     20         35              8      0.229      0.5      10.5
      35             51     26         39             14      0.359     -8.5      13.0
      42             37     19         20              2      0.100      5.0      14.0
      50             34     18         22              6      0.273     -5.5      14.0
      51             37     22         23              8      0.348     -7.0      11.5
      71             38     18         23              3      0.130     -8.0      11.0
      86             43     24         27              8      0.296     15.5      13.0

subject 11: ROIs 161 here, 200 exported; distinct names exported 0, here 41, shared 0
  first named ROIs (index, exported, here): [(1, '', 'I2L'), (3, '', 'MCL'), (4, '', 'RMEL'), (6, '', 'I1L'), (7, '', 'AMso'), (8, '', 'M3L'), (9, '', 'OLLL'), (11, '', 'AVEL')]

subject 64: ROIs 110 here, 110 exported; distinct names exported 70, here 4, shared 0
  first named ROIs (index, exported, here): [(2, 'DB1', ''), (3, 'SABD', 'AIY3'), (4, 'RIAR', ''), (5, 'RMDVR', 'AFDR'), (6, 'BAGR', ''), (8, 'DA1', ''), (14, 'DB2', ''), (16, 'SABVL', '')]

subject 76: ROIs 162 here, 162 exported; distinct names exported 102, here 0, shared 0
  first named ROIs (index, exported, here): [(2, 'RMDVL', ''), (3, 'AWCON', ''), (5, 'I2L', ''), (7, 'RMEL', ''), (8, 'OLLL', ''), (9, 'SAAVL', ''), (11, 'AVDL', ''), (12, 'AWAL', '')]

subject 77: ROIs 140 here, 140 exported; distinct names exported 87, here 15, shared 2
  first named ROIs (index, exported, here): [(0, 'RMDVR', ''), (1, 'OLQDR', ''), (2, 'FLPR', ''), (4, 'I1R', ''), (6, '', 'target'), (7, 'M3R', ''), (8, 'OLQVR', 'target'), (9, 'URYVR', '')]

subject 86: ROIs 119 here, 119 exported; distinct names exported 76, here 69, shared 35
  first named ROIs (index, exported, here): [(8, 'ADAL', 'AVHL'), (9, 'RMDVL', 'ADAL'), (10, 'AINL', 'AWCON'), (12, 'RIAL', ''), (13, 'AWBL', 'FLPL'), (14, 'M5', 'URBR'), (15, 'FLPL', ''), (16, '', 'ASJR')]

subject 91: ROIs 147 here, 147 exported; distinct names exported 97, here 81, shared 59
  first named ROIs (index, exported, here): [(0, 'RMDVL', 'AVEL'), (1, 'URBL', 'RIFL'), (2, 'I2L', 'DB2'), (4, 'M5', 'AIYR'), (5, 'ASHL', 'target'), (7, 'AVEL', 'AVDL'), (8, 'CEPVL', ''), (9, 'URYDL', 'M4')]

Exported ROI labels RID: 12; exported stimulations whose ROI is labelled RID: 12
In [17]:
# Variant: pairs-measured with the exported assignment of the stimulated ROI (and its exclusions).
# The autoresponse is re-evaluated for the exported ROI.
res_osf = []
for r, roi in zip(res, osf_roi):
    res_osf.append(roi)
obs_osf = sp.observations(res, head, stim_roi=res_osf)
for name, dg in [("(a) with i = j", True), ("(b) i != j", False)]:
    n, hh = pair_counts(obs_osf, True, dg)
    print("Exported assignment, reading %s: %d pairs, %d neurons recorded, max %d observations"
          % (name, len(n), hh[hh.i != hh.j].i.nunique(), n.max()))
h_osf = obs_osf[obs_osf.i_head & obs_osf.j_head & obs_osf.auto]
main_osf = h_osf.groupby(["i", "j"]).dff.agg(["size", "mean"]).rename(columns={"size": "n"})
# Variant: exported assignment and exported ROI labels.
osf_lab = []
for r in res:
    key = r["session_start"][:10].replace("-", "") + "-" + r["session_start"][11:19].replace(":", "")
    e = exp.get(key)
    le = None
    if e is not None:
        le = e["labels"][:-1] if e["labels"] and e["labels"][-1] == "" else e["labels"]
        le = np.array([x.strip() for x in le]) if len(le) == r["n_roi"] else None
    osf_lab.append(le)
obs_osf2 = sp.observations(res, head, stim_roi=res_osf, labels=osf_lab)
n2, hh2 = pair_counts(obs_osf2, True, True)
print("Exported assignment and labels, reading (a): %d pairs, %d neurons recorded, max %d observations (recordings used: %d)"
      % (len(n2), hh2[hh2.i != hh2.j].i.nunique(), n2.max(), obs_osf2.rec.nunique()))
h_osf2 = obs_osf2[obs_osf2.i_head & obs_osf2.j_head & obs_osf2.auto]
main_osf2 = h_osf2.groupby(["i", "j"]).dff.agg(["size", "mean"]).rename(columns={"size": "n"})
print(pd.DataFrame([link_stats(main_osf, "exported assignment"),
                    link_stats(main_osf2, "exported assignment and labels")]).round(3).to_string(index=False))
Exported assignment, reading (a) with i = j: 20351 pairs, 183 neurons recorded, max 55 observations
Exported assignment, reading (b) i != j: 20189 pairs, 183 neurons recorded, max 55 observations
Exported assignment and labels, reading (a): 20564 pairs, 184 neurons recorded, max 56 observations (recordings used: 107)
                       reading  paper_pairs_found  pairs_here_not_in_paper  spearman_counts  pearson_dff  spearman_dff  median_count_ratio  pairs_compared
           exported assignment              0.833                      824            0.820        0.675         0.573                 1.0           19527
exported assignment and labels              0.845                      757            0.834        0.684         0.586                 1.0           19807

Result: partially reproduced. All 110 recordings with traces match one exported recording by session date and time, with the same number of stimulations, and 109 with the same number of ROIs; the three exported recordings without a match are the three subjects that have only imaging files. Of the 4,479 stimulations both assign to an ROI, 3,913 (87.4%) get the same ROI here, which meets the 80% of the criterion as worded. But all 2,812 manual targets agree by construction, since the file's target_pumpprobe_id is the pipeline's own assignment. The claim is about the ROI found from the target position, and of those 1,101 of 1,667 (66.0%) agree, inside the 60 to 80% band of a partial result; the outcome is judged on this figure. Agreement is below 0.6 in nine recordings, where the position-found agreement is 0.10 to 0.36. Only 3 recordings have a fitted z slope of exactly 0 (median position-found agreement 0.579, against 0.739 in the other 107), and agreement rises with the absolute slope (Spearman 0.478). The export marks 950 stimulations as excluded (-2), which the dandiset does not mark. The six recordings whose labels disagree do so for three reasons: in subject 11 the export has no label at all (41 distinct names here); in subjects 64 and 76 the dandiset has almost none (4 and 0 distinct names, against 70 and 102 exported), and in 77 it has 15, several of them "target"; in 86 and 91 most names are shared (35 of 69 and 59 of 81 names here) but sit on other ROIs, so in one of the two sources the labels are attached to the wrong ROIs. The export labels 12 ROIs RID, and 12 of its stimulations target an ROI labelled RID.

sourcedata-link¶

The per-pair measurements computed from the dandiset agree with the paper's published per-pair values, the number of observations (Extended Data Fig. 4a) and the mean response <dF/F0>_t (Extended Data Fig. 3a).

In [18]:
h = obs[obs.i_head & obs.j_head & obs.auto]
main = h.groupby(["i", "j"]).dff.agg(["size", "mean"]).rename(columns={"size": "n"})
dec = h.groupby(["i", "j"]).dff_decay.agg(["size", "mean"]).rename(columns={"size": "n"})
rsp = h[h.responded | (h.i == h.j)].groupby(["i", "j"]).dff.agg(["size", "mean"]).rename(columns={"size": "n"})
link = pd.DataFrame([link_stats(main, "main: all observations, Methods dF/F0"),
                     link_stats(dec, "variant: code's decaying correction"),
                     link_stats(rsp, "variant: detected responses only")])
print(link.round(3).to_string(index=False))
j = sd.join(main, how="inner")
print("\nCount differences (here minus paper) over pairs found: median %.0f, 5th %.0f, 95th %.0f percentile; equal in %.3f"
      % (np.median(j.n - j.n_paper), np.percentile(j.n - j.n_paper, 5), np.percentile(j.n - j.n_paper, 95), np.mean(j.n == j.n_paper)))
print("Sign of the mean response agrees in %.3f of pairs compared" % np.mean(np.sign(j["mean"]) == np.sign(j.dff_paper)))
# Variants that remove rows a non-positive F0, a failed photobleaching fit, the export's exclusions or
# a poorly linked recording could distort.
low_rec_idx = {k for k, r in enumerate(res) if int(r["subject_id"]) in low_recs}
def filtered(o):
    h = o[o.i_head & o.j_head & o.auto]
    ex = pd.Series([(a, b) in excluded for a, b in zip(h.rec, h.stim)], index=h.index)
    f = {"main": pd.Series(True, index=h.index),
         "F0 > 0 only": h.f0 > 0,
         "without the 9 recordings with negative traces": ~h.rec.isin(neg_recs),
         "photobleaching-corrected traces only": h.pb_ok,
         "without the export's excluded stimulations": ~ex,
         "without the 9 recordings with agreement < 0.6": ~h.rec.isin(low_rec_idx)}
    f["all of these filters"] = np.logical_and.reduce([v for k, v in f.items() if k != "main"])
    return {k: h[v].groupby(["i", "j"]).dff.agg(["size", "mean"]).rename(columns={"size": "n"}) for k, v in f.items()}
var_tabs = filtered(obs)
print("\nVariants (main reading otherwise):")
print(pd.DataFrame([link_stats(t, k) for k, t in var_tabs.items()]).round(3).to_string(index=False))
                              reading  paper_pairs_found  pairs_here_not_in_paper  spearman_counts  pearson_dff  spearman_dff  median_count_ratio  pairs_compared
main: all observations, Methods dF/F0              0.817                     1440            0.768        0.551         0.493               1.000           19147
  variant: code's decaying correction              0.817                     1440            0.768        0.616         0.620               1.000           19147
     variant: detected responses only              0.360                      305            0.471        0.458         0.517               0.286            8443

Count differences (here minus paper) over pairs found: median 0, 5th -5, 95th 3 percentile; equal in 0.274
Sign of the mean response agrees in 0.652 of pairs compared
Variants (main reading otherwise):
                                      reading  paper_pairs_found  pairs_here_not_in_paper  spearman_counts  pearson_dff  spearman_dff  median_count_ratio  pairs_compared
                                         main              0.817                     1440            0.768        0.551         0.493               1.000           19147
                                  F0 > 0 only              0.814                     1425            0.766        0.552         0.493               1.000           19069
without the 9 recordings with negative traces              0.799                     1369            0.752        0.534         0.477               0.846           18724
         photobleaching-corrected traces only              0.816                     1438            0.767        0.551         0.493               1.000           19126
   without the export's excluded stimulations              0.808                     1197            0.780        0.601         0.511               0.900           18936
without the 9 recordings with agreement < 0.6              0.799                     1246            0.770        0.543         0.494               0.900           18727
                         all of these filters              0.768                      994            0.765        0.569         0.489               0.731           17985
In [19]:
fig, ax = plt.subplots(1, 2, figsize=(10, 4))
ax[0].scatter(j.n_paper, j.n, s=3, alpha=0.3)
ax[0].plot([0, 60], [0, 60], "k--", lw=0.8)
ax[0].set_xlabel("observations, Source Data"); ax[0].set_ylabel("observations, this analysis")
lim = 1.0
inside = (j.dff_paper.abs() <= lim) & (j["mean"].abs() <= lim)
ax[1].scatter(j.dff_paper[inside], j["mean"][inside], s=3, alpha=0.3)
ax[1].plot([-lim, lim], [-lim, lim], "k--", lw=0.8)
ax[1].set_xlim(-lim, lim); ax[1].set_ylim(-lim, lim)
ax[1].set_xlabel("mean dF/F0, Source Data"); ax[1].set_ylabel("mean dF/F0, this analysis")
plt.tight_layout(); plt.show()
print("Counts: paper 1 to %d, here %d to %d. dF/F0 panel: %d of %d pairs outside +-%.0f not shown"
      % (j.n_paper.max(), j.n.min(), j.n.max(), (~inside).sum(), len(j), lim))
No description has been provided for this image
Counts: paper 1 to 59, here 1 to 50. dF/F0 panel: 22 of 19147 pairs outside +-1 not shown

Result: partially reproduced. 81.7% of the paper's 23,433 observed pairs are observed here (the criterion asks 90% for reproduced, 70% for partial), the Spearman correlation of observation counts is 0.768 (0.9; 0.7), and the Pearson correlation of mean <dF/F0>_t is 0.551 (0.7; 0.5). Counts are equal in 27.4% of pairs, with a median difference of 0, and the sign of the mean agrees in 65.2% of pairs. With the code's correction of decaying traces the correlation of means is 0.617. With the stimulated ROI taken from the paper's export (osf-link), 83.3% of pairs are found and the correlations are 0.820 and 0.676, and with its labels as well 84.5%, 0.834 and 0.684, so part of the difference comes from how the stimulated neuron is identified here. Keeping only rows with F0 > 0 (Pearson 0.552) or traces corrected for photobleaching (0.551), or leaving out the nine recordings with negative traces (0.534), the export's excluded stimulations (0.601) or the nine recordings with agreement below 0.6 (0.544), changes little; all of these filters together give 76.8%, 0.765 and 0.569. The filter and exported-assignment variants all fall in the partial band. The mean over detected responses only falls below it (36.0% of pairs found, 0.471 and 0.458), as it keeps only 8,443 pairs.

fig1e-avjr-avdr¶

Stimulation of the interneuron AVJR evoked activity in AVDR.

In [20]:
x = obs[(obs.j == "AVJR") & (obs.i == "AVDR") & obs.auto]
x_all = obs[(obs.j == "AVJR") & (obs.i == "AVDR")]
print("AVJR stimulations with an autoresponse and AVDR observed: %d trials in %d animals (%d without the autoresponse requirement)"
      % (len(x), x.rec.nunique(), len(x_all)))
print("Mean <dF/F0>_t of AVDR: %.4f (median %.4f, range %.3f to %.3f); trials with a detected AVDR response: %d"
      % (x.dff.mean(), x.dff.median(), x.dff.min(), x.dff.max(), x.responded.sum()))
print("Source Data value for this pair: mean %.4f over %d observations, q = %.3g"
      % (sd.dff_paper.get(("AVDR", "AVJR"), np.nan), sd.n_paper.get(("AVDR", "AVJR"), 0), sd.q.get(("AVDR", "AVJR"), np.nan)))
print("Variants: code's decaying correction, mean %.4f; detected responses only, mean %.4f (n = %d); without the autoresponse requirement, mean %.4f"
      % (x.dff_decay.mean(), x[x.responded].dff.mean(), x.responded.sum(), x_all.dff.mean()))
if len(x) > 1:
    w = stats.wilcoxon(x.dff)
    print("Wilcoxon signed-rank test of the trials against 0: p = %.3g" % w.pvalue)
print("Per animal:")
per_an = x.groupby("subject").agg(trials=("dff", "size"), mean_dff=("dff", "mean"), responses=("responded", "sum"))
print("Animals whose mean has the expected sign (positive): %d of %d" % ((np.sign(per_an.mean_dff) == 1).sum(), len(per_an)))
print(per_an.round(4).to_string())
AVJR stimulations with an autoresponse and AVDR observed: 27 trials in 17 animals (37 without the autoresponse requirement)
Mean <dF/F0>_t of AVDR: 0.2364 (median 0.1689, range -0.136 to 1.156); trials with a detected AVDR response: 12
Source Data value for this pair: mean 0.2461 over 24 observations, q = 6.07e-07
Variants: code's decaying correction, mean 0.1971; detected responses only, mean 0.3476 (n = 12); without the autoresponse requirement, mean 0.1787
Wilcoxon signed-rank test of the trials against 0: p = 5.49e-05
Per animal:
Animals whose mean has the expected sign (positive): 16 of 17
         trials  mean_dff  responses
subject                             
0             3    0.1071          0
107           1    0.4532          1
108           2    0.1089          0
110           1    0.4715          1
111           1    0.0482          0
14            1    0.4495          1
30            2    0.1368          1
34            4    0.3289          4
36            3    0.1130          2
4             1    0.1018          0
40            1    0.1321          1
45            1    1.1556          0
63            1    0.2825          0
67            2   -0.0907          0
69            1    0.1774          0
75            1    0.1029          0
98            1    0.7217          1

Result: reproduced. Over 27 AVJR stimulations with an autoresponse in 17 animals, the mean <dF/F0>_t of AVDR is 0.236 (Source Data 0.246), above 0 (Wilcoxon p = 5.5e-05), and an AVDR response is detected in 12 trials. 16 of the 17 animals have a positive mean.

fig1f-aver-avar¶

Activation of the premotor interneuron AVER evoked activity transients in AVAR.

In [21]:
x = obs[(obs.j == "AVER") & (obs.i == "AVAR") & obs.auto]
x_all = obs[(obs.j == "AVER") & (obs.i == "AVAR")]
print("AVER stimulations with an autoresponse and AVAR observed: %d trials in %d animals (%d without the autoresponse requirement)"
      % (len(x), x.rec.nunique(), len(x_all)))
print("Mean <dF/F0>_t of AVAR: %.4f (median %.4f, range %.3f to %.3f); trials with a detected AVAR response: %d"
      % (x.dff.mean(), x.dff.median(), x.dff.min(), x.dff.max(), x.responded.sum()))
print("Source Data value for this pair: mean %.4f over %d observations, q = %.3g"
      % (sd.dff_paper.get(("AVAR", "AVER"), np.nan), sd.n_paper.get(("AVAR", "AVER"), 0), sd.q.get(("AVAR", "AVER"), np.nan)))
print("Variants: code's decaying correction, mean %.4f; detected responses only, mean %.4f (n = %d); without the autoresponse requirement, mean %.4f"
      % (x.dff_decay.mean(), x[x.responded].dff.mean(), x.responded.sum(), x_all.dff.mean()))
if len(x) > 1:
    w = stats.wilcoxon(x.dff)
    print("Wilcoxon signed-rank test of the trials against 0: p = %.3g" % w.pvalue)
print("Per animal:")
per_an = x.groupby("subject").agg(trials=("dff", "size"), mean_dff=("dff", "mean"), responses=("responded", "sum"))
print("Animals whose mean has the expected sign (positive): %d of %d" % ((np.sign(per_an.mean_dff) == 1).sum(), len(per_an)))
print(per_an.round(4).to_string())
AVER stimulations with an autoresponse and AVAR observed: 7 trials in 5 animals (13 without the autoresponse requirement)
Mean <dF/F0>_t of AVAR: 0.1589 (median 0.1204, range -0.065 to 0.495); trials with a detected AVAR response: 4
Source Data value for this pair: mean 0.2044 over 11 observations, q = 0.00104
Variants: code's decaying correction, mean 0.1632; detected responses only, mean 0.3015 (n = 4); without the autoresponse requirement, mean 0.0921
Wilcoxon signed-rank test of the trials against 0: p = 0.219
Per animal:
Animals whose mean has the expected sign (positive): 3 of 5
         trials  mean_dff  responses
subject                             
26            1   -0.0179          0
4             1    0.1204          1
53            2    0.3153          2
70            2   -0.0376          0
73            1    0.4548          1

Result: reproduced. Over 7 AVER stimulations with an autoresponse in 5 animals, the mean <dF/F0>_t of AVAR is 0.159 (Source Data 0.204), and an AVAR response is detected in 4 trials. The evidence is thin: the trials do not differ from 0 by a Wilcoxon test (p = 0.22), and 3 of the 5 animals have a positive mean.

fig1g-saadl-ollr¶

Activation of SAADL inhibited the activity of the sensory neuron OLLR.

In [22]:
x = obs[(obs.j == "SAADL") & (obs.i == "OLLR") & obs.auto]
x_all = obs[(obs.j == "SAADL") & (obs.i == "OLLR")]
print("SAADL stimulations with an autoresponse and OLLR observed: %d trials in %d animals (%d without the autoresponse requirement)"
      % (len(x), x.rec.nunique(), len(x_all)))
print("Mean <dF/F0>_t of OLLR: %.4f (median %.4f, range %.3f to %.3f); trials with a detected OLLR response: %d"
      % (x.dff.mean(), x.dff.median(), x.dff.min(), x.dff.max(), x.responded.sum()))
print("Source Data value for this pair: mean %.4f over %d observations, q = %.3g"
      % (sd.dff_paper.get(("OLLR", "SAADL"), np.nan), sd.n_paper.get(("OLLR", "SAADL"), 0), sd.q.get(("OLLR", "SAADL"), np.nan)))
print("Variants: code's decaying correction, mean %.4f; detected responses only, mean %.4f (n = %d); without the autoresponse requirement, mean %.4f"
      % (x.dff_decay.mean(), x[x.responded].dff.mean(), x.responded.sum(), x_all.dff.mean()))
if len(x) > 1:
    w = stats.wilcoxon(x.dff)
    print("Wilcoxon signed-rank test of the trials against 0: p = %.3g" % w.pvalue)
print("Per animal:")
per_an = x.groupby("subject").agg(trials=("dff", "size"), mean_dff=("dff", "mean"), responses=("responded", "sum"))
print("Animals whose mean has the expected sign (negative): %d of %d" % ((np.sign(per_an.mean_dff) == -1).sum(), len(per_an)))
print(per_an.round(4).to_string())
SAADL stimulations with an autoresponse and OLLR observed: 5 trials in 4 animals (9 without the autoresponse requirement)
Mean <dF/F0>_t of OLLR: -0.0591 (median -0.0940, range -0.233 to 0.202); trials with a detected OLLR response: 2
Source Data value for this pair: mean -0.0404 over 5 observations, q = 0.00326
Variants: code's decaying correction, mean 0.0136; detected responses only, mean -0.0154 (n = 2); without the autoresponse requirement, mean -0.0249
Wilcoxon signed-rank test of the trials against 0: p = 0.438
Per animal:
Animals whose mean has the expected sign (negative): 3 of 4
         trials  mean_dff  responses
subject                             
1             2   -0.1170          0
101           1   -0.2330          1
50            1    0.2023          1
94            1   -0.0307          0

Result: reproduced. Over 5 SAADL stimulations with an autoresponse in 4 animals, the mean <dF/F0>_t of OLLR is -0.059 (Source Data -0.040), and an OLLR response is detected in 2 trials. The evidence is weak: 3 of the 4 animals have a negative mean, the trials do not differ from 0 (p = 0.44), and with the code's correction of decaying traces the mean is +0.014.

edfig2ij-spatial-specificity¶

Stimulation does not elicit responses in most close neighbours of the targeted neuron; at 75% of the cumulative distribution, neighbours' responses are below a dF/F0 of 0.1.

In [23]:
step = dict(zip(dep.subject, dep.plane_step_um))
rows = []
for r in res:
    s = step[int(r["subject_id"])]
    for e in np.where(r["auto"])[0]:
        jj = r["stim_roi"][e]
        dist = sp.roi_distance_um(r, np.arange(r["n_roi"]), jj, s)
        v = r["dff"][e]
        ok = np.isfinite(v)
        rows.append(pd.DataFrame({"rec": int(r["subject_id"]), "dist": dist[ok], "dff": v[ok], "f0": r["f0"][e][ok]}))
sdist = pd.concat(rows, ignore_index=True)
bands = {"0 um (target)": sdist.dist == 0, "4-10 um": (sdist.dist >= 4) & (sdist.dist < 10),
         "10-16 um": (sdist.dist >= 10) & (sdist.dist < 16), "4-16 um": (sdist.dist >= 4) & (sdist.dist < 16)}
tab = pd.DataFrame([{"band": b, "n": int(m.sum()), "median": sdist.dff[m].median(),
                     "p75": sdist.dff[m].quantile(0.75), "frac_below_0.1": (sdist.dff[m] < 0.1).mean()}
                    for b, m in bands.items()])
print(tab.round(3).to_string(index=False))
ratio = tab.loc[0, "median"] / tab.loc[1, "median"]
print("Target median over 4-10 um median: %.1f (criterion at least 3)" % ratio)
print("Expected fraction below 0.1: 0.75 (tolerance 0.075); the Source Data give 0.616 (4-10 um), 0.823 (10-16 um), 0.749 pooled")
for thr in (0.05, 0.1, 0.2):
    print("Variant threshold %.2f: fraction of 4-16 um neighbours below it %.3f" % (thr, (sdist.dff[bands['4-16 um']] < thr).mean()))
per = sdist[bands["4-16 um"]].groupby("rec").dff.apply(lambda v: (v < 0.1).mean())
print("Per animal, fraction of 4-16 um neighbours below 0.1: min %.2f, median %.2f, max %.2f (%d animals)"
      % (per.min(), per.median(), per.max(), len(per)))
pos = sdist.f0 > 0
for b in ["4-10 um", "10-16 um", "4-16 um"]:
    print("Variant F0 > 0 only, %s: fraction below 0.1 %.3f (n = %d)" % (b, (sdist.dff[bands[b] & pos] < 0.1).mean(), (bands[b] & pos).sum()))
         band     n  median   p75  frac_below_0.1
0 um (target)  2448   0.376 0.601           0.092
      4-10 um 20358   0.061 0.183           0.604
     10-16 um 36157   0.006 0.071           0.817
      4-16 um 56515   0.022 0.105           0.741
Target median over 4-10 um median: 6.2 (criterion at least 3)
Expected fraction below 0.1: 0.75 (tolerance 0.075); the Source Data give 0.616 (4-10 um), 0.823 (10-16 um), 0.749 pooled
Variant threshold 0.05: fraction of 4-16 um neighbours below it 0.608
Variant threshold 0.10: fraction of 4-16 um neighbours below it 0.741
Variant threshold 0.20: fraction of 4-16 um neighbours below it 0.869
Per animal, fraction of 4-16 um neighbours below 0.1: min 0.50, median 0.77, max 0.97 (110 animals)
Variant F0 > 0 only, 4-10 um: fraction below 0.1 0.605 (n = 20180)
Variant F0 > 0 only, 10-16 um: fraction below 0.1 0.817 (n = 35913)
Variant F0 > 0 only, 4-16 um: fraction below 0.1 0.741 (n = 56093)
In [24]:
fig, ax = plt.subplots(figsize=(6, 4))
for b in ["0 um (target)", "4-10 um", "10-16 um"]:
    v = np.sort(sdist.dff[bands[b]].values)
    ax.plot(v, np.arange(1, len(v) + 1) / len(v), label=b)
ax.axvline(0.1, color="k", lw=0.8, ls="--"); ax.axhline(0.75, color="k", lw=0.8, ls=":")
ax.set_xlim(-0.5, 1.5); ax.set_xlabel("<dF/F0>_t"); ax.set_ylabel("cumulative fraction"); ax.legend()
plt.show()
print("Values outside the x axis (-0.5 to 1.5): %d of %d" % (((sdist.dff < -0.5) | (sdist.dff > 1.5)).sum(), len(sdist)))
No description has been provided for this image
Values outside the x axis (-0.5 to 1.5): 508 of 244219

Result: reproduced. Pooling the neighbours 4 to 16 um from the target (reading a), 74.1% have <dF/F0>_t below 0.1 (expected 0.75, tolerance 0.075; the Source Data give 0.749), and the targeted ROI's median, 0.376, is 6.2 times the 4 to 10 um median of 0.061. The bands alone give 60.4% (4 to 10 um) and 81.7% (10 to 16 um), against 61.6% and 82.3% in the Source Data. Leaving out rows with F0 <= 0 gives the same fractions (0.605, 0.817 and 0.741).

fig2d-inhibitory-fraction¶

11% of the functional connections at q < 0.05 are inhibitory.

In [25]:
sig_pairs = sd[sd.q < 0.05]
jq = sig_pairs.join(main, how="left")
for name, sel in [("(a) including i = j", np.ones(len(jq), bool)),
                  ("(b) excluding i = j", jq.index.get_level_values(0) != jq.index.get_level_values(1))]:
    q = jq[sel]
    found = q["mean"].notna()
    print("%s: q < 0.05 pairs %d, observed here %d; negative mean here %d (%.4f); negative in the Source Data %d (%.4f)"
          % (name, len(q), found.sum(), (q["mean"][found] < 0).sum(), (q["mean"][found] < 0).mean(),
             (q.dff_paper < 0).sum(), (q.dff_paper < 0).mean()))
print("Expected 0.11 (tolerance 0.011)")
qd = jq[jq["mean"].notna()]
print("Sign agreement with the Source Data over these pairs: %.3f" % np.mean(np.sign(qd["mean"]) == np.sign(qd.dff_paper)))
dq = sig_pairs.join(dec, how="left")
print("Variant, code's decaying correction, reading (a): negative %.4f" % (dq["mean"].dropna() < 0).mean())
for thr in (0.01, 0.02, 0.1):
    qq = sd[sd.q < thr].join(main, how="left")["mean"].dropna()
    print("Variant q < %.2f: %d pairs, negative %.4f" % (thr, len(qq), (qq < 0).mean()))
def neg_frac(t):
    qq = sig_pairs.join(t, how="left")["mean"]
    off = qq.index.get_level_values(0) != qq.index.get_level_values(1)
    return {"observed (a)": int(qq.notna().sum()), "negative (a)": (qq.dropna() < 0).mean(),
            "negative (b)": (qq[off].dropna() < 0).mean()}
print("\nVariants, fraction of q < 0.05 pairs with a negative mean (expected 0.11, tolerance 0.011):")
vt = {**var_tabs, "exported assignment (its exclusions applied)": main_osf, "exported assignment and labels": main_osf2,
      "exported assignment, F0 > 0 and corrected traces only": h_osf[(h_osf.f0 > 0) & h_osf.pb_ok].groupby(["i", "j"]).dff.agg(["size", "mean"]).rename(columns={"size": "n"})}
print(pd.DataFrame([{"variant": k, **neg_frac(t)} for k, t in vt.items()]).round(4).to_string(index=False))
(a) including i = j: q < 0.05 pairs 1310, observed here 1161; negative mean here 175 (0.1507); negative in the Source Data 151 (0.1153)
(b) excluding i = j: q < 0.05 pairs 1151, observed here 1005; negative mean here 175 (0.1741); negative in the Source Data 151 (0.1312)
Expected 0.11 (tolerance 0.011)
Sign agreement with the Source Data over these pairs: 0.891
Variant, code's decaying correction, reading (a): negative 0.0956
Variant q < 0.01: 689 pairs, negative 0.0726
Variant q < 0.02: 837 pairs, negative 0.0920
Variant q < 0.10: 1681 pairs, negative 0.2124

Variants, fraction of q < 0.05 pairs with a negative mean (expected 0.11, tolerance 0.011):
                                              variant  observed (a)  negative (a)  negative (b)
                                                 main          1161        0.1507        0.1741
                                          F0 > 0 only          1160        0.1509        0.1743
        without the 9 recordings with negative traces          1137        0.1530        0.1772
                 photobleaching-corrected traces only          1161        0.1507        0.1741
           without the export's excluded stimulations          1157        0.1521        0.1758
        without the 9 recordings with agreement < 0.6          1141        0.1560        0.1803
                                 all of these filters          1108        0.1579        0.1832
         exported assignment (its exclusions applied)          1179        0.1399        0.1611
                       exported assignment and labels          1190        0.1336        0.1535
exported assignment, F0 > 0 and corrected traces only          1179        0.1416        0.1631

Result: not reproduced. Of the 1,310 pairs with q < 0.05 in the Source Data, 1,161 are observed here, and 15.1% of them have a negative mean <dF/F0>_t (reading a; 17.4% without i = j), outside 0.11 with tolerance 0.011. The Source Data's own means give 11.5% for the same 1,310 pairs, and the signs here agree with them in 89.1% of the pairs observed, so the difference comes from the response amplitudes computed here, not from the selection. With the code's correction of decaying traces the fraction is 9.6%, also outside the tolerance. The fraction depends on the q threshold (7.3% at q < 0.01, 21.2% at q < 0.1); inhibitory pairs are a minority under every variant. Neither a non-positive F0 nor a failed photobleaching fit explains the excess: keeping only rows with F0 > 0 or corrected traces leaves 15.1%, and leaving out the nine recordings with negative traces (15.3%), the export's excluded stimulations (15.2%) or the nine poorly linked recordings (15.6%) does not lower it; all of these filters together give 15.8%. With the stimulated ROI from the export, which applies its exclusions, the fraction is 14.0%, and 13.4% with its labels as well, both outside the tolerance.

fig4-rid-wt¶

URX, ADL and AWB, predicted from anatomy to have no response, respond strongly to RID stimulation in WT animals.

RID stimulations are those whose stimulated ROI, found as everywhere else here, carries the label RID. As the Methods state for Fig. 4, no autoresponse is required and values beyond +-2 are excluded. The paper's "matchless" traces, which it substitutes for dim neurons such as ADL, are not in the dandiset.

In [26]:
FIG4 = {"URX": "URXL", "ADL": "ADLR", "AWB": "AWBR"}
def rid_rows(o, label):
    x = o[o.j == "RID"]
    n_stim = x.groupby(["rec", "stim"]).ngroups
    x = x[x.dff.abs() <= 2]
    rows = []
    for cls, shown in FIG4.items():
        for reading, names in [("(a) %s, L and R" % cls, [cls + "L", cls + "R"]), ("(b) %s" % shown, [shown])]:
            y = x[x.i.isin(names)]
            rows.append({"source": label, "reading": reading, "neuron": cls, "rid_stims": n_stim, "trials": len(y),
                         "animals": y.rec.nunique(), "mean": y.dff.mean() if len(y) else np.nan,
                         "median": y.dff.median() if len(y) else np.nan, "detected": int(y.responded.sum()),
                         "positive_trials": int((y.dff > 0).sum()),
                         "pctile_among_head_pairs": stats.percentileofscore(main["mean"].dropna(), y.dff.mean()) if len(y) else np.nan})
    return rows

rid_stims = obs[obs.j == "RID"].groupby(["rec", "stim"]).agg(subject=("subject", "first"), how=("how", "first"), auto=("auto", "first"))
print("RID stimulations here: %d in %d animals (manual targets %d, nearest centroid %d); with a RID autoresponse: %d"
      % (len(rid_stims), rid_stims.subject.nunique(), (rid_stims.how == "manual").sum(), (rid_stims.how == "nearest").sum(), rid_stims.auto.sum()))
xr = obs[(obs.j == "RID")]
print("Observations of URX, ADL or AWB after a RID stimulation: %d; beyond +-2 and excluded: %d"
      % (xr.i.str[:3].isin(list(FIG4)).sum(), (xr.i.str[:3].isin(list(FIG4)) & (xr.dff.abs() > 2)).sum()))
rid_wt = pd.DataFrame(rid_rows(obs, "dandiset, assignment here")
                      + rid_rows(obs[obs.auto], "dandiset, autoresponse required")
                      + rid_rows(obs_osf, "dandiset traces, exported assignment")
                      + rid_rows(obs_osf2, "dandiset traces, exported assignment and labels"))
print(rid_wt.round(3).to_string(index=False))
print("\nPer trial, main source (subject, stimulation, observed neuron, <dF/F0>_t, detected):")
xx = xr[xr.i.str[:3].isin(list(FIG4)) & (xr.dff.abs() <= 2)]
print(xx[["subject", "stim", "how", "i", "dff", "responded"]].round(3).to_string(index=False))
RID stimulations here: 8 in 5 animals (manual targets 5, nearest centroid 3); with a RID autoresponse: 5
Observations of URX, ADL or AWB after a RID stimulation: 14; beyond +-2 and excluded: 0
                                         source          reading neuron  rid_stims  trials  animals  mean  median  detected  positive_trials  pctile_among_head_pairs
                      dandiset, assignment here (a) URX, L and R    URX          8       1        1 0.032   0.032         0                1                   67.309
                      dandiset, assignment here         (b) URXL    URX          8       0        0   NaN     NaN         0                0                      NaN
                      dandiset, assignment here (a) ADL, L and R    ADL          8       0        0   NaN     NaN         0                0                      NaN
                      dandiset, assignment here         (b) ADLR    ADL          8       0        0   NaN     NaN         0                0                      NaN
                      dandiset, assignment here (a) AWB, L and R    AWB          8      12        5 0.076   0.052         3                7                   81.692
                      dandiset, assignment here         (b) AWBR    AWB          8       7        4 0.094   0.053         2                4                   85.233
                dandiset, autoresponse required (a) URX, L and R    URX          5       1        1 0.032   0.032         0                1                   67.309
                dandiset, autoresponse required         (b) URXL    URX          5       0        0   NaN     NaN         0                0                      NaN
                dandiset, autoresponse required (a) ADL, L and R    ADL          5       0        0   NaN     NaN         0                0                      NaN
                dandiset, autoresponse required         (b) ADLR    ADL          5       0        0   NaN     NaN         0                0                      NaN
                dandiset, autoresponse required (a) AWB, L and R    AWB          5       8        4 0.056   0.019         2                4                   76.922
                dandiset, autoresponse required         (b) AWBR    AWB          5       4        3 0.071   0.020         1                2                   80.585
           dandiset traces, exported assignment (a) URX, L and R    URX         11       1        1 0.032   0.032         0                1                   67.309
           dandiset traces, exported assignment         (b) URXL    URX         11       0        0   NaN     NaN         0                0                      NaN
           dandiset traces, exported assignment (a) ADL, L and R    ADL         11       0        0   NaN     NaN         0                0                      NaN
           dandiset traces, exported assignment         (b) ADLR    ADL         11       0        0   NaN     NaN         0                0                      NaN
           dandiset traces, exported assignment (a) AWB, L and R    AWB         11      14        5 0.100   0.071         5                9                   86.394
           dandiset traces, exported assignment         (b) AWBR    AWB         11       9        4 0.127   0.116         4                6                   89.853
dandiset traces, exported assignment and labels (a) URX, L and R    URX         12       1        1 0.032   0.032         0                1                   67.309
dandiset traces, exported assignment and labels         (b) URXL    URX         12       0        0   NaN     NaN         0                0                      NaN
dandiset traces, exported assignment and labels (a) ADL, L and R    ADL         12       0        0   NaN     NaN         0                0                      NaN
dandiset traces, exported assignment and labels         (b) ADLR    ADL         12       0        0   NaN     NaN         0                0                      NaN
dandiset traces, exported assignment and labels (a) AWB, L and R    AWB         12      16        6 0.087   0.052         5                9                   83.995
dandiset traces, exported assignment and labels         (b) AWBR    AWB         12      10        5 0.113   0.085         4                6                   88.255

Per trial, main source (subject, stimulation, observed neuron, <dF/F0>_t, detected):
subject  stim     how    i    dff  responded
     25     0  manual AWBL -0.074      False
     25     0  manual AWBR -0.054      False
     25     1  manual AWBL  0.090      False
     25     1  manual AWBR -0.008      False
     25     2  manual AWBL  0.051      False
     25     2  manual AWBR -0.013      False
     26    49 nearest AWBL -0.103      False
     26    49 nearest  ADL  0.011       True
     41    50  manual AWBR  0.298       True
     41    50  manual URXR  0.032      False
     41    50  manual AWBL  0.294       True
     42     0 nearest AWBR  0.264       True
     42     2  manual AWBR  0.053      False
     84    19 nearest AWBR  0.116      False

Result: partially reproduced. Eight stimulations in 5 animals target an ROI labelled RID (5 manually located, 3 nearest-centroid). Under both readings only AWB meets the criterion: AWB, left and right pooled, has a mean <dF/F0>_t of 0.076 over 12 trials in 5 animals with 3 detected responses, at the 82nd percentile of the head-pair means, and AWBR 0.094 over 7 trials with 2 detected. URX is observed once (URXR, 0.032, no detected response), and ADL is never observed with a side label after a RID stimulation (one trace labelled "ADL" without a side, 0.011, is not counted). The paper substitutes matchless traces for dim neurons such as ADL and includes RID stimulations in which RID was not segmented; neither is in the dandiset. With the export's assignment (11 stimulations) AWB has a mean of 0.100 with 5 detected responses, and URX and ADL are covered no better.

fig4-rid-unc31¶

The responses of URXL, ADLR and AWBR to RID stimulation are reduced or absent in unc-31 mutants.

The unc-31 recordings are not in the dandiset; they are read from the paper's OSF export (exported_data_unc31.tar.gz, OSF guid ty6sp, version 1), processed with the same functions, and compared with the WT values above. The WT values computed in the same way from the WT export are a variant, since the dandiset's traces and the exported ones differ.

In [27]:
# The paper's unc-31 export on OSF (doi 10.17605/OSF.IO/E2SYT), file exported_data_unc31.tar.gz
# (guid ty6sp, version 1, md5 e5c83242baa131bca95ee58bc7b58bec), streamed once.
U_URL, U_MD5 = "https://osf.io/download/ty6sp/", "e5c83242baa131bca95ee58bc7b58bec"
utx = {}
with urllib.request.urlopen(U_URL, timeout=600) as r:
    hr = _Hashed(r)
    with tarfile.open(fileobj=hr, mode="r|gz") as tf:
        for m in tf:
            mm = re.search(r"/(\d+)_(ds_name|stim_neurons|labels|gcamp|t|stim_volume_i)\.txt$", m.name)
            if mm:
                utx[(int(mm.group(1)), mm.group(2))] = tf.extractfile(m).read().decode()
    while hr.read(1 << 20):
        pass
print("exported_data_unc31.tar.gz: %d bytes, md5 %s, matches the OSF record: %s" % (hr.n, hr.md5.hexdigest(), hr.md5.hexdigest() == U_MD5))

def export_recording(get, k, name):
    g = np.loadtxt(io.StringIO(get(k, "gcamp")), ndmin=2)
    t = np.loadtxt(io.StringIO(get(k, "t")))
    sv = np.loadtxt(io.StringIO(get(k, "stim_volume_i")), ndmin=1).astype(int)
    stim = np.array([int(x) for x in get(k, "stim_neurons").split()])
    lab = [x.strip() for x in get(k, "labels").split("\n")]
    lab = (lab + [""] * g.shape[1])[:g.shape[1]]
    same = len(sv) == len(stim)
    return sp.analyse_export(g, t, sv, lab, name), (stim if same else None), same, g.shape

u_keys = sorted({k for k, _ in utx})
u_res, u_stim, u_info = [], [], []
for k in u_keys:
    an, stim, same, shape = export_recording(lambda kk, w: utx[(kk, w)], k, "unc31-%d" % k)
    le = list(an["roi_label"])
    n_rid = sum(1 for s in (stim if stim is not None else []) if 0 <= s < len(le) and le[s] == "RID")
    u_info.append({"k": k, "folder": utx[(k, "ds_name")].strip().split("/")[-2], "volumes": shape[0], "rois": shape[1],
                   "stims": len(stim) if stim is not None else np.nan, "stim_counts_match": same, "rid_stims": n_rid,
                   "pb_not_ok": int(np.sum(an["photobleach_status"] != "ok"))})
    u_res.append(an); u_stim.append(stim)
print(pd.DataFrame(u_info).to_string(index=False))
obs_u = sp.observations(u_res, head, stim_roi=u_stim)

wt_res, wt_stim = [], []
for k in rid_k:
    an, stim, same, shape = export_recording(lambda kk, w: zlib.decompress(big_txt[(kk, w)]).decode() if w in ("gcamp", "t", "stim_volume_i") else texts[(kk, w)], k, "wt-export-%d" % k)
    wt_res.append(an); wt_stim.append(stim)
obs_wte = sp.observations(wt_res, head, stim_roi=wt_stim)
print("WT export recordings processed for the variant:", len(wt_res))
exported_data_unc31.tar.gz: 84924311 bytes, md5 e5c83242baa131bca95ee58bc7b58bec, matches the OSF record: True
 k                    folder  volumes  rois  stims  stim_counts_match  rid_stims  pb_not_ok
 0 pumpprobe_20211216_164305     4080    91     60               True          0          0
 1 pumpprobe_20220113_101730     5035   126     79               True          0          4
 2 pumpprobe_20220113_112922     3853    89     60               True          0          1
 3 pumpprobe_20220113_142740     3853    82     60               True          0          0
 4 pumpprobe_20220113_165125     3854   118     60               True          0          1
 5 pumpprobe_20220211_094256     1462   100     19               True          4          1
 6 pumpprobe_20220211_141039      683   116      5               True          5          0
 7 pumpprobe_20220211_151211      550   107      4               True          4         19
 8 pumpprobe_20220412_112143     4493   134     68               True          3         11
 9 pumpprobe_20220422_104335     3416   110     52               True          0          3
10 pumpprobe_20220415_104205     4599   150     69               True          4         12
11 pumpprobe_20220422_144057     4522   135     65               True          2          4
12 pumpprobe_20220505_111403     4571   131     68               True          0         11
13 pumpprobe_20220510_110807     4785   148     75               True          0         22
14 pumpprobe_20220510_162733     4748   160     75               True          0         37
15 pumpprobe_20220511_124404     4920   111     71               True          3          8
16 pumpprobe_20220511_150909     4448   135     67               True          3         23
17 pumpprobe_20220511_161734     4566   115     68               True          0          4
WT export recordings processed for the variant: 7
In [28]:
rid_u = pd.DataFrame(rid_rows(obs_u, "unc-31 export") + rid_rows(obs_wte, "WT export"))
print(rid_u.round(3).to_string(index=False))
wt_main = rid_wt[rid_wt.source == "dandiset, assignment here"].set_index("reading")
wt_exp = rid_u[rid_u.source == "WT export"].set_index("reading")
un = rid_u[rid_u.source == "unc-31 export"].set_index("reading")
cmp_ = pd.DataFrame({"WT dandiset mean": wt_main["mean"], "WT dandiset trials": wt_main.trials,
                     "WT export mean": wt_exp["mean"], "WT export trials": wt_exp.trials,
                     "unc-31 mean": un["mean"], "unc-31 trials": un.trials})
cmp_["reduced vs WT dandiset"] = np.where(cmp_[["WT dandiset mean", "unc-31 mean"]].notna().all(axis=1), cmp_["unc-31 mean"] < cmp_["WT dandiset mean"], np.nan)
cmp_["reduced vs WT export"] = np.where(cmp_[["WT export mean", "unc-31 mean"]].notna().all(axis=1), cmp_["unc-31 mean"] < cmp_["WT export mean"], np.nan)
def trials(o, names):
    y = o[(o.j == "RID") & o.i.isin(names) & (o.dff.abs() <= 2)]
    return y.dff.values
mw = {}
for cls, shown in FIG4.items():
    for reading, names in [("(a) %s, L and R" % cls, [cls + "L", cls + "R"]), ("(b) %s" % shown, [shown])]:
        a_, b_, e_ = trials(obs, names), trials(obs_u, names), trials(obs_wte, names)
        mw[reading] = {"p, unc-31 below WT dandiset": stats.mannwhitneyu(b_, a_, alternative="less").pvalue if len(a_) and len(b_) else np.nan,
                       "p, unc-31 below WT export": stats.mannwhitneyu(b_, e_, alternative="less").pvalue if len(e_) and len(b_) else np.nan}
cmp_ = cmp_.join(pd.DataFrame(mw).T)
print("\n" + cmp_.round(3).to_string())
print("(one-sided Mann-Whitney tests of the trials, for information; the criterion compares means)")
for (lab_, o_) in [("unc-31", obs_u), ("WT export", obs_wte)]:
    y = o_[(o_.j == "RID") & o_.i.str[:3].isin(list(FIG4)) & (o_.dff.abs() <= 2)]
    print("\n%s, per trial (recording, stimulation, observed neuron, <dF/F0>_t, detected):" % lab_)
    print(y[["subject", "stim", "i", "dff", "responded"]].round(3).to_string(index=False))
       source          reading neuron  rid_stims  trials  animals   mean  median  detected  positive_trials  pctile_among_head_pairs
unc-31 export (a) URX, L and R    URX         28      25        5  0.031   0.050         7               14                   66.819
unc-31 export         (b) URXL    URX         28      16        5  0.018   0.035         3                9                   60.116
unc-31 export (a) ADL, L and R    ADL         28      14        4  0.061   0.036         3                9                   78.205
unc-31 export         (b) ADLR    ADL         28       9        3  0.033   0.031         2                6                   67.980
unc-31 export (a) AWB, L and R    AWB         28      50        7  0.067   0.047        17               32                   79.783
unc-31 export         (b) AWBR    AWB         28      25        7  0.048   0.048         5               18                   74.328
    WT export (a) URX, L and R    URX         12       5        4  0.109   0.092         3                5                   87.745
    WT export         (b) URXL    URX         12       3        3  0.136   0.092         2                3                   90.810
    WT export (a) ADL, L and R    ADL         12       1        1 -0.062  -0.062         0                0                   16.656
    WT export         (b) ADLR    ADL         12       0        0    NaN     NaN         0                0                      NaN
    WT export (a) AWB, L and R    AWB         12      18        7  0.086   0.068         7               12                   83.849
    WT export         (b) AWBR    AWB         12      11        6  0.112   0.126         6                7                   88.051
                  WT dandiset mean  WT dandiset trials  WT export mean  WT export trials  unc-31 mean  unc-31 trials  reduced vs WT dandiset  reduced vs WT export  p, unc-31 below WT dandiset  p, unc-31 below WT export
reading                                                                                                                                                                                                                   
(a) URX, L and R             0.032                   1           0.109                 5        0.031             25                     1.0                   1.0                        0.538                      0.085
(b) URXL                       NaN                   0           0.136                 3        0.018             16                     NaN                   1.0                          NaN                      0.086
(a) ADL, L and R               NaN                   0          -0.062                 1        0.061             14                     NaN                   0.0                          NaN                      1.000
(b) ADLR                       NaN                   0             NaN                 0        0.033              9                     NaN                   NaN                          NaN                        NaN
(a) AWB, L and R             0.076                  12           0.086                18        0.067             50                     1.0                   1.0                        0.440                      0.268
(b) AWBR                     0.094                   7           0.112                11        0.048             25                     1.0                   1.0                        0.312                      0.129
(one-sided Mann-Whitney tests of the trials, for information; the criterion compares means)

unc-31, per trial (recording, stimulation, observed neuron, <dF/F0>_t, detected):
 subject  stim    i    dff  responded
 unc31-5     0 AWBR  0.005      False
 unc31-5     0 ADLR -0.058      False
 unc31-5     0 URXL -0.099      False
 unc31-5     0 AWBL -0.053      False
 unc31-5     1 AWBR -0.029      False
 unc31-5     1 ADLR  0.041      False
 unc31-5     1 URXL  0.122      False
 unc31-5     1 AWBL -0.008      False
 unc31-5     2 AWBR  0.081      False
 unc31-5     2 ADLR  0.097      False
 unc31-5     2 URXL -0.056      False
 unc31-5     2 AWBL  0.088      False
 unc31-5     3 AWBR -0.091      False
 unc31-5     3 ADLR -0.037      False
 unc31-5     3 URXL  0.020      False
 unc31-5     3 AWBL -0.066      False
 unc31-6     0 AWBR -0.031      False
 unc31-6     0  URX -0.070      False
 unc31-6     0 AWBL -0.060      False
 unc31-6     1 AWBR  0.011      False
 unc31-6     1  URX  0.050       True
 unc31-6     1 AWBL -0.027       True
 unc31-6     2 AWBR -0.034      False
 unc31-6     2  URX -0.033      False
 unc31-6     2 AWBL  0.084       True
 unc31-6     3 AWBR  0.008      False
 unc31-6     3  URX -0.007      False
 unc31-6     3 AWBL  0.011       True
 unc31-6     4 AWBR -0.081      False
 unc31-6     4  URX -0.079      False
 unc31-6     4 AWBL -0.053      False
 unc31-7     0 AWBR -0.092      False
 unc31-7     0 AWBL -0.022      False
 unc31-7     1 AWBR  0.069      False
 unc31-7     1 AWBL  0.140      False
 unc31-7     2 AWBR  0.078      False
 unc31-7     2 AWBL  0.261       True
 unc31-7     3 AWBR  0.082       True
 unc31-7     3 AWBL  0.212       True
 unc31-8     0 AWBL  0.005      False
 unc31-8     0 URXL -0.106      False
 unc31-8     0 ADLR -0.025      False
 unc31-8     0 AWBR  0.012      False
 unc31-8     1 AWBL -0.018      False
 unc31-8     1 URXL  0.051      False
 unc31-8     1 ADLR  0.031      False
 unc31-8     1 AWBR  0.045      False
 unc31-8     2 AWBL -0.021      False
 unc31-8     2 URXL -0.031      False
 unc31-8     2 ADLR  0.013      False
 unc31-8     2 AWBR  0.048      False
unc31-10     0 AWBR  0.065      False
unc31-10     0 URXR -0.001      False
unc31-10     0 URXL -0.044      False
unc31-10     0 AWBL  0.130       True
unc31-10     1 AWBR  0.091       True
unc31-10     1 URXR -0.044      False
unc31-10     1 URXL  0.059      False
unc31-10     1 AWBL  0.328       True
unc31-10     2 AWBR  0.165      False
unc31-10     2 URXR -0.016      False
unc31-10     2 URXL -0.026      False
unc31-10     2 AWBL  0.270       True
unc31-10     3 AWBR  0.260       True
unc31-10     3 URXR  0.080       True
unc31-10     3 URXL  0.050      False
unc31-10     3 AWBL  0.537      False
unc31-11     3 AWBL -0.118       True
unc31-11     3 ADLL -0.051      False
unc31-11     3 URXL  0.113       True
unc31-11     3 URXR  0.065       True
unc31-11     3 ADLR  0.042       True
unc31-11     3 AWBR -0.013      False
unc31-11     4 AWBL -0.028       True
unc31-11     4 ADLL  0.437       True
unc31-11     4 URXL  0.074       True
unc31-11     4 URXR  0.288       True
unc31-11     4 ADLR  0.194       True
unc31-11     4 AWBR  0.189      False
unc31-15     0 URXR -0.079      False
unc31-15     0 URXL -0.097      False
unc31-15     0 ADLL  0.109      False
unc31-15     1 URXR  0.099       True
unc31-15     1 URXL  0.079      False
unc31-15     1 ADLL  0.073      False
unc31-15     2 URXR  0.096      False
unc31-15     2 URXL  0.179       True
unc31-15     2 ADLL -0.010      False
unc31-16     0 AWBR  0.080      False
unc31-16     0 AWBL  0.182       True
unc31-16     1 AWBR  0.139       True
unc31-16     1 AWBL  0.194       True
unc31-16     2 AWBR  0.150       True
unc31-16     2 AWBL  0.168      False

WT export, per trial (recording, stimulation, observed neuron, <dF/F0>_t, detected):
     subject  stim    i    dff  responded
wt-export-25     0 AWBL -0.107      False
wt-export-25     0 AWBR -0.076      False
wt-export-25     1 AWBL  0.093      False
wt-export-25     1 AWBR -0.007      False
wt-export-25     2 AWBL  0.044      False
wt-export-25     2 AWBR -0.018      False
wt-export-26    49 AWBL -0.104      False
wt-export-26    49  ADL  0.009       True
wt-export-41    50 AWBR  0.299       True
wt-export-41    50 URXR  0.033      False
wt-export-41    50 AWBL  0.291       True
wt-export-42     0 AWBR  0.262       True
wt-export-42     1 AWBR  0.236       True
wt-export-42     2 AWBR  0.043       True
wt-export-42     3 AWBR  0.243       True
wt-export-76    22 URXL  0.048      False
wt-export-76    22 AWBL  0.003      False
wt-export-76    22 ADLL -0.062      False
wt-export-76    22 AWBR -0.020      False
wt-export-84    19 AWBR  0.126      False
wt-export-84    19 URXR  0.104       True
wt-export-84    19 AWBL  0.099      False
wt-export-84    19  ADL  0.131      False
wt-export-84    19 URXL  0.269       True
wt-export-89    19 URXL  0.092       True
wt-export-89    19 AWBR  0.139       True

Result: reproduced, on thin evidence. The 18 unc-31 recordings of the export hold 28 stimulations of an ROI labelled RID, in 8 recordings. The neurons observed after RID stimulation in both strains are URX and AWB under reading (a) and AWBR under reading (b). Each has a lower mean in unc-31 than in the WT dandiset: URX 0.031 against 0.032 from a single WT trial, AWB 0.067 over 50 trials against 0.076 over 12, and AWBR 0.048 over 25 trials against 0.094 over 7, so the criterion holds under both readings. The differences are small and the one-sided Mann-Whitney tests do not find them (p = 0.54 for URX, 0.44 for AWB and 0.31 for AWBR against the WT dandiset). The responses are not absent in unc-31: AWB responses are detected in 17 of 50 trials. Against the WT export, URX and AWB are also lower in unc-31, while ADL, observed once in the WT export (-0.062), is higher in unc-31 (0.061 over 14 trials).

edfig6-kernel-stereotypy (blocked)¶

Kernels of one neuron pair are more similar to each other than to kernels of other pairs, and kernel rise times characterize the speed of signal propagation.

This claim is blocked as not runnable. The kernels are fitted with the paper's pumpprobe package, whose fits call C++ extensions and which imports mistofrutta, wormbrain and wormdatamodel. The cell below tries to build these packages at fixed commits in a temporary environment with this kernel's Python and numpy, and prints how each build ends. The fits were not reimplemented without the package.

In [29]:
# Build the paper's kernel-fitting package and the packages it imports, at fixed commits, in a
# temporary environment with this kernel's Python and numpy (own uv cache, so nothing is reused).
import shutil, subprocess, sys, tempfile
UV = shutil.which("uv")
assert UV, "uv is needed for this check"
PKGS = [("pumpprobe", "https://github.com/leiferlab/pumpprobe", "1dbc5e0a2b609d54bc9b1c90c73d4e3bf183d3c7"),
        ("mistofrutta", "https://github.com/francescorandi/mistofrutta", "11ac8c3e40743b6e35f54568e2de36b95466bbc4"),
        ("wormbrain", "https://github.com/leiferlab/wormbrain", "d94f7a1456622f77eebcc181de1d8504618764c4"),
        ("wormdatamodel", "https://github.com/leiferlab/wormdatamodel", "2ab956199e3931de41a190d2b9985e961df3810c")]
with tempfile.TemporaryDirectory() as tmp:
    env0 = {**os.environ, "UV_CACHE_DIR": tmp + "/cache"}
    envpy = tmp + "/env/bin/python"
    subprocess.run([UV, "venv", "-q", "-p", "%d.%d" % sys.version_info[:2], tmp + "/env"], check=True, env=env0)
    subprocess.run([UV, "pip", "install", "-q", "-p", envpy, "numpy==" + np.__version__, "setuptools", "gitpython"],
                   check=True, env=env0)
    inc = subprocess.run([envpy, "-c", "import numpy; print(numpy.get_include())"],
                         capture_output=True, text=True, check=True).stdout.strip()
    print("Python %d.%d, numpy %s" % (*sys.version_info[:2], np.__version__))
    def build(name, url, commit, env):
        p = subprocess.run([UV, "pip", "install", "-p", envpy, "--no-build-isolation", "--no-deps",
                            "%s @ git+%s@%s" % (name, url, commit)], capture_output=True, text=True, env=env)
        if p.returncode == 0:
            return "built"
        err = [l.strip() for l in p.stderr.splitlines()
               if re.search(r"error:|Invalid version", l) and not re.search(r"Failed to build|build backend|error: Command", l)]
        return "failed: " + (err[0] if err else p.stderr.strip().splitlines()[-1])
    print("pumpprobe %s, as published: %s" % (PKGS[0][2][:7], build(*PKGS[0], env0)))
    with_inc = {**env0, "CPATH": inc}
    for name, url, commit in PKGS:
        print("%s %s, numpy headers added by hand: %s" % (name, commit[:7], build(name, url, commit, with_inc)))
    p = subprocess.run([envpy, "-c", "import pumpprobe"], capture_output=True, text=True)
    print("import pumpprobe:", "ok" if p.returncode == 0 else p.stderr.strip().splitlines()[-1])
Python 3.13, numpy 2.5.4
pumpprobe 1dbc5e0, as published: failed: pumpprobe/_integration.cpp:3:10: fatal error: numpy/arrayobject.h: No such file or directory
pumpprobe 1dbc5e0, numpy headers added by hand: built
mistofrutta 11ac8c3, numpy headers added by hand: failed: raise InvalidVersion(f"Invalid version: {version!r}")
wormbrain d94f7a1, numpy headers added by hand: failed: dsmm/dsmm.cpp:6:10: fatal error: Eigen/Dense: No such file or directory
wormdatamodel 2ab9561, numpy headers added by hand: failed: wormdatamodel/data/_legacy_c.cpp:63:65: error: ‘NPY_IN_ARRAY’ was not declared in this scope
import pumpprobe: ModuleNotFoundError: No module named 'mistofrutta'

The package as published does not build: its setup script defines a step that adds numpy's headers but never uses it, so the compiler cannot find numpy/arrayobject.h. With the headers added by hand it builds, but it cannot be imported, as none of the packages it imports builds here: mistofrutta fails on its version string (a commit hash), wormbrain needs the Eigen library, and wormdatamodel uses numpy C API names that numpy 2 removed. Running the paper's kernel fits would mean patching these packages or reimplementing the fits, which this attempt did not do, so the claim stays blocked as not runnable.

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 Data Outcome
recordings-protocol dandiset partially reproduced (110 of 113 animals have traces)
pairs-measured dandiset partially reproduced (20,587 pairs, 184 neurons, at most 50 observations)
fig1e-avjr-avdr dandiset reproduced
fig1f-aver-avar dandiset reproduced (7 trials, 5 animals)
fig1g-saadl-ollr dandiset reproduced (5 trials, 4 animals; sign not robust)
edfig2ij-spatial-specificity dandiset reproduced
sourcedata-link with inputs partially reproduced
fig2d-inhibitory-fraction with inputs not reproduced (15.1% against 11%)
osf-link with labels partially reproduced (66% of position-found targets agree)
fig2c-1310-connected, fig2c-bilateral, fig2efg-path-length dandiset blocked: data missing
fig3-anatomy-predictions dandiset blocked: data missing
edfig6-kernel-stereotypy dandiset blocked: not runnable
edfig7b-unc31-fewer, fig5-extrasynaptic-screen dandiset blocked: data missing
fig4-rid-wt dandiset partially reproduced (AWB only; URX seen once, ADL never)
fig4-rid-unc31 elsewhere reproduced (small differences, few WT trials)
fig6-spontaneous elsewhere blocked: not runnable (needs the kernels of edfig6)

How the analysis differs from the paper's.

  • The stimulated neuron is the manually located ROI where the file gives one (half of the stimulations), otherwise the ROI centroid nearest the target in the reference volume. The paper finds the nearest neuron in the volume at the stimulation and maps it through tracking matches that are not in the dandiset. For automatically placed targets the two agree in 66% of cases.
  • The paper's pipeline excludes some stimulations (marked -2 in its export, 950 of them) and substitutes "matchless" traces for some dim neurons; neither is in the dandiset, so all stimulations and only the regular traces are used.
  • <dF/F0>_t follows the Methods (F0 the mean of the 30 s before); the code's correction of decaying traces is shown as a variant.
  • The photobleaching fit is the code's, run on about 500 evenly spaced volumes per trace for speed; on 12 recordings the full fit gives <dF/F0>_t values with a median correlation of 0.997 and the same classifier call for 99.5% of stimulation-ROI pairs.
  • F0 is used as it is; the 3,173 rows with F0 <= 0 and the traces without photobleaching correction are kept in the main results and left out in variants, which change no outcome.
  • For the RID claims, as in the Methods for Fig. 4, no autoresponse is required and values beyond +-2 are excluded. Only RID stimulations whose target is an ROI labelled RID can be found, and the matchless traces the paper uses for dim neurons are missing, so ADL is not observed in WT. The unc-31 values come from the paper's OSF export, processed with the same functions.
  • No threshold on label confidence is applied, as in the code.
  • The kernel fits of Extended Data Fig. 6 were not run: the paper's package builds only with numpy's headers added by hand, and three packages it imports do not build (shown above).
  • Runs on different machines have printed counts that differ by one (3,172 or 3,173 rows with F0 <= 0, 58 or 59 failed fits) and correlations that differ by 0.001 (0.616 or 0.617), presumably from floating-point differences in the photobleaching fits; no conclusion depends on them.

What was found about the dandiset.

  • Three of the 113 subjects (20, 23, 33) have only the raw imaging file, without traces or a stimulation table.
  • The unc-31 recordings, the spontaneous recordings of Fig. 6 and the unstimulated control recordings are not in it. The control recordings, which every q value needs, are not public anywhere the paper names.
  • The files do not say which neuron was stimulated, except for the manually located targets; for the rest only a target position is given. RID stimulations can be recognised only where RID was segmented and labelled (8 stimulations in 5 animals).
  • Labels differ from the paper's export in six recordings: the dandiset has almost no labels in subjects 64, 76 and 77, places shared names on other ROIs in subjects 86 and 91, and has 161 ROIs against 200 in subject 11, for which the export has no labels.
  • The stimulation durations and powers are constants from the paper, the target depth_in_um is not in micrometres, the red channel's indicator is given as GCaMP6s, two subjects have strain "none", the age is P1D for every animal, neuropal_ids holds text such as "merge", some ROIs are labelled "target", one confidence is 11, and nine recordings hold large negative intensities.
  • With the files and the paper's own code, the per-pair counts and means of the paper's Source Data are recovered only in part (Spearman 0.77 for counts, Pearson 0.55 for means).
In [30]:
print(f"Total run time {time.time() - started:.0f} s")
Total run time 1045 s