Alarm pheromones at two concentrations: Hart, Frank, Lopes et al. (2023), dandiset 000467¶

Paper: Hart T, Frank DD, Lopes LE et al. "Sparse and stereotyped encoding implicates a core glomerulus for ant alarm behavior", Cell 186 (2023), doi:10.1016/j.cell.2023.05.025.

Dataset: DANDI:000467, version 0.261004.1655 (doi:10.48324/dandi.000467/0.261004.1655), the only published version; the paper cites none (see alarm_pheromones.ipynb).

Claims file: claims/000467/10.1016-j.cell.2023.05.025.yaml (accepted manuscript, PMC10334690).

This notebook tests one claim, figs5a-concentration-more-glomeruli, which needs whole-volume maps (M1) of every pheromone trial at both of each ant's concentrations: 312 trials of 33 planes. It is kept apart from alarm_pheromones.ipynb so that each notebook stays under the 30-minute limit.

Claim Here
figs5a-concentration-more-glomeruli tested
all other claims the other notebooks, or blocked (see the attempt record)
In [1]:
import time

import numpy as np
import pandas as pd
from scipy import stats

import al_imaging as al

pd.set_option("display.width", 200)
pd.set_option("display.max_columns", 40)
T0 = time.time()
print("dandiset", al.DANDISET_ID, "version", al.DANDISET_VERSION)
dandiset 000467 version 0.261004.1655
In [2]:
assets = al.list_assets()
trials = al.trials(assets)
alarm_ants = sorted(trials[trials.odor == "heptanone"].ant.unique())
sel = trials[trials.ant.isin(alarm_ants) & trials.odor.isin(al.PHEROMONES) & (trials.nvol == 40)].copy()
pairs = sel.groupby("ant").conc.agg(lambda c: tuple(sorted(set(c))))
sel["level"] = np.where(sel.conc == sel.ant.map(pairs.map(max)), "higher", "lower")
In [3]:
print("ants", len(alarm_ants), "; concentration pairs:", pairs.value_counts().to_dict())
print(sel.groupby(["ant", "level", "odor"]).size().unstack("odor").to_string())
print(len(sel), "trials,", sum(len(b) for b in sel.blobs), "plane files")
ants 13 ; concentration pairs: {(0.03, 0.48): 5, (0.12, 0.48): 3, (0.0075, 0.12): 3, (0.03, 0.12): 2}
odor                      6MHO  heptanol  heptanone  hexanol
ant               level                                     
ant1-m4-d21-y2022 higher     3         3          3        3
                  lower      3         3          3        3
ant1-m4-d26-y2022 higher     3         3          3        3
                  lower      3         3          3        3
ant1-m4-d27-y2022 higher     3         3          3        3
                  lower      3         3          3        3
ant2-m4-d20-y2022 higher     3         3          3        3
                  lower      3         3          3        3
ant2-m4-d21-y2022 higher     3         3          3        3
                  lower      3         3          3        3
ant2-m4-d26-y2022 higher     3         3          3        3
                  lower      3         3          3        3
ant2-m4-d27-y2022 higher     3         3          3        3
                  lower      3         3          3        3
ant3-m4-d21-y2022 higher     3         3          3        3
                  lower      3         3          3        3
ant3-m4-d26-y2022 higher     3         3          3        3
                  lower      3         3          3        3
ant3-m4-d27-y2022 higher     3         3          3        3
                  lower      3         3          3        3
ant4-m4-d20-y2022 higher     3         3          3        3
                  lower      3         3          3        3
ant4-m4-d26-y2022 higher     3         3          3        3
                  lower      3         3          3        3
ant5-m4-d26-y2022 higher     3         3          3        3
                  lower      3         3          3        3
312 trials, 10296 plane files
In [4]:
t = time.time()
maps = al.trial_maps(sel, progress=lambda a, b: print(f"{a}/{b} files, {time.time() - t:.0f} s", flush=True) if a % 2500 == 0 else None)
print(f"maps in {time.time() - t:.0f} s")
2500/10296 files, 292 s
5000/10296 files, 590 s
7500/10296 files, 883 s
10000/10296 files, 1174 s
maps in 1209 s
In [5]:
shift = pd.Series({i: maps[i]["shift"].max() for i in sel.index})
print("largest frame shift per trial: median", shift.median(), "max", shift.max(), "pixels")
print("baseline F0 range", min(maps[i]["f0"].min() for i in sel.index), "to", max(maps[i]["f0"].max() for i in sel.index))
largest frame shift per trial: median 10.0 max 42 pixels
baseline F0 range 0.0 to 8191.0

figs5a-concentration-more-glomeruli¶

Higher concentrations give more robust responses and a few more glomeruli passing the threshold, while the overall spatial pattern stays similar across concentrations.

Criterion: within each ant and pheromone, M2 counts at the higher minus the lower concentration. Reproduced when, pooled over pheromones, positive differences outnumber negative ones by a two-sided sign test at p < 0.05, and in more than half of ant and pheromone pairs every ROI that responds at the lower concentration also responds at the higher one. Shown under every M2 reading. The ROIs are those drawn at each ant's higher concentration (Methods: ROIs were drawn on the max projections "at the highest odor concentration for each ant").

In [6]:
rows = []
for ant in alarm_ants:
    g = sel[sel.ant == ant]
    mask = al.al_mask(maps, g.index)
    for d in "ab":
        rois = al.draw_rois(maps, g[g.level == "higher"], d, mask)
        for v in ("i", "ii"):
            for c in "xy":
                for od in al.PHEROMONES:
                    rh, nh = al.responding(maps, rois, g[(g.odor == od) & (g.level == "higher")].index, v, c)
                    rl, nl = al.responding(maps, rois, g[(g.odor == od) & (g.level == "lower")].index, v, c)
                    rows.append({"ant": ant, "pair": pairs[ant], "reading": d + v + c, "odor": od, "higher": nh, "lower": nl,
                                 "lower subset of higher": bool(np.all(rh[rl])) if len(rl) else True})
cc = pd.DataFrame(rows)
cc["diff"] = cc.higher - cc.lower
out = []
for k, g in cc.groupby("reading"):
    pos, neg = int((g["diff"] > 0).sum()), int((g["diff"] < 0).sum())
    p = stats.binomtest(pos, pos + neg, 0.5).pvalue if pos + neg else 1.0
    out.append({"reading": k, "pairs": len(g), "higher > lower": pos, "higher < lower": neg, "ties": len(g) - pos - neg,
                "sign test p": round(p, 4), "median diff": g["diff"].median(), "lower within higher": int(g["lower subset of higher"].sum()),
                "sign test met": bool(pos > neg and p < 0.05), "subset met": bool(g["lower subset of higher"].sum() > len(g) / 2)})
out = pd.DataFrame(out)
out["met"] = out["sign test met"] & out["subset met"]
print(out.to_string(index=False))
print("\nPer ant and pheromone, reading bix (higher, lower):")
b = cc[cc.reading == "bix"]
print(b.pivot_table(index=["ant", "pair"], columns="odor", values=["higher", "lower"]).to_string())
print("\nBy concentration pair, reading bix: mean difference", b.groupby("pair")["diff"].mean().round(2).to_dict())
reading  pairs  higher > lower  higher < lower  ties  sign test p  median diff  lower within higher  sign test met  subset met   met
   aiix     52              36              12     4       0.0007     3.000000                   21           True       False False
   aiiy     52              39              11     2       0.0001     2.500000                   23           True       False False
    aix     52              41               2     9       0.0000     2.000000                   43           True        True  True
    aiy     52              47               2     3       0.0000     2.500000                   46           True        True  True
   biix     52              14              12    26       0.8450     0.000000                   37          False        True False
   biiy     52              31              13     8       0.0096     0.333333                   36           True        True  True
    bix     52              28               2    22       0.0000     1.000000                   50           True        True  True
    biy     52              43               2     7       0.0000     0.666667                   50           True        True  True

Per ant and pheromone, reading bix (higher, lower):
                                 higher                            lower                           
odor                               6MHO heptanol heptanone hexanol  6MHO heptanol heptanone hexanol
ant               pair                                                                             
ant1-m4-d21-y2022 (0.12, 0.48)      1.0      1.0       2.0     3.0   0.0      0.0       0.0     0.0
ant1-m4-d26-y2022 (0.03, 0.48)      1.0      2.0       1.0     3.0   0.0      1.0       1.0     3.0
ant1-m4-d27-y2022 (0.0075, 0.12)    1.0      2.0       4.0    10.0   0.0      6.0       2.0     0.0
ant2-m4-d20-y2022 (0.03, 0.12)      0.0      0.0       1.0     3.0   0.0      0.0       0.0     1.0
ant2-m4-d21-y2022 (0.12, 0.48)      0.0      0.0       1.0     2.0   0.0      0.0       0.0     2.0
ant2-m4-d26-y2022 (0.03, 0.48)      0.0      2.0       3.0     5.0   0.0      0.0       1.0     3.0
ant2-m4-d27-y2022 (0.0075, 0.12)    1.0      0.0       0.0     2.0   0.0      0.0       0.0     0.0
ant3-m4-d21-y2022 (0.12, 0.48)      0.0      0.0       1.0     2.0   1.0      0.0       1.0     0.0
ant3-m4-d26-y2022 (0.03, 0.48)      0.0      0.0       2.0     3.0   0.0      0.0       0.0     2.0
ant3-m4-d27-y2022 (0.0075, 0.12)    0.0      0.0       0.0     5.0   0.0      0.0       0.0     0.0
ant4-m4-d20-y2022 (0.03, 0.12)      1.0      0.0       1.0     2.0   0.0      0.0       0.0     2.0
ant4-m4-d26-y2022 (0.03, 0.48)      2.0      2.0       1.0     8.0   0.0      2.0       1.0     3.0
ant5-m4-d26-y2022 (0.03, 0.48)      0.0      1.0       1.0     3.0   0.0      0.0       0.0     1.0

By concentration pair, reading bix: mean difference {(0.0075, 0.12): 1.42, (0.03, 0.12): 0.62, (0.03, 0.48): 1.1, (0.12, 0.48): 0.75}

Result: reproduced, resting on the adopted M2 reading. Under bix the count is higher at the higher concentration in 28 of 52 ant and pheromone pairs, lower in 2 and tied in 22 (two-sided sign test p < 0.0001), and every ROI responding at the lower concentration also responds at the higher in 50 of 52 pairs. aix, aiy, biy and biiy also meet the criterion; aiix and aiiy fail the subset condition (21 and 23 of 52) and biix the sign test (14 against 12, p = 0.845). The ROIs are drawn at the higher concentration, as in the Methods, which favours ROIs that respond there.

Summary¶

The outcomes of all claims, the differences from the paper's method and the findings about the dataset are summarised in alarm_pheromones.ipynb and the attempt record. Here: figs5a-concentration-more-glomeruli: reproduced, resting on the adopted M2 reading.

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.

Runtime of this notebook:

In [7]:
print(f"notebook ran in {(time.time() - T0) / 60:.1f} min")
notebook ran in 20.3 min