NeuroPAL whole-brain activity (Yemini et al. 2021) from dandiset 000541¶

Paper: Yemini, Lin, Nejatbakhsh et al. (2021), "NeuroPAL: A Multicolor Atlas for Whole-Brain Neuronal Identification in C. elegans", Cell 184, 272-288, doi:10.1016/j.cell.2020.12.012.

Dataset: DANDI:000541, "NeuroPAL Microfluidic Chip Images and GCaMP activity", version 0.241009.1457 (doi:10.48324/dandi.000541/0.241009.1457). The paper cites no version of the dandiset (it cites its Zenodo deposit). Version 0.241009.1457 is the most recent published version; the three published versions (0.240611.1947, 0.240625.0452, 0.241009.1457) list the same 21 assets (the same asset identifiers), so none matches the claims file better, and the draft can change.

Claims file: claims/000541/10.1016-j.cell.2020.12.012.yaml, written from the accepted author manuscript in PubMed Central (PMC10494711) with Methods S1 and S2. Outside inputs: the paper's own deposit of the traces on Zenodo (doi:10.5281/zenodo.3906530), used for a link check, a variant reading of the trace selection and a variant of the AWC claim; the paper's Tables S1 and S6, from the publisher, used to choose between readings of methods the paper leaves open (which fluorophore srsx-3 drives; the per-class sample counts, p values and FDR family of the response tests), never to decide a claim; and the synapse counts of Cook et al. (2019) (doi:10.1038/s41586-019-1352-7), for the connectome claims.

The dandiset holds the 21 OH16230 head recordings of Figure 6 and nothing else: no tail recordings, no OH15500 animals, and none of the imaging behind Figures 1 to 5 and 7.

Main readings, both assumptions (see "Trace processing" and "The tests against the paper's Table S6"): one member of each near-duplicate pair is removed, and each trace is z-scored before the response tests. Every claim is also shown under the other readings.

Claim Tested here
link-zenodo-traces tested (link)
fig6-sample-sizes tested (heads only; tails not in the dandiset)
fig6-all-heads-responsive tested
fig6d-ase-nacl tested
fig6-nacl-unknown-interneurons tested
fig6c-odors-more-inhibitory tested
fig6d-awc-asymmetric blocked: the dandiset does not label AWC ON and OFF (variants with the Zenodo labels and with labels read from the NeuroPAL image are shown)
fig6-asj-ric tested
fig6-interneurons-downstream tested
fig6-tail-responses blocked: no tail recordings in the dandiset
fig6f-pharyngeal tested
fig6-stimulus-sets-distinct tested
figs1h-avf-cyclic tested
fig6gi-stimulus-specific-correlations tested
figs2a-pca-trajectories tested
link-connectome-names tested (link)
fig6jk-connectome-correlation tested (heads only)
fig6-connectome-variations tested (heads only)
fig1, figs1, fig2, fig4c, fig5 and fig7bc claims (8) blocked: the data are not in the dandiset
fig4bd-gaba-network, fig4e-extrasynaptic, fig7de-optimal-coloring blocked: outside inputs or software, not tested here (see the attempt record)
In [1]:
import os
os.environ.setdefault("OMP_NUM_THREADS", "1")
import time
import warnings
import collections
import numpy as np
import pandas as pd
from scipy import stats, signal
import matplotlib.pyplot as plt
import neuropal as npl

T_START = time.time()
warnings.filterwarnings("ignore", category=RuntimeWarning)  # empty means over animals lacking a pair
pd.set_option("display.width", 200)
pd.set_option("display.max_columns", 30)
print("dandiset", npl.DANDISET_ID, "version", npl.DANDISET_VERSION)
dandiset 000541 version 0.241009.1457
In [2]:
assets = npl.list_assets()
print(len(assets), "assets;", round(sum(a["size"] for a in assets) / 1e9, 2), "GB")
recs = npl.load_all()
recs = sorted(recs, key=lambda r: r["identifier"])
print("loaded in", round(time.time() - T_START), "s")
21 assets; 30.53 GB
loaded in 118 s

Data check¶

What was loaded, and the checks of the description against the values.

In [3]:
rows = []
for r in recs:
    named = [n for n in r["labels"] if n]
    allnan = int(np.sum(np.all(~np.isfinite(r["raw"]), axis=0)))
    order = sorted(r["stimuli"], key=lambda s: r["stimuli"][s][0])
    rows.append({
        "file": r["path"].split("/")[-1], "identifier": r["identifier"], "strain": r["strain"],
        "stage": r["growth_stage"], "description": r["session_description"],
        "frames": r["raw"].shape[0], "movie_frames": r["n_movie_frames"], "rate_Hz": r["rate"],
        "duration_s": r["raw"].shape[0] / r["rate"], "rois": r["raw"].shape[1], "named": len(named),
        "unnamed": r["raw"].shape[1] - len(named), "dup_names": len(named) - len(set(named)),
        "all_nan_traces": allnan, "two_series_equal": r["raw_equals_other_series"],
        "stim_order": "/".join(s[:4] for s in order),
        "stim_times": ";".join(f"{r['stimuli'][s][0]}-{r['stimuli'][s][1]}" for s in order),
        "raw_min": float(np.nanmin(r["raw"])), "raw_max": float(np.nanmax(r["raw"])),
    })
sanity = pd.DataFrame(rows)
print(sanity.drop(columns=["file"]).to_string())
print()
print("files:", len(recs), "| strains:", list(sanity.strain.unique()), "| stages:", list(sanity.stage.unique()),
      "| descriptions:", list(sanity.description.unique()))
print("trace unit:", {r["unit"] for r in recs}, "| rate:", {r["rate"] for r in recs},
      "| starting time:", {r["t0"] for r in recs})
print("grid spacing (um):", {tuple(np.round(r["grid_spacing_um"], 4)) for r in recs})
print("frames per trace: %d to %d (%.1f to %.1f s at 4 Hz)" % (sanity.frames.min(), sanity.frames.max(),
      sanity.frames.min() / 4, sanity.frames.max() / 4))
print("trace frames = movie frames - 1 in every file:", bool((sanity.frames == sanity.movie_frames - 1).all()))
print("stimulus orders:", dict(collections.Counter(sanity.stim_order)))
print("stimulus windows:", list(sanity.stim_times.unique()))
print("stimulus table description:", {r["stim_description"] for r in recs})
print("the dNMF series equals the 'raw' series (NaN equal to NaN) in", int(sanity.two_series_equal.sum()),
      "of", len(recs), "files")
print("raw trace values: %.3f to %.3f (unitless)" % (sanity.raw_min.min(), sanity.raw_max.max()))
     identifier   strain stage                                   description  frames  movie_frames  rate_Hz  duration_s  rois  named  unnamed  dup_names  all_nan_traces  two_series_equal      stim_order                         stim_times   raw_min   raw_max
0   20190924_01  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     961           962      4.0      240.25   181    180        1          0               0              True  pent/buta/nacl  60.5-70.5;120.5-130.5;180.5-190.5  0.042480  3.887675
1   20190924_02  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     961           962      4.0      240.25   178    176        2          0               0              True  pent/buta/nacl  60.5-70.5;120.5-130.5;180.5-190.5  0.018610  4.197489
2   20190924_03  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     962           963      4.0      240.50   188    184        4          0               0              True  pent/buta/nacl  60.5-70.5;120.5-130.5;180.5-190.5  0.052671  3.450128
3   20190925_01  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     962           963      4.0      240.50   184    180        4          0               1              True  pent/buta/nacl  60.5-70.5;120.5-130.5;180.5-190.5  0.016917  3.346579
4   20190925_04  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     960           961      4.0      240.00   177    176        1          0               2              True  pent/buta/nacl  60.5-70.5;120.5-130.5;180.5-190.5  0.066597  2.707961
5   20190928_01  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     961           962      4.0      240.25   172    169        3          0               0              True  nacl/pent/buta  60.5-70.5;120.5-130.5;180.5-190.5  0.024956  2.610775
6   20190928_03  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     961           962      4.0      240.25   168    167        1          0               0              True  nacl/pent/buta  60.5-70.5;120.5-130.5;180.5-190.5  0.022305  3.683224
7   20190928_05  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     961           962      4.0      240.25   172    164        8          0               0              True  nacl/pent/buta  60.5-70.5;120.5-130.5;180.5-190.5  0.013182  2.592344
8   20190928_07  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     959           960      4.0      239.75   180    179        1          0               2              True  nacl/pent/buta  60.5-70.5;120.5-130.5;180.5-190.5  0.012988  3.904508
9   20190928_08  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     960           961      4.0      240.00   183    183        0          0               1              True  nacl/pent/buta  60.5-70.5;120.5-130.5;180.5-190.5  0.033154  2.316842
10  20190928_11  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     962           963      4.0      240.50   181    177        4          0               2              True  nacl/pent/buta  60.5-70.5;120.5-130.5;180.5-190.5  0.008192  4.099419
11  20190928_13  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     961           962      4.0      240.25   175    175        0          0               0              True  nacl/pent/buta  60.5-70.5;120.5-130.5;180.5-190.5  0.031965  2.536674
12  20190929_02  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     961           962      4.0      240.25   166    164        2          0               0              True  buta/nacl/pent  60.5-70.5;120.5-130.5;180.5-190.5  0.011046  3.383612
13  20190929_03  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     961           962      4.0      240.25   178    175        3          0               0              True  buta/nacl/pent  60.5-70.5;120.5-130.5;180.5-190.5  0.018072  3.224996
14  20190929_05  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     961           962      4.0      240.25   170    168        2          0               0              True  buta/nacl/pent  60.5-70.5;120.5-130.5;180.5-190.5  0.019185  3.251345
15  20190929_06  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     961           962      4.0      240.25   178    175        3          0               0              True  buta/nacl/pent  60.5-70.5;120.5-130.5;180.5-190.5  0.011302  3.321618
16  20190929_07  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     936           937      4.0      234.00   177    176        1          0               0              True  buta/nacl/pent  60.5-70.5;120.5-130.5;180.5-190.5  0.035262  2.745039
17  20191030_03  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     961           962      4.0      240.25   185    182        3          0               2              True  pent/buta/nacl  60.5-70.5;120.5-130.5;180.5-190.5  0.029313  3.400185
18  20191030_07  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     960           961      4.0      240.00   180    175        5          0               3              True  buta/nacl/pent  60.5-70.5;120.5-130.5;180.5-190.5  0.005829  2.209842
19  20191104_08  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     961           962      4.0      240.25   179    177        2          0               0              True  buta/nacl/pent  60.5-70.5;120.5-130.5;180.5-190.5  0.125432  3.199358
20  20191104_10  OH16230    YA  C. elegans head NeuroPAL and Calcium imaging     961           962      4.0      240.25   174    174        0          0               0              True  pent/buta/nacl  60.5-70.5;120.5-130.5;180.5-190.5  0.033234  2.487772

files: 21 | strains: ['OH16230'] | stages: ['YA'] | descriptions: ['C. elegans head NeuroPAL and Calcium imaging']
trace unit: {'unitless'} | rate: {4.0} | starting time: {0.0}
grid spacing (um): {(np.float64(0.2667), np.float64(0.2667), np.float64(1.5))}
frames per trace: 936 to 962 (234.0 to 240.5 s at 4 Hz)
trace frames = movie frames - 1 in every file: True
stimulus orders: {'pent/buta/nacl': 7, 'nacl/pent/buta': 7, 'buta/nacl/pent': 7}
stimulus windows: ['60.5-70.5;120.5-130.5;180.5-190.5']
stimulus table description: {'Chemical stimuli delivered to  the worm. Each worms gets 160 mM NaCl, 10^-4mM 2-butanone, and 10^-4 2,3-pentanedione in a random order. Start and stop time represent seconds from start of the experiment'}
the dNMF series equals the 'raw' series (NaN equal to NaN) in 21 of 21 files
raw trace values: 0.006 to 4.197 (unitless)

The dandiset holds 21 files, one per animal, each described as a head of strain OH16230 at the young-adult stage. The paper's 21 tails and its OH15500 animals are not in it. Each file holds about 240 s at 4 Hz, the 4 min recording of the STAR Methods, and three 10 s stimuli starting at 60.5, 120.5 and 180.5 s, one per minute after the first minute, as the STAR Methods describe. The stimulus order takes three of the six possible orders, seven heads each, which fits the STAR Methods' "rotated periodically" better than the file's "in a random order". The table's description gives 2-butanone as "10^-4mM" and 2,3-pentanedione as "10^-4"; the paper gives both odors as 10^-4 dilutions, so the unit "mM" is likely wrong. The two trace series in each file (SignalRawFluor and SignalRawFluordNMF) are the same numbers, so only one is used.

Next: recordings deposited twice, and the names of the neurons.

In [4]:
# The calcium volume's stated grid spacing against its coordinates: the same named neurons in the
# NeuroPAL volume and in the first frame of the calcium segmentation.
sc = pd.DataFrame([{"file": r["identifier"], "x": r["xyz_scale"][0], "y": r["xyz_scale"][1], "z": r["xyz_scale"][2]}
                   for r in recs])
print("NeuroPAL voxel coordinate per calcium voxel coordinate, same neurons (slope per axis), over 21 files:")
print(sc[["x", "y", "z"]].describe().loc[["min", "50%", "max"]].round(3).to_string())
print("NeuroPAL volume shape:", {r["neuropal_shape"] for r in recs}, "| calcium movie shape (t, x, y, z):",
      {r["movie_shape"][1:] for r in recs})
print("stated grid spacing (um), calcium:", {tuple(np.round(r["grid_spacing_um"], 4)) for r in recs},
      "| NeuroPAL:", {tuple(np.round(r["neuropal_grid_spacing_um"], 4)) for r in recs})
print("calcium spacing used here (NeuroPAL spacing times the rounded slope):",
      {tuple(np.round(r["neuropal_grid_spacing_um"] * np.round(r["xyz_scale"]), 4)) for r in recs})
print("extent of the named neurons (um, corrected), x/y/z median over files:",
      np.round(np.median([np.ptp(r["centre_um"], axis=0) for r in recs], axis=0), 1))
NeuroPAL voxel coordinate per calcium voxel coordinate, same neurons (slope per axis), over 21 files:
         x      y      z
min  1.951  1.937  0.970
50%  1.999  2.015  1.033
max  2.046  2.088  1.072
NeuroPAL volume shape: {(512, 256, 21, 5)} | calcium movie shape (t, x, y, z): {(256, 128, 21)}
stated grid spacing (um), calcium: {(np.float64(0.2667), np.float64(0.2667), np.float64(1.5))} | NeuroPAL: {(np.float64(0.2667), np.float64(0.2667), np.float64(1.5))}
calcium spacing used here (NeuroPAL spacing times the rounded slope): {(np.float64(0.5333), np.float64(0.5333), np.float64(1.5))}
extent of the named neurons (um, corrected), x/y/z median over files: [130.7  49.1  21. ]

In every file the x and y coordinates of a neuron in the NeuroPAL volume are twice its coordinates in the calcium segmentation, and z is the same, while both volumes state the same spacing (0.267, 0.267, 1.5 um). The calcium movie is 256 x 128 pixels against 512 x 256 for the NeuroPAL volume, so the movie is binned 2 x 2 and its x and y spacing is 0.533 um, not the stated 0.267 um. The NeuroPAL spacing fits the Key Resources Table (Andor iXon 897 EMCCD, 60x objective: 16 um pixels / 60 = 0.267 um). The data are trusted over the stated spacing: distances between calcium neurons (the 5 um rule below) use 0.533 um in x and y. This is recorded as a metadata error.

In [5]:
# Recordings deposited more than once: identical trace matrices, or any trace identical across files.
fp = {}
for r in recs:
    x = np.nan_to_num(r["raw"])
    fp[r["identifier"]] = x
same = []
ids = list(fp)
for i in range(len(ids)):
    for j in range(i + 1, len(ids)):
        a, b = fp[ids[i]], fp[ids[j]]
        n = min(len(a), len(b))
        ca = a[:n].T[np.std(a[:n], axis=0) > 0]
        cb = b[:n].T[np.std(b[:n], axis=0) > 0]
        za = (ca - ca.mean(1, keepdims=True)) / ca.std(1, keepdims=True)
        zb = (cb - cb.mean(1, keepdims=True)) / cb.std(1, keepdims=True)
        cmax = (za @ zb.T / n).max()
        if cmax > 0.999:
            same.append((ids[i], ids[j], cmax))
print("pairs of files sharing a trace correlated above 0.999:", same if same else "none")

names = sorted({n for r in recs for n in r["labels"] if n})
print("distinct neuron names over the 21 heads:", len(names))
cnt = collections.Counter(n for r in recs for n in set(r["labels"]) if n)
print("names in 3 or fewer heads:", {n: c for n, c in cnt.items() if c <= 3})
nonneuronal = [n for n in names if n in ("AMSOL", "AMSOR", "HMC")]
print("names that are not neurons (amphid sheath glia AMSO, head mesodermal cell HMC):", nonneuronal,
      {n: cnt[n] for n in nonneuronal})
print("DD6 (a motor neuron of the posterior ventral cord) named in", cnt["DD6"], "head file(s):",
      [r["identifier"] for r in recs if "DD6" in r["labels"]])
pairs of files sharing a trace correlated above 0.999: none
distinct neuron names over the 21 heads: 193
names in 3 or fewer heads: {'SABD': 3, 'RIGR': 3, 'DA1': 2, 'VD2': 2, 'AS1': 2, 'DD6': 1, 'AMSOR': 3}
names that are not neurons (amphid sheath glia AMSO, head mesodermal cell HMC): ['AMSOL', 'AMSOR', 'HMC'] {'AMSOL': 4, 'AMSOR': 3, 'HMC': 6}
DD6 (a motor neuron of the posterior ventral cord) named in 1 head file(s): ['20190929_06']

Trace processing¶

The STAR Methods ("Brainwide imaging of neuronal activity traces") give the processing: remove the first 1 s and the last frame, subtract a fitted first-order exponential bleach curve, divide by the 5th percentile of the trace, and remove neurons within 5 um of each other whose traces correlate at 0.95 or more. The paper does not say which member of such a pair is removed. Four readings are run throughout: remove one of the two, the later in the file's order ("one"); remove both ("both"); remove none ("none"); and keep exactly the named traces that the paper's own deposit of these 21 heads holds ("deposit"; Zenodo 3906530, matched to the dandiset in link-zenodo-traces). The main reading is "one", an assumption: of the four, it comes closest to the number of traces the paper tested per class (Table S6, below). Distances use the corrected spacing of the calcium volume (0.533 um in x and y).

In [6]:
Z = npl.load_zenodo_heads()          # the paper's deposit, for the "deposit" reading and the link check
DEP = npl.deposit_slots(Z)
DFF = {r["identifier"]: npl.dff(r["raw"], r["rate"]) for r in recs}
MODES = ("one", "deposit", "both", "none")   # "one" is the main reading
data = {mode: npl.prepare(recs, mode, DEP, DFF) for mode in MODES}
D = data["one"]
cls_of = D["class_of"]
for mode, d in data.items():
    kept = [set(a["traces"]) for a in d["animals"]]
    print(f"reading '{mode}': removed {sum(len(a['duplicates']) for a in d['animals'])} named traces;"
          f" kept {sum(len(k) for k in kept)} named traces;"
          f" traces dropped as all NaN: {sum(len(a['dropped_nan']) for a in d['animals'])}")
allvals = np.concatenate([np.concatenate([t[4:] for t in a["traces"].values()]) for a in D["animals"]])
print("dF/F0 over all kept traces (main reading): %.2f to %.2f, median %.3f; values beyond +-10: %d of %d"
      % (allvals.min(), allvals.max(), np.median(allvals), np.sum(np.abs(allvals) > 10), allvals.size))
print("classes (left and right pooled):", len(set(cls_of.values())))
reading 'one': removed 338 named traces; kept 3325 named traces; traces dropped as all NaN: 13
reading 'deposit': removed 126 named traces; kept 3550 named traces; traces dropped as all NaN: 0
reading 'both': removed 651 named traces; kept 3012 named traces; traces dropped as all NaN: 13
reading 'none': removed 0 named traces; kept 3663 named traces; traces dropped as all NaN: 13
dF/F0 over all kept traces (main reading): -41.59 to 89.48, median -0.010; values beyond +-10: 4955 of 3174325
classes (left and right pooled): 112
In [7]:
# Per file: nearest-neighbour distance between named neurons, pairs within 5 um, pairs within 5 um
# at r >= 0.95, traces each reading removes, and how many of those the deposit also lacks.
rows = []
for r in recs:
    d = DFF[r["identifier"]]
    idx = [i for i, n in enumerate(r["labels"]) if n and np.all(np.isfinite(d[4:, i]))]
    xyz = r["centre_um"][idx]
    dist = np.linalg.norm(xyz[:, None] - xyz[None], axis=-1)
    np.fill_diagonal(dist, np.inf)
    cc = np.corrcoef(d[4:, idx].T)
    iu = np.triu_indices(len(idx), 1)
    named = {i for i, n in enumerate(r["labels"]) if n}
    lacking = {i for i in named if (r["identifier"], r["labels"][i]) not in DEP}
    row = {"file": r["identifier"], "named": len(named), "nn_um_median": np.median(dist.min(1)),
           "nn_um_p5": np.percentile(dist.min(1), 5), "pairs_5um": int((dist[iu] <= 5).sum()),
           "pairs_5um_r95": int(((dist[iu] <= 5) & (cc[iu] >= 0.95)).sum()),
           "pairs_any_r95": int((cc[iu] >= 0.95).sum()), "deposit_lacks": len(lacking)}
    for mode in ("both", "one"):
        rm = npl.near_duplicates(r, d, mode)
        row[f"rm_{mode}"] = len(rm)
        row[f"rm_{mode}_and_lacking"] = len(rm & lacking)
    rows.append(row)
xt = pd.DataFrame(rows)
print(xt.round(2).to_string(index=False))
tot = xt.drop(columns=["file", "nn_um_median", "nn_um_p5"]).sum()
print("totals:", tot.to_dict())
lack_names = collections.Counter(n for r in recs for n in r["labels"] if n and (r["identifier"], n) not in DEP)
print("named traces the deposit lacks, by name (top 12):", dict(lack_names.most_common(12)))
print("of these, all NaN in the dandiset:", sum(1 for r in recs for i, n in enumerate(r["labels"])
      if n and (r["identifier"], n) not in DEP and not np.all(np.isfinite(DFF[r["identifier"]][4:, i]))))
# How close and how correlated are the traces the deposit lacks, against their nearest named neighbour?
near = []
for r in recs:
    d = DFF[r["identifier"]]
    idx = [i for i, n in enumerate(r["labels"]) if n and np.all(np.isfinite(d[4:, i]))]
    cc = np.corrcoef(d[4:, idx].T)
    xyz = r["centre_um"][idx]
    dist = np.linalg.norm(xyz[:, None] - xyz[None], axis=-1)
    np.fill_diagonal(dist, np.inf)
    for k, i in enumerate(idx):
        if (r["identifier"], r["labels"][i]) not in DEP:
            j = int(np.argmin(dist[k]))
            near.append({"dist_um": dist[k, j], "r": cc[k, j], "max_r_within_5um": np.max(np.where(dist[k] <= 5, cc[k], -1))})
near = pd.DataFrame(near)
print("finite traces the deposit lacks: %d; nearest neighbour within 5 um: %d; with r >= 0.95 to a neuron within 5 um: %d"
      % (len(near), int((near.dist_um <= 5).sum()), int((near.max_r_within_5um >= 0.95).sum())))
       file  named  nn_um_median  nn_um_p5  pairs_5um  pairs_5um_r95  pairs_any_r95  deposit_lacks  rm_both  rm_both_and_lacking  rm_one  rm_one_and_lacking
20190924_01    180          3.06      1.51        246             27             38              3       33                    1      19                   0
20190924_02    176          3.06      1.19        219             20             35              6       34                    1      18                   1
20190924_03    184          2.72      1.19        279             35             91             10       53                    2      28                   2
20190925_01    180          2.87      1.19        210             29             72              6       50                    2      24                   2
20190925_04    176          2.67      1.19        325             34             73              5       37                    0      20                   0
20190928_01    169          3.24      1.19        239             19             32              4       36                    0      18                   0
20190928_03    167          3.45      1.51        193             18             47              4       26                    2      14                   1
20190928_05    164          3.43      1.51        171             11             24              3       20                    1      10                   1
20190928_07    179          3.24      1.50        200             19             42              6       32                    1      17                   1
20190928_08    183          2.77      1.19        320             26             65              4       35                    1      18                   0
20190928_11    177          3.06      1.60        210             11             23              4       16                    1       9                   1
20190928_13    175          3.06      1.07        249             19             38              3       26                    1      13                   0
20190929_02    164          3.45      1.59        155             12             12             18       22                    3      12                   3
20190929_03    175          3.11      1.19        195             19             40              4       29                    0      15                   0
20190929_05    168          2.87      1.50        211             17             22              5       34                    3      17                   2
20190929_06    175          3.11      1.59        172             14             16              6       27                    1      14                   1
20190929_07    176          2.82      1.19        264             15             16              3       29                    2      14                   2
20191030_03    182          3.08      1.50        301              9             22              9       17                    3       8                   1
20191030_07    175          3.44      1.19        147              9             24             10       17                    0       8                   0
20191104_08    177          2.82      0.53        285             37             69              7       48                    1      26                   1
20191104_10    174          3.06      1.07        250             19             33              6       30                    1      16                   1
totals: {'named': 3676, 'pairs_5um': 4841, 'pairs_5um_r95': 419, 'pairs_any_r95': 834, 'deposit_lacks': 126, 'rm_both': 651, 'rm_both_and_lacking': 27, 'rm_one': 338, 'rm_one_and_lacking': 20}
named traces the deposit lacks, by name (top 12): {'AVL': 11, 'I5': 8, 'HMC': 6, 'AMSOL': 4, 'RIMR': 4, 'DB2': 4, 'SABVL': 4, 'RMHR': 3, 'VA1': 3, 'SIBDL': 3, 'I1L': 3, 'I1R': 3}
of these, all NaN in the dandiset: 13
finite traces the deposit lacks: 113; nearest neighbour within 5 um: 79; with r >= 0.95 to a neuron within 5 um: 27

The rule run here does not find the traces the paper left out of its deposit. In the 21 files the 5 um rule at r of 0.95 removes 338 traces under "one" and 651 under "both" (with the corrected spacing), while the deposit lacks 126 named traces. Only 20 (one) and 27 (both) of the removed traces are among those 126. Of the 126, 13 are all NaN in the dandiset, 11 are AVL and 8 are I5. Of the 113 finite ones, 79 have a named neighbour within 5 um, but only 27 correlate at 0.95 or more with a neuron within 5 um. Pairs at r of 0.95 or more are common here (419 within 5 um, 834 at any distance), so the paper's correlation was likely computed on other traces or with other steps. The paper's analysis is closer to "one" than to the deposit (Table S6 below): the deposit holds 2.9 more traces per class on average than the paper tested.

A few traces have a 5th percentile near zero, which gives the extreme dF/F0 values in the range above (4,955 values beyond 10 in absolute value under the main reading, of 3.2 million). The response tests use each trace z-scored, so no trace weighs more than another; dF/F0 itself is a variant.

Clock of the stimulus table¶

The responses of the primary sensory neurons should line up with the stimulus table. For ASE and NaCl, and for AWC and the two odors, the response (mean during the 10 s stimulus minus the 10 s before) is computed at the table's times and with each stimulus's times moved to the other two stimulus windows of the same animal (a shift of 60 or 120 s), which keeps the clock but breaks the labels. The ASE response to NaCl is also computed with the table shifted by -20 to +20 s.

In [8]:
def resp_at(a, n, s0, s1):
    dur, pre = npl.window_means(a["traces"][n], s0, s1, a["rate"])
    return dur - pre

clock = []
for cls, stims, sign in (("ASE", ["nacl"], 1), ("AWC", ["butanone", "pentanedione"], -1)):
    for s in stims:
        true, shifted = [], []
        for a in D["animals"]:
            for n in (cls + "L", cls + "R"):
                if n not in a["traces"]:
                    continue
                true.append(resp_at(a, n, *a["stimuli"][s]))
                for o in npl.STIMULI:
                    if o != s:
                        shifted.append(resp_at(a, n, *a["stimuli"][o]))
        clock.append({"neuron": cls, "stimulus": s, "true_mean": np.mean(true), "n_true": len(true),
                      "other_windows_mean": np.mean(shifted), "n_other": len(shifted),
                      "mannwhitney_p": stats.mannwhitneyu(true, shifted).pvalue})
print(pd.DataFrame(clock).round(4).to_string())
# The ASE NaCl response with the stimulus table shifted in time: a response locked to the table peaks at 0 s.
shift_rows = []
for sh in np.arange(-20, 20.1, 2.5):
    v = [resp_at(a, n, a["stimuli"]["nacl"][0] + sh, a["stimuli"]["nacl"][1] + sh)
         for a in D["animals"] for n in ("ASEL", "ASER") if n in a["traces"]]
    shift_rows.append({"shift_s": sh, "ASE_nacl_mean_response": np.mean(v), "n": len(v)})
print(pd.DataFrame(shift_rows).round(3).to_string(index=False))
  neuron      stimulus  true_mean  n_true  other_windows_mean  n_other  mannwhitney_p
0    ASE          nacl     1.1937      40              0.3628       80         0.0000
1    AWC      butanone    -0.5924      42             -0.4794       84         0.2899
2    AWC  pentanedione    -0.4772      42             -0.5370       84         0.7075
 shift_s  ASE_nacl_mean_response  n
   -20.0                  -0.043 40
   -17.5                  -0.044 40
   -15.0                  -0.038 40
   -12.5                  -0.021 40
   -10.0                   0.007 40
    -7.5                   0.176 40
    -5.0                   0.580 40
    -2.5                   0.940 40
     0.0                   1.194 40
     2.5                   1.069 40
     5.0                   0.363 40
     7.5                  -0.309 40
    10.0                  -0.814 40
    12.5                  -0.999 40
    15.0                  -0.771 40
    17.5                  -0.515 40
    20.0                  -0.313 40

ASE responds more to NaCl at the table's NaCl time than in the other two windows (mean 1.19 against 0.36, Mann-Whitney p below 1e-4), and its NaCl response is largest with the table unshifted (1.19 at 0 s, 1.07 at +2.5 s, 0.58 at -5 s, 0.36 at +5 s). This is consistent with the stimulus table sharing the clock of the traces. AWC gives no such check: it falls about as much in the other windows as at the odor times (2-butanone -0.59 against -0.48, p = 0.29; 2,3-pentanedione -0.48 against -0.54, p = 0.71), so the clock of the two odor windows is supported only through the NaCl window of the same table.

link-zenodo-traces¶

The dandiset's head recordings are the 21 OH16230 heads of the paper's Zenodo deposit: files match by identifier, stimulus orders agree, and the dF/F0 computed from the dandiset's traces by the STAR Methods matches the deposit's traces.

The deposit (doi:10.5281/zenodo.3906530, Head_Activity_OH16230.mat) is the paper's own record of these traces; it is read here only to check the link, and for the AWC ON/OFF variant of fig6d-awc-asymmetric. Its README says traces are stored per class, left traces then right traces, each in file order; the deposit keeps one slot per file for each neuron, empty (NaN) where the file lacks it, which is checked below.

Criterion (written by the author of this attempt, as link claims are): reproduced when every dandiset file matches a deposit file by identifier, the stimulus orders agree in every file, and at least 90% of the traces present in both correlate at r of 0.9 or more; partially reproduced when files and stimuli match but fewer traces agree.

In [9]:
zfiles = ["2019" + str(f) for f in Z["files"]]
our_ids = [a["id"] for a in data["none"]["animals"]]
print("deposit files:", len(zfiles), "| matched to dandiset identifiers:", sum(f in our_ids for f in zfiles),
      "| same order:", zfiles == our_ids)
znames = [str(s) for s in Z["stim_names"]]
zorder = {f: [znames[k - 1] for k in np.atleast_1d(Z["stims"][i])] for i, f in enumerate(zfiles)}
ourorder = {a["id"]: sorted(a["stimuli"], key=lambda s: a["stimuli"][s][0]) for a in data["none"]["animals"]}
print("stimulus order agrees in", sum(zorder[f] == ourorder[f] for f in zfiles), "of", len(zfiles), "files")
print("deposit stimulus windows:", np.asarray(Z["stim_times"]).tolist(), "| fps:", Z["fps"])

zt = npl.zenodo_traces_by_file(Z)
zt_slot = DEP
NONE = {a["id"]: a for a in data["none"]["animals"]}
link_rows, count_mismatch = [], []
for cls, (trs, is_l) in zt.items():
    if np.all(np.isnan(is_l)):
        groups = [(cls, list(range(len(trs))))]
    else:
        groups = [(cls + "L", [k for k in range(len(trs)) if is_l[k] == 1]),
                  (cls + "R", [k for k in range(len(trs)) if is_l[k] == 0])]
    for name, ks in groups:
        if len(ks) != len(our_ids):
            count_mismatch.append((name, len(ks)))
            continue
        for f, k in zip(our_ids, ks):
            z = np.asarray(trs[k], float)
            in_dep = bool(np.isfinite(z).any())
            in_dan = name in NONE[f]["traces"]
            row = {"file": f, "neuron": name, "in_deposit": in_dep, "in_dandiset": in_dan, "r": np.nan,
                   "slope": np.nan, "deposit_len": len(z), "nan_head": -1}
            if in_dep and in_dan:
                ours = NONE[f]["traces"][name]
                nn = min(len(z), len(ours))
                z, ours = z[:nn], ours[:nn]
                ok = np.isfinite(z) & np.isfinite(ours)
                row.update(r=np.corrcoef(z[ok], ours[ok])[0, 1], slope=np.polyfit(ours[ok], z[ok], 1)[0],
                           nan_head=int(np.argmax(np.isfinite(z))))
            link_rows.append(row)
link = pd.DataFrame(link_rows)
deposit_names = sum(1 if np.all(np.isnan(il)) else int((il == 1).any()) + int((il == 0).any()) for _, il in zt.values())
print("deposit neuron names:", deposit_names, "| classes:", len(zt))
print("names without one slot per file in the deposit:", count_mismatch if count_mismatch else "none")
allnamed = {(a["id"], n) for a in NONE.values() for n in a["traces"]}
print("named dandiset traces:", len(allnamed), "| deposit slots with a trace:", int(link.in_deposit.sum()))
print("in both:", int((link.in_deposit & link.in_dandiset).sum()),
      "| in the deposit only:", int((link.in_deposit & ~link.in_dandiset).sum()),
      "| in the dandiset only:", int((~link.in_deposit & link.in_dandiset).sum()),
      "| dandiset traces with a name the deposit lacks:", sorted({n for f, n in allnamed} - set(link.neuron)))
only_dan = link[~link.in_deposit & link.in_dandiset]
print("dandiset-only traces by name:", only_dan.neuron.value_counts().to_dict())
link = link[link.in_deposit & link.in_dandiset].copy()
print("Pearson r between dandiset dF/F0 and deposit trace: median %.4f, 5th percentile %.4f, min %.4f"
      % (link.r.median(), link.r.quantile(0.05), link.r.min()))
print("fraction with r >= 0.9: %.3f; r >= 0.95: %.3f; r >= 0.99: %.3f"
      % ((link.r >= 0.9).mean(), (link.r >= 0.95).mean(), (link.r >= 0.99).mean()))
print("regression slope deposit on dandiset: median %.3f (IQR %.3f to %.3f)"
      % (link.slope.median(), link.slope.quantile(0.25), link.slope.quantile(0.75)))
link["len_diff"] = [row.deposit_len - len(NONE[row.file]["traces"][row.neuron]) for row in link.itertuples()]
print("deposit trace length minus dandiset frames (after dropping the last frame):",
      link.groupby("file").len_diff.first().value_counts().to_dict(),
    "| leading NaN frames in the deposit:", sorted(link.nan_head.unique()))
# Frame alignment: the deposit's trace against the dandiset's, shifted by -1 to +1 frames.
lag_r = collections.defaultdict(list)
for row in link.itertuples():
    z = np.asarray(zt_slot[(row.file, row.neuron)], float)
    ours = NONE[row.file]["traces"][row.neuron]
    for lag in (-1, 0, 1):
        a, b = z[max(lag, 0):], ours[max(-lag, 0):]
        nn = min(len(a), len(b))
        ok = np.isfinite(a[:nn]) & np.isfinite(b[:nn])
        lag_r[(row.file, lag)].append(np.corrcoef(a[:nn][ok], b[:nn][ok])[0, 1])
lag_tab = pd.DataFrame({lag: {f: np.median(lag_r[(f, lag)]) for f in our_ids} for lag in (-1, 0, 1)})
print("median r per file with the deposit shifted by -1, 0, +1 frames; best lag per file:",
      dict(collections.Counter(lag_tab.idxmax(axis=1))))
r1 = np.concatenate([lag_r[(f, 1)] for f in our_ids])
print("with deposit frame k + 1 against dandiset frame k: median r %.4f, fraction with r >= 0.9: %.3f"
      % (np.median(r1), np.mean(r1 >= 0.9)))
print(link.groupby("file").r.agg(["count", "median", "min"]).join(lag_tab.rename(columns=lambda c: f"r_lag{c:+d}")).round(4).to_string())
deposit files: 21 | matched to dandiset identifiers: 21 | same order: True
stimulus order agrees in 21 of 21 files
deposit stimulus windows: [[60.5, 70.5], [120.5, 130.5], [180.5, 190.5]] | fps: 4
deposit neuron names: 189 | classes: 109
names without one slot per file in the deposit: none
named dandiset traces: 3663 | deposit slots with a trace: 3550
in both: 3550 | in the deposit only: 0 | in the dandiset only: 101 | dandiset traces with a name the deposit lacks: ['AMSOL', 'AMSOR', 'DD6', 'HMC']
dandiset-only traces by name: {'AVL': 11, 'I5': 8, 'DB2': 4, 'RIMR': 4, 'SABVL': 4, 'AIAR': 3, 'RMHR': 3, 'SIBDL': 3, 'VA1': 3, 'AIAL': 2, 'ASJL': 2, 'AVBR': 2, 'RIML': 2, 'RIR': 2, 'RMGR': 2, 'SAADR': 2, 'VD1': 2, 'ADAR': 1, 'AIBL': 1, 'AINL': 1, 'AIYL': 1, 'AIYR': 1, 'AIZL': 1, 'ASHR': 1, 'ASIL': 1, 'ASIR': 1, 'ASJR': 1, 'AVBL': 1, 'AVDL': 1, 'AVFL': 1, 'AVFR': 1, 'AVG': 1, 'AVJL': 1, 'CEPDL': 1, 'FLPL': 1, 'FLPR': 1, 'I1L': 1, 'I4': 1, 'M3L': 1, 'M3R': 1, 'RIAR': 1, 'RIBR': 1, 'RIFL': 1, 'RIFR': 1, 'RIH': 1, 'RIPL': 1, 'RIPR': 1, 'RIS': 1, 'RMEL': 1, 'RMHL': 1, 'SAADL': 1, 'SABVR': 1, 'SIADR': 1, 'SIBDR': 1, 'SIBVL': 1, 'SIBVR': 1, 'SMDDL': 1, 'SMDDR': 1, 'VB2': 1}
Pearson r between dandiset dF/F0 and deposit trace: median 0.9718, 5th percentile 0.7582, min 0.2573
fraction with r >= 0.9: 0.817; r >= 0.95: 0.665; r >= 0.99: 0.191
regression slope deposit on dandiset: median 0.985 (IQR 0.948 to 0.998)
deposit trace length minus dandiset frames (after dropping the last frame): {0: 11, -1: 8, 1: 2} | leading NaN frames in the deposit: [np.int64(4)]
median r per file with the deposit shifted by -1, 0, +1 frames; best lag per file: {1: 21}
with deposit frame k + 1 against dandiset frame k: median r 0.9803, fraction with r >= 0.9: 0.858
             count  median     min  r_lag-1  r_lag+0  r_lag+1
file                                                         
20190924_01    177  0.9820  0.5925   0.9719   0.9820   0.9868
20190924_02    170  0.9855  0.6681   0.9764   0.9855   0.9890
20190924_03    174  0.9857  0.4538   0.9776   0.9857   0.9883
20190925_01    174  0.9736  0.4257   0.9606   0.9736   0.9807
20190925_04    171  0.9772  0.5673   0.9612   0.9772   0.9814
20190928_01    165  0.9696  0.5740   0.9462   0.9696   0.9772
20190928_03    163  0.9772  0.7462   0.9647   0.9772   0.9824
20190928_05    161  0.9659  0.4538   0.9471   0.9659   0.9757
20190928_07    173  0.9763  0.5164   0.9636   0.9763   0.9829
20190928_08    179  0.9518  0.2573   0.9279   0.9518   0.9794
20190928_11    173  0.9795  0.4005   0.9683   0.9795   0.9843
20190928_13    172  0.9723  0.4827   0.9548   0.9723   0.9798
20190929_02    146  0.9651  0.6987   0.9526   0.9651   0.9731
20190929_03    171  0.9636  0.5493   0.9497   0.9636   0.9718
20190929_05    163  0.9718  0.6229   0.9583   0.9718   0.9792
20190929_06    169  0.9695  0.6611   0.9570   0.9695   0.9772
20190929_07    173  0.9722  0.4762   0.9565   0.9722   0.9806
20191030_03    173  0.9703  0.4734   0.9470   0.9703   0.9797
20191030_07    165  0.9470  0.4938   0.9288   0.9470   0.9798
20191104_08    170  0.9612  0.5264   0.9233   0.9612   0.9706
20191104_10    168  0.9741  0.2803   0.9538   0.9741   0.9818

Result: partially reproduced. All 21 dandiset files match a deposit file by identifier, in the same order, and the stimulus order agrees in all 21. The deposit has one slot per file for each of its 189 neuron names, so traces pair by file. Of the 3,663 named dandiset traces, 3,550 are also in the deposit. 101 are in the dandiset only (11 of them AVL, which the deposit leaves empty in every head), and 4 names (AMSOL, AMSOR, HMC, DD6) are not in the deposit at all. dF/F0 computed from the dandiset by the STAR Methods correlates with the deposit's traces at a median r of 0.972, with a median slope of 0.985. 81.7% of traces reach r of 0.9, below the 90% the criterion asks for. The deposit is one frame (0.25 s) later than the dandiset in all 21 files: deposit frame k + 1 against dandiset frame k gives a median r of 0.980 and 85.8% of traces at r of 0.9, still below 90%. The deposit's traces have their first 4 frames (1 s) and their last frame empty (printed in the Table S6 check below), and their length differs from the dandiset's (after its last frame is dropped) by -1 to +1 frame.

fig6-sample-sizes¶

Whole-brain activity was recorded in 21 heads (189 identified neurons) and, separately, 21 tails (42 neurons), with a median of 18 animals per neuron type.

Readings from the criterion: neurons as (a) distinct names identified in at least one head, (b) the same after the STAR Methods removals (near-duplicate traces, N of 3 or fewer); neuron type as (a) a name, (b) a class. The removals are run under each near-duplicate reading. The dandiset holds no tails, so the tail counts are 0 here.

In [10]:
ss = []
for mode in MODES:
    d = data[mode]
    kept = [set(a["traces"]) for a in d["animals"]]
    by_name = collections.Counter(n for k in kept for n in k)
    by_cls = collections.Counter()
    for k in kept:
        by_cls.update({cls_of[n] for n in k})
    named_any = {n for r in recs for n in r["labels"] if n}
    ss.append({"duplicates": mode, "heads": len(d["animals"]),
               "names_any_head": len(named_any),
               "names_any_head_neurons_only": len(named_any - {"AMSOL", "AMSOR", "HMC"}),
               "names_after_removals": sum(v > npl.MIN_N for v in by_name.values()),
               "median_animals_per_name": float(np.median(list(by_name.values()))),
               "median_animals_per_name_N>3": float(np.median([v for v in by_name.values() if v > npl.MIN_N])),
               "median_animals_per_class": float(np.median(list(by_cls.values()))),
               "median_animals_per_class_N>3": float(np.median([v for v in by_cls.values() if v > npl.MIN_N]))})
ss = pd.DataFrame(ss)
print(ss.to_string())
dep_name = collections.Counter(n for f, n in DEP)
dep_cls = collections.Counter(c for f, c in {(f, npl.class_maps({m for _, m in DEP})[n]) for f, n in DEP})
print("the deposit itself: names", len(dep_name), "| median animals per name", np.median(list(dep_name.values())),
      "| classes", len(dep_cls), "| median animals per class", np.median(list(dep_cls.values())))
print("tail recordings in the dandiset:", sum("tail" in r["session_description"].lower() for r in recs))
print("expected: heads 21 (tol 0), head neurons 189 (170.1 to 207.9), tails 21, tail neurons 42, median 18 (16.2 to 19.8)")
  duplicates  heads  names_any_head  names_any_head_neurons_only  names_after_removals  median_animals_per_name  median_animals_per_name_N>3  median_animals_per_class  median_animals_per_class_N>3
0        one     21             193                          190                   184                     19.0                         19.0                      20.0                          20.0
1    deposit     21             193                          190                   184                     21.0                         21.0                      21.0                          21.0
2       both     21             193                          190                   184                     17.0                         17.0                      19.5                          20.0
3       none     21             193                          190                   185                     21.0                         21.0                      21.0                          21.0
the deposit itself: names 189 | median animals per name 21.0 | classes 109 | median animals per class 21.0
tail recordings in the dandiset: 0
expected: heads 21 (tol 0), head neurons 189 (170.1 to 207.9), tails 21, tail neurons 42, median 18 (16.2 to 19.8)

Result: partially reproduced. The head counts match: 21 heads (expected 21); 193 neuron names in at least one head, 190 of them neurons, and 184 after the removals, all within 170.1 to 207.9. The median number of animals per name is 19 under the main reading (one member of a near-duplicate pair removed), 17 with both removed, and 21 with none removed or under the deposit reading; per class it is 20, 19.5 and 21. The main reading is within 16.2 to 19.8 per name, not per class. Whether the median matches rests on the selection reading, an assumption: under the deposit and none readings it is 21, outside 16.2 to 19.8, and the outcome would stay partially reproduced, as the tails are missing. The deposit itself has 189 names, the paper's number, with a median of 21 animals per name and per class, so the median of 18 is not the heads' median in the paper's own deposit. The paper's sentence may give the median over heads and tails together, which cannot be computed here (a question for a reader). The dandiset holds no tail recordings, so the tail counts (21 tails, 42 neurons) cannot be matched.

fig6-all-heads-responsive¶

21 head recordings showed the published responses of the primary sensory neurons, ASE excitation to NaCl and AWC inhibition to both odors, in the left or the right neuron, and were kept for analysis.

Readings: (a) the mean dF/F0 during the 10 s pulse exceeds (ASE) or falls below (AWC) the mean of the 10 s before; (b) the difference exceeds two standard deviations of the 10 s before. Near-duplicate removal is not applied here (reading 'none'), as the paper's review of the heads precedes its statistics; the main reading is shown as a variant.

In [11]:
def head_checks(d, k_sd):
    out = []
    for a in d["animals"]:
        row = {"animal": a["id"]}
        for cls, stim, sign in (("ASE", "nacl", 1), ("AWC", "butanone", -1), ("AWC", "pentanedione", -1)):
            ok = False
            for n in (cls + "L", cls + "R"):
                if n not in a["traces"]:
                    continue
                tr = a["traces"][n]
                s0, s1 = a["stimuli"][stim]
                t = np.arange(len(tr)) / a["rate"]
                pre = tr[(t >= s0 - 10) & (t < s0)]
                diff = tr[(t >= s0) & (t < s1)].mean() - pre.mean()
                ok |= sign * diff > k_sd * pre.std()
            row[f"{cls}_{stim}"] = ok
        row["all_three"] = all(v for k, v in row.items() if k != "animal")
        out.append(row)
    return pd.DataFrame(out)

for label, mode, k in [(f"({'a' if k == 0 else 'b'}) {m}", m, k) for m in ("none", "one", "deposit", "both") for k in (0, 2)]:
    hc = head_checks(data[mode], k)
    print(label, "heads passing all three:", int(hc.all_three.sum()), "of", len(hc),
          "| per stimulus:", hc.drop(columns=["animal", "all_three"]).sum().to_dict())
hc_a = head_checks(data["none"], 0)
print(hc_a.to_string())
(a) none heads passing all three: 19 of 21 | per stimulus: {'ASE_nacl': 21, 'AWC_butanone': 20, 'AWC_pentanedione': 19}
(b) none heads passing all three: 16 of 21 | per stimulus: {'ASE_nacl': 20, 'AWC_butanone': 19, 'AWC_pentanedione': 18}
(a) one heads passing all three: 19 of 21 | per stimulus: {'ASE_nacl': 21, 'AWC_butanone': 20, 'AWC_pentanedione': 19}
(b) one heads passing all three: 16 of 21 | per stimulus: {'ASE_nacl': 20, 'AWC_butanone': 19, 'AWC_pentanedione': 18}
(a) deposit heads passing all three: 19 of 21 | per stimulus: {'ASE_nacl': 21, 'AWC_butanone': 20, 'AWC_pentanedione': 19}
(b) deposit heads passing all three: 16 of 21 | per stimulus: {'ASE_nacl': 20, 'AWC_butanone': 19, 'AWC_pentanedione': 18}
(a) both heads passing all three: 18 of 21 | per stimulus: {'ASE_nacl': 19, 'AWC_butanone': 20, 'AWC_pentanedione': 19}
(b) both heads passing all three: 15 of 21 | per stimulus: {'ASE_nacl': 18, 'AWC_butanone': 19, 'AWC_pentanedione': 18}
         animal  ASE_nacl  AWC_butanone  AWC_pentanedione  all_three
0   20190924_01      True          True              True       True
1   20190924_02      True          True              True       True
2   20190924_03      True          True              True       True
3   20190925_01      True          True              True       True
4   20190925_04      True          True              True       True
5   20190928_01      True          True              True       True
6   20190928_03      True          True              True       True
7   20190928_05      True          True              True       True
8   20190928_07      True          True              True       True
9   20190928_08      True          True              True       True
10  20190928_11      True          True              True       True
11  20190928_13      True          True              True       True
12  20190929_02      True          True              True       True
13  20190929_03      True          True              True       True
14  20190929_05      True          True              True       True
15  20190929_06      True          True              True       True
16  20190929_07      True          True              True       True
17  20191030_03      True          True              True       True
18  20191030_07      True          True             False      False
19  20191104_08      True          True              True       True
20  20191104_10      True         False             False      False

Result: not reproduced. Under reading (a), the more lenient one, 19 of the 21 heads pass all three checks with no neuron removed: ASE to NaCl in 21, AWC to 2-butanone in 20, AWC to 2,3-pentanedione in 19. Under reading (b) 16 pass. The main and deposit readings give the same counts; with both members of near-duplicates removed, 18 pass under (a) and 15 under (b). Head 20191030_07 fails on 2,3-pentanedione and head 20191104_10 on both odors. The dandiset holds exactly the 21 heads the paper kept, so the paper's review, which it does not describe, passed two heads that the claims file's rules do not.

The response tests used by the claims below¶

Per the STAR Methods: for each class (left and right pooled), each stimulus, a two-tailed paired t-test of the mean during the 10 s stimulus against the identical period immediately before; premotor interneurons and ventral-cord motor neurons excluded, and the two named cells that are not neurons (AMSO, HMC); classes in 3 or fewer animals not evaluated. q values by Storey (2002). The FDR family has two readings in the claims file: (a) all class and stimulus tests of the heads together, (b) each stimulus apart; (c) uncorrected p is the third reading where the claims file lists it. Table S6's legend gives (a) as the paper's. Benjamini-Hochberg over all tests is added as a conservative check, as Storey's pi0 is low here.

Each trace is z-scored before the test, an assumption taken from the STAR Methods' "When comparing neuronal activity across animals, we used dNMF to normalize activity traces per neuron class", which does not say how; Table S6, below, is matched far better by traces scaled one by one than by dF/F0. The table is built for each selection reading with z-scored traces, and for the main selection with dF/F0 and with min-max scaling.

In [12]:
def test_table(d, norm="zscore"):
    rows = []
    for s in npl.STIMULI:
        for c, v in npl.class_tests(npl.responses(d, s, norm)).items():
            rows.append({"stimulus": s, "cls": c, **v})
    t = pd.DataFrame(rows)
    t["q_all"] = npl.storey_q(t.p.values)
    t["q_stim"] = np.nan
    for s in npl.STIMULI:
        m = t.stimulus == s
        t.loc[m, "q_stim"] = npl.storey_q(t.loc[m, "p"].values)
    t["q_bh_all"] = npl.bh_q(t.p.values)
    return t

TT = {mode: test_table(data[mode]) for mode in MODES}
TT["one, dF/F0"] = test_table(D, "raw")
TT["one, min-max"] = test_table(D, "minmax")
T = TT["one"]
print("tests per reading:", {m: len(t) for m, t in TT.items()}, "| classes tested:", T.cls.nunique())
print("non-neurons tested:", sorted(set(T.cls) & npl.NON_NEURONS))
print("Storey pi0, all tests:", round(npl.storey_pi0(T.p.values), 3),
      "| per stimulus:", {s: round(npl.storey_pi0(T[T.stimulus == s].p.values), 3) for s in npl.STIMULI})
for mode, t in TT.items():
    print(f"[{mode}] significant tests: q over all {int((t.q_all <= 0.05).sum())}, q per stimulus "
          f"{int((t.q_stim <= 0.05).sum())}, Benjamini-Hochberg over all {int((t.q_bh_all <= 0.05).sum())}, "
          f"p {int((t.p <= 0.05).sum())} of {len(t)}")
print("Storey pi0 over all tests, bootstrap seeds 0 to 9 (main reading):", [round(npl.storey_pi0(T.p.values, seed=k), 3) for k in range(10)])
READINGS = {"q_all": "(a) q over all tests", "q_stim": "(b) q per stimulus", "p": "(c) uncorrected p",
            "q_bh_all": "(a') Benjamini-Hochberg over all tests, a conservative check"}

def sig(t, cls, stim, key):
    r = t[(t.cls == cls) & (t.stimulus == stim)]
    if r.empty:
        return None
    return bool(r[key].iloc[0] <= 0.05), float(r.mean_diff.iloc[0])

def show(t, classes):
    sub = t[t.cls.isin(classes)].set_index(["cls", "stimulus"])[["n_traces", "n_animals", "mean_diff", "t", "p", "q_all", "q_stim"]]
    print(sub.round(5).to_string())
tests per reading: {'one': 282, 'deposit': 282, 'both': 282, 'none': 282, 'one, dF/F0': 282, 'one, min-max': 282} | classes tested: 94
non-neurons tested: []
Storey pi0, all tests: 0.222 | per stimulus: {'nacl': 0.236, 'butanone': 0.177, 'pentanedione': 0.411}
[one] significant tests: q over all 141, q per stimulus 128, Benjamini-Hochberg over all 64, p 106 of 282
[deposit] significant tests: q over all 106, q per stimulus 113, Benjamini-Hochberg over all 77, p 109 of 282
[both] significant tests: q over all 93, q per stimulus 114, Benjamini-Hochberg over all 61, p 105 of 282
[none] significant tests: q over all 103, q per stimulus 112, Benjamini-Hochberg over all 76, p 111 of 282
[one, dF/F0] significant tests: q over all 61, q per stimulus 72, Benjamini-Hochberg over all 50, p 82 of 282
[one, min-max] significant tests: q over all 111, q per stimulus 118, Benjamini-Hochberg over all 67, p 112 of 282
Storey pi0 over all tests, bootstrap seeds 0 to 9 (main reading): [0.222, 0.222, 0.222, 0.222, 0.222, 0.222, 0.222, 0.222, 0.222, 0.222]

The tests against the paper's Table S6¶

The paper's Table S6 (sheet "OH16230 Heads") gives, per class, the number of traces tested ("Samples") and, per stimulus, the 95% confidence interval, p and q of the response. It is read here only to choose between readings of the processing that the paper leaves open, not to decide any claim. Its legend also says "FDR was used to correct the p-values of all NaCl, 2-butanone, and 2,3-pentanedione stimulus responses together", which is reading (a) of the FDR family. Below, for each reading of trace selection and of the per-trace normalisation: how the traces tested per class compare with the paper's, the rank correlation of log p with the paper's, the median distance between them in log10 units, and how often p <= 0.05 agrees.

In [13]:
S6 = npl.load_table_s6()
pcol = {"nacl": "NaCl p", "butanone": "2-butanone p", "pentanedione": "2,3-pentanedione p"}
cicol = {"nacl": "NaCl CI (ΔF/F0)", "butanone": "2-butanone CI (ΔF/F0)", "pentanedione": "2,3-pentanedione CI (ΔF/F0)"}
print("Table S6 classes:", len(S6), "| classes tested here and in Table S6:", len(set(T.cls) & set(S6.index)))
# The same tests on the deposit's own traces (deposit frame k + 1 is dandiset frame k, link-zenodo-traces;
# the deposit's last frame is NaN, the frame the STAR Methods remove).
stim_of = {a["id"]: a["stimuli"] for a in D["animals"]}
dep_data = {"animals": [{"id": f, "stimuli": stim_of[f], "rate": npl.RATE_HZ,
                         "traces": {n: z[1:-1] for (ff, n), z in DEP.items() if ff == f}} for f in stim_of],
            "class_of": npl.class_maps({n for _, n in DEP})}
print("deposit traces: NaN in the first 4 frames and the last frame, and nowhere else, in",
      sum(1 for z in DEP.values() if np.isfinite(z[4:-1]).all() and not np.isfinite(z[:4]).any() and not np.isfinite(z[-1])), "of", len(DEP))
TT_dep = {"deposit traces, dF/F0": test_table(dep_data, "raw"), "deposit traces, z-scored": test_table(dep_data)}
cmp_rows = []
for key, t in {**TT, **TT_dep}.items():
    row = {"reading": key}
    nt = t[t.stimulus == "nacl"].set_index("cls").n_traces
    com = [c for c in nt.index if c in S6.index]
    diff = nt[com] - S6.loc[com, "Samples"].astype(float)
    row.update(traces_equal=int((diff == 0).sum()), traces_minus_paper_median=float(np.median(diff)),
               traces_minus_paper_mean=round(float(diff.mean()), 2))
    for s in npl.STIMULI:
        ts = t[t.stimulus == s].set_index("cls")
        pp = pd.to_numeric(S6.loc[com, pcol[s]], errors="coerce")
        ok = pp.notna()
        lp, lq = np.log10(ts.loc[com, "p"][ok]), np.log10(pp[ok].astype(float))
        lohi = S6.loc[com, cicol[s]].astype(str).str.split(",", expand=True).apply(pd.to_numeric, errors="coerce")
        mid = lohi.mean(axis=1)[ok]
        row.update({f"{s[:4]}_rho": round(stats.spearmanr(lp, lq)[0], 3), f"{s[:4]}_dlog10p": round(float(np.median(np.abs(lp - lq))), 3),
                    f"{s[:4]}_sig_agree": int(((ts.loc[com, "p"][ok] <= 0.05) == (pp[ok] <= 0.05)).sum()),
                    f"{s[:4]}_effect_ratio": round(float(np.median(mid / ts.loc[com, "mean_diff"][ok])), 3)})
    row["n_classes"] = int(ok.sum())
    cmp_rows.append(row)
print(pd.DataFrame(cmp_rows).to_string(index=False))

# The q values of this attempt on the paper's own p values, against the paper's q values.
p6 = np.concatenate([pd.to_numeric(S6[pcol[s]], errors="coerce").dropna().values for s in npl.STIMULI])
q6 = np.concatenate([pd.to_numeric(S6[pcol[s].replace(" p", " q")], errors="coerce")[pd.to_numeric(S6[pcol[s]], errors="coerce").notna()].values for s in npl.STIMULI])
rank6 = np.empty(len(p6)); rank6[np.argsort(p6)] = np.arange(1, len(p6) + 1)
pos = p6 > 0
print("Table S6: %d stimulus tests, p <= 0.05 in %d, q <= 0.05 in %d; pi0 implied by its q values (q * rank / (m p)): median %.4f"
      % (len(p6), (p6 <= 0.05).sum(), (q6 <= 0.05).sum(), np.median((q6 * rank6 / (len(p6) * p6))[pos])))
print("storey_pi0 on the paper's p values, seeds 0 to 9:", [round(npl.storey_pi0(p6, seed=k), 3) for k in range(10)])
q6_ours = npl.storey_q(p6)
print("our q on the paper's p against the paper's q: q <= 0.05 agrees in %d of %d tests; largest |log10 ratio| %.3f"
      % (((q6_ours <= 0.05) == (q6 <= 0.05)).sum(), len(p6), np.max(np.abs(np.log10(q6_ours[pos] / q6[pos])))))
Table S6 classes: 109 | classes tested here and in Table S6: 94
deposit traces: NaN in the first 4 frames and the last frame, and nowhere else, in 3550 of 3550
                 reading  traces_equal  traces_minus_paper_median  traces_minus_paper_mean  nacl_rho  nacl_dlog10p  nacl_sig_agree  nacl_effect_ratio  buta_rho  buta_dlog10p  buta_sig_agree  buta_effect_ratio  pent_rho  pent_dlog10p  pent_sig_agree  pent_effect_ratio  n_classes
                     one            18                        0.0                     0.60     0.960         0.187              84              0.127     0.902         0.145              87              0.142     0.933         0.153              89              0.178         94
                 deposit            18                        2.0                     2.91     0.959         0.157              87              0.123     0.869         0.140              87              0.144     0.938         0.180              87              0.139         94
                    both            12                       -2.0                    -2.30     0.944         0.204              87              0.129     0.929         0.127              87              0.124     0.913         0.208              85              0.128         94
                    none            10                        3.0                     3.84     0.928         0.157              83              0.123     0.869         0.140              88              0.167     0.916         0.184              86              0.164         94
              one, dF/F0            18                        0.0                     0.60     0.728         0.400              74              0.271     0.637         0.382              80              0.448     0.621         0.385              80              0.421         94
            one, min-max            18                        0.0                     0.60     0.944         0.207              82              0.769     0.865         0.157              85              0.882     0.918         0.231              89              0.980         94
   deposit traces, dF/F0            18                        2.0                     2.91     0.748         0.361              72              0.329     0.569         0.395              80              0.445     0.654         0.448              78              0.389         94
deposit traces, z-scored            18                        2.0                     2.91     0.960         0.123              87              0.133     0.869         0.152              85              0.162     0.938         0.167              88              0.153         94
Table S6: 285 stimulus tests, p <= 0.05 in 106, q <= 0.05 in 102; pi0 implied by its q values (q * rank / (m p)): median 0.4236
storey_pi0 on the paper's p values, seeds 0 to 9: [0.478, 0.478, 0.421, 0.478, 0.478, 0.478, 0.421, 0.421, 0.478, 0.478]
our q on the paper's p against the paper's q: q <= 0.05 agrees in 273 of 285 tests; largest |log10 ratio| 0.060

The paper's p values are reproduced far better by tests on z-scored traces than on dF/F0: under the main selection the rank correlation of log p is 0.960, 0.902 and 0.933 for the three stimuli (dF/F0: 0.728, 0.637 and 0.621), the median gap is 0.15 to 0.19 log10 units (dF/F0: 0.38 to 0.40), and p <= 0.05 agrees in 84 to 89 of 94 classes (dF/F0: 74 to 80). Min-max scaling is close behind on p (0.944, 0.865, 0.918) and is the one whose effect sizes match the paper's confidence intervals in scale (median ratio 0.77 to 0.98, against 0.13 to 0.18 for z-scores), so the paper's normalisation is likely a per-trace scaling, though none of these reproduces Table S6 exactly. The paper's own deposited traces give the same picture: on their dF/F0 the rank correlation is 0.748, 0.569 and 0.654, and z-scored 0.960, 0.869 and 0.938, so the table was not computed on the deposited dF/F0 with the test the STAR Methods describe. The selection readings differ less: "one" has the paper's number of traces per class at the median (mean +0.6; "deposit" +2.9, "none" +3.8, "both" -2.3), and the four give similar p agreement. Storey's q of this attempt, applied to the paper's own p values, agrees with the paper's q at 0.05 in 273 of 285 tests (largest gap 0.06 log10 units), with pi0 0.42 or 0.48 depending on the bootstrap seed against 0.4236 implied by the paper's q; on this attempt's p values pi0 is 0.222 for every seed, so q falls below p for many tests (141 tests at q of 0.05 or less against 106 at p of 0.05 or less), and Benjamini-Hochberg gives 64.

fig6d-ase-nacl¶

NaCl evokes left/right asymmetric responses in the two ASE neurons, and ASER shows an increase in calcium upon NaCl presentation.

ASER alone, paired t-test over heads, with its p added to the FDR family (the family's tests pool left and right; adding the one-sided test is a choice made here). Asymmetry: two-tailed unpaired t-test of ASEL against ASER, on (a) the response (during minus before) or (b) the mean during the pulse.

In [14]:
def side_values(d, name, stim, norm="zscore"):
    out = []
    for a in d["animals"]:
        if name in a["traces"]:
            dur, pre = npl.window_means(npl.normalise(a["traces"][name], norm), *a["stimuli"][stim], a["rate"])
            out.append((a["id"], dur, pre))
    return out

for mode, norm in [(m, "zscore") for m in MODES] + [("one", "raw"), ("one", "minmax")]:
    d, t = data[mode], TT[mode if norm == "zscore" else ("one, dF/F0" if norm == "raw" else "one, min-max")]
    L = side_values(d, "ASEL", "nacl", norm)
    R = side_values(d, "ASER", "nacl", norm)
    mode = mode if norm == "zscore" else f"{mode}, {norm}"
    rd = np.array([x[1] for x in R]); rb = np.array([x[2] for x in R])
    pr = stats.ttest_rel(rd, rb).pvalue
    q_all = npl.storey_q(np.r_[t.p.values, pr])[-1]
    q_st = npl.storey_q(np.r_[t[t.stimulus == "nacl"].p.values, pr])[-1]
    ld = np.array([x[1] for x in L]); lb = np.array([x[2] for x in L])
    asym_resp = stats.ttest_ind(ld - lb, rd - rb)
    asym_mean = stats.ttest_ind(ld, rd)
    print(f"[{mode}] ASER NaCl: n={len(R)}, mean during-before={np.mean(rd - rb):+.3f}, "
          f"rising in {np.sum(rd > rb)} of {len(R)}, p={pr:.3g}, q(a)={q_all:.3g}, q(b)={q_st:.3g}")
    print(f"[{mode}] ASEL NaCl: n={len(L)}, mean during-before={np.mean(ld - lb):+.3f}, rising in {np.sum(ld > lb)} of {len(L)}")
    print(f"[{mode}] asymmetry (a) responses: t={asym_resp.statistic:.3f}, p={asym_resp.pvalue:.3g};"
          f" (b) means during: t={asym_mean.statistic:.3f}, p={asym_mean.pvalue:.3g}")
[one] ASER NaCl: n=21, mean during-before=+1.338, rising in 14 of 21, p=0.00282, q(a)=0.0036, q(b)=0.00313
[one] ASEL NaCl: n=19, mean during-before=+3.450, rising in 19 of 19
[one] asymmetry (a) responses: t=4.587, p=4.79e-05; (b) means during: t=4.955, p=1.53e-05
[deposit] ASER NaCl: n=21, mean during-before=+1.338, rising in 14 of 21, p=0.00282, q(a)=0.00709, q(b)=0.00575
[deposit] ASEL NaCl: n=21, mean during-before=+3.357, rising in 21 of 21
[deposit] asymmetry (a) responses: t=4.456, p=6.57e-05; (b) means during: t=4.475, p=6.2e-05
[both] ASER NaCl: n=15, mean during-before=+1.384, rising in 10 of 15, p=0.0151, q(a)=0.0272, q(b)=0.0124
[both] ASEL NaCl: n=17, mean during-before=+3.522, rising in 17 of 17
[both] asymmetry (a) responses: t=4.075, p=0.000311; (b) means during: t=4.845, p=3.61e-05
[none] ASER NaCl: n=21, mean during-before=+1.338, rising in 14 of 21, p=0.00282, q(a)=0.00753, q(b)=0.00578
[none] ASEL NaCl: n=21, mean during-before=+3.357, rising in 21 of 21
[none] asymmetry (a) responses: t=4.456, p=6.57e-05; (b) means during: t=4.475, p=6.2e-05
[one, raw] ASER NaCl: n=21, mean during-before=+0.594, rising in 14 of 21, p=0.00547, q(a)=0.0177, q(b)=0.00945
[one, raw] ASEL NaCl: n=19, mean during-before=+1.856, rising in 19 of 19
[one, raw] asymmetry (a) responses: t=4.565, p=5.11e-05; (b) means during: t=4.587, p=4.78e-05
[one, minmax] ASER NaCl: n=21, mean during-before=+0.208, rising in 14 of 21, p=0.00258, q(a)=0.00594, q(b)=0.00443
[one, minmax] ASEL NaCl: n=19, mean during-before=+0.553, rising in 19 of 19
[one, minmax] asymmetry (a) responses: t=5.014, p=1.27e-05; (b) means during: t=5.031, p=1.21e-05

Result: reproduced. Under the main reading, ASER rises during NaCl in 14 of 21 heads (z-scored response +1.34, p = 0.0028), q = 0.0036 with all tests as the FDR family (reading a) and 0.0031 with the NaCl tests (reading b). Every selection reading gives p of 0.016 or less and q of 0.028 or less; dF/F0 gives p = 0.0055, q(a) = 0.018. ASEL against ASER, unpaired t-test, p = 4.8e-05 on the responses and 1.5e-05 on the means during the pulse, and p of 3.1e-04 or less under every reading. ASEL responds more strongly (+3.45, 19 of 19 heads), which the paper's "However, ASER exhibited an increase" does not contradict.

fig6-nacl-unknown-interneurons¶

NaCl evokes significant responses in six interneurons with no previously known function, AIN, AVF, AVH, AVJ, I2 and MI.

In [15]:
SIX = ["AIN", "AVF", "AVH", "AVJ", "I2", "MI"]
show(T[T.stimulus == "nacl"], SIX)
for mode, t in TT.items():
    for key, lab in READINGS.items():
        res = {c: sig(t, c, "nacl", key) for c in SIX}
        print(f"[{mode}] {lab}: significant {sum(v[0] for v in res.values())} of 6:",
              [c for c, v in res.items() if v[0]])
              n_traces  n_animals  mean_diff        t        p    q_all   q_stim
cls stimulus                                                                    
AVH nacl            31         19    0.85212  5.32426  0.00001  0.00004  0.00004
AVJ nacl            35         21    0.38690  2.40719  0.02165  0.01574  0.01574
AIN nacl            41         21    0.41730  3.20669  0.00264  0.00344  0.00309
I2  nacl            39         20    0.39842  3.90890  0.00037  0.00068  0.00063
MI  nacl            17         17    0.29184  3.86399  0.00137  0.00192  0.00171
AVF nacl            32         19    0.24723  2.24774  0.03185  0.02074  0.01913
[one] (a) q over all tests: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[one] (b) q per stimulus: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[one] (c) uncorrected p: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[one] (a') Benjamini-Hochberg over all tests, a conservative check: significant 4 of 6: ['AIN', 'AVH', 'I2', 'MI']
[deposit] (a) q over all tests: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[deposit] (b) q per stimulus: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[deposit] (c) uncorrected p: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[deposit] (a') Benjamini-Hochberg over all tests, a conservative check: significant 4 of 6: ['AIN', 'AVH', 'I2', 'MI']
[both] (a) q over all tests: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[both] (b) q per stimulus: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[both] (c) uncorrected p: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[both] (a') Benjamini-Hochberg over all tests, a conservative check: significant 4 of 6: ['AIN', 'AVH', 'I2', 'MI']
[none] (a) q over all tests: significant 5 of 6: ['AIN', 'AVH', 'AVJ', 'I2', 'MI']
[none] (b) q per stimulus: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[none] (c) uncorrected p: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[none] (a') Benjamini-Hochberg over all tests, a conservative check: significant 4 of 6: ['AIN', 'AVH', 'I2', 'MI']
[one, dF/F0] (a) q over all tests: significant 4 of 6: ['AIN', 'AVH', 'I2', 'MI']
[one, dF/F0] (b) q per stimulus: significant 4 of 6: ['AIN', 'AVH', 'I2', 'MI']
[one, dF/F0] (c) uncorrected p: significant 4 of 6: ['AIN', 'AVH', 'I2', 'MI']
[one, dF/F0] (a') Benjamini-Hochberg over all tests, a conservative check: significant 3 of 6: ['AVH', 'I2', 'MI']
[one, min-max] (a) q over all tests: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[one, min-max] (b) q per stimulus: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[one, min-max] (c) uncorrected p: significant 6 of 6: ['AIN', 'AVF', 'AVH', 'AVJ', 'I2', 'MI']
[one, min-max] (a') Benjamini-Hochberg over all tests, a conservative check: significant 4 of 6: ['AIN', 'AVH', 'I2', 'MI']

Result: reproduced, resting on the normalisation. Under the main reading all six classes respond significantly to NaCl under every FDR reading: AVH (q 4e-05), AIN (0.0034), I2 (0.0007), MI (0.0019), AVJ (0.016) and AVF (0.021), all with positive means. The deposit and "both" readings give six of six too; with none removed, five of six under reading (a) and six under (b) and (c). On dF/F0 only four of six (AIN, AVH, I2, MI) are significant, AVJ and AVF not, as in the first round of this attempt. Benjamini-Hochberg gives four of six under every selection.

fig6c-odors-more-inhibitory¶

Both odors evoke responses in many sensory and interneurons throughout the brain, with more inhibitory (decreasing calcium) activity than NaCl.

Readings: (a) the number of head classes with a significant decrease (q of 0.05 or less, under each FDR reading); (b) the fraction of head classes whose mean response is negative.

In [16]:
for mode, t in TT.items():
    rows = []
    for s in npl.STIMULI:
        ts = t[t.stimulus == s]
        row = {"stimulus": s, "classes": len(ts), "frac_negative_mean": round((ts.mean_diff < 0).mean(), 3)}
        for key in READINGS:
            row[f"down_{key}"] = int(((ts[key] <= 0.05) & (ts.mean_diff < 0)).sum())
            row[f"up_{key}"] = int(((ts[key] <= 0.05) & (ts.mean_diff > 0)).sum())
        rows.append(row)
    print(f"[{mode}]")
    print(pd.DataFrame(rows).to_string(index=False))
[one]
    stimulus  classes  frac_negative_mean  down_q_all  up_q_all  down_q_stim  up_q_stim  down_p  up_p  down_q_bh_all  up_q_bh_all
        nacl       94               0.234           6        48            6         49       3    41              2           22
    butanone       94               0.245           6        34            6         34       5    24              4           16
pentanedione       94               0.362          17        30           11         22      11    22              7           13
[deposit]
    stimulus  classes  frac_negative_mean  down_q_all  up_q_all  down_q_stim  up_q_stim  down_p  up_p  down_q_bh_all  up_q_bh_all
        nacl       94               0.234           2        36            3         36       3    36              2           22
    butanone       94               0.234           5        23            5         25       5    24              5           18
pentanedione       94               0.372          14        26           15         29      14    27             12           18
[both]
    stimulus  classes  frac_negative_mean  down_q_all  up_q_all  down_q_stim  up_q_stim  down_p  up_p  down_q_bh_all  up_q_bh_all
        nacl       94               0.223           2        36            5         49       4    39              2           21
    butanone       94               0.245           4        23            4         23       4    25              4           15
pentanedione       94               0.362           8        20           10         23      10    23              5           14
[none]
    stimulus  classes  frac_negative_mean  down_q_all  up_q_all  down_q_stim  up_q_stim  down_p  up_p  down_q_bh_all  up_q_bh_all
        nacl       94               0.223           2        36            3         38       3    38              2           23
    butanone       94               0.234           5        23            5         25       5    25              5           17
pentanedione       94               0.362          14        23           15         26      14    26             11           18
[one, dF/F0]
    stimulus  classes  frac_negative_mean  down_q_all  up_q_all  down_q_stim  up_q_stim  down_p  up_p  down_q_bh_all  up_q_bh_all
        nacl       94               0.266           3        15            4         26       4    26              3           13
    butanone       94               0.277           5        14            4         14       5    17              4           12
pentanedione       94               0.457          10        14           10         14      13    17              7           11
[one, min-max]
    stimulus  classes  frac_negative_mean  down_q_all  up_q_all  down_q_stim  up_q_stim  down_p  up_p  down_q_bh_all  up_q_bh_all
        nacl       94               0.234           5        39            5         40       5    39              2           21
    butanone       94               0.255           6        23            6         22       6    23              4           17
pentanedione       94               0.372          14        24           17         28      15    24             10           13

Result: reproduced. Under the main reading, reading (b) holds: the fraction of classes with a negative mean response is 0.234 for NaCl, 0.245 for 2-butanone and 0.362 for 2,3-pentanedione. Reading (a) ties for 2-butanone (6 significant decreases with q over all tests, as for NaCl; 2,3-pentanedione 17). Every other reading holds under (a) or (b): deposit (a) 2, 5 and 14; both (a) 2, 4 and 8 and (b) 0.223, 0.245, 0.362; none (a) 2, 5 and 14; dF/F0 (a) 3, 5 and 10. The 2-butanone margin is thin under every reading. The odors evoke significant responses in many classes (under the main reading, 40 classes for 2-butanone and 47 for 2,3-pentanedione with q over all tests), which supports "many".

fig6d-awc-asymmetric (blocked)¶

2-butanone and 2,3-pentanedione evoke stochastically asymmetric responses in the AWC ON and AWC OFF neurons, AWC OFF being the one srsx-3 colors in NeuroPAL.

The criterion requires the dataset to label AWC ON and AWC OFF, or AWCL and AWCR with the ON/OFF side. The files name only AWCL and AWCR, so the claim is blocked. The paper's Zenodo deposit gives the side (is_AWCL_OFF); as a variant only, the test of the criterion is run with that label on the dandiset's traces.

In [17]:
text = {r["identifier"]: [*r["labels"], *r["neuropal_labels"], r["subject_description"], r["session_description"],
                         r["stim_description"]] for r in recs}
import re
onoff = sorted({s for v in text.values() for s in v if s and re.search(r"(^|[^A-Z])(ON|OFF)([^A-Z]|$)|AWC_?(ON|OFF)", s.upper())})
print("AWC names in the files:", sorted({n for r in recs for n in r["labels"] if n.startswith("AWC")}),
      "| strings naming ON or OFF as a word, or AWCON/AWCOFF, among the trace labels, NeuroPAL labels and descriptions of all 21 files:", onoff)
is_off = dict(zip(zfiles, np.asarray(Z["is_AWCL_OFF"], float)))
print("deposit: AWCL is OFF in", int(np.nansum(list(is_off.values()))), "of", len(is_off), "heads")

# The NeuroPAL volume: srsx-3 colors AWC OFF (Results). Per head, each channel at AWCL over AWCR,
# divided by the same ratio for the panneuronal TagRFP-T.
col = []
for r in recs:
    c = r["awc_color"]
    row = {"file": r["identifier"], "deposit_AWCL_OFF": int(is_off[r["identifier"]])}
    for ch in c["AWCL"]:
        if ch != "Tag RFP-T":
            row[ch] = (c["AWCL"][ch] / c["AWCL"]["Tag RFP-T"]) / (c["AWCR"][ch] / c["AWCR"]["Tag RFP-T"])
    col.append(row)
col = pd.DataFrame(col)
print(col.round(2).to_string(index=False))
for ch in col.columns[2:]:
    agree = int(((col[ch] > 1) == (col.deposit_AWCL_OFF == 1)).sum())
    print(f"{ch}: AWCL brighter (relative to TagRFP-T) where the deposit has AWCL OFF in {agree} of 21 heads, "
          f"binomial p = {stats.binomtest(max(agree, 21 - agree), 21).pvalue:.2g}")
img_off = dict(zip(col.file, (col["CyOFP1"] > 1).astype(float)))

print("NeuroPAL fluorophore of srsx-3 in the paper's Table S1:", npl.reporter_fluorophore("srsx-3"))
for label, side in (("deposit label", is_off), ("NeuroPAL CyOFP1 label", img_off)):
    for mode, norm in [(m, "zscore") for m in MODES] + [("one", "raw")]:
        d = data[mode]
        for s in ("butanone", "pentanedione"):
            on, off = {"resp": [], "mean": []}, {"resp": [], "mean": []}
            for a in d["animals"]:
                for sd in "LR":
                    n = "AWC" + sd
                    if n not in a["traces"]:
                        continue
                    dur, pre = npl.window_means(npl.normalise(a["traces"][n], norm), *a["stimuli"][s], a["rate"])
                    tgt = off if (side[a["id"]] == 1) == (sd == "L") else on
                    tgt["resp"].append(dur - pre)
                    tgt["mean"].append(dur)
            for meas in ("resp", "mean"):
                r = stats.ttest_ind(on[meas], off[meas])
                print(f"[{label}, {mode}{'' if norm == 'zscore' else ', ' + norm}] {s}, {meas}: ON {np.mean(on[meas]):+.3f} (n={len(on[meas])}) vs OFF "
                      f"{np.mean(off[meas]):+.3f} (n={len(off[meas])}), t={r.statistic:.2f}, p={r.pvalue:.3g}")
AWC names in the files: ['AWCL', 'AWCR'] | strings naming ON or OFF as a word, or AWCON/AWCOFF, among the trace labels, NeuroPAL labels and descriptions of all 21 files: []
deposit: AWCL is OFF in 10 of 21 heads
       file  deposit_AWCL_OFF  mTagBFP2  CyOFP1  mNeptune 2.5  GFP-GCaMP
20190924_01                 1      1.30    3.75          1.02       1.50
20190924_02                 0      0.48    0.00          0.41       0.81
20190924_03                 1      1.05    4.72          1.24       2.31
20190925_01                 1      1.04    1.90          1.76       1.70
20190925_04                 0      0.83    0.34          1.68       0.63
20190928_01                 0      1.21    1.43          0.79       0.87
20190928_03                 0      0.61    0.27          0.66       0.40
20190928_05                 1      0.78    1.42          0.80       0.29
20190928_07                 0      0.63    0.63          0.44       0.54
20190928_08                 0      1.31    2.18          1.28       1.33
20190928_11                 1      1.05    1.87          1.30       1.37
20190928_13                 0      1.61    0.49          0.94       0.73
20190929_02                 1      1.20    7.99          1.54       3.67
20190929_03                 1      0.55    1.22          0.68       1.01
20190929_05                 0      1.79    0.21          1.26       1.99
20190929_06                 1      2.00    2.16          2.28       2.39
20190929_07                 1      0.55    1.04          0.62       0.75
20191030_03                 0      1.11    0.77          1.32       1.00
20191030_07                 1      0.89    1.02          1.00       1.31
20191104_08                 0      1.21    0.78          0.82       0.96
20191104_10                 0      0.98    0.55          0.90       0.82
mTagBFP2: AWCL brighter (relative to TagRFP-T) where the deposit has AWCL OFF in 11 of 21 heads, binomial p = 1
CyOFP1: AWCL brighter (relative to TagRFP-T) where the deposit has AWCL OFF in 19 of 21 heads, binomial p = 0.00022
mNeptune 2.5: AWCL brighter (relative to TagRFP-T) where the deposit has AWCL OFF in 14 of 21 heads, binomial p = 0.19
GFP-GCaMP: AWCL brighter (relative to TagRFP-T) where the deposit has AWCL OFF in 17 of 21 heads, binomial p = 0.0072
NeuroPAL fluorophore of srsx-3 in the paper's Table S1: CyOFP1
[deposit label, one] butanone, resp: ON -1.015 (n=21) vs OFF -1.168 (n=21), t=0.50, p=0.62
[deposit label, one] butanone, mean: ON -1.079 (n=21) vs OFF -0.989 (n=21), t=-0.29, p=0.771
[deposit label, one] pentanedione, resp: ON -1.044 (n=21) vs OFF -0.685 (n=21), t=-1.50, p=0.143
[deposit label, one] pentanedione, mean: ON -1.083 (n=21) vs OFF -0.800 (n=21), t=-1.18, p=0.246
[deposit label, deposit] butanone, resp: ON -1.015 (n=21) vs OFF -1.168 (n=21), t=0.50, p=0.62
[deposit label, deposit] butanone, mean: ON -1.079 (n=21) vs OFF -0.989 (n=21), t=-0.29, p=0.771
[deposit label, deposit] pentanedione, resp: ON -1.044 (n=21) vs OFF -0.685 (n=21), t=-1.50, p=0.143
[deposit label, deposit] pentanedione, mean: ON -1.083 (n=21) vs OFF -0.800 (n=21), t=-1.18, p=0.246
[deposit label, both] butanone, resp: ON -0.953 (n=19) vs OFF -1.139 (n=19), t=0.57, p=0.575
[deposit label, both] butanone, mean: ON -1.051 (n=19) vs OFF -0.932 (n=19), t=-0.37, p=0.717
[deposit label, both] pentanedione, resp: ON -1.091 (n=19) vs OFF -0.698 (n=19), t=-1.52, p=0.138
[deposit label, both] pentanedione, mean: ON -1.133 (n=19) vs OFF -0.772 (n=19), t=-1.40, p=0.171
[deposit label, none] butanone, resp: ON -1.015 (n=21) vs OFF -1.168 (n=21), t=0.50, p=0.62
[deposit label, none] butanone, mean: ON -1.079 (n=21) vs OFF -0.989 (n=21), t=-0.29, p=0.771
[deposit label, none] pentanedione, resp: ON -1.044 (n=21) vs OFF -0.685 (n=21), t=-1.50, p=0.143
[deposit label, none] pentanedione, mean: ON -1.083 (n=21) vs OFF -0.800 (n=21), t=-1.18, p=0.246
[deposit label, one, raw] butanone, resp: ON -0.648 (n=21) vs OFF -0.536 (n=21), t=-0.58, p=0.563
[deposit label, one, raw] butanone, mean: ON -0.632 (n=21) vs OFF -0.488 (n=21), t=-0.67, p=0.508
[deposit label, one, raw] pentanedione, resp: ON -0.599 (n=21) vs OFF -0.356 (n=21), t=-1.75, p=0.0873
[deposit label, one, raw] pentanedione, mean: ON -0.598 (n=21) vs OFF -0.382 (n=21), t=-1.70, p=0.0961
[NeuroPAL CyOFP1 label, one] butanone, resp: ON -1.155 (n=21) vs OFF -1.027 (n=21), t=-0.42, p=0.68
[NeuroPAL CyOFP1 label, one] butanone, mean: ON -1.100 (n=21) vs OFF -0.967 (n=21), t=-0.44, p=0.666
[NeuroPAL CyOFP1 label, one] pentanedione, resp: ON -1.092 (n=21) vs OFF -0.636 (n=21), t=-1.94, p=0.06
[NeuroPAL CyOFP1 label, one] pentanedione, mean: ON -1.109 (n=21) vs OFF -0.775 (n=21), t=-1.40, p=0.17
[NeuroPAL CyOFP1 label, deposit] butanone, resp: ON -1.155 (n=21) vs OFF -1.027 (n=21), t=-0.42, p=0.68
[NeuroPAL CyOFP1 label, deposit] butanone, mean: ON -1.100 (n=21) vs OFF -0.967 (n=21), t=-0.44, p=0.666
[NeuroPAL CyOFP1 label, deposit] pentanedione, resp: ON -1.092 (n=21) vs OFF -0.636 (n=21), t=-1.94, p=0.06
[NeuroPAL CyOFP1 label, deposit] pentanedione, mean: ON -1.109 (n=21) vs OFF -0.775 (n=21), t=-1.40, p=0.17
[NeuroPAL CyOFP1 label, both] butanone, resp: ON -1.108 (n=19) vs OFF -0.984 (n=19), t=-0.38, p=0.708
[NeuroPAL CyOFP1 label, both] butanone, mean: ON -1.075 (n=19) vs OFF -0.908 (n=19), t=-0.51, p=0.611
[NeuroPAL CyOFP1 label, both] pentanedione, resp: ON -1.145 (n=19) vs OFF -0.644 (n=19), t=-1.97, p=0.0563
[NeuroPAL CyOFP1 label, both] pentanedione, mean: ON -1.161 (n=19) vs OFF -0.743 (n=19), t=-1.63, p=0.112
[NeuroPAL CyOFP1 label, none] butanone, resp: ON -1.155 (n=21) vs OFF -1.027 (n=21), t=-0.42, p=0.68
[NeuroPAL CyOFP1 label, none] butanone, mean: ON -1.100 (n=21) vs OFF -0.967 (n=21), t=-0.44, p=0.666
[NeuroPAL CyOFP1 label, none] pentanedione, resp: ON -1.092 (n=21) vs OFF -0.636 (n=21), t=-1.94, p=0.06
[NeuroPAL CyOFP1 label, none] pentanedione, mean: ON -1.109 (n=21) vs OFF -0.775 (n=21), t=-1.40, p=0.17
[NeuroPAL CyOFP1 label, one, raw] butanone, resp: ON -0.694 (n=21) vs OFF -0.491 (n=21), t=-1.07, p=0.293
[NeuroPAL CyOFP1 label, one, raw] butanone, mean: ON -0.624 (n=21) vs OFF -0.496 (n=21), t=-0.59, p=0.555
[NeuroPAL CyOFP1 label, one, raw] pentanedione, resp: ON -0.596 (n=21) vs OFF -0.358 (n=21), t=-1.71, p=0.0941
[NeuroPAL CyOFP1 label, one, raw] pentanedione, mean: ON -0.586 (n=21) vs OFF -0.393 (n=21), t=-1.51, p=0.139

Result: blocked (metadata insufficient). The files name only AWCL and AWCR, and no trace label, NeuroPAL label or description in any of the 21 files names ON or OFF. The NeuroPAL image does carry the side: CyOFP1, the fluorophore of srsx-3 in Table S1, is brighter (relative to TagRFP-T) in the AWC that the deposit marks as OFF in 19 of 21 heads (binomial p = 0.0002), while mTagBFP2 and mNeptune 2.5 do not separate the two (11 and 14 of 21) and GFP-GCaMP does less well (17 of 21, p = 0.007). As variants only, with the deposit's labels, the unpaired t-test of ON against OFF responses gives p = 0.62 for 2-butanone and 0.14 for 2,3-pentanedione under the main reading (dF/F0: 0.56 and 0.087), and with the image-derived labels 0.68 and 0.060. No variant shown reaches p of 0.05 for either odor; the smallest is 0.056 (2,3-pentanedione, image-derived labels, both removed). So even with a label the asymmetry would not be reproduced here.

fig6-asj-ric¶

The ASJ sensory neurons are excited by NaCl but inhibited by both odors, whereas the RIC interneurons are excited only by 2,3-pentanedione.

Six parts: ASJ excited by NaCl, inhibited by 2-butanone, inhibited by 2,3-pentanedione (each significant); RIC excited by 2,3-pentanedione (significant), not significantly excited by NaCl, not by 2-butanone.

In [18]:
for mode, t in TT.items():
    print(f"[{mode}]")
    show(t, ["ASJ", "RIC"])
for mode, t in TT.items():
    for key, lab in READINGS.items():
        def part(cls, stim, want):
            s, m = sig(t, cls, stim, key)
            if want == "up":
                return s and m > 0
            if want == "down":
                return s and m < 0
            return not (s and m > 0)
        parts = {"ASJ+NaCl": part("ASJ", "nacl", "up"), "ASJ-but": part("ASJ", "butanone", "down"),
                 "ASJ-pent": part("ASJ", "pentanedione", "down"), "RIC+pent": part("RIC", "pentanedione", "up"),
                 "RIC not+NaCl": part("RIC", "nacl", "not_up"), "RIC not+but": part("RIC", "butanone", "not_up")}
        print(f"[{mode}] {lab}: {sum(parts.values())} of 6 hold:", parts)
[one]
                  n_traces  n_animals  mean_diff        t        p    q_all   q_stim
cls stimulus                                                                        
ASJ nacl                37         21    0.79770  4.13358  0.00020  0.00042  0.00043
RIC nacl                40         21    0.16765  2.20836  0.03317  0.02137  0.01930
ASJ butanone            37         21   -0.51942 -4.18396  0.00018  0.00038  0.00033
RIC butanone            40         21   -0.02232 -0.25536  0.79979  0.19262  0.15006
ASJ pentanedione        37         21   -0.24859 -1.78883  0.08206  0.04007  0.07555
RIC pentanedione        40         21    0.28813  2.96973  0.00508  0.00557  0.01034
[deposit]
                  n_traces  n_animals  mean_diff        t        p    q_all   q_stim
cls stimulus                                                                        
ASJ nacl                37         21    0.78267  3.99527  0.00031  0.00112  0.00081
RIC nacl                42         21    0.14539  1.96412  0.05632  0.06534  0.05321
ASJ butanone            37         21   -0.56025 -4.54764  0.00006  0.00034  0.00019
RIC butanone            42         21    0.00025  0.00294  0.99767  0.46791  0.26534
ASJ pentanedione        37         21   -0.37866 -2.72692  0.00982  0.01881  0.01201
RIC pentanedione        42         21    0.32572  3.39271  0.00154  0.00445  0.00307
[both]
                  n_traces  n_animals  mean_diff        t        p    q_all   q_stim
cls stimulus                                                                        
ASJ nacl                33         20    0.82111  3.93333  0.00042  0.00172  0.00094
RIC nacl                37         21    0.16770  2.24313  0.03114  0.04415  0.01821
ASJ butanone            33         20   -0.56218 -4.24290  0.00018  0.00086  0.00073
RIC butanone            37         21   -0.03129 -0.33705  0.73804  0.38466  0.33456
ASJ pentanedione        33         20   -0.26510 -1.75423  0.08896  0.08840  0.08155
RIC pentanedione        37         21    0.31931  3.24987  0.00251  0.00703  0.00674
[none]
                  n_traces  n_animals  mean_diff        t        p    q_all   q_stim
cls stimulus                                                                        
ASJ nacl                40         21    0.73535  4.01898  0.00026  0.00114  0.00082
RIC nacl                42         21    0.14539  1.96412  0.05632  0.06965  0.05368
ASJ butanone            40         21   -0.52642 -4.51461  0.00006  0.00037  0.00020
RIC butanone            42         21    0.00025  0.00294  0.99767  0.49878  0.26534
ASJ pentanedione        40         21   -0.31768 -2.35636  0.02357  0.03693  0.02510
RIC pentanedione        42         21    0.32572  3.39271  0.00154  0.00475  0.00339
[one, dF/F0]
                  n_traces  n_animals  mean_diff        t        p    q_all   q_stim
cls stimulus                                                                        
ASJ nacl                37         21    0.04841  0.22056  0.82668  0.49336  0.26539
RIC nacl                40         21   -0.00747 -0.23295  0.81702  0.49094  0.26527
ASJ butanone            37         21   -0.17191 -3.96714  0.00033  0.00260  0.00383
RIC butanone            40         21    0.01731  0.74737  0.45933  0.34393  0.43252
ASJ pentanedione        37         21   -0.05524 -0.85163  0.40005  0.31880  0.40972
RIC pentanedione        40         21    0.03060  1.01310  0.31726  0.27328  0.33083
[one, min-max]
                  n_traces  n_animals  mean_diff        t        p    q_all   q_stim
cls stimulus                                                                        
ASJ nacl                37         21    0.12054  3.95560  0.00034  0.00128  0.00098
RIC nacl                40         21    0.02331  1.87023  0.06897  0.06981  0.05162
ASJ butanone            37         21   -0.08713 -4.40902  0.00009  0.00048  0.00044
RIC butanone            40         21   -0.00172 -0.12177  0.90371  0.38478  0.32275
ASJ pentanedione        37         21   -0.04887 -2.19761  0.03449  0.04190  0.02112
RIC pentanedione        40         21    0.04286  2.82293  0.00745  0.01450  0.00808
[one] (a) q over all tests: 5 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': True, 'RIC+pent': True, 'RIC not+NaCl': False, 'RIC not+but': True}
[one] (b) q per stimulus: 4 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': False, 'RIC+pent': True, 'RIC not+NaCl': False, 'RIC not+but': True}
[one] (c) uncorrected p: 4 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': False, 'RIC+pent': True, 'RIC not+NaCl': False, 'RIC not+but': True}
[one] (a') Benjamini-Hochberg over all tests, a conservative check: 5 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': False, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}
[deposit] (a) q over all tests: 6 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': True, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}
[deposit] (b) q per stimulus: 6 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': True, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}
[deposit] (c) uncorrected p: 6 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': True, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}
[deposit] (a') Benjamini-Hochberg over all tests, a conservative check: 6 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': True, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}
[both] (a) q over all tests: 4 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': False, 'RIC+pent': True, 'RIC not+NaCl': False, 'RIC not+but': True}
[both] (b) q per stimulus: 4 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': False, 'RIC+pent': True, 'RIC not+NaCl': False, 'RIC not+but': True}
[both] (c) uncorrected p: 4 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': False, 'RIC+pent': True, 'RIC not+NaCl': False, 'RIC not+but': True}
[both] (a') Benjamini-Hochberg over all tests, a conservative check: 5 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': False, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}
[none] (a) q over all tests: 6 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': True, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}
[none] (b) q per stimulus: 6 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': True, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}
[none] (c) uncorrected p: 6 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': True, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}
[none] (a') Benjamini-Hochberg over all tests, a conservative check: 5 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': False, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}
[one, dF/F0] (a) q over all tests: 3 of 6 hold: {'ASJ+NaCl': False, 'ASJ-but': True, 'ASJ-pent': False, 'RIC+pent': False, 'RIC not+NaCl': True, 'RIC not+but': True}
[one, dF/F0] (b) q per stimulus: 3 of 6 hold: {'ASJ+NaCl': False, 'ASJ-but': True, 'ASJ-pent': False, 'RIC+pent': False, 'RIC not+NaCl': True, 'RIC not+but': True}
[one, dF/F0] (c) uncorrected p: 3 of 6 hold: {'ASJ+NaCl': False, 'ASJ-but': True, 'ASJ-pent': False, 'RIC+pent': False, 'RIC not+NaCl': True, 'RIC not+but': True}
[one, dF/F0] (a') Benjamini-Hochberg over all tests, a conservative check: 3 of 6 hold: {'ASJ+NaCl': False, 'ASJ-but': True, 'ASJ-pent': False, 'RIC+pent': False, 'RIC not+NaCl': True, 'RIC not+but': True}
[one, min-max] (a) q over all tests: 6 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': True, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}
[one, min-max] (b) q per stimulus: 6 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': True, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}
[one, min-max] (c) uncorrected p: 6 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': True, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}
[one, min-max] (a') Benjamini-Hochberg over all tests, a conservative check: 5 of 6 hold: {'ASJ+NaCl': True, 'ASJ-but': True, 'ASJ-pent': False, 'RIC+pent': True, 'RIC not+NaCl': True, 'RIC not+but': True}

Result: partially reproduced, resting on the readings. Under the main reading, five of six parts hold with q over all tests (reading a): ASJ is excited by NaCl (+0.80, q 0.0004), inhibited by 2-butanone (-0.52, q 0.0004) and by 2,3-pentanedione (-0.25, q 0.040, p 0.082), and RIC is excited by 2,3-pentanedione (+0.29, q 0.0056) and not by 2-butanone, but RIC is also significantly excited by NaCl (+0.17, p 0.033, q 0.021). Readings (b) and (c) give four of six (ASJ's 2,3-pentanedione response misses). With the deposit's selection or none removed, all six parts hold under every FDR reading, because RIC's NaCl response falls to p = 0.056; with both removed, four of six; on dF/F0, three of six. Benjamini-Hochberg gives five of six under the main reading.

fig6-interneurons-downstream¶

Downstream of the sensory neurons many interneurons respond significantly, many with no previously known function, for example AIN, AVH, RIF, RIG and RIR.

A class counts when it responds significantly to at least one of the three stimuli.

In [19]:
FIVE = ["AIN", "AVH", "RIF", "RIG", "RIR"]
show(T, FIVE)
for mode, t in TT.items():
    for key, lab in READINGS.items():
        res = {c: [s[:4] for s in npl.STIMULI if sig(t, c, s, key) and sig(t, c, s, key)[0]] for c in FIVE}
        print(f"[{mode}] {lab}: {sum(bool(v) for v in res.values())} of 5 respond:", res)
                  n_traces  n_animals  mean_diff        t        p    q_all   q_stim
cls stimulus                                                                        
AVH nacl                31         19    0.85212  5.32426  0.00001  0.00004  0.00004
AIN nacl                41         21    0.41730  3.20669  0.00264  0.00344  0.00309
RIR nacl                17         17    0.21230  0.94101  0.36069  0.11865  0.11963
RIG nacl                11          9    0.14383  0.58616  0.57076  0.15925  0.16662
RIF nacl                20         11   -0.17708 -0.89512  0.38192  0.12354  0.12300
AVH butanone            31         19    0.40669  2.60357  0.01421  0.01206  0.01076
AIN butanone            41         21    0.21074  2.12263  0.04003  0.02429  0.02382
RIR butanone            17         17    0.86443  3.28451  0.00467  0.00540  0.00474
RIG butanone            11          9   -0.09540 -0.34168  0.73966  0.18497  0.14703
RIF butanone            20         11   -0.07642 -0.71213  0.48504  0.14716  0.12631
AVH pentanedione        31         19    0.92494  4.82456  0.00004  0.00014  0.00018
AIN pentanedione        41         21    0.14416  1.48531  0.14530  0.06054  0.10710
RIR pentanedione        17         17    1.68121  6.71619  0.00000  0.00002  0.00003
RIG pentanedione        11          9   -0.54415 -3.03571  0.01255  0.01137  0.01936
RIF pentanedione        20         11   -0.34784 -3.13495  0.00545  0.00588  0.01054
[one] (a) q over all tests: 5 of 5 respond: {'AIN': ['nacl', 'buta'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[one] (b) q per stimulus: 5 of 5 respond: {'AIN': ['nacl', 'buta'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[one] (c) uncorrected p: 5 of 5 respond: {'AIN': ['nacl', 'buta'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[one] (a') Benjamini-Hochberg over all tests, a conservative check: 4 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'pent'], 'RIF': ['pent'], 'RIG': [], 'RIR': ['buta', 'pent']}
[deposit] (a) q over all tests: 5 of 5 respond: {'AIN': ['nacl', 'buta'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[deposit] (b) q per stimulus: 5 of 5 respond: {'AIN': ['nacl', 'buta'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[deposit] (c) uncorrected p: 5 of 5 respond: {'AIN': ['nacl', 'buta'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[deposit] (a') Benjamini-Hochberg over all tests, a conservative check: 4 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': [], 'RIR': ['buta', 'pent']}
[both] (a) q over all tests: 5 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[both] (b) q per stimulus: 5 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[both] (c) uncorrected p: 5 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[both] (a') Benjamini-Hochberg over all tests, a conservative check: 4 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'pent'], 'RIF': ['pent'], 'RIG': [], 'RIR': ['buta', 'pent']}
[none] (a) q over all tests: 5 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[none] (b) q per stimulus: 5 of 5 respond: {'AIN': ['nacl', 'buta'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[none] (c) uncorrected p: 5 of 5 respond: {'AIN': ['nacl', 'buta'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[none] (a') Benjamini-Hochberg over all tests, a conservative check: 4 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': [], 'RIR': ['buta', 'pent']}
[one, dF/F0] (a) q over all tests: 5 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta']}
[one, dF/F0] (b) q per stimulus: 5 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta']}
[one, dF/F0] (c) uncorrected p: 5 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta']}
[one, dF/F0] (a') Benjamini-Hochberg over all tests, a conservative check: 3 of 5 respond: {'AIN': [], 'AVH': ['nacl', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': []}
[one, min-max] (a) q over all tests: 5 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[one, min-max] (b) q per stimulus: 5 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[one, min-max] (c) uncorrected p: 5 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}
[one, min-max] (a') Benjamini-Hochberg over all tests, a conservative check: 5 of 5 respond: {'AIN': ['nacl'], 'AVH': ['nacl', 'buta', 'pent'], 'RIF': ['pent'], 'RIG': ['pent'], 'RIR': ['buta', 'pent']}

Result: reproduced. All five classes respond significantly to at least one stimulus under every selection reading and every FDR reading: under the main reading, AIN to NaCl and 2-butanone, AVH to all three, RIF and RIG to 2,3-pentanedione (decreases), RIR to both odors. On dF/F0 the five respond too. Benjamini-Hochberg gives four of five under every selection reading (RIG misses), five under min-max scaling and three on dF/F0.

fig6-tail-responses (blocked)¶

In the tail, PVQ is significantly excited by 2,3-pentanedione, and a mix of sensory and interneurons show significant post-stimulus responses to all three stimuli, including LUA, PVN, PVQ, PVR and PVW.

The dandiset holds no tail recordings (data check above: every file is a head), so the claim cannot be tested on it. The tails are in the paper's Zenodo deposit (Tail_Activity_OH16230.mat).

In [20]:
tail_names = [n for r in recs for n in r["labels"] if n[:3] in ("LUA", "PVN", "PVQ", "PVR", "PVW")]
print("tail neurons named in the dandiset:", tail_names if tail_names else "none",
      "| session descriptions:", sorted({r["session_description"] for r in recs}))
tail neurons named in the dandiset: none | session descriptions: ['C. elegans head NeuroPAL and Calcium imaging']

fig6f-pharyngeal¶

Both salt and odors elicit responses across the pharyngeal nervous system.

Pharyngeal classes from the criterion: I1 to I6, M1 to M5, MC, MI and NSM. Reproduced when at least two respond significantly to NaCl and at least two to one of the odors.

In [21]:
PHAR = ["I1", "I2", "I3", "I4", "I5", "I6", "M1", "M2", "M3", "M4", "M5", "MC", "MI", "NSM"]
print("pharyngeal classes tested:", sorted(set(T.cls) & set(PHAR)), "| not tested:", sorted(set(PHAR) - set(T.cls)))
show(T, PHAR)
for mode, t in TT.items():
    for key, lab in READINGS.items():
        res = {s: [c for c in PHAR if sig(t, c, s, key) and sig(t, c, s, key)[0]] for s in npl.STIMULI}
        print(f"[{mode}] {lab}:", {s: (len(v), v) for s, v in res.items()})
pharyngeal classes tested: ['I1', 'I2', 'I3', 'I4', 'I5', 'I6', 'M1', 'M2', 'M3', 'M4', 'M5', 'MC', 'MI', 'NSM'] | not tested: []
                  n_traces  n_animals  mean_diff        t        p    q_all   q_stim
cls stimulus                                                                        
M1  nacl                20         20    0.10669  1.96166  0.06462  0.03484  0.02992
I5  nacl                17         17    0.06933  0.47295  0.64264  0.16767  0.17702
M5  nacl                21         21    0.10821  1.28716  0.21274  0.07812  0.07786
M2  nacl                42         21    0.11934  2.24518  0.03021  0.01988  0.01865
I4  nacl                20         20   -0.05871 -0.62598  0.53878  0.15662  0.16180
I6  nacl                21         21    0.01952  0.11575  0.90900  0.20659  0.21957
I2  nacl                39         20    0.39842  3.90890  0.00037  0.00068  0.00063
NSM nacl                42         21    0.06078  1.04341  0.30287  0.10288  0.10355
MC  nacl                35         21    0.25755  4.58954  0.00006  0.00018  0.00022
M3  nacl                41         21    0.16858  2.14084  0.03843  0.02378  0.02082
M4  nacl                21         21    0.16300  2.58336  0.01775  0.01356  0.01364
MI  nacl                17         17    0.29184  3.86399  0.00137  0.00192  0.00171
I3  nacl                17         17    0.68431  3.00213  0.00844  0.00879  0.00816
I1  nacl                18         10    0.00320  0.02293  0.98198  0.21850  0.23215
M1  butanone            20         20    0.10684  2.48328  0.02252  0.01592  0.01501
I5  butanone            17         17    0.05927  0.32435  0.74987  0.18525  0.14703
M5  butanone            21         21    0.18559  1.95533  0.06466  0.03484  0.03476
M2  butanone            42         21    0.12626  2.61147  0.01254  0.01137  0.00995
I4  butanone            20         20    0.20140  1.75819  0.09481  0.04294  0.03951
I6  butanone            21         21    0.34721  2.88889  0.00908  0.00920  0.00800
I2  butanone            39         20    0.23049  1.85844  0.07087  0.03691  0.03579
NSM butanone            42         21    0.28666  3.74039  0.00056  0.00093  0.00078
MC  butanone            35         21    0.22767  3.03489  0.00459  0.00540  0.00474
M3  butanone            41         21    0.22630  4.13878  0.00017  0.00038  0.00033
M4  butanone            21         21    0.13871  1.53189  0.14122  0.05923  0.05604
MI  butanone            17         17    0.13888  2.17342  0.04511  0.02660  0.02592
I3  butanone            17         17    0.18742  2.64726  0.01757  0.01356  0.01220
I1  butanone            18         10    0.05963  0.60344  0.55418  0.15793  0.12667
M1  pentanedione        20         20    0.11161  1.77085  0.09263  0.04243  0.08140
I5  pentanedione        17         17    0.03608  0.38653  0.70419  0.17819  0.32415
M5  pentanedione        21         21    0.00143  0.01858  0.98536  0.21850  0.40553
M2  pentanedione        42         21    0.05991  0.70905  0.48231  0.14705  0.26267
I4  pentanedione        20         20   -0.02909 -0.32821  0.74634  0.18525  0.33951
I6  pentanedione        21         21    0.06399  0.64090  0.52887  0.15578  0.28013
I2  pentanedione        39         20    0.32530  2.64399  0.01184  0.01121  0.01936
NSM pentanedione        42         21    0.08518  1.18213  0.24396  0.08609  0.14513
MC  pentanedione        35         21    0.22014  3.30680  0.00223  0.00297  0.00576
M3  pentanedione        41         21    0.02746  0.57301  0.56985  0.15925  0.29379
M4  pentanedione        21         21    0.13537  1.47640  0.15541  0.06348  0.10926
MI  pentanedione        17         17    0.11460  2.58436  0.01996  0.01471  0.02417
I3  pentanedione        17         17    0.06050  1.62312  0.12410  0.05380  0.09793
I1  pentanedione        18         10    0.05738  1.32107  0.20399  0.07680  0.12722
[one] (a) q over all tests: {'nacl': (8, ['I2', 'I3', 'M1', 'M2', 'M3', 'M4', 'MC', 'MI']), 'butanone': (11, ['I2', 'I3', 'I4', 'I6', 'M1', 'M2', 'M3', 'M5', 'MC', 'MI', 'NSM']), 'pentanedione': (4, ['I2', 'M1', 'MC', 'MI'])}
[one] (b) q per stimulus: {'nacl': (8, ['I2', 'I3', 'M1', 'M2', 'M3', 'M4', 'MC', 'MI']), 'butanone': (11, ['I2', 'I3', 'I4', 'I6', 'M1', 'M2', 'M3', 'M5', 'MC', 'MI', 'NSM']), 'pentanedione': (3, ['I2', 'MC', 'MI'])}
[one] (c) uncorrected p: {'nacl': (7, ['I2', 'I3', 'M2', 'M3', 'M4', 'MC', 'MI']), 'butanone': (8, ['I3', 'I6', 'M1', 'M2', 'M3', 'MC', 'MI', 'NSM']), 'pentanedione': (3, ['I2', 'MC', 'MI'])}
[one] (a') Benjamini-Hochberg over all tests, a conservative check: {'nacl': (4, ['I2', 'I3', 'MC', 'MI']), 'butanone': (4, ['I6', 'M3', 'MC', 'NSM']), 'pentanedione': (1, ['MC'])}
[deposit] (a) q over all tests: {'nacl': (7, ['I2', 'I3', 'M2', 'M3', 'M4', 'MC', 'MI']), 'butanone': (8, ['I3', 'I6', 'M1', 'M2', 'M3', 'MC', 'MI', 'NSM']), 'pentanedione': (4, ['I2', 'I3', 'MC', 'MI'])}
[deposit] (b) q per stimulus:
 {'nacl': (7, ['I2', 'I3', 'M2', 'M3', 'M4', 'MC', 'MI']), 'butanone': (8, ['I3', 'I6', 'M1', 'M2', 'M3', 'MC', 'MI', 'NSM']), 'pentanedione': (4, ['I2', 'I3', 'MC', 'MI'])}
[deposit] (c) uncorrected p: {'nacl': (7, ['I2', 'I3', 'M2', 'M3', 'M4', 'MC', 'MI']), 'butanone': (8, ['I3', 'I6', 'M1', 'M2', 'M3', 'MC', 'MI', 'NSM']), 'pentanedione': (4, ['I2', 'I3', 'MC', 'MI'])}
[deposit] (a') Benjamini-Hochberg over all tests, a conservative check: {'nacl': (4, ['I2', 'I3', 'MC', 'MI']), 'butanone': (6, ['I3', 'I6', 'M2', 'M3', 'MC', 'NSM']), 'pentanedione': (1, ['MC'])}
[both] (a) q over all tests: {'nacl': (6, ['I2', 'I3', 'M2', 'M4', 'MC', 'MI']), 'butanone': (7, ['I3', 'I6', 'M1', 'M2', 'M3', 'MC', 'NSM']), 'pentanedione': (2, ['I2', 'MC'])}
[both] (b) q per stimulus: {'nacl': (8, ['I2', 'I3', 'M1', 'M2', 'M3', 'M4', 'MC', 'MI']), 'butanone': (7, ['I3', 'I6', 'M1', 'M2', 'M3', 'MC', 'NSM']), 'pentanedione': (3, ['I2', 'I3', 'MC'])}
[both] (c) uncorrected p: {'nacl': (7, ['I2', 'I3', 'M2', 'M3', 'M4', 'MC', 'MI']), 'butanone': (7, ['I3', 'I6', 'M1', 'M2', 'M3', 'MC', 'NSM']), 'pentanedione': (3, ['I2', 'I3', 'MC'])}
[both] (a') Benjamini-Hochberg over all tests, a conservative check: {'nacl': (3, ['I2', 'MC', 'MI']), 'butanone': (4, ['I6', 'M3', 'MC', 'NSM']), 'pentanedione': (1, ['MC'])}
[none] (a) q over all tests: {'nacl': (6, ['I2', 'I3', 'M2', 'M4', 'MC', 'MI']), 'butanone': (8, ['I3', 'I6', 'M1', 'M2', 'M3', 'MC', 'MI', 'NSM']), 'pentanedione': (3, ['I2', 'I3', 'MC'])}
[none] (b) q per stimulus: {'nacl': (6, ['I2', 'I3', 'M2', 'M4', 'MC', 'MI']), 'butanone': (8, ['I3', 'I6', 'M1', 'M2', 'M3', 'MC', 'MI', 'NSM']), 'pentanedione': (4, ['I2', 'I3', 'MC', 'MI'])}
[none] (c) uncorrected p: {'nacl': (6, ['I2', 'I3', 'M2', 'M4', 'MC', 'MI']), 'butanone': (8, ['I3', 'I6', 'M1', 'M2', 'M3', 'MC', 'MI', 'NSM']), 'pentanedione': (4, ['I2', 'I3', 'MC', 'MI'])}
[none] (a') Benjamini-Hochberg over all tests, a conservative check: {'nacl': (4, ['I2', 'I3', 'MC', 'MI']), 'butanone': (6, ['I3', 'I6', 'M2', 'M3', 'MC', 'NSM']), 'pentanedione': (1, ['MC'])}
[one, dF/F0] (a) q over all tests: {'nacl': (3, ['I2', 'I3', 'MI']), 'butanone': (4, ['I2', 'I3', 'MC', 'NSM']), 'pentanedione': (2, ['I2', 'MI'])}
[one, dF/F0] (b) q per stimulus: {'nacl': (5, ['I2', 'I3', 'M3', 'M4', 'MI']), 'butanone': (4, ['I2', 'I3', 'MC', 'NSM']), 'pentanedione': (2, ['I2', 'MI'])}
[one, dF/F0] (c) uncorrected p: {'nacl': (5, ['I2', 'I3', 'M3', 'M4', 'MI']), 'butanone': (4, ['I2', 'I3', 'MC', 'NSM']), 'pentanedione': (2, ['I2', 'MI'])}
[one, dF/F0] (a') Benjamini-Hochberg over all tests, a conservative check: {'nacl': (3, ['I2', 'I3', 'MI']), 'butanone': (3, ['I3', 'MC', 'NSM']), 'pentanedione': (1, ['I2'])}
[one, min-max] (a) q over all tests: {'nacl': (6, ['I2', 'I3', 'M3', 'M4', 'MC', 'MI']), 'butanone': (8, ['I3', 'I6', 'M1', 'M2', 'M3', 'MC', 'MI', 'NSM']), 'pentanedione': (3, ['I2', 'MC', 'MI'])}
[one, min-max] (b) q per stimulus: {'nacl': (7, ['I2', 'I3', 'M2', 'M3', 'M4', 'MC', 'MI']), 'butanone': (8, ['I3', 'I6', 'M1', 'M2', 'M3', 'MC', 'MI', 'NSM']), 'pentanedione': (5, ['I2', 'I3', 'M1', 'MC', 'MI'])}
[one, min-max] (c) uncorrected p: {'nacl': (6, ['I2', 'I3', 'M3', 'M4', 'MC', 'MI']), 'butanone': (8, ['I3', 'I6', 'M1', 'M2', 'M3', 'MC', 'MI', 'NSM']), 'pentanedione': (3, ['I2', 'MC', 'MI'])}
[one, min-max] (a') Benjamini-Hochberg over all tests, a conservative check: {'nacl': (4, ['I2', 'I3', 'MC', 'MI']), 'butanone': (5, ['I6', 'M3', 'MC', 'MI', 'NSM']), 'pentanedione': (1, ['MC'])}

Result: reproduced. All 14 pharyngeal classes are tested. Under the main reading with q over all tests, 8 classes respond to NaCl, 11 to 2-butanone and 4 to 2,3-pentanedione; every reading, including dF/F0 (3, 4 and 2) and Benjamini-Hochberg (at least 3 for NaCl and 3 for 2-butanone), gives at least two for NaCl and for an odor.

fig6-stimulus-sets-distinct¶

The sets of neurons activated by the three stimuli are partly overlapping but distinct for each stimulus.

Head classes significant for each stimulus; reproduced when each pair of stimuli shares at least one class and no two stimuli have the same set.

In [22]:
for mode, t in TT.items():
    for key, lab in READINGS.items():
        sets = {s: set(t[(t.stimulus == s) & (t[key] <= 0.05)].cls) for s in npl.STIMULI}
        pairs = {f"{a[:4]}&{b[:4]}": (len(sets[a] & sets[b]), sets[a] == sets[b])
                 for i, a in enumerate(npl.STIMULI) for b in npl.STIMULI[i + 1:]}
        print(f"[{mode}] {lab}: sizes", {s[:4]: len(v) for s, v in sets.items()},
              "| shared, identical:", pairs, "| in all three:", len(set.intersection(*sets.values())))
[one] (a) q over all tests: sizes {'nacl': 54, 'buta': 40, 'pent': 47} | shared, identical: {'nacl&buta': (25, False), 'nacl&pent': (28, False), 'buta&pent': (26, False)} | in all three: 18
[one] (b) q per stimulus: sizes {'nacl': 55, 'buta': 40, 'pent': 33} | shared, identical: {'nacl&buta': (26, False), 'nacl&pent': (21, False), 'buta&pent': (19, False)} | in all three: 14
[one] (c) uncorrected p: sizes {'nacl': 44, 'buta': 29, 'pent': 33} | shared, identical: {'nacl&buta': (16, False), 'nacl&pent': (17, False), 'buta&pent': (17, False)} | in all three: 9
[one] (a') Benjamini-Hochberg over all tests, a conservative check: sizes {'nacl': 24, 'buta': 20, 'pent': 20} | shared, identical: {'nacl&buta': (8, False), 'nacl&pent': (6, False), 'buta&pent': (12, False)} | in all three: 5
[deposit] (a) q over all tests: sizes {'nacl': 38, 'buta': 28, 'pent': 40} | shared, identical: {'nacl&buta': (15, False), 'nacl&pent': (20, False), 'buta&pent': (19, False)} | in all three: 11
[deposit] (b) q per stimulus: sizes {'nacl': 39, 'buta': 30, 'pent': 44} | shared, identical: {'nacl&buta': (17, False), 'nacl&pent': (22, False), 'buta&pent': (20, False)} | in all three: 13
[deposit] (c) uncorrected p: sizes {'nacl': 39, 'buta': 29, 'pent': 41} | shared, identical: {'nacl&buta': (16, False), 'nacl&pent': (21, False), 'buta&pent': (19, False)} | in all three: 12
[deposit] (a') Benjamini-Hochberg over all tests, a conservative check: sizes {'nacl': 24, 'buta': 23, 'pent': 30} | shared, identical: {'nacl&buta': (10, False), 'nacl&pent': (10, False), 'buta&pent': (16, False)} | in all three: 8
[both] (a) q over all tests: sizes {'nacl': 38, 'buta': 27, 'pent': 28} | shared, identical: {'nacl&buta': (14, False), 'nacl&pent': (12, False), 'buta&pent': (14, False)} | in all three: 7
[both] (b) q per stimulus: sizes {'nacl': 54, 'buta': 27, 'pent': 33} | shared, identical: {'nacl&buta': (20, False), 'nacl&pent': (22, False), 'buta&pent': (16, False)} | in all three: 12
[both] (c) uncorrected p: sizes {'nacl': 43, 'buta': 29, 'pent': 33} | shared, identical: {'nacl&buta': (16, False), 'nacl&pent': (16, False), 'buta&pent': (16, False)} | in all three: 10
[both] (a') Benjamini-Hochberg over all tests, a conservative check: sizes {'nacl': 23, 'buta': 19, 'pent': 19} | shared, identical: {'nacl&buta': (7, False), 'nacl&pent': (6, False), 'buta&pent': (13, False)} | in all three: 5
[none] (a) q over all tests: sizes {'nacl': 38, 'buta': 28, 'pent': 37} | shared, identical: {'nacl&buta': (14, False), 'nacl&pent': (17, False), 'buta&pent': (18, False)} | in all three: 10
[none] (b) q per stimulus: sizes {'nacl': 41, 'buta': 30, 'pent': 41} | shared, identical: {'nacl&buta': (17, False), 'nacl&pent': (21, False), 'buta&pent': (20, False)} | in all three: 13
[none] (c) uncorrected p: sizes {'nacl': 41, 'buta': 30, 'pent': 40} | shared, identical: {'nacl&buta': (17, False), 'nacl&pent': (21, False), 'buta&pent': (20, False)} | in all three: 13
[none] (a') Benjamini-Hochberg over all tests, a conservative check: sizes {'nacl': 25, 'buta': 22, 'pent': 29} | shared, identical: {'nacl&buta': (10, False), 'nacl&pent': (9, False), 'buta&pent': (14, False)} | in all three: 7
[one, dF/F0] (a) q over all tests: sizes {'nacl': 18, 'buta': 19, 'pent': 24} | shared, identical: {'nacl&buta': (9, False), 'nacl&pent': (10, False), 'buta&pent': (12, False)} | in all three: 7
[one, dF/F0] (b) q per stimulus: sizes {'nacl': 30, 'buta': 18, 'pent': 24} | shared, identical: {'nacl&buta': (10, False), 'nacl&pent': (10, False), 'buta&pent': (11, False)} | in all three: 7
[one, dF/F0] (c) uncorrected p: sizes {'nacl': 30, 'buta': 22, 'pent': 30} | shared, identical: {'nacl&buta': (11, False), 'nacl&pent': (11, False), 'buta&pent': (14, False)} | in all three: 8
[one, dF/F0] (a') Benjamini-Hochberg over all tests, a conservative check: sizes {'nacl': 16, 'buta': 16, 'pent': 18} | shared, identical: {'nacl&buta': (7, False), 'nacl&pent': (8, False), 'buta&pent': (9, False)} | in all three: 5
[one, min-max] (a) q over all tests: sizes {'nacl': 44, 'buta': 29, 'pent': 38} | shared, identical: {'nacl&buta': (15, False), 'nacl&pent': (19, False), 'buta&pent': (19, False)} | in all three: 11
[one, min-max] (b) q per stimulus: sizes {'nacl': 45, 'buta': 28, 'pent': 45} | shared, identical: {'nacl&buta': (16, False), 'nacl&pent': (21, False), 'buta&pent': (22, False)} | in all three: 12
[one, min-max] (c) uncorrected p: sizes {'nacl': 44, 'buta': 29, 'pent': 39} | shared, identical: {'nacl&buta': (15, False), 'nacl&pent': (19, False), 'buta&pent': (19, False)} | in all three: 11
[one, min-max] (a') Benjamini-Hochberg over all tests, a conservative check: sizes {'nacl': 23, 'buta': 21, 'pent': 23} | shared, identical: {'nacl&buta': (10, False), 'nacl&pent': (7, False), 'buta&pent': (12, False)} | in all three: 6

Result: reproduced. Under the main reading with q over all tests, 54 classes respond to NaCl, 40 to 2-butanone and 47 to 2,3-pentanedione; the pairs share 25, 28 and 26, 18 are in all three, and no two sets are equal. Every reading gives the same pattern (on dF/F0, 18, 19 and 24 classes, sharing 9 to 12). With sets of 16 to 55 classes out of 94, a pair sharing none, or two identical sets, is unlikely, so the test is weak.

figs1h-avf-cyclic¶

In many animals the AVF interneurons show robust cyclical activity of about 0.3 Hz for the whole 4 min recording, whatever the stimulus, similar to the crawling frequency of freely moving worms of this strain.

Per the Figure S1H legend: traces median filtered at 0.5 s (2 frames at 4 Hz, MATLAB medfilt1 convention), frequency as the number of peaks per recording. Peaks: local maxima with prominence of at least (a) 1 or (b) 0.5 standard deviations of the filtered trace. Animals: (a) every head with AVF identified; (b) those whose AVF trace has its largest spectral peak (Welch, 256-frame segments) between 0.1 and 1 Hz. The crawling frequency is not in the dandiset.

In [23]:
def avf_rows(d):
    rows = []
    for a in d["animals"]:
        for n in ("AVFL", "AVFR"):
            if n not in a["traces"]:
                continue
            x = a["traces"][n][4:]
            y = npl.medfilt_matlab(x, int(round(0.5 * a["rate"])))
            dur = len(y) / a["rate"]
            f, pxx = signal.welch(y - y.mean(), fs=a["rate"], nperseg=256)
            fpk = f[1:][np.argmax(pxx[1:])]
            row = {"animal": a["id"], "neuron": n, "duration_s": dur, "spectral_peak_Hz": fpk,
                   "spectral_ok": 0.1 <= fpk <= 1.0}
            for k in (1.0, 0.5):
                pk, _ = signal.find_peaks(y, prominence=k * y.std())
                row[f"freq_{k}sd"] = len(pk) / dur
            rows.append(row)
    return pd.DataFrame(rows)

for mode in MODES:
    av = avf_rows(data[mode])
    print(f"[{mode}] AVF traces: {len(av)} from {av.animal.nunique()} heads; spectral peak in 0.1-1 Hz: {int(av.spectral_ok.sum())}")
    for k in ("1.0", "0.5"):
        col = f"freq_{k}sd"
        print(f"[{mode}] peaks at {k} SD: median over all {av[col].median():.3f} Hz "
              f"(range {av[col].min():.3f} to {av[col].max():.3f});"
              f" over spectral-peak traces {av[av.spectral_ok][col].median():.3f} Hz (n={int(av.spectral_ok.sum())})")
    print(f"[{mode}] median spectral peak {av.spectral_peak_Hz.median():.3f} Hz")
av = avf_rows(D)
print(av.round(3).to_string())
fig, axs = plt.subplots(5, 1, figsize=(9, 8), sharex=True)
for ax, a in zip(axs, [a for a in D["animals"] if "AVFL" in a["traces"]][:5]):
    for n, col in (("AVFL", "tab:blue"), ("AVFR", "tab:red")):
        if n in a["traces"]:
            x = a["traces"][n][4:]
            ax.plot(np.arange(len(x)) / 4 + 1, npl.medfilt_matlab(x, 2), color=col, lw=0.7)
    ax.set_ylabel(a["id"][-7:], fontsize=7)
axs[-1].set_xlabel("time (s)")
plt.tight_layout()
plt.show()
[one] AVF traces: 32 from 19 heads; spectral peak in 0.1-1 Hz: 3
[one] peaks at 1.0 SD: median over all 0.121 Hz (range 0.046 to 0.268); over spectral-peak traces 0.172 Hz (n=3)
[one] peaks at 0.5 SD: median over all 0.199 Hz (range 0.104 to 0.301); over spectral-peak traces 0.180 Hz (n=3)
[one] median spectral peak 0.016 Hz
[deposit] AVF traces: 31 from 18 heads; spectral peak in 0.1-1 Hz: 3
[deposit] peaks at 1.0 SD: median over all 0.125 Hz (range 0.046 to 0.268); over spectral-peak traces 0.172 Hz (n=3)
[deposit] peaks at 0.5 SD: median over all 0.205 Hz (range 0.109 to 0.301); over spectral-peak traces 0.180 Hz (n=3)
[deposit] median spectral peak 0.016 Hz
[both] AVF traces: 29 from 18 heads; spectral peak in 0.1-1 Hz: 3
[both] peaks at 1.0 SD: median over all 0.121 Hz (range 0.046 to 0.268); over spectral-peak traces 0.172 Hz (n=3)
[both] peaks at 0.5 SD: median over all 0.192 Hz (range 0.104 to 0.301); over spectral-peak traces 0.180 Hz (n=3)
[both] median spectral peak 0.016 Hz
[none] AVF traces: 33 from 19 heads; spectral peak in 0.1-1 Hz: 3
[none] peaks at 1.0 SD: median over all 0.121 Hz (range 0.046 to 0.268); over spectral-peak traces 0.172 Hz (n=3)
[none] peaks at 0.5 SD: median over all 0.201 Hz (range 0.104 to 0.301); over spectral-peak traces 0.180 Hz (n=3)
[none] median spectral peak 0.016 Hz
         animal neuron  duration_s  spectral_peak_Hz  spectral_ok  freq_1.0sd  freq_0.5sd
0   20190924_01   AVFL      239.00             0.188         True       0.172       0.180
1   20190924_01   AVFR      239.00             0.016        False       0.138       0.197
2   20190924_02   AVFL      239.00             0.016        False       0.176       0.226
3   20190924_02   AVFR      239.00             0.031        False       0.146       0.201
4   20190924_03   AVFL      239.25             0.016        False       0.205       0.247
5   20190924_03   AVFR      239.25             0.016        False       0.067       0.104
6   20190925_01   AVFL      239.25             0.016        False       0.109       0.238
7   20190925_01   AVFR      239.25             0.016        False       0.125       0.247
8   20190925_04   AVFL      238.75             0.016        False       0.121       0.138
9   20190925_04   AVFR      238.75             0.016        False       0.117       0.184
10  20190928_03   AVFR      239.00             0.250         True       0.268       0.289
11  20190928_05   AVFL      239.00             0.031        False       0.142       0.222
12  20190928_07   AVFL      238.50             0.016        False       0.164       0.252
13  20190928_07   AVFR      238.50             0.016        False       0.151       0.226
14  20190928_08   AVFL      238.75             0.234         True       0.113       0.155
15  20190928_08   AVFR      238.75             0.016        False       0.109       0.184
16  20190928_11   AVFL      239.25             0.016        False       0.113       0.184
17  20190928_11   AVFR      239.25             0.016        False       0.117       0.142
18  20190928_13   AVFL      239.00             0.016        False       0.155       0.301
19  20190928_13   AVFR      239.00             0.016        False       0.050       0.146
20  20190929_03   AVFL      239.00             0.016        False       0.079       0.163
21  20190929_03   AVFR      239.00             0.016        False       0.121       0.205
22  20190929_05   AVFL      239.00             0.016        False       0.105       0.167
23  20190929_06   AVFL      239.00             0.016        False       0.046       0.109
24  20190929_06   AVFR      239.00             0.016        False       0.142       0.192
25  20190929_07   AVFR      232.75             0.016        False       0.052       0.133
26  20191030_03   AVFL      239.00             0.016        False       0.121       0.238
27  20191030_07   AVFL      238.75             0.016        False       0.071       0.239
28  20191030_07   AVFR      238.75             0.016        False       0.230       0.272
29  20191104_08   AVFL      239.00             0.016        False       0.084       0.184
30  20191104_10   AVFL      239.00             0.016        False       0.180       0.280
31  20191104_10   AVFR      239.00             0.016        False       0.205       0.276
No description has been provided for this image

Result: not reproduced. Under the main reading, 32 AVF traces from 19 heads give a median of 0.121 Hz with peaks of 1 SD prominence (0.046 to 0.268 Hz) and 0.199 Hz with 0.5 SD (0.104 to 0.301 Hz), against 0.27 to 0.33 Hz. Only 3 traces have their largest spectral peak between 0.1 and 1 Hz (median 0.172 and 0.180 Hz); the median spectral peak is 0.016 Hz, the lowest resolved frequency. The other selection readings give medians of 0.121 to 0.125 Hz and 0.192 to 0.205 Hz. Per-trace scaling does not change these measures.

fig6gi-stimulus-specific-correlations¶

Pairwise correlations between neurons are similar across individuals for a stimulus, but each stimulus generates its own brainwide correlation pattern, even the two attractive odors.

Per the Figure 6 legend, Pearson correlations between head neurons in each animal over the 30 s following stimulus onset. Readings: (a) for each pair of animals, the correlation between their correlation matrices over shared neuron pairs, same stimulus against different stimuli, one-sided Wilcoxon signed-rank test over animal pairs; (b) 100 random splits of the animals into halves, comparing the correlation of the halves' mean matrices for the same stimulus with that for different stimuli.

In [24]:
def corr_mats(d, window=30.0):
    allnames = sorted({n for a in d["animals"] for n in a["traces"]})
    ix = {n: i for i, n in enumerate(allnames)}
    M = {}
    for a in d["animals"]:
        names = sorted(a["traces"])
        X = np.array([a["traces"][n] for n in names]).T
        t = np.arange(len(X)) / a["rate"]
        for s in npl.STIMULI:
            s0 = a["stimuli"][s][0]
            C = np.corrcoef(X[(t >= s0) & (t < s0 + window)].T)
            full = np.full((len(allnames), len(allnames)), np.nan)
            ii = [ix[n] for n in names]
            full[np.ix_(ii, ii)] = C
            M[(a["id"], s)] = full
    return M, allnames

def mat_sim(A, B):
    iu = np.triu_indices_from(A, 1)
    a, b = A[iu], B[iu]
    ok = np.isfinite(a) & np.isfinite(b)
    return np.corrcoef(a[ok], b[ok])[0, 1]

SPAIRS = [("nacl", "butanone"), ("nacl", "pentanedione"), ("butanone", "pentanedione")]
for mode in MODES:
    M, allnames = corr_mats(data[mode])
    ids = [a["id"] for a in data[mode]["animals"]]
    sims = {}
    for i in range(len(ids)):
        for j in range(i + 1, len(ids)):
            for x in npl.STIMULI:
                for y in npl.STIMULI:
                    sims[(i, j, x, y)] = mat_sim(M[(ids[i], x)], M[(ids[j], y)])
    for x, y in SPAIRS:
        same = np.array([(sims[(i, j, x, x)] + sims[(i, j, y, y)]) / 2 for i in range(len(ids)) for j in range(i + 1, len(ids))])
        diff = np.array([(sims[(i, j, x, y)] + sims[(i, j, y, x)]) / 2 for i in range(len(ids)) for j in range(i + 1, len(ids))])
        w = stats.wilcoxon(same, diff, alternative="greater")
        print(f"[{mode}] (a) {x[:4]}/{y[:4]}: same {np.mean(same):.4f}, different {np.mean(diff):.4f}, "
              f"same > different in {np.sum(same > diff)} of {len(same)} animal pairs, Wilcoxon p={w.pvalue:.3g}")
    print(f"[{mode}] (a) mean similarity between animals, same stimulus:",
          {x[:4]: round(np.mean([sims[(i, j, x, x)] for i in range(len(ids)) for j in range(i + 1, len(ids))]), 4) for x in npl.STIMULI})
    # (b) random halves
    rng = np.random.default_rng(611)
    wins = collections.Counter()
    gaps = collections.defaultdict(list)
    for rep in range(100):
        perm = rng.permutation(len(ids))
        h1, h2 = perm[: len(ids) // 2], perm[len(ids) // 2:]
        with np.errstate(all="ignore"):
            mean1 = {s: np.nanmean([M[(ids[k], s)] for k in h1], axis=0) for s in npl.STIMULI}
            mean2 = {s: np.nanmean([M[(ids[k], s)] for k in h2], axis=0) for s in npl.STIMULI}
        for x, y in SPAIRS:
            same = (mat_sim(mean1[x], mean2[x]) + mat_sim(mean1[y], mean2[y])) / 2
            diff = (mat_sim(mean1[x], mean2[y]) + mat_sim(mean1[y], mean2[x])) / 2
            wins[(x, y)] += same > diff
            gaps[(x, y)].append(same - diff)
    for x, y in SPAIRS:
        print(f"[{mode}] (b) {x[:4]}/{y[:4]}: same > different in {wins[(x, y)]} of 100 splits, "
              f"mean gap {np.mean(gaps[(x, y)]):.4f} (range {np.min(gaps[(x, y)]):.4f} to {np.max(gaps[(x, y)]):.4f})")
[one] (a) nacl/buta: same 0.0558, different 0.0481, same > different in 126 of 210 animal pairs, Wilcoxon p=0.000634
[one] (a) nacl/pent: same 0.0487, different 0.0410, same > different in 131 of 210 animal pairs, Wilcoxon p=0.000138
[one] (a) buta/pent: same 0.0497, different 0.0467, same > different in 107 of 210 animal pairs, Wilcoxon p=0.172
[one] (a) mean similarity between animals, same stimulus: {'nacl': np.float64(0.0548), 'buta': np.float64(0.0567), 'pent': np.float64(0.0427)}
[one] (b) nacl/buta: same > different in 91 of 100 splits, mean gap 0.0222 (range -0.0217 to 0.0479)
[one] (b) nacl/pent: same > different in 96 of 100 splits, mean gap 0.0288 (range -0.0128 to 0.0564)
[one] (b) buta/pent: same > different in 95 of 100 splits, mean gap 0.0212 (range -0.0077 to 0.0526)
[deposit] (a) nacl/buta: same 0.0671, different 0.0614, same > different in 116 of 210 animal pairs, Wilcoxon p=0.00783
[deposit] (a) nacl/pent: same 0.0579, different 0.0514, same > different in 130 of 210 animal pairs, Wilcoxon p=0.00177
[deposit] (a) buta/pent: same 0.0596, different 0.0573, same > different in 102 of 210 animal pairs, Wilcoxon p=0.269
[deposit] (a) mean similarity between animals, same stimulus: {'nacl': np.float64(0.0654), 'buta': np.float64(0.0688), 'pent': np.float64(0.0505)}
[deposit] (b) nacl/buta: same > different in 50 of 100 splits, mean gap -0.0009 (range -0.0481 to 0.0575)
[deposit] (b) nacl/pent: same > different in 71 of 100 splits, mean gap 0.0087 (range -0.0377 to 0.0440)
[deposit] (b) buta/pent: same > different in 16 of 100 splits, mean gap -0.0084 (range -0.0329 to 0.0279)
[both] (a) nacl/buta: same 0.0507, different 0.0402, same > different in 131 of 210 animal pairs, Wilcoxon p=1.24e-05
[both] (a) nacl/pent: same 0.0452, different 0.0357, same > different in 128 of 210 animal pairs, Wilcoxon p=2.61e-05
[both] (a) buta/pent: same 0.0463, different 0.0443, same > different in 101 of 210 animal pairs, Wilcoxon p=0.337
[both] (a) mean similarity between animals, same stimulus: {'nacl': np.float64(0.0496), 'buta': np.float64(0.0519), 'pent': np.float64(0.0408)}
[both] (b) nacl/buta: same > different in 98 of 100 splits, mean gap 0.0299 (range -0.0094 to 0.0617)
[both] (b) nacl/pent: same > different in 98 of 100 splits, mean gap 0.0382 (range -0.0167 to 0.0677)
[both] (b) buta/pent: same > different in 99 of 100 splits, mean gap 0.0249 (range -0.0012 to 0.0529)
[none] (a) nacl/buta: same 0.0627, different 0.0573, same > different in 123 of 210 animal pairs, Wilcoxon p=0.00762
[none] (a) nacl/pent: same 0.0535, different 0.0473, same > different in 130 of 210 animal pairs, Wilcoxon p=0.00135
[none] (a) buta/pent: same 0.0553, different 0.0531, same > different in 110 of 210 animal pairs, Wilcoxon p=0.196
[none] (a) mean similarity between animals, same stimulus: {'nacl': np.float64(0.0608), 'buta': np.float64(0.0645), 'pent': np.float64(0.0462)}
[none] (b) nacl/buta: same > different in 56 of 100 splits, mean gap 0.0075 (range -0.0508 to 0.0663)
[none] (b) nacl/pent: same > different in 82 of 100 splits, mean gap 0.0175 (range -0.0315 to 0.0546)
[none] (b) buta/pent: same > different in 71 of 100 splits, mean gap 0.0103 (range -0.0277 to 0.0506)

The same comparison with a window without stimulus (30 s starting 30 s after each stimulus onset, ending before the next stimulus), on the main reading, is a null: if same-window similarity is higher there too, part of the effect comes from the time in the recording or the order of the stimuli, not the stimulus itself. The window runs from 20 to 50 s after each 10 s pulse ends, a period in which the paper reports post-stimulus responses, so it does not separate the time in the recording from lingering effects of the stimulus.

In [25]:
def shifted_mats(d, offset=30.0, window=30.0):
    allnames = sorted({n for a in d["animals"] for n in a["traces"]})
    ix = {n: i for i, n in enumerate(allnames)}
    M = {}
    for a in d["animals"]:
        names = sorted(a["traces"])
        X = np.array([a["traces"][n] for n in names]).T
        t = np.arange(len(X)) / a["rate"]
        for s in npl.STIMULI:
            s0 = a["stimuli"][s][0] + offset
            C = np.corrcoef(X[(t >= s0) & (t < s0 + window)].T)
            full = np.full((len(allnames), len(allnames)), np.nan)
            ii = [ix[n] for n in names]
            full[np.ix_(ii, ii)] = C
            M[(a["id"], s)] = full
    return M

Mn = shifted_mats(D)
ids = [a["id"] for a in D["animals"]]
for x, y in SPAIRS:
    same, diff = [], []
    for i in range(len(ids)):
        for j in range(i + 1, len(ids)):
            same.append((mat_sim(Mn[(ids[i], x)], Mn[(ids[j], x)]) + mat_sim(Mn[(ids[i], y)], Mn[(ids[j], y)])) / 2)
            diff.append((mat_sim(Mn[(ids[i], x)], Mn[(ids[j], y)]) + mat_sim(Mn[(ids[i], y)], Mn[(ids[j], x)])) / 2)
    same, diff = np.array(same), np.array(diff)
    print(f"[one] null window {x[:4]}/{y[:4]}: same {same.mean():.4f}, different {diff.mean():.4f}, "
          f"Wilcoxon p={stats.wilcoxon(same, diff, alternative='greater').pvalue:.3g}")
rng = np.random.default_rng(612)
wins = collections.Counter()
for rep in range(100):
    perm = rng.permutation(len(ids))
    h1, h2 = perm[: len(ids) // 2], perm[len(ids) // 2:]
    with np.errstate(all="ignore"):
        m1 = {s: np.nanmean([Mn[(ids[k], s)] for k in h1], axis=0) for s in npl.STIMULI}
        m2 = {s: np.nanmean([Mn[(ids[k], s)] for k in h2], axis=0) for s in npl.STIMULI}
    for x, y in SPAIRS:
        wins[(x, y)] += (mat_sim(m1[x], m2[x]) + mat_sim(m1[y], m2[y])) / 2 > (mat_sim(m1[x], m2[y]) + mat_sim(m1[y], m2[x])) / 2
print("[one] null window (b): same > different in", {f"{x[:4]}/{y[:4]}": wins[(x, y)] for x, y in SPAIRS}, "of 100 splits")
[one] null window nacl/buta: same 0.0548, different 0.0552, Wilcoxon p=0.531
[one] null window nacl/pent: same 0.0592, different 0.0502, Wilcoxon p=7.49e-06
[one] null window buta/pent: same 0.0575, different 0.0535, Wilcoxon p=0.0134
[one] null window (b): same > different in {'nacl/buta': np.int64(40), 'nacl/pent': np.int64(100), 'buta/pent': np.int64(100)} of 100 splits

Result: partially reproduced, resting on the selection reading; the null window shows a similar pattern. Under the main reading, reading (a) gives NaCl against 2-butanone p = 6.3e-04 and against 2,3-pentanedione p = 1.4e-04, but not the two odors (p = 0.17), which is the partial outcome. Reading (b) gives 91, 96 and 95 of 100 splits, short of 95 for NaCl against 2-butanone. The deposit and none readings give the partial outcome through reading (a) too (odors p = 0.27 and 0.20), with reading (b) far from 95 (50, 71, 16 and 56, 82, 71). Only with both members of near-duplicates removed does reading (b) pass all three pairs (98, 98 and 99), which was the outcome of the first round of this attempt. The null window, with no stimulus, also gives 100 of 100 splits for NaCl against 2,3-pentanedione and for the two odors under reading (b), and Wilcoxon p = 7.5e-06 and 0.013 under reading (a), so differences between the three periods of the recording, not the stimuli alone, may drive part of the effect. As the null window falls 20 to 50 s after each pulse ends, when the paper reports post-stimulus responses, lingering stimulus effects may also explain it. Similarities between animals are small (mean 0.043 to 0.057 under the main reading). Per-trace scaling does not change Pearson correlations.

figs2a-pca-trajectories¶

The average trajectories of whole-brain activity in the space of its first three principal components, over the 10 s stimulus and 20 s after, show similarities but remain distinguishable between the three stimuli.

Classes present in all 21 heads, traces averaged per class and head: z-scored traces in the main reading, dF/F0 and dF/F0 scaled per class as variants. PCA (a) on the whole recordings of all heads, (b) on the stimulus windows only. Trajectories are the 30 s from stimulus onset projected on the first three components, averaged over the 21 heads. Two trajectories are distinguishable when their mean distance exceeds twice the mean standard error (the norm of the per-component SEM, averaged over time and the two stimuli).

In [26]:
def class_traces(d, norm="zscore"):
    out = {}
    for a in d["animals"]:
        g = collections.defaultdict(list)
        for n, tr in a["traces"].items():
            g[cls_of[n]].append(npl.normalise(tr, norm))
        out[a["id"]] = {c: np.mean(v, axis=0) for c, v in g.items()}
    return out

def class_scale(CT):
    """Each class's traces divided by the SD of that class's values pooled over animals."""
    sd = collections.defaultdict(list)
    for d_ in CT.values():
        for c, v in d_.items():
            sd[c].append(v[4:])
    sd = {c: np.nanstd(np.concatenate(v)) for c, v in sd.items()}
    return {k: {c: v / sd[c] for c, v in d_.items()} for k, d_ in CT.items()}

for mode, norm in [(m, "z-scored") for m in MODES] + [("one", "dF/F0"), ("one", "class-scaled dF/F0"),
                                                     ("none", "dF/F0"), ("none", "class-scaled dF/F0")]:
    CT = class_traces(data[mode], "zscore" if norm == "z-scored" else "raw")
    if norm == "class-scaled dF/F0":
        CT = class_scale(CT)
    common = sorted(set.intersection(*[set(v) for v in CT.values()]))
    nfr = min(len(next(iter(v.values()))) for v in CT.values())
    A = {k: np.array([v[c][:nfr] for c in common]).T for k, v in CT.items()}
    stim_win = {}
    for a in data[mode]["animals"]:
        t = np.arange(nfr) / a["rate"]
        for s in npl.STIMULI:
            s0 = a["stimuli"][s][0]
            stim_win[(a["id"], s)] = A[a["id"]][(t >= s0) & (t < s0 + 30)][:120]
    for reading in ("a", "b"):
        if reading == "a":
            X = np.vstack([A[k][4:] for k in A])
            X = X[np.all(np.isfinite(X), axis=1)]
        else:
            X = np.vstack(list(stim_win.values()))
            X = X[np.all(np.isfinite(X), axis=1)]
        mu = X.mean(0)
        U, S, Vt = np.linalg.svd(X - mu, full_matrices=False)
        ev = S ** 2 / np.sum(S ** 2)
        P = {k: (w - mu) @ Vt[:3].T for k, w in stim_win.items()}
        mean = {s: np.mean([P[(a, s)] for a in A], axis=0) for s in npl.STIMULI}
        sem = {s: np.linalg.norm(stats.sem([P[(a, s)] for a in A], axis=0), axis=1) for s in npl.STIMULI}
        res = []
        for x, y in SPAIRS:
            dist = np.linalg.norm(mean[x] - mean[y], axis=1).mean()
            thr = 2 * np.mean([sem[x].mean(), sem[y].mean()])
            res.append(f"{x[:4]}/{y[:4]} dist {dist:.3f} vs 2xSEM {thr:.3f} -> {'distinguishable' if dist > thr else 'not'}")
        top = [common[i] for i in np.argsort(-np.abs(Vt[0]))[:3]]
        print(f"[{mode}, {norm}] ({reading}) {len(common)} classes, variance explained by PC1-3: {np.round(ev[:3], 3)}, "
              f"largest PC1 loadings {top}; " + "; ".join(res))
[one, z-scored] (a) 48 classes, variance explained by PC1-3: [0.276 0.114 0.046], largest PC1 loadings ['NSM', 'URAD', 'M3']; nacl/buta dist 1.049 vs 2xSEM 1.479 -> not; nacl/pent dist 1.701 vs 2xSEM 1.328 -> distinguishable; buta/pent dist 1.339 vs 2xSEM 1.445 -> not
[one, z-scored] (b) 48 classes, variance explained by PC1-3: [0.173 0.121 0.067], largest PC1 loadings ['AIB', 'AFD', 'URYV']; nacl/buta dist 1.067 vs 2xSEM 1.473 -> not; nacl/pent dist 1.724 vs 2xSEM 1.316 -> distinguishable; buta/pent dist 1.302 vs 2xSEM 1.432 -> not
[deposit, z-scored] (a) 71 classes, variance explained by PC1-3: [0.278 0.164 0.045], largest PC1 loadings ['URB', 'ASK', 'OLL']; nacl/buta dist 1.314 vs 2xSEM 1.941 -> not; nacl/pent dist 2.143 vs 2xSEM 1.745 -> distinguishable; buta/pent dist 1.869 vs 2xSEM 1.845 -> distinguishable
[deposit, z-scored] (b) 71 classes, variance explained by PC1-3: [0.209 0.143 0.07 ], largest PC1 loadings ['AVA', 'AVE', 'RMDV']; nacl/buta dist 1.385 vs 2xSEM 1.916 -> not; nacl/pent dist 2.730 vs 2xSEM 1.718 -> distinguishable; buta/pent dist 2.175 vs 2xSEM 1.816 -> distinguishable
[both, z-scored] (a) 29 classes, variance explained by PC1-3: [0.306 0.09  0.056], largest PC1 loadings ['NSM', 'M3', 'AUA']; nacl/buta dist 0.787 vs 2xSEM 1.155 -> not; nacl/pent dist 1.364 vs 2xSEM 1.072 -> distinguishable; buta/pent dist 0.958 vs 2xSEM 1.163 -> not
[both, z-scored] (b) 29 classes, variance explained by PC1-3: [0.203 0.102 0.096], largest PC1 loadings ['M5', 'AUA', 'NSM']; nacl/buta dist 1.059 vs 2xSEM 1.156 -> not; nacl/pent dist 1.600 vs 2xSEM 1.066 -> distinguishable; buta/pent dist 0.902 vs 2xSEM 1.160 -> not
[none, z-scored] (a) 81 classes, variance explained by PC1-3: [0.274 0.159 0.042], largest PC1 loadings ['URB', 'ASK', 'RIP']; nacl/buta dist 1.337 vs 2xSEM 2.049 -> not; nacl/pent dist 2.211 vs 2xSEM 1.834 -> distinguishable; buta/pent dist 1.942 vs 2xSEM 1.951 -> not
[none, z-scored] (b) 81 classes, variance explained by PC1-3: [0.199 0.148 0.062], largest PC1 loadings ['AVA', 'AVE', 'RMDV']; nacl/buta dist 1.397 vs 2xSEM 2.024 -> not; nacl/pent dist 2.716 vs 2xSEM 1.811 -> distinguishable; buta/pent dist 2.246 vs 2xSEM 1.921 -> distinguishable
[one, dF/F0] (a) 48 classes, variance explained by PC1-3: [0.757 0.054 0.036], largest PC1 loadings ['RIM', 'ASJ', 'RID']; nacl/buta dist 2.220 vs 2xSEM 2.562 -> not; nacl/pent dist 2.697 vs 2xSEM 2.725 -> not; buta/pent dist 1.117 vs 2xSEM 1.440 -> not
[one, dF/F0] (b) 48 classes, variance explained by PC1-3: [0.751 0.056 0.031], largest PC1 loadings ['RIM', 'AIY', 'ASJ']; nacl/buta dist 2.234 vs 2xSEM 2.567 -> not; nacl/pent dist 2.794 vs 2xSEM 2.747 -> distinguishable; buta/pent dist 1.145 vs 2xSEM 1.458 -> not
[one, class-scaled dF/F0] (a) 48 classes, variance explained by PC1-3: [0.176 0.117 0.094], largest PC1 loadings ['URAV', 'M3', 'NSM']; nacl/buta dist 1.609 vs 2xSEM 1.868 -> not; nacl/pent dist 1.404 vs 2xSEM 1.597 -> not; buta/pent dist 1.229 vs 2xSEM 1.474 -> not
[one, class-scaled dF/F0] (b) 48 classes, variance explained by PC1-3: [0.173 0.109 0.09 ], largest PC1 loadings ['CEPV', 'IL1V', 'AIY']; nacl/buta dist 1.651 vs 2xSEM 1.894 -> not; nacl/pent dist 1.632 vs 2xSEM 1.712 -> not; buta/pent dist 1.265 vs 2xSEM 1.581 -> not
[none, dF/F0] (a) 81 classes, variance explained by PC1-3: [0.493 0.176 0.069], largest PC1 loadings ['SIBV', 'RIM', 'RMEV']; nacl/buta dist 2.162 vs 2xSEM 2.887 -> not; nacl/pent dist 2.692 vs 2xSEM 2.851 -> not; buta/pent dist 2.550 vs 2xSEM 2.200 -> distinguishable
[none, dF/F0] (b) 81 classes, variance explained by PC1-3: [0.464 0.215 0.068], largest PC1 loadings ['SIBV', 'RIM', 'RMEV']; nacl/buta dist 2.194 vs 2xSEM 2.909 -> not; nacl/pent dist 2.809 vs 2xSEM 2.905 -> not; buta/pent dist 2.605 vs 2xSEM 2.237 -> distinguishable
[none, class-scaled dF/F0] (a) 81 classes, variance explained by PC1-3: [0.178 0.151 0.084], largest PC1 loadings ['M3', 'RIP', 'URB']; nacl/buta dist 1.914 vs 2xSEM 2.440 -> not; nacl/pent dist 2.045 vs 2xSEM 2.106 -> not; buta/pent dist 2.290 vs 2xSEM 2.104 -> distinguishable
[none, class-scaled dF/F0] (b) 81 classes, variance explained by PC1-3: [0.171 0.148 0.082], largest PC1 loadings ['URYV', 'RMDV', 'CEPV']; nacl/buta dist 2.053 vs 2xSEM 2.462 -> not; nacl/pent dist 2.358 vs 2xSEM 2.234 -> distinguishable; buta/pent dist 2.360 vs 2xSEM 2.193 -> distinguishable

Result: not reproduced. Under the main reading, 48 classes are present in all 21 heads. On z-scored traces, both PCA readings separate only NaCl from 2,3-pentanedione (1.70 against 1.33 under reading a); NaCl and 2-butanone (1.05 against 1.48) and the two odors (1.34 against 1.45) are not separated. The deposit and none readings separate one or two pairs, never NaCl from 2-butanone; dF/F0, where a few classes dominate PC1 (0.76 of the variance under the main reading), separates at most one pair, and dF/F0 scaled per class at most two. No reading separates all three pairs.

link-connectome-names¶

The dandiset's neuron names can be matched to the cell names of the Cook et al. (2019) hermaphrodite connectome.

Names are matched as written, after removing the zero padding of motor neuron numbers in Cook et al. (VA01 is VA1). Reproduced when every name that denotes a neuron matches.

In [27]:
chem, gap = npl.load_cook()
print("Cook et al. synapse-count matrices: chemical", chem.shape, "total", chem.values.sum(), "| gap junction", gap.shape,
      "total", gap.values.sum(), "| gap matrix symmetric:", bool(np.allclose(gap.values, gap.values.T)))
dnames = sorted({n for r in recs for n in r["labels"] if n})
unmatched = [n for n in dnames if n not in chem.index or n not in gap.index]
print("dandiset names:", len(dnames), "| matched:", len(dnames) - len(unmatched), "| unmatched:", unmatched)
Cook et al. synapse-count matrices: chemical (468, 468) total 11760.0 | gap junction (466, 466) total 4094.0 | gap matrix symmetric: True
dandiset names: 193 | matched: 190 | unmatched: ['AMSOL', 'AMSOR', 'HMC']

Result: reproduced. 190 of the 193 dandiset names match a cell of the Cook et al. synapse-count matrices after the zero padding of motor neuron numbers is removed (VA01 against VA1). The three that do not match (AMSOL, AMSOR, HMC) are not neurons.

fig6jk-connectome-correlation¶

Functional correlations correspond little to the anatomical connectome; the R2 of the Pearson correlation between absolute functional correlations and synapse counts is 2.8% (head) and 1.7% (tail) for electrical synapses, and 0.5% (head) and 1.5% (tail) for chemical synapses.

Head only; the dandiset has no tails. Readings: functional matrix (a) per-animal correlation over the whole recording, averaged over animals, (b) per-animal pairs pooled; neuron pairs (a) all off-diagonal pairs of identified neurons, zero where no synapse, (b) only pairs with at least one synapse; chemical counts (a) summed over both directions, (b) the larger direction. Gap junction counts are symmetric in the source. Expected head values: electrical 2.8% (2.52 to 3.08), chemical 0.5% (0.45 to 0.55).

In [28]:
def animal_corr(d, which="all"):
    """Per-animal correlation matrices on a common name index; which = all, stim or nonstim frames."""
    names = sorted({n for a in d["animals"] for n in a["traces"]} & set(chem.index) & set(gap.index))
    ix = {n: i for i, n in enumerate(names)}
    mats = []
    for a in d["animals"]:
        nm = sorted(set(a["traces"]) & set(ix))
        X = np.array([a["traces"][n] for n in nm]).T[4:]
        t = (np.arange(len(X)) + 4) / a["rate"]
        st = np.zeros(len(X), bool)
        for s0, s1 in a["stimuli"].values():
            st |= (t >= s0) & (t < s1)
        if which == "stim":
            X = X[st]
        elif which == "nonstim":
            X = X[~st]
        full = np.full((len(names), len(names)), np.nan)
        ii = [ix[n] for n in nm]
        full[np.ix_(ii, ii)] = np.corrcoef(X.T)
        mats.append(full)
    return names, np.array(mats)

def r2_table(names, mats, chem_m, gap_m, corr="pearson", logc=False, min_animals=0):
    iu = np.triu_indices(len(names), 1)
    G = gap_m.loc[names, names].values
    Cm = chem_m.loc[names, names].values
    syn = {"electrical": G[iu], "chemical (a) sum": (Cm + Cm.T)[iu], "chemical (b) max": np.maximum(Cm, Cm.T)[iu]}
    present = np.isfinite(mats[:, iu[0], iu[1]])
    navail = present.sum(0)
    with np.errstate(all="ignore"):
        fa = np.abs(np.nanmean(mats[:, iu[0], iu[1]], axis=0))
    out = {}
    for kind, cnt in syn.items():
        cnt = np.log1p(cnt) if logc else cnt
        for fread in ("a", "b"):
            for pread in ("a", "b"):
                keep = (navail > min_animals) & (navail > 0)
                if pread == "b":
                    keep &= cnt > 0
                if fread == "a":
                    f, c = fa[keep], cnt[keep]
                else:
                    sel = present[:, keep]
                    f = np.abs(mats[:, iu[0][keep], iu[1][keep]][sel])
                    c = np.broadcast_to(cnt[keep], sel.shape)[sel]
                r = stats.pearsonr(f, c)[0] if corr == "pearson" else stats.spearmanr(f, c)[0]
                out[(kind, f"functional ({fread})", f"pairs ({pread})")] = (100 * r ** 2, len(f))
    return out

cn_names, cn_mats = animal_corr(D, "all")
print("neurons in the comparison:", len(cn_names))
base = r2_table(cn_names, cn_mats, chem, gap)
tab = pd.DataFrame([{"synapses": k[0], "functional": k[1], "pairs": k[2], "R2_percent": v[0], "n": v[1]} for k, v in base.items()])
print(tab.round(3).to_string(index=False))
for mode in ("deposit", "both", "none"):
    nm, mt = animal_corr(data[mode], "all")
    alt = r2_table(nm, mt, chem, gap)
    print(f"[{mode}]", {f"{k[0][:12]} {k[1][-3:]} {k[2][-3:]}": round(v[0], 3) for k, v in alt.items()})
chem5, gap5 = npl.load_cook(npl.COOK_ADJACENCY, "hermaphrodite chemical", "herm gap jn symmetric")
alt5 = r2_table(cn_names, cn_mats, chem5, gap5)
print("[SI 5 weighted adjacency, a variant]", {f"{k[0][:12]} {k[1][-3:]} {k[2][-3:]}": round(v[0], 3) for k, v in alt5.items()})
neurons in the comparison: 190
        synapses     functional     pairs  R2_percent      n
      electrical functional (a) pairs (a)       1.282  17869
      electrical functional (a) pairs (b)       3.465    478
      electrical functional (b) pairs (a)       0.342 260172
      electrical functional (b) pairs (b)       1.078   6966
chemical (a) sum functional (a) pairs (a)       0.231  17869
chemical (a) sum functional (a) pairs (b)       0.203   1688
chemical (a) sum functional (b) pairs (a)       0.117 260172
chemical (a) sum functional (b) pairs (b)       0.127  25707
chemical (b) max functional (a) pairs (a)       0.173  17869
chemical (b) max functional (a) pairs (b)       0.037   1688
chemical (b) max functional (b) pairs (a)       0.102 260172
chemical (b) max functional (b) pairs (b)       0.067  25707
[deposit] {'electrical (a) (a)': np.float64(1.457), 'electrical (a) (b)': np.float64(3.68), 'electrical (b) (a)': np.float64(0.451), 'electrical (b) (b)': np.float64(1.221), 'chemical (a) (a) (a)': np.float64(0.314), 'chemical (a) (a) (b)': np.float64(0.248), 'chemical (a) (b) (a)': np.float64(0.165), 'chemical (a) (b) (b)': np.float64(0.178), 'chemical (b) (a) (a)': np.float64(0.251), 'chemical (b) (a) (b)': np.float64(0.07), 'chemical (b) (b) (a)': np.float64(0.147), 'chemical (b) (b) (b)': np.float64(0.104)}
[both] {'electrical (a) (a)': np.float64(1.375), 'electrical (a) (b)': np.float64(3.538), 'electrical (b) (a)': np.float64(0.312), 'electrical (b) (b)': np.float64(0.829), 'chemical (a) (a) (a)': np.float64(0.212), 'chemical (a) (a) (b)': np.float64(0.182), 'chemical (a) (b) (a)': np.float64(0.098), 'chemical (a) (b) (b)': np.float64(0.088), 'chemical (b) (a) (a)': np.float64(0.162), 'chemical (b) (a) (b)': np.float64(0.038), 'chemical (b) (b) (a)': np.float64(0.087), 'chemical (b) (b) (b)': np.float64(0.045)}
[none] {'electrical (a) (a)': np.float64(1.354), 'electrical (a) (b)': np.float64(3.367), 'electrical (b) (a)': np.float64(0.405), 'electrical (b) (b)': np.float64(1.155), 'chemical (a) (a) (a)': np.float64(0.294), 'chemical (a) (a) (b)': np.float64(0.225), 'chemical (a) (b) (a)': np.float64(0.152), 'chemical (a) (b) (b)': np.float64(0.172), 'chemical (b) (a) (a)': np.float64(0.234), 'chemical (b) (a) (b)': np.float64(0.058), 'chemical (b) (b) (a)': np.float64(0.136), 'chemical (b) (b) (b)': np.float64(0.103)}
[SI 5 weighted adjacency, a variant] {'electrical (a) (a)': np.float64(1.093), 'electrical (a) (b)': np.float64(3.483), 'electrical (b) (a)': np.float64(0.376), 'electrical (b) (b)': np.float64(1.807), 'chemical (a) (a) (a)': np.float64(0.192), 'chemical (a) (a) (b)': np.float64(0.061), 'chemical (a) (b) (a)': np.float64(0.113), 'chemical (a) (b) (b)': np.float64(0.089), 'chemical (b) (a) (a)': np.float64(0.14), 'chemical (b) (a) (b)': np.float64(0.001), 'chemical (b) (b) (a)': np.float64(0.097), 'chemical (b) (b) (b)': np.float64(0.044)}

Result: partially reproduced. Every head R2 is below 5%, but none reaches the paper's values. Electrical synapses give 1.28% under readings (a, a) and 3.47% over pairs with a synapse (a, b), against 2.52 to 3.08%; chemical synapses give 0.04 to 0.23% against 0.45 to 0.55%. The other selection readings and the size-weighted matrices give electrical 0.31 to 3.68% and chemical at most 0.31%. The tails are not in the dandiset, so the tail values are not measured. Per-trace scaling does not change these correlations.

fig6-connectome-variations¶

Variations of the comparison (thresholding for low sampling, ranked correlation, log-scaled synapse counts, stimulus or non-stimulus periods only) do not noticeably improve the correlation between functional activity and the connectome.

Each variation on the readings of fig6jk-connectome-correlation (all readings are shown; the claims file asks for the reading that matched, or the first listed). Reproduced when no variation gives an R2 above 3.1%.

In [29]:
var = {}
for th in (5, 10, 15):
    var[f"pairs in > {th} animals"] = r2_table(cn_names, cn_mats, chem, gap, min_animals=th)
var["Spearman"] = r2_table(cn_names, cn_mats, chem, gap, corr="spearman")
var["log(1 + count)"] = r2_table(cn_names, cn_mats, chem, gap, logc=True)
for which in ("stim", "nonstim"):
    nm, mt = animal_corr(D, which)
    var[f"{which} frames only"] = r2_table(nm, mt, chem, gap)
vt = pd.DataFrame([{"variation": v, "synapses": k[0], "functional": k[1], "pairs": k[2], "R2_percent": x[0], "n": x[1]}
                   for v, tbl in var.items() for k, x in tbl.items()])
print(vt.pivot_table(index=["synapses", "functional", "pairs"], columns="variation", values="R2_percent").round(3).to_string())
first = vt[(vt.functional == "functional (a)") & (vt.pairs == "pairs (a)") & (vt.synapses != "chemical (b) max")]
print("largest R2 under the first-listed readings: %.3f%% (%s)" % (first.R2_percent.max(),
      first.loc[first.R2_percent.idxmax(), ["variation", "synapses"]].tolist()))
print("largest R2 over all readings and variations: %.3f%%" % vt.R2_percent.max())
variation                                  Spearman  log(1 + count)  nonstim frames only  pairs in > 10 animals  pairs in > 15 animals  pairs in > 5 animals  stim frames only
synapses         functional     pairs                                                                                                                                         
chemical (a) sum functional (a) pairs (a)     0.202           0.324                0.178                  0.309                  0.300                 0.312             0.191
                                pairs (b)     0.030           0.133                0.099                  0.256                  0.260                 0.256             0.520
                 functional (b) pairs (a)     0.123           0.165                0.095                  0.117                  0.080                 0.118             0.058
                                pairs (b)     0.061           0.108                0.095                  0.123                  0.068                 0.132             0.113
chemical (b) max functional (a) pairs (a)     0.198           0.276                0.129                  0.225                  0.208                 0.233             0.149
                                pairs (b)     0.011           0.010                0.005                  0.046                  0.026                 0.050             0.264
                 functional (b) pairs (a)     0.121           0.152                0.083                  0.101                  0.066                 0.103             0.050
                                pairs (b)     0.023           0.050                0.049                  0.063                  0.022                 0.070             0.069
electrical       functional (a) pairs (a)     0.677           1.493                1.200                  1.508                  1.217                 1.443             0.730
                                pairs (b)     2.434           3.743                3.486                  3.329                  2.325                 3.061             1.987
                 functional (b) pairs (a)     0.250           0.406                0.313                  0.348                  0.281                 0.342             0.081
                                pairs (b)     0.865           1.240                1.091                  1.120                  1.093                 1.061             0.148
largest R2 under the first-listed readings: 1.508% (['pairs in > 10 animals', 'electrical'])
largest R2 over all readings and variations: 3.743%

Result: reproduced. Under the first-listed readings, which the criterion uses because no reading matched in fig6jk-connectome-correlation, the largest R2 over the seven variations is 1.51% (electrical, pairs seen in more than 10 animals), below 3.1%. Over all readings the variations reach at most 3.74% (electrical, pairs with a synapse, log-scaled counts), against 3.47% for the same reading without the variation.

Claims not testable on this dandiset¶

The dandiset holds only the 21 OH16230 head recordings. Eight claims rest on imaging the dandiset does not hold (the NeuroPAL strain characterisation, the integrant health assays, the positional atlas, receptor reporter strains, mutants, and the 10 atlas worms of the identification software); they are blocked as data_missing. Three with_inputs claims need outside material and no dandiset data: the two receptor-network claims need the paper's Table S4 (public), the neurotransmitter maps the paper cites (Gendrel et al., 2016; Pereira et al., 2015; Serrano-Saiz et al., 2013) and the Cook et al. (2019) connectome loaded above; all are named in the paper and public, but this attempt did not build the analysis, so both claims are untested here (blocked as other; methods_underspecified was proposed and the round-3 verifier did not agree, as the inputs are named); the optimal-coloring claim needs the paper's MATLAB solver (public on GitHub) and cannot run in this environment. The attempt record gives the reason for each.

In [30]:
present = {"acquisition": sorted({k for r in recs[:1] for k in ("CalciumImageSeries", "NeuroPALImageRaw")}),
           "strains": sorted({r["strain"] for r in recs}), "descriptions": sorted({r["session_description"] for r in recs})}
print(present)
print("runtime so far: %.1f min" % ((time.time() - T_START) / 60))
{'acquisition': ['CalciumImageSeries', 'NeuroPALImageRaw'], 'strains': ['OH16230'], 'descriptions': ['C. elegans head NeuroPAL and Calcium imaging']}
runtime so far: 6.7 min

Summary¶

Claim Outcome
link-zenodo-traces partially reproduced: files and stimuli match, 81.7% of traces at r of 0.9 (85.8% with the one-frame offset)
link-connectome-names reproduced: 190 of 193 names, the 3 others not neurons
fig6-sample-sizes partially reproduced (assumed): head values match under the main selection reading, no tails
fig6-all-heads-responsive not reproduced: 19 of 21 heads
fig6d-ase-nacl reproduced
fig6-nacl-unknown-interneurons reproduced on z-scored traces (assumed); 4 of 6 on dF/F0
fig6c-odors-more-inhibitory reproduced, thin margin for 2-butanone
fig6d-awc-asymmetric blocked: metadata insufficient (no ON/OFF label; the NeuroPAL image carries the side)
fig6-asj-ric partially reproduced (assumed): 5 of 6 parts; 6 of 6 under two selection readings, 3 on dF/F0
fig6-interneurons-downstream reproduced
fig6-tail-responses blocked: data missing
fig6f-pharyngeal reproduced
fig6-stimulus-sets-distinct reproduced (a weak test)
figs1h-avf-cyclic not reproduced: 0.12 to 0.20 Hz
fig6gi-stimulus-specific-correlations partially reproduced (assumed): the odors are not told apart; the null window also passes in part
figs2a-pca-trajectories not reproduced: at most two of three pairs
fig6jk-connectome-correlation partially reproduced: all R2 below 5%, none in tolerance, heads only
fig6-connectome-variations reproduced: largest R2 1.51%
fig1, figs1, fig2, fig4c, fig5, fig7bc claims (8) blocked: data missing
fig4bd-gaba-network, fig4e-extrasynaptic blocked (other): not tested in this attempt; the inputs the paper names (Table S4, the cited transmitter maps, the Cook et al. connectome) are public
fig7de-optimal-coloring blocked: not runnable (MATLAB solver)

How the analysis differs from the paper's:

  • Which member of a near-duplicate pair is removed is not stated; one is removed (the later in the file), as this comes closest to the paper's per-class sample counts in Table S6. Removing one member matches Table S6 in the per-class trace counts only; agreement with its p values is about the same under all four readings (260, 261, 259 and 257 class agreements at 0.05 for one, deposit, both and none), and only 20 of the 338 traces removed are among those the deposit lacks. Three outcomes change with this choice: fig6-asj-ric (6 of 6 under the deposit and none readings), fig6gi-stimulus-specific-correlations (reproduced with both removed) and the head median of fig6-sample-sizes (21 under the deposit and none readings, outside 16.2 to 19.8).
  • The paper's normalisation "per neuron class" is not described; each trace is z-scored before the response tests, which matches the paper's p values in Table S6 far better than dF/F0 but not exactly. Two outcomes change with this choice (fig6-nacl-unknown-interneurons, 4 of 6 on dF/F0, the measure the claims file's criterion names, and fig6-asj-ric, 3 of 6 on dF/F0); all readings are shown. The claims file leaves out the paper's normalisation sentence; this is asked of a reader.
  • Storey's pi0 is low on this attempt's p values (0.222), against 0.42 to 0.48 on the paper's own p values, so q falls below p for many tests and reading (a) is more lenient here than in the paper; the same code reproduces the paper's q values from its p values. It decides one part of fig6-asj-ric under reading (a) (ASJ against 2,3-pentanedione, q = 0.040, p = 0.082), not the outcome. Benjamini-Hochberg is shown as a conservative check.
  • The tails, the OH15500 heads and the imaging behind Figures 1 to 5 and 7 are not in the dandiset.

What was found about the dataset:

  • The calcium volume's grid spacing (0.267 um in x and y) does not fit its coordinates: neurons sit at half the NeuroPAL volume's x and y coordinates in every file, so the binned movie's spacing is 0.533 um.
  • The two trace series hold the same numbers; the names include two glia, the head mesodermal cell and DD6; the files do not say which AWC is ON or OFF, though the NeuroPAL image shows it (CyOFP1, the srsx-3 color, agrees with the paper's deposit in 19 of 21 heads); the stimulus table gives 2-butanone in "mM" and the order as random.
  • 126 named traces, 113 of them with finite values, are not in the paper's deposit (101 have an empty slot there), and the dandiset does not mark which traces the paper excluded. The dandiset's traces are one frame earlier than the deposit's in every file.

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.