Alarm pheromone imaging: 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 paper cites the dandiset without a version. This is the only published version (published 2026-10-04, "after adding additional meta data"); the draft has the same 14,685 assets and size, so the most recent published version is pinned, as the protocol says when the paper cites none.
Claims file: claims/000467/10.1016-j.cell.2023.05.025.yaml, written from the accepted author
manuscript in PubMed Central (PMC10334690) with its supplement. The shared methods M1 to M4 are in
its opening comment; the helper al_imaging.py says where each parameter comes from.
This notebook holds the unilateral alarm pheromone experiment (13 ants). The general odorant and
bilateral experiments are in general_odorants.ipynb. Claims tested here:
| Claim | Here |
|---|---|
| fig4b-vehicle-no-response | tested |
| figs5a-sparse-median-6 | tested |
| figs5b-shared-glomerulus | tested |
| figs5c-mho-rarely-overlaps | tested |
| fig6-shared-panic-pathway | imaging half tested |
| fig4-no-decreases | tested at each ant's higher concentration |
| fig4-large-lasting-responses | tested |
| fig5ab-focal-glomeruli | tested |
| fig5d-stereotyped-positions | tested |
| fig5c-s6-tuning | tested |
| fig5c-6g-slow | tested |
| figs7a-6g-extended | tested |
| figs7c-temporal-lme | tested |
| figs5a-concentration-more-glomeruli | in concentration.ipynb |
| fig3f, fig3fg, figs4a | in general_odorants.ipynb |
| behaviour, transgenesis, confocal and genome claims (15) | blocked; see the attempt record |
Whole-volume peak maps (M1) are computed for each ant's higher-concentration pheromone trials, its vehicle trial and its isopropanol trial: 182 trials of 33 planes, about 6,000 files of 84 MB, of which only volumes 1 to 5, 8 and 9 to 14 are read. The single-glomerulus traces (M3) read every volume of a band of rows around the focal glomeruli, on five planes, for every pheromone trial.
import time
import warnings
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
from scipy import ndimage, stats
import al_imaging as al
from statsmodels.tools.sm_exceptions import ConvergenceWarning
warnings.filterwarnings("ignore", category=RuntimeWarning)
warnings.filterwarnings("ignore", category=ConvergenceWarning) # convergence is reported with each fit
warnings.filterwarnings("ignore", message="Random effects covariance is singular")
pd.set_option("display.width", 200)
pd.set_option("display.max_columns", 40)
pd.set_option("display.max_rows", 200)
T0 = time.time()
print("dandiset", al.DANDISET_ID, "version", al.DANDISET_VERSION)
dandiset 000467 version 0.261004.1655
assets = al.list_assets()
trials = al.trials(assets)
print(len(assets), "assets;", assets.blob.nunique(), "distinct blobs; all file names parsed:", assets.parsed.all())
alarm_ants = sorted(trials[trials.odor == "heptanone"].ant.unique())
general_ants = sorted(trials[trials.odor == "3-hexanone"].ant.unique())
bilateral_ants = sorted(set(trials.ant) - set(alarm_ants) - set(general_ants))
print("alarm ants", len(alarm_ants), "general odorant ants", len(general_ants), "bilateral ants", len(bilateral_ants), bilateral_ants)
alarm = trials[trials.ant.isin(alarm_ants)].copy()
conc_pairs = alarm[alarm.odor.isin(al.PHEROMONES)].groupby("ant").conc.agg(lambda c: tuple(sorted(set(c))))
alarm["higher"] = alarm.ant.map(conc_pairs.map(max))
print(conc_pairs.value_counts())
14685 assets; 14685 distinct blobs; all file names parsed: True alarm ants 13 general odorant ants 6 bilateral ants 3 ['ant2-m2-d10-y2023', 'ant3-m2-d10-y2023', 'ant4-m2-d10-y2023'] conc (0.03, 0.48) 5 (0.12, 0.48) 3 (0.0075, 0.12) 3 (0.03, 0.12) 2 Name: count, dtype: int64
print("Trials per ant, odor and concentration (alarm experiment; planes per trial:",
sorted(alarm.n_planes.unique()), "; volumes per trial:", sorted(alarm.nvol.unique()), ")")
print(alarm.groupby(["ant", "conc", "odor"]).size().unstack("odor").fillna(0).astype(int).to_string())
print()
print("Recordings of 120 volumes (144 s):")
print(alarm[alarm.nvol == 120][["ant", "trial", "odor", "conc", "n_planes"]].to_string(index=False))
# Each ant's sessions: date and ant number in the name; one subject per ant
print("subjects:", assets[assets.ant.isin(alarm_ants)]["sub"].nunique(), "; sessions (date):",
assets[assets.ant.isin(alarm_ants)].date.value_counts().to_dict())
Trials per ant, odor and concentration (alarm experiment; planes per trial: [np.int64(33)] ; volumes per trial: [np.int64(40), np.int64(120)] )
odor 6MHO heptanol heptanone hexanol isopropanol vehicle
ant conc
ant1-m4-d21-y2022 0.1200 3 3 3 3 0 0
0.4800 3 3 3 3 0 0
1.0000 0 0 0 0 1 1
ant1-m4-d26-y2022 0.0300 3 3 3 3 0 0
0.4800 4 3 3 3 0 0
1.0000 0 0 0 0 1 1
ant1-m4-d27-y2022 0.0075 3 3 3 3 0 0
0.1200 3 3 3 3 0 0
1.0000 0 0 0 0 1 1
ant2-m4-d20-y2022 0.0300 3 3 3 3 0 0
0.1200 3 3 3 3 0 0
1.0000 0 0 0 0 1 1
ant2-m4-d21-y2022 0.1200 3 3 3 3 0 0
0.4800 3 3 3 3 0 0
1.0000 0 0 0 0 1 1
ant2-m4-d26-y2022 0.0300 3 3 3 3 0 0
0.4800 4 3 3 3 0 0
1.0000 0 0 0 0 1 1
ant2-m4-d27-y2022 0.0075 3 3 3 3 0 0
0.1200 3 3 3 3 0 0
1.0000 0 0 0 0 1 1
ant3-m4-d21-y2022 0.1200 3 3 3 3 0 0
0.4800 3 3 3 3 0 0
1.0000 0 0 0 0 1 1
ant3-m4-d26-y2022 0.0300 3 3 3 3 0 0
0.4800 4 3 3 3 0 0
1.0000 0 0 0 0 1 1
ant3-m4-d27-y2022 0.0075 3 3 3 3 0 0
0.1200 3 3 3 3 0 0
1.0000 0 0 0 0 1 1
ant4-m4-d20-y2022 0.0300 3 3 3 3 0 0
0.1200 3 3 3 3 0 0
1.0000 0 0 0 0 1 1
ant4-m4-d26-y2022 0.0300 3 3 3 3 0 0
0.4800 4 3 3 3 0 0
1.0000 0 0 0 0 1 1
ant5-m4-d26-y2022 0.0300 3 3 3 3 0 0
0.4800 4 3 3 3 0 0
1.0000 0 0 0 0 1 1
Recordings of 120 volumes (144 s):
ant trial odor conc n_planes
ant1-m4-d26-y2022 157 6MHO 0.48 33
ant2-m4-d26-y2022 194 6MHO 0.48 33
ant3-m4-d26-y2022 228 6MHO 0.48 33
ant4-m4-d26-y2022 265 6MHO 0.48 33
ant5-m4-d26-y2022 300 6MHO 0.48 33
subjects: 13 ; sessions (date): {'m4-d26-y2022': 4455, 'm4-d27-y2022': 2574, 'm4-d21-y2022': 2574, 'm4-d20-y2022': 1716}
# Metadata from the files themselves: one file per ant, plus all 33 planes of one trial and one 144 s file
meta_rows = []
for ant in alarm_ants:
r = alarm[(alarm.ant == ant) & (alarm.nvol == 40)].iloc[0]
m = al.file_metadata(r.blobs[16])
meta_rows.append({"ant": ant, **{k: m[k] for k in ("shape", "dtype", "chunks", "compression", "offset", "unit",
"imaging_rate", "grid_spacing", "age", "location")}})
meta = pd.DataFrame(meta_rows)
print(meta.to_string(index=False))
r = alarm[alarm.nvol == 120].iloc[0]
m120 = al.file_metadata(r.blobs[0])
print("144 s file:", m120["shape"], "offset", m120["offset"], "|", m120["series_description"])
print("experiment description of one file:", al.file_metadata(alarm.iloc[0].blobs[0])["experiment_description"])
one = alarm[(alarm.nvol == 40)].iloc[3]
starts = [al.file_metadata(b)["starting_time"] for b in one.blobs]
print("starting_time by plane (s), trial", one.ant, one.trial, ":", np.round(starts, 4).tolist())
print("step between planes", np.round(np.diff(starts).mean(), 5), "s; x 33 =", np.round(np.diff(starts).mean() * 33, 4),
"s per volume; 1/imaging_rate =", round(1 / 0.83, 4))
PLANE_DT = float(np.diff(starts).mean()) # each plane's volumes start PLANE_DT x plane later
# the raw range reads equal the h5py reads
with al.open_nwb(one.blobs[10]) as f:
h = f["acquisition/TwoPhotonSeries1/data"][8:10, 100:110, :]
print("range read equals h5py read:", np.array_equal(al.read_frames(one.blobs[10], 8, 10)[:, 100:110, :], h))
print("byte offset is", sorted(set(meta.offset) | {m120["offset"]}), "in every file read; file sizes:",
assets.groupby("nvol")["size"].agg(["min", "max"]).to_dict("index"))
ant shape dtype chunks compression offset unit imaging_rate grid_spacing age location ant1-m4-d21-y2022 (40, 512, 512) float64 None None 3464 raw fluorescence 0.83 (0.288867, 0.288867, 5.0) P92D/P99D right antennal lobe, up is lateral, right is anterior ant1-m4-d26-y2022 (40, 512, 512) float64 None None 3464 raw fluorescence 0.83 (0.288867, 0.288867, 5.0) P97D/P104D right antennal lobe, up is lateral, right is anterior ant1-m4-d27-y2022 (40, 512, 512) float64 None None 3464 raw fluorescence 0.83 (0.288867, 0.288867, 5.0) P98D/P104D right antennal lobe, up is lateral, right is anterior ant2-m4-d20-y2022 (40, 512, 512) float64 None None 3464 raw fluorescence 0.83 (0.288867, 0.288867, 5.0) P91D/P98D right antennal lobe, up is lateral, right is anterior ant2-m4-d21-y2022 (40, 512, 512) float64 None None 3464 raw fluorescence 0.83 (0.288867, 0.288867, 5.0) P92D/P99D right antennal lobe, up is lateral, right is anterior ant2-m4-d26-y2022 (40, 512, 512) float64 None None 3464 raw fluorescence 0.83 (0.288867, 0.288867, 5.0) P97D/P104D right antennal lobe, up is lateral, right is anterior ant2-m4-d27-y2022 (40, 512, 512) float64 None None 3464 raw fluorescence 0.83 (0.288867, 0.288867, 5.0) P98D/P104D right antennal lobe, up is lateral, right is anterior ant3-m4-d21-y2022 (40, 512, 512) float64 None None 3464 raw fluorescence 0.83 (0.288867, 0.288867, 5.0) P92D/P99D right antennal lobe, up is lateral, right is anterior ant3-m4-d26-y2022 (40, 512, 512) float64 None None 3464 raw fluorescence 0.83 (0.288867, 0.288867, 5.0) P97D/P104D right antennal lobe, up is lateral, right is anterior ant3-m4-d27-y2022 (40, 512, 512) float64 None None 3464 raw fluorescence 0.83 (0.288867, 0.288867, 5.0) P98D/P104D right antennal lobe, up is lateral, right is anterior ant4-m4-d20-y2022 (40, 512, 512) float64 None None 3464 raw fluorescence 0.83 (0.288867, 0.288867, 5.0) P91D/P98D right antennal lobe, up is lateral, right is anterior ant4-m4-d26-y2022 (40, 512, 512) float64 None None 3464 raw fluorescence 0.83 (0.288867, 0.288867, 5.0) P97D/P104D right antennal lobe, up is lateral, right is anterior ant5-m4-d26-y2022 (40, 512, 512) float64 None None 3464 raw fluorescence 0.83 (0.288867, 0.288867, 5.0) P97D/P104D right antennal lobe, up is lateral, right is anterior
144 s file: (120, 512, 512) offset 3464 | two photon recording, 120 cycles total, recording time 144.0s, odor stimulus triggered at 3s with duration 5s
experiment description of one file: 40 cycle 2photon recording, 5s odor stim triggered at 3s, using 1x paraffin-oil at z depth 0 um in the clonal raider ant antennal lobe, transgenic line expressing GCaMP6s in olfactory sensory neurons
starting_time by plane (s), trial ant1-m4-d21-y2022 126 : [0.1004, 0.1339, 0.1674, 0.2008, 0.2343, 0.2678, 0.3013, 0.3347, 0.3682, 0.4017, 0.4352, 0.4686, 0.5021, 0.5356, 0.5691, 0.6025, 0.636, 0.6695, 0.703, 0.7364, 0.7699, 0.8034, 0.8369, 0.8703, 0.9038, 0.9373, 0.9708, 1.0042, 1.0377, 1.0712, 1.1047, 1.1381, 1.1716] step between planes 0.03347 s; x 33 = 1.1047 s per volume; 1/imaging_rate = 1.2048
range read equals h5py read: True
byte offset is [3464] in every file read; file sizes: {40: {'min': 84086336, 'max': 84086336}, 120: {'min': 251858496, 'max': 251858496}}
The files give the orientation ("right antennal lobe, up is lateral, right is anterior"), so image
rows run lateral (top) to medial and columns posterior (left) to anterior. Pixel size 0.2889 um and 5
um between planes, as the paper states. Ages are given as ranges ("P97D/P104D"). The plane's
starting_time grows by about 1.2 s / 33 per plane, so each file's volumes are timed from its plane's
place in the volume scan.
# Whole-volume peak maps (M1) for each ant's higher concentration, vehicle and isopropanol
sel = alarm[(alarm.nvol == 40) & ((alarm.conc == alarm.higher) | (alarm.odor.isin(["vehicle", "isopropanol"])))]
sel = sel[sel.odor.isin(al.PHEROMONES + ["vehicle", "isopropanol"])]
print(len(sel), "trials,", sum(len(b) for b in sel.blobs), "plane files")
t = time.time()
maps = al.trial_maps(sel, keep_frames=True, progress=lambda a, b: print(f"{a}/{b} files, {time.time() - t:.0f} s", flush=True) if a % 2000 == 0 else None)
print(f"maps in {time.time() - t:.0f} s")
182 trials, 6006 plane files
2000/6006 files, 262 s
4000/6006 files, 523 s
6000/6006 files, 783 s
maps in 783 s
shift = pd.Series({i: maps[i]["shift"].max() for i in sel.index})
sat = pd.Series({i: maps[i]["saturated"].mean() for i in sel.index})
f0max = pd.Series({i: maps[i]["f0"].max() for i in sel.index})
print("largest frame shift per trial (pixels): median", shift.median(), "max", shift.max(),
"; trials with a shift over 10 pixels (2.9 um):", int((shift > 10).sum()))
print("fraction of baseline pixels at the detector maximum (8191), mean over planes: median",
round(sat.median(), 4), "max", round(sat.max(), 4))
print("baseline F0 (4x4 bins) maximum per trial: range", f0max.min().round(0), "to", f0max.max().round(0))
proj_rng = pd.Series({i: (maps[i]["proj"].min(), maps[i]["proj"].max()) for i in sel.index})
print("max projections are clipped to [-1, 4]: min", min(p[0] for p in proj_rng), "max", max(p[1] for p in proj_rng))
# duplicate recordings: identical baseline maps between two trials of an ant
dups = []
for ant, g in sel.groupby("ant"):
idx = list(g.index)
for a in range(len(idx)):
for b in range(a + 1, len(idx)):
if np.array_equal(maps[idx[a]]["f0"], maps[idx[b]]["f0"]):
dups.append((ant, sel.loc[idx[a]].trial, sel.loc[idx[b]].trial))
print("pairs of trials with identical baseline volumes:", dups if dups else "none")
largest frame shift per trial (pixels): median 10.0 max 42 ; trials with a shift over 10 pixels (2.9 um): 77 fraction of baseline pixels at the detector maximum (8191), mean over planes: median 0.001 max 0.0046 baseline F0 (4x4 bins) maximum per trial: range 8036.0 to 8191.0 max projections are clipped to [-1, 4]: min -1.0 max 4.0 pairs of trials with identical baseline volumes: none
ROIs and responding glomeruli (M2)¶
For each ant, ROIs are drawn on its higher-concentration pheromone trials (the vehicle and isopropanol have one trial each, so they cannot meet "at least two trials"), inside a mask of the AL: the connected regions of at least 0.2 in each trial's map that recur (overlap by at least half the smaller region) in another trial of the same odor, drawing a keeping all of them and drawing b those of glomerulus size (10 to 20 um, wider ones split). Each claim built on M2 is shown under the eight readings: ROI drawing a or b, ROI value i (mean) or ii (maximum), counting x (trial mean at least 0.2) or y (mean of per-trial counts). The figure shows every ant's ROIs under both drawings.
al_masks = {ant: al.al_mask(maps, sel[sel.ant == ant].index) for ant in alarm_ants}
rois = {}
for ant in alarm_ants:
hi = sel[(sel.ant == ant) & sel.odor.isin(al.PHEROMONES)]
for d in "ab":
rois[ant, d] = al.draw_rois(maps, hi, d, al_masks[ant])
print(pd.DataFrame({d: {ant: f"{len(rois[ant, d])} regions / {al.n_groups(rois[ant, d])} ROIs" for ant in alarm_ants} for d in "ab"}))
ODORS = al.PHEROMONES + ["vehicle", "isopropanol"]
resp, counts, depth = {}, [], {}
for ant in alarm_ants:
g = sel[sel.ant == ant]
for d, v, c in al.READINGS:
for od in ODORS:
idx = g[g.odor == od].index
r_, n_ = al.responding(maps, rois[ant, d], idx, v, c)
resp[ant, d, v, c, od] = r_
counts.append({"ant": ant, "higher": g.conc.max() if od in al.PHEROMONES else g[g.odor.isin(al.PHEROMONES)].conc.max(),
"reading": d + v + c, "odor": od, "count": n_})
for d in "ab":
for od in al.PHEROMONES:
depth[ant, d, od] = al.group_depths(maps, rois[ant, d], g[g.odor == od].index)
counts = pd.DataFrame(counts)
counts["higher"] = counts.ant.map(conc_pairs.map(max))
wide = counts.pivot_table(index=["ant", "higher"], columns=["reading", "odor"], values="count")
print(wide.round(1).to_string())
# ROI sizes and an image of the ROIs of each ant over its max projection (maximum over its
# higher-concentration pheromone trials), for both drawings
size_rows = []
for (ant, d), rr in rois.items():
for r_ in rr:
size_rows.append({"drawing": d, "diameter_um": 2 * np.sqrt(r_["mask"].sum() / np.pi) * al.PIXEL_UM * al.BIN})
size_rows = pd.DataFrame(size_rows)
print(size_rows.groupby("drawing").diameter_um.describe(percentiles=[0.1, 0.5, 0.9]).round(1).to_string())
print("regions under 10 um across, by drawing:", size_rows[size_rows.diameter_um < 10].groupby("drawing").size().to_dict())
fig, ax = plt.subplots(2, len(alarm_ants), figsize=(1.9 * len(alarm_ants), 4.2), squeeze=False)
for k, ant in enumerate(alarm_ants):
hi = sel[(sel.ant == ant) & sel.odor.isin(al.PHEROMONES)]
bg = np.max([al._bin(maps[i]["proj"], al.BIN) for i in hi.index], axis=0)
for j, d in enumerate("ab"):
rgb = np.repeat(((np.clip(bg, -0.2, 1) + 0.2) / 1.2)[..., None], 3, 2)
edge = np.zeros(bg.shape, bool)
for m in al.group_masks(rois[ant, d]):
edge |= m & ~ndimage.binary_erosion(m)
rgb[edge] = (1, 0, 0)
rgb[al_masks[ant] & ~ndimage.binary_erosion(al_masks[ant])] = (0, 0.8, 0.8)
ax[j, k].imshow(rgb); ax[j, k].axis("off")
ax[j, k].set_title(f"{ant.split('-')[0]}-{ant.split('-')[1]}-{ant.split('-')[2]} {d}: {al.n_groups(rois[ant, d])}", fontsize=6)
plt.suptitle("ROI outlines (red) and AL mask (cyan) over the max projection, clipped to [-0.2, 1]; top drawing a, bottom b", fontsize=8)
plt.tight_layout(); plt.show()
a b ant1-m4-d21-y2022 26 regions / 17 ROIs 7 regions / 4 ROIs ant1-m4-d26-y2022 32 regions / 23 ROIs 7 regions / 5 ROIs ant1-m4-d27-y2022 49 regions / 25 ROIs 37 regions / 32 ROIs ant2-m4-d20-y2022 34 regions / 26 ROIs 6 regions / 5 ROIs ant2-m4-d21-y2022 31 regions / 26 ROIs 6 regions / 4 ROIs ant2-m4-d26-y2022 50 regions / 38 ROIs 11 regions / 6 ROIs ant2-m4-d27-y2022 9 regions / 8 ROIs 3 regions / 3 ROIs ant3-m4-d21-y2022 22 regions / 18 ROIs 4 regions / 3 ROIs ant3-m4-d26-y2022 29 regions / 20 ROIs 4 regions / 4 ROIs ant3-m4-d27-y2022 20 regions / 18 ROIs 8 regions / 7 ROIs ant4-m4-d20-y2022 41 regions / 34 ROIs 4 regions / 3 ROIs ant4-m4-d26-y2022 52 regions / 40 ROIs 13 regions / 10 ROIs ant5-m4-d26-y2022 41 regions / 35 ROIs 7 regions / 4 ROIs
reading aiix aiiy aix aiy biix biiy bix biy
odor 6MHO heptanol heptanone hexanol isopropanol vehicle 6MHO heptanol heptanone hexanol isopropanol vehicle 6MHO heptanol heptanone hexanol isopropanol vehicle 6MHO heptanol heptanone hexanol isopropanol vehicle 6MHO heptanol heptanone hexanol isopropanol vehicle 6MHO heptanol heptanone hexanol isopropanol vehicle 6MHO heptanol heptanone hexanol isopropanol vehicle 6MHO heptanol heptanone hexanol isopropanol vehicle
ant higher
ant1-m4-d21-y2022 0.48 6.0 2.0 6.0 12.0 5.0 3.0 5.7 5.3 6.7 10.3 5.0 3.0 1.0 1.0 2.0 5.0 0.0 0.0 2.0 3.0 3.0 6.3 0.0 0.0 3.0 2.0 3.0 4.0 2.0 2.0 2.7 2.3 3.3 4.0 2.0 2.0 1.0 1.0 2.0 3.0 0.0 0.0 0.7 0.7 2.0 3.0 0.0 0.0
ant1-m4-d26-y2022 0.48 3.0 4.0 11.0 17.0 6.0 0.0 4.3 5.0 10.7 17.3 6.0 0.0 2.0 3.0 5.0 11.0 1.0 0.0 2.7 3.0 5.7 12.0 1.0 0.0 1.0 3.0 5.0 5.0 4.0 0.0 1.7 3.7 4.7 5.0 4.0 0.0 1.0 2.0 1.0 3.0 0.0 0.0 0.7 2.0 1.0 3.3 0.0 0.0
ant1-m4-d27-y2022 0.12 8.0 15.0 15.0 16.0 3.0 4.0 8.3 14.0 11.3 17.7 3.0 4.0 1.0 3.0 4.0 6.0 0.0 0.0 2.7 5.3 7.7 9.7 0.0 0.0 32.0 32.0 32.0 32.0 16.0 22.0 25.0 31.0 21.0 28.7 16.0 22.0 1.0 2.0 4.0 10.0 0.0 0.0 7.3 8.0 11.0 14.0 0.0 0.0
ant2-m4-d20-y2022 0.12 2.0 8.0 15.0 21.0 10.0 6.0 3.3 7.3 13.3 19.0 10.0 6.0 0.0 1.0 5.0 5.0 1.0 0.0 0.3 1.7 7.3 8.7 1.0 0.0 1.0 2.0 3.0 5.0 2.0 4.0 1.7 2.3 3.0 4.7 2.0 4.0 0.0 0.0 1.0 3.0 0.0 0.0 0.3 0.0 1.0 2.3 0.0 0.0
ant2-m4-d21-y2022 0.48 9.0 20.0 7.0 14.0 7.0 1.0 9.3 15.0 10.3 12.3 7.0 1.0 0.0 2.0 1.0 4.0 0.0 0.0 2.0 7.3 3.7 5.0 0.0 0.0 3.0 4.0 2.0 4.0 2.0 0.0 2.3 3.3 2.3 4.0 2.0 0.0 0.0 0.0 1.0 2.0 0.0 0.0 0.3 0.3 0.3 2.0 0.0 0.0
ant2-m4-d26-y2022 0.48 12.0 22.0 20.0 21.0 9.0 27.0 14.7 21.3 18.7 18.3 9.0 27.0 0.0 4.0 5.0 8.0 0.0 5.0 3.3 10.7 9.3 11.3 0.0 5.0 5.0 6.0 6.0 6.0 4.0 6.0 4.7 5.7 5.3 5.7 4.0 6.0 0.0 2.0 3.0 5.0 0.0 0.0 0.3 1.0 3.0 4.7 0.0 0.0
ant2-m4-d27-y2022 0.12 3.0 0.0 6.0 6.0 1.0 0.0 3.3 0.0 4.3 6.3 1.0 0.0 2.0 0.0 0.0 2.0 0.0 0.0 1.3 0.0 2.0 2.7 0.0 0.0 2.0 0.0 3.0 3.0 0.0 0.0 2.0 0.0 2.0 3.0 0.0 0.0 1.0 0.0 0.0 2.0 0.0 0.0 0.7 0.0 0.7 2.0 0.0 0.0
ant3-m4-d21-y2022 0.48 1.0 3.0 4.0 10.0 17.0 0.0 2.0 3.0 4.3 12.3 17.0 0.0 0.0 2.0 2.0 4.0 15.0 0.0 0.3 1.7 2.7 7.3 15.0 0.0 1.0 1.0 1.0 2.0 3.0 0.0 1.3 1.0 1.7 2.0 3.0 0.0 0.0 0.0 1.0 2.0 3.0 0.0 0.3 0.3 1.0 2.0 3.0 0.0
ant3-m4-d26-y2022 0.48 6.0 3.0 6.0 11.0 4.0 1.0 7.3 4.0 5.7 9.7 4.0 1.0 3.0 2.0 6.0 7.0 0.0 0.0 3.0 2.7 4.7 8.3 0.0 0.0 1.0 2.0 2.0 4.0 3.0 1.0 1.7 2.0 2.3 4.0 3.0 1.0 0.0 0.0 2.0 3.0 0.0 0.0 0.0 0.3 2.0 3.3 0.0 0.0
ant3-m4-d27-y2022 0.12 3.0 6.0 4.0 14.0 5.0 2.0 2.7 6.7 4.7 13.0 5.0 2.0 0.0 0.0 2.0 8.0 0.0 0.0 0.3 1.3 1.7 8.0 0.0 0.0 3.0 4.0 4.0 7.0 3.0 0.0 3.0 3.3 4.0 6.7 3.0 0.0 0.0 0.0 0.0 5.0 0.0 0.0 0.3 0.0 0.0 4.0 0.0 0.0
ant4-m4-d20-y2022 0.12 12.0 14.0 12.0 14.0 14.0 1.0 16.0 12.7 15.7 11.3 14.0 1.0 2.0 3.0 1.0 4.0 4.0 0.0 6.3 3.0 7.0 6.3 4.0 0.0 3.0 3.0 3.0 3.0 3.0 0.0 3.0 2.3 3.0 3.0 3.0 0.0 1.0 0.0 1.0 2.0 2.0 0.0 0.3 0.0 1.0 2.0 2.0 0.0
ant4-m4-d26-y2022 0.48 6.0 17.0 9.0 32.0 6.0 3.0 6.7 17.3 9.7 31.3 6.0 3.0 2.0 4.0 4.0 12.0 1.0 0.0 1.7 8.7 3.0 18.7 1.0 0.0 9.0 7.0 7.0 10.0 6.0 2.0 7.3 6.7 6.3 10.0 6.0 2.0 2.0 2.0 1.0 8.0 0.0 0.0 1.0 2.0 1.3 6.7 0.0 0.0
ant5-m4-d26-y2022 0.48 13.0 9.0 19.0 14.0 8.0 2.0 18.0 8.7 17.0 16.3 8.0 2.0 1.0 1.0 1.0 5.0 0.0 0.0 5.0 3.0 7.7 7.3 0.0 0.0 4.0 4.0 4.0 4.0 3.0 2.0 3.7 3.0 3.0 4.0 3.0 2.0 0.0 1.0 1.0 3.0 0.0 0.0 0.0 1.0 1.3 2.7 0.0 0.0
count mean std min 10% 50% 90% max
drawing
a 436.0 6.2 6.5 1.3 1.8 3.9 12.5 67.5
b 117.0 13.7 3.2 10.1 10.5 13.0 18.8 23.3
regions under 10 um across, by drawing: {'a': 356}
# Variant used below: leave out trials where most of the AL is at least 0.2 (a non-specific change,
# as with the "large motion artifacts" the Methods excluded without giving a rule)
frac_al = pd.Series({i: (al._bin(maps[i]["proj"], al.BIN)[al_masks[sel.loc[i].ant]] >= al.THRESHOLD).mean() for i in sel.index})
print("fraction of AL bins at least 0.2 per trial: median", frac_al.median().round(3), "max", frac_al.max().round(3))
flagged = sel.loc[frac_al[frac_al > 0.5].index, ["ant", "trial", "odor", "conc"]]
print("trials with more than half the AL at least 0.2:", len(flagged))
print(flagged.to_string(index=False))
veh_idx = sel[sel.odor == "vehicle"].index
print("vehicle trials: fraction of AL bins at least 0.2, and the largest frame shift (pixels):")
print(pd.DataFrame({"ant": sel.loc[veh_idx].ant, "AL fraction >= 0.2": frac_al[veh_idx].round(3),
"largest shift": [maps[i]["shift"].max() for i in veh_idx]}).to_string(index=False))
counts_x = []
for ant in alarm_ants:
g = sel[(sel.ant == ant) & ~sel.index.isin(flagged.index)]
for d, v, c in al.READINGS:
for od in al.PHEROMONES:
idx = g[g.odor == od].index
counts_x.append({"ant": ant, "reading": d + v + c, "odor": od,
"count": al.responding(maps, rois[ant, d], idx, v, c)[1] if len(idx) else np.nan})
counts_x = pd.DataFrame(counts_x)
counts_x["higher"] = counts_x.ant.map(conc_pairs.map(max))
fraction of AL bins at least 0.2 per trial: median 0.029 max 0.571
trials with more than half the AL at least 0.2: 1
ant trial odor conc
ant1-m4-d27-y2022 140 heptanone 0.12
vehicle trials: fraction of AL bins at least 0.2, and the largest frame shift (pixels):
ant AL fraction >= 0.2 largest shift
ant1-m4-d21-y2022 0.011 10
ant1-m4-d26-y2022 0.001 12
ant1-m4-d27-y2022 0.024 14
ant2-m4-d20-y2022 0.016 12
ant2-m4-d21-y2022 0.010 10
ant2-m4-d26-y2022 0.135 12
ant2-m4-d27-y2022 0.004 8
ant3-m4-d21-y2022 0.001 8
ant3-m4-d26-y2022 0.002 12
ant3-m4-d27-y2022 0.008 18
ant4-m4-d20-y2022 0.013 18
ant4-m4-d26-y2022 0.001 8
ant5-m4-d26-y2022 0.012 10
fig4b-vehicle-no-response¶
Sparse, unique subsets of glomeruli respond to each alarm pheromone, while the paraffin oil vehicle generates no responses.
Criterion: no ROI responds in any ant's vehicle trial, and each pheromone has at least one responding ROI in most ants at their higher concentration, under every M2 reading; the largest vehicle ROI value is reported.
rows = []
for d, v, c in al.READINGS:
k = d + v + c
sub = counts[counts.reading == k]
veh = sub[sub.odor == "vehicle"]
vmax = []
for ant in alarm_ants:
vi = sel[(sel.ant == ant) & (sel.odor == "vehicle")].index[0]
vals = al.roi_values(maps, rois[ant, d], vi)[0 if v == "i" else 1]
vmax.append(vals.max() if len(vals) else np.nan)
row = {"reading": k, "ants with a vehicle-responding ROI": int((veh["count"] > 0).sum()),
"largest vehicle ROI value": round(float(np.nanmax(vmax)), 3)}
for od in al.PHEROMONES:
row[f"ants with >=1 {od} ROI"] = int((sub[sub.odor == od]["count"] > 0).sum())
rows.append(row)
fig4b = pd.DataFrame(rows)
print("of", len(alarm_ants), "ants")
print(fig4b.to_string(index=False))
ok = (fig4b["ants with a vehicle-responding ROI"] == 0) & np.all([fig4b[f"ants with >=1 {od} ROI"] > len(alarm_ants) / 2 for od in al.PHEROMONES], axis=0)
print("criterion met under readings:", list(fig4b.reading[ok]), "; not met under:", list(fig4b.reading[~ok]))
print("by ant, vehicle counts per reading:")
print(counts[counts.odor == "vehicle"].pivot_table(index="ant", columns="reading", values="count").to_string())
# Variant: the flagged trials (most of the AL at least 0.2) left out, vehicle trials included
print("\nVariant without the flagged trials (vehicle trials flagged:", int(veh_idx.isin(flagged.index).sum()), ")")
rows = []
for d, v, c in al.READINGS:
k = d + v + c
n_veh = 0
for ant in alarm_ants:
vi = sel[(sel.ant == ant) & (sel.odor == "vehicle")].index
vi = vi[~vi.isin(flagged.index)]
if len(vi) and al.responding(maps, rois[ant, d], vi, v, c)[1] > 0:
n_veh += 1
sub = counts_x[counts_x.reading == k]
rows.append({"reading": k, "ants with a vehicle-responding ROI": n_veh,
**{f"ants with >=1 {od} ROI": int((sub[sub.odor == od]["count"] > 0).sum()) for od in al.PHEROMONES}})
fig4b_x = pd.DataFrame(rows)
print(fig4b_x.to_string(index=False))
of 13 ants
reading ants with a vehicle-responding ROI largest vehicle ROI value ants with >=1 heptanone ROI ants with >=1 heptanol ROI ants with >=1 hexanol ROI ants with >=1 6MHO ROI
aix 1 0.654 12 11 13 8
aiy 1 0.654 13 12 13 13
aiix 10 1.531 13 12 13 13
aiiy 10 1.531 13 12 13 13
bix 0 0.110 11 6 13 6
biy 0 0.110 12 9 13 11
biix 7 1.531 13 12 13 13
biiy 7 1.531 13 12 13 13
criterion met under readings: ['biy'] ; not met under: ['aix', 'aiy', 'aiix', 'aiiy', 'bix', 'biix', 'biiy']
by ant, vehicle counts per reading:
reading aiix aiiy aix aiy biix biiy bix biy
ant
ant1-m4-d21-y2022 3.0 3.0 0.0 0.0 2.0 2.0 0.0 0.0
ant1-m4-d26-y2022 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
ant1-m4-d27-y2022 4.0 4.0 0.0 0.0 22.0 22.0 0.0 0.0
ant2-m4-d20-y2022 6.0 6.0 0.0 0.0 4.0 4.0 0.0 0.0
ant2-m4-d21-y2022 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
ant2-m4-d26-y2022 27.0 27.0 5.0 5.0 6.0 6.0 0.0 0.0
ant2-m4-d27-y2022 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
ant3-m4-d21-y2022 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
ant3-m4-d26-y2022 1.0 1.0 0.0 0.0 1.0 1.0 0.0 0.0
ant3-m4-d27-y2022 2.0 2.0 0.0 0.0 0.0 0.0 0.0 0.0
ant4-m4-d20-y2022 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
ant4-m4-d26-y2022 3.0 3.0 0.0 0.0 2.0 2.0 0.0 0.0
ant5-m4-d26-y2022 2.0 2.0 0.0 0.0 2.0 2.0 0.0 0.0
Variant without the flagged trials (vehicle trials flagged: 0 )
reading ants with a vehicle-responding ROI ants with >=1 heptanone ROI ants with >=1 heptanol ROI ants with >=1 hexanol ROI ants with >=1 6MHO ROI
aix 1 12 11 13 8
aiy 1 13 12 13 13
aiix 10 13 12 13 13
aiiy 10 13 12 13 13
bix 0 11 6 13 6
biy 0 12 9 13 11
biix 7 13 12 13 13
biiy 7 13 12 13 13
Result: not reproduced. Only biy meets the criterion. Under bix no ant's vehicle trial has a responding ROI (largest vehicle ROI value 0.110), but heptanol and 6MHO each have a responding ROI in only 6 of 13 ants, not most; under biy the vehicle gives none and the four pheromones give 12, 9, 13 and 11 of 13. Under the readings with the ROI maximum (ii) 7 to 10 ants have a vehicle-responding ROI (largest value 1.531), and under aix and aiy one ant does (ant2-m4-d26-y2022, 5 ROIs), whose vehicle trial has 13.5% of the AL at 0.2 or more, against at most 2.4% in the other vehicle trials. No vehicle trial is flagged as a non-specific change (more than half the AL), so the variant without the flagged trial gives the same vehicle counts.
figs5a-sparse-median-6¶
Each alarm pheromone activates few of the roughly 500 glomeruli; even at the highest concentration the median number of responding glomeruli is at most 6.
Criterion: counts per ant from M2 (each reading); concentration read as (a) 48% (8 ants) or (b) each ant's higher concentration (13 ants). Reproduced when, under a reading, the median over ants is at most 6.6 for every pheromone.
def medians(cnt):
out = []
for k in [d + v + c for d, v, c in al.READINGS]:
for conc_reading, ants in (("48%", [a for a in alarm_ants if conc_pairs[a][-1] == 0.48]), ("higher", alarm_ants)):
sub = cnt[(cnt.reading == k) & cnt.ant.isin(ants) & cnt.odor.isin(al.PHEROMONES)]
med = sub.groupby("odor")["count"].median()
out.append({"M2 reading": k, "concentration": conc_reading, "n ants": len(ants),
**{od: med[od] for od in al.PHEROMONES}, "largest median": med.max(),
"<= 6.6": bool(med.max() <= 6.6)})
return pd.DataFrame(out)
s5a = medians(counts)
print(s5a.to_string(index=False))
print("readings with every median at most 6.6:", int(s5a["<= 6.6"].sum()), "of", len(s5a))
s5a_x = medians(counts_x)
print("\nVariant without the", len(flagged), "trials where most of the AL is at least 0.2:")
print(s5a_x[["M2 reading", "concentration", "largest median", "<= 6.6"]].to_string(index=False))
print("\nPer ant, reading bix (b, mean value, Methods counting):")
print(counts[(counts.reading == "bix") & counts.odor.isin(al.PHEROMONES)].pivot_table(index=["ant", "higher"], columns="odor", values="count").to_string())
M2 reading concentration n ants heptanone heptanol hexanol 6MHO largest median <= 6.6
aix 48% 8 3.000000 2.000000 6.000000 1.000000 6.000000 True
aix higher 13 2.000000 2.000000 5.000000 1.000000 5.000000 True
aiy 48% 8 4.166667 3.000000 7.833333 2.333333 7.833333 False
aiy higher 13 4.666667 3.000000 8.000000 2.000000 8.000000 False
aiix 48% 8 8.000000 6.500000 14.000000 6.000000 14.000000 False
aiix higher 13 9.000000 8.000000 14.000000 6.000000 14.000000 False
aiiy 48% 8 10.000000 7.000000 14.333333 7.000000 14.333333 False
aiiy higher 13 10.333333 7.333333 13.000000 6.666667 13.000000 False
bix 48% 8 1.000000 1.000000 3.000000 0.000000 3.000000 True
bix higher 13 1.000000 0.000000 3.000000 0.000000 3.000000 True
biy 48% 8 1.333333 0.833333 3.166667 0.333333 3.166667 True
biy higher 13 1.000000 0.333333 3.000000 0.333333 3.000000 True
biix 48% 8 3.500000 3.500000 4.000000 3.000000 4.000000 True
biix higher 13 3.000000 3.000000 4.000000 3.000000 4.000000 True
biiy 48% 8 3.166667 3.166667 4.000000 2.500000 4.000000 True
biiy higher 13 3.000000 3.000000 4.000000 2.666667 4.000000 True
readings with every median at most 6.6: 10 of 16
Variant without the 1 trials where most of the AL is at least 0.2:
M2 reading concentration largest median <= 6.6
aix 48% 6.000000 True
aix higher 5.000000 True
aiy 48% 7.833333 False
aiy higher 8.000000 False
aiix 48% 14.000000 False
aiix higher 14.000000 False
aiiy 48% 14.333333 False
aiiy higher 13.000000 False
bix 48% 3.000000 True
bix higher 3.000000 True
biy 48% 3.166667 True
biy higher 3.000000 True
biix 48% 4.000000 True
biix higher 4.000000 True
biiy 48% 4.000000 True
biiy higher 4.000000 True
Per ant, reading bix (b, mean value, Methods counting):
odor 6MHO heptanol heptanone hexanol
ant higher
ant1-m4-d21-y2022 0.48 1.0 1.0 2.0 3.0
ant1-m4-d26-y2022 0.48 1.0 2.0 1.0 3.0
ant1-m4-d27-y2022 0.12 1.0 2.0 4.0 10.0
ant2-m4-d20-y2022 0.12 0.0 0.0 1.0 3.0
ant2-m4-d21-y2022 0.48 0.0 0.0 1.0 2.0
ant2-m4-d26-y2022 0.48 0.0 2.0 3.0 5.0
ant2-m4-d27-y2022 0.12 1.0 0.0 0.0 2.0
ant3-m4-d21-y2022 0.48 0.0 0.0 1.0 2.0
ant3-m4-d26-y2022 0.48 0.0 0.0 2.0 3.0
ant3-m4-d27-y2022 0.12 0.0 0.0 0.0 5.0
ant4-m4-d20-y2022 0.12 1.0 0.0 1.0 2.0
ant4-m4-d26-y2022 0.48 2.0 2.0 1.0 8.0
ant5-m4-d26-y2022 0.48 0.0 1.0 1.0 3.0
Result: reproduced, resting on the adopted M2 reading. Under bix the largest median is 3 (hexanol) at 48% (8 ants) and at each ant's higher concentration (13 ants). The four readings b and aix keep every median at or below 6.6 (largest 3 to 6); aiy (7.8 to 8.0) and aiix and aiiy (13 to 14.3) do not. The readings disagree, so the outcome rests on the adopted reading, the ROI's mean value (ImageJ's default measure) with the Methods' trial-mean rule (x), under which both drawings meet the criterion. Leaving out the one flagged trial changes nothing.
figs5b-shared-glomerulus¶
The three compounds that elicit panic alarm activate partly overlapping sets of glomeruli, with a single glomerulus, PGb, activated by all three.
Criterion: per ant, the number of M2 ROIs responding to all three (heptanone, heptanol, hexanol) at (a) each ant's higher concentration or (b) 48%. Reproduced when, under each M2 reading and under (a) or (b), the median over ants is 1 and more than half of the ants have exactly one such ROI. A response of one ROI to two odors counts as one glomerulus only at the same depth (Methods).
def shared_counts(exclude=None):
rows = []
for ant in alarm_ants:
for d, v, c in al.READINGS:
if exclude is not None:
g = sel[(sel.ant == ant) & ~sel.index.isin(exclude)]
R = {od: al.responding(maps, rois[ant, d], g[g.odor == od].index, v, c)[0] for od in al.PHEROMONES}
else:
R = {od: resp[ant, d, v, c, od] for od in al.PHEROMONES}
D = {od: depth[ant, d, od] for od in al.PHEROMONES}
three = R["heptanone"] & R["heptanol"] & R["hexanol"] & al.same_depth(D["heptanone"], D["heptanol"]) \
& al.same_depth(D["heptanone"], D["hexanol"]) & al.same_depth(D["heptanol"], D["hexanol"])
mho = R["6MHO"] & np.logical_or.reduce([R[o] & al.same_depth(D["6MHO"], D[o]) for o in ("heptanone", "heptanol", "hexanol")])
mho_hk = R["6MHO"] & np.logical_or.reduce([R[o] & al.same_depth(D["6MHO"], D[o]) for o in ("heptanone", "hexanol")])
rows.append({"ant": ant, "reading": d + v + c, "shared3": int(three.sum()),
"pairs": {f"{a}+{b}": int((R[a] & R[b]).sum()) for a, b in (("heptanone", "heptanol"), ("heptanone", "hexanol"), ("heptanol", "hexanol"))},
"mho_any": int(mho.sum()), "mho_heptanone_or_hexanol": int(mho_hk.sum())})
out = pd.DataFrame(rows)
out["higher"] = out.ant.map(conc_pairs.map(max))
return out
sh = shared_counts()
def judge_shared(sh):
rows = []
for k in sh.reading.unique():
for conc_reading in ("higher", "48%"):
sub = sh[(sh.reading == k) & ((sh.higher == 0.48) if conc_reading == "48%" else True)]
rows.append({"M2 reading": k, "concentration": conc_reading, "n ants": len(sub),
"median": sub.shared3.median(), "ants with exactly 1": int((sub.shared3 == 1).sum()),
"ants with 0": int((sub.shared3 == 0).sum()), "ants with >1": int((sub.shared3 > 1).sum()),
"met": bool(sub.shared3.median() == 1 and (sub.shared3 == 1).sum() > len(sub) / 2)})
return pd.DataFrame(rows)
s5b = judge_shared(sh)
print(s5b.to_string(index=False))
print("\nPer ant, ROIs responding to all three:")
print(sh.pivot_table(index=["ant", "higher"], columns="reading", values="shared3").to_string())
print("\nPairwise overlaps (ROIs responding to both), reading bix:")
print(pd.DataFrame(list(sh[sh.reading == "bix"].pairs), index=sh[sh.reading == "bix"].ant).to_string())
sh_x = shared_counts(exclude=flagged.index)
print("\nVariant without the flagged trials:")
print(judge_shared(sh_x)[["M2 reading", "concentration", "median", "ants with exactly 1", "met"]].to_string(index=False))
M2 reading concentration n ants median ants with exactly 1 ants with 0 ants with >1 met
aix higher 13 1.0 5 6 2 False
aix 48% 8 1.0 4 2 2 False
aiy higher 13 1.0 6 5 2 False
aiy 48% 8 1.0 4 2 2 False
aiix higher 13 2.0 3 1 9 False
aiix 48% 8 2.5 1 0 7 False
aiiy higher 13 2.0 2 1 10 False
aiiy 48% 8 2.0 1 0 7 False
bix higher 13 0.0 4 8 1 False
bix 48% 8 1.0 4 3 1 False
biy higher 13 0.0 5 8 0 False
biy 48% 8 1.0 5 3 0 True
biix higher 13 2.0 3 3 7 False
biix 48% 8 2.0 2 0 6 False
biiy higher 13 2.0 4 2 7 False
biiy 48% 8 2.0 2 0 6 False
Per ant, ROIs responding to all three:
reading aiix aiiy aix aiy biix biiy bix biy
ant higher
ant1-m4-d21-y2022 0.48 2.0 2.0 1.0 1.0 2.0 2.0 1.0 1.0
ant1-m4-d26-y2022 0.48 3.0 3.0 2.0 2.0 3.0 3.0 1.0 1.0
ant1-m4-d27-y2022 0.12 3.0 2.0 0.0 1.0 5.0 5.0 0.0 0.0
ant2-m4-d20-y2022 0.12 2.0 2.0 0.0 0.0 0.0 0.0 0.0 0.0
ant2-m4-d21-y2022 0.48 2.0 2.0 0.0 0.0 1.0 1.0 0.0 0.0
ant2-m4-d26-y2022 0.48 6.0 4.0 2.0 1.0 4.0 4.0 2.0 1.0
ant2-m4-d27-y2022 0.12 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
ant3-m4-d21-y2022 0.48 1.0 1.0 0.0 0.0 1.0 1.0 0.0 0.0
ant3-m4-d26-y2022 0.48 2.0 2.0 1.0 2.0 2.0 2.0 0.0 0.0
ant3-m4-d27-y2022 0.12 1.0 2.0 0.0 0.0 0.0 1.0 0.0 0.0
ant4-m4-d20-y2022 0.12 1.0 1.0 1.0 1.0 1.0 1.0 0.0 0.0
ant4-m4-d26-y2022 0.48 3.0 3.0 1.0 1.0 3.0 3.0 1.0 1.0
ant5-m4-d26-y2022 0.48 3.0 2.0 1.0 1.0 2.0 2.0 1.0 1.0
Pairwise overlaps (ROIs responding to both), reading bix:
heptanone+heptanol heptanone+hexanol heptanol+hexanol
ant
ant1-m4-d21-y2022 1 2 1
ant1-m4-d26-y2022 1 1 1
ant1-m4-d27-y2022 0 2 0
ant2-m4-d20-y2022 0 0 0
ant2-m4-d21-y2022 0 0 0
ant2-m4-d26-y2022 2 3 2
ant2-m4-d27-y2022 0 0 0
ant3-m4-d21-y2022 0 1 0
ant3-m4-d26-y2022 0 1 0
ant3-m4-d27-y2022 0 0 0
ant4-m4-d20-y2022 0 1 0
ant4-m4-d26-y2022 1 1 2
ant5-m4-d26-y2022 1 1 1
Variant without the flagged trials:
M2 reading concentration median ants with exactly 1 met
aix higher 1.0 5 False
aix 48% 1.0 4 False
aiy higher 1.0 6 False
aiy 48% 1.0 4 False
aiix higher 2.0 3 False
aiix 48% 2.5 1 False
aiiy higher 2.0 2 False
aiiy 48% 2.0 1 False
bix higher 0.0 4 False
bix 48% 1.0 4 False
biy higher 0.0 5 False
biy 48% 1.0 5 True
biix higher 2.0 3 False
biix 48% 2.0 2 False
biiy higher 2.0 4 False
biiy 48% 2.0 2 False
Result: not reproduced. Only biy at 48% meets the criterion (median 1, exactly one shared ROI in 5 of 8 ants). Under bix the median is 0 at the higher concentration (exactly one in 4 of 13 ants, none in 8) and 1 at 48% (exactly one in 4 of 8, not more than half). aix and aiy give a median of 1 with exactly one in 5 or 6 of 13 ants, and the readings ii medians of 2 to 2.5. Under bix heptanone and hexanol share an ROI in 9 of 13 ants, so the overlap is partial and the three-way overlap is often missing. Leaving out the flagged trial gives the same.
figs5c-mho-rarely-overlaps¶
6MHO only rarely and inconsistently activates glomeruli shared with 4-methyl-3-heptanone or 4-methyl-3-hexanol.
Criterion: per ant, at (a) its higher concentration or (b) 48%, the number of M2 ROIs responding to 6MHO and to any other pheromone (overlaps at different depths excluded). Reproduced when, under each M2 reading and under (a) or (b), the median over ants is 0 and the count is non-zero in fewer than half of the ants.
def judge_mho(sh, col):
rows = []
for k in sh.reading.unique():
for conc_reading in ("higher", "48%"):
sub = sh[(sh.reading == k) & ((sh.higher == 0.48) if conc_reading == "48%" else True)]
rows.append({"M2 reading": k, "concentration": conc_reading, "n ants": len(sub), "median": sub[col].median(),
"ants with >0": int((sub[col] > 0).sum()),
"met": bool(sub[col].median() == 0 and (sub[col] > 0).sum() < len(sub) / 2)})
return pd.DataFrame(rows)
s5c = judge_mho(sh, "mho_any")
print("6MHO and any other pheromone (the criterion):")
print(s5c.to_string(index=False))
print("\nThe criterion as written (met under each M2 reading), per concentration reading:")
for conc_reading in ("higher", "48%"):
s_ = s5c[s5c.concentration == conc_reading]
print(f" {conc_reading}: met under {int(s_.met.sum())} of {len(s_)} readings; under every reading: {bool(s_.met.all())};",
"fails under", ", ".join(s_[~s_.met]["M2 reading"]) or "none")
print("\n6MHO and heptanone or hexanol (the statement's wording):")
print(judge_mho(sh, "mho_heptanone_or_hexanol").to_string(index=False))
print("\nPer ant, ROIs shared by 6MHO and another pheromone:")
print(sh.pivot_table(index=["ant", "higher"], columns="reading", values="mho_any").to_string())
print("\nVariant without the flagged trials:")
print(judge_mho(sh_x, "mho_any")[["M2 reading", "concentration", "median", "ants with >0", "met"]].to_string(index=False))
6MHO and any other pheromone (the criterion):
M2 reading concentration n ants median ants with >0 met
aix higher 13 0.0 1 True
aix 48% 8 0.0 1 True
aiy higher 13 0.0 2 True
aiy 48% 8 0.0 2 True
aiix higher 13 2.0 11 False
aiix 48% 8 3.0 6 False
aiiy higher 13 2.0 11 False
aiiy 48% 8 3.0 6 False
bix higher 13 0.0 1 True
bix 48% 8 0.0 1 True
biy higher 13 0.0 0 True
biy 48% 8 0.0 0 True
biix higher 13 1.0 11 False
biix 48% 8 1.0 6 False
biiy higher 13 1.0 11 False
biiy 48% 8 1.0 6 False
The criterion as written (met under each M2 reading), per concentration reading:
higher: met under 4 of 8 readings; under every reading: False; fails under aiix, aiiy, biix, biiy
48%: met under 4 of 8 readings; under every reading: False; fails under aiix, aiiy, biix, biiy
6MHO and heptanone or hexanol (the statement's wording):
M2 reading concentration n ants median ants with >0 met
aix higher 13 0.0 1 True
aix 48% 8 0.0 1 True
aiy higher 13 0.0 2 True
aiy 48% 8 0.0 2 True
aiix higher 13 2.0 10 False
aiix 48% 8 2.0 5 False
aiiy higher 13 1.0 10 False
aiiy 48% 8 1.5 5 False
bix higher 13 0.0 1 True
bix 48% 8 0.0 1 True
biy higher 13 0.0 0 True
biy 48% 8 0.0 0 True
biix higher 13 1.0 10 False
biix 48% 8 1.0 5 False
biiy higher 13 1.0 10 False
biiy 48% 8 1.0 5 False
Per ant, ROIs shared by 6MHO and another pheromone:
reading aiix aiiy aix aiy biix biiy bix biy
ant higher
ant1-m4-d21-y2022 0.48 2.0 2.0 0.0 1.0 1.0 1.0 0.0 0.0
ant1-m4-d26-y2022 0.48 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
ant1-m4-d27-y2022 0.12 6.0 5.0 0.0 0.0 27.0 24.0 0.0 0.0
ant2-m4-d20-y2022 0.12 2.0 2.0 0.0 0.0 1.0 1.0 0.0 0.0
ant2-m4-d21-y2022 0.48 4.0 4.0 0.0 0.0 1.0 1.0 0.0 0.0
ant2-m4-d26-y2022 0.48 10.0 11.0 0.0 0.0 4.0 5.0 0.0 0.0
ant2-m4-d27-y2022 0.12 1.0 1.0 0.0 0.0 1.0 1.0 0.0 0.0
ant3-m4-d21-y2022 0.48 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
ant3-m4-d26-y2022 0.48 2.0 1.0 0.0 0.0 1.0 1.0 0.0 0.0
ant3-m4-d27-y2022 0.12 1.0 1.0 0.0 0.0 2.0 1.0 0.0 0.0
ant4-m4-d20-y2022 0.12 6.0 7.0 0.0 0.0 3.0 3.0 0.0 0.0
ant4-m4-d26-y2022 0.48 4.0 4.0 1.0 1.0 5.0 5.0 1.0 0.0
ant5-m4-d26-y2022 0.48 5.0 6.0 0.0 0.0 2.0 2.0 0.0 0.0
Variant without the flagged trials:
M2 reading concentration median ants with >0 met
aix higher 0.0 1 True
aix 48% 0.0 1 True
aiy higher 0.0 2 True
aiy 48% 0.0 2 True
aiix higher 2.0 11 False
aiix 48% 3.0 6 False
aiiy higher 2.0 11 False
aiiy 48% 3.0 6 False
bix higher 0.0 1 True
bix 48% 0.0 1 True
biy higher 0.0 0 True
biy 48% 0.0 0 True
biix higher 1.0 11 False
biix 48% 1.0 6 False
biiy higher 1.0 11 False
biiy 48% 1.0 6 False
Result: not reproduced. The criterion asks for the result under each M2 reading, and the four readings with the ROI's maximum (aiix, aiiy, biix, biiy) fail at both concentration readings (printed above: met under 4 of 8 readings, under every reading False). At the higher concentration their medians are 1 to 3, with shared ROIs in 11 of 13 ants. The four readings with the ROI's mean value (aix, aiy, bix, biy) meet the criterion: under bix the median number of ROIs shared by 6MHO and another pheromone is 0, with any in 1 of 13 ants (1 of 8 at 48%), and under the four mean-value readings shared ROIs occur in 0 to 2 ants. With heptanone or hexanol only (the statement's wording), and without the flagged trial, the result is the same. In round 1 the outcome was reproduced, resting on the ROI mean; that set aside the criterion's own wording, which figs5b-shared-glomerulus and fig4b-vehicle-no-response are judged by, and the third verification round agreed to the change. Under drawing a the maximum-value readings call a response from one pixel of at least 0.2 in mostly sub-glomerular regions, which makes overlaps common (one ant, ant1-m4-d27, has 27 shared ROIs under biix and none under bix); the failure may come from that reading as much as from the data.
fig6-shared-panic-pathway (imaging half)¶
The three pheromones with overlapping AL responses all elicit panic alarm, while 6MHO does not elicit panic and gives a non-overlapping response.
Criterion: reproduced in its imaging half when figs5b-shared-glomerulus and figs5c-mho-rarely-overlaps are reproduced; the behaviour half rests on fig1e-categorical-response, whose data are not in the dataset.
both = s5b.merge(s5c, on=["M2 reading", "concentration"], suffixes=(" shared3", " mho"))
print(both[["M2 reading", "concentration", "met shared3", "met mho"]].to_string(index=False))
print("figs5b met under every M2 reading at the higher concentration:", bool(s5b[s5b.concentration == "higher"].met.all()),
"; at 48%:", bool(s5b[s5b.concentration == "48%"].met.all()))
print("figs5c met under every M2 reading at the higher concentration:", bool(s5c[s5c.concentration == "higher"].met.all()),
"; at 48%:", bool(s5c[s5c.concentration == "48%"].met.all()))
M2 reading concentration met shared3 met mho
aix higher False True
aix 48% False True
aiy higher False True
aiy 48% False True
aiix higher False False
aiix 48% False False
aiiy higher False False
aiiy 48% False False
bix higher False True
bix 48% False True
biy higher False True
biy 48% True True
biix higher False False
biix 48% False False
biiy higher False False
biiy 48% False False
figs5b met under every M2 reading at the higher concentration: False ; at 48%: False
figs5c met under every M2 reading at the higher concentration: False ; at 48%: False
Result: not reproduced, imaging half only. figs5b-shared-glomerulus is met under no reading at the higher concentration and only under biy at 48%; figs5c-mho-rarely-overlaps is not reproduced: it is met under the four mean-value readings and fails under the four maximum-value readings. The behaviour half rests on fig1e-categorical-response, whose data are not in the dataset.
fig4-no-decreases¶
Odors cause no fluorescence decreases; only small non-specific decreases from shifts in AL position and photobleaching occur.
Criterion: per-plane peak fold change maps of the first look (stabilization, 1-sigma Gaussian blur); reproduced when no glomerulus-sized region (10 to 20 um) reaches -0.2 or below in at least two trials of the same odor and concentration without doing so in that ant's vehicle trial. Only each ant's higher concentration is mapped here. A region is counted when it is at least 10 um across (the lower end of a glomerulus); larger regions are counted too, and their sizes are shown. "Doing so in the vehicle trial" is read as at least half of the region at -0.2 or below in the vehicle map of the same plane.
min_bins = np.pi * (al.GLOM_MIN_UM / 2) ** 2 / (al.PIXEL_UM * al.BIN) ** 2
dec = []
for ant in alarm_ants:
g = sel[sel.ant == ant]
f0mean = np.mean([maps[i]["f0"] for i in g.index], axis=0)
from skimage.filters import threshold_otsu
thr = threshold_otsu(np.log1p(f0mean.max(0)))
vi = g[g.odor == "vehicle"].index[0]
for od in al.PHEROMONES + ["isopropanol"]:
idx = g[g.odor == od].index
if len(idx) < 2:
continue
blur = np.stack([maps[i]["blur"] for i in idx])
for p in range(al.N_PLANES):
plane_mask = np.log1p(f0mean[p]) > thr
edge_um = ndimage.distance_transform_edt(plane_mask) * al.PIXEL_UM * al.BIN
neg = ((blur[:, p] <= -al.THRESHOLD).sum(0) >= 2) & plane_mask
lab, n = ndimage.label(neg)
for k in range(1, n + 1):
reg = lab == k
if reg.sum() < min_bins:
continue
in_vehicle = (maps[vi]["blur"][p][reg] <= -al.THRESHOLD).mean() >= 0.5
dec.append({"ant": ant, "odor": od, "plane": p, "diameter_um": 2 * np.sqrt(reg.sum() / np.pi) * al.PIXEL_UM * al.BIN,
"mean_value": float(blur[:, p][:, reg].mean()), "in_vehicle": bool(in_vehicle),
"edge_um": float(edge_um[reg].min()), "mask": reg})
dec = pd.DataFrame(dec, columns=["ant", "odor", "plane", "diameter_um", "mean_value", "in_vehicle", "edge_um", "mask"])
odor_dec = dec[~dec.in_vehicle]
print("glomerulus-sized regions at -0.2 or below in >=2 trials of one odor:", len(dec),
"; of these also in the vehicle trial:", int(dec.in_vehicle.sum()), "; odor-specific:", len(odor_dec))
print("odor-specific regions per ant and odor:")
print(odor_dec.groupby(["ant", "odor"]).size().unstack(fill_value=0).to_string() if len(odor_dec) else "none")
if len(odor_dec):
print("diameters (um): median", odor_dec.diameter_um.median().round(1), "range", odor_dec.diameter_um.min().round(1), "to", odor_dec.diameter_um.max().round(1))
print("regions of 10 to 20 um:", int(odor_dec.diameter_um.between(10, 20).sum()))
print("most negative odor-specific region:")
print(odor_dec.drop(columns="mask").sort_values("mean_value").head(5).round(3).to_string(index=False))
print("ants with at least one:", odor_dec.ant.nunique(), "of", len(alarm_ants))
# Where the odor-specific negative regions lie: on the AL border (motion moves the edge of the
# fluorescent lobe), and in the same place for other odors (a decrease specific to an odor would not be)
if len(odor_dec):
other = []
for k, r in odor_dec.iterrows():
same = odor_dec[(odor_dec.ant == r.ant) & (odor_dec.plane == r.plane) & (odor_dec.odor != r.odor)]
u = np.logical_or.reduce(list(same["mask"])) if len(same) else np.zeros_like(r["mask"])
other.append((u & r["mask"]).sum() / r["mask"].sum())
odor_dec = odor_dec.assign(overlap_other_odor=other)
print("odor-specific regions overlapping a negative region of another odor (same ant and plane):",
int((odor_dec.overlap_other_odor > 0).sum()), "of", len(odor_dec), "; by half or more:", int((odor_dec.overlap_other_odor >= 0.5).sum()))
print("distance of the region's nearest bin to the AL border (um): median", round(odor_dec.edge_um.median(), 1),
"; regions touching the border (within one bin, 1.2 um):", int((odor_dec.edge_um <= al.PIXEL_UM * al.BIN).sum()),
"; within 5 um:", int((odor_dec.edge_um <= 5).sum()), "of", len(odor_dec))
# reference: how often a 10 um region centred at random in a plane's AL mask comes within 5 um of the border
ref = []
for ant in alarm_ants:
g = sel[sel.ant == ant]
f0mean = np.mean([maps[i]["f0"] for i in g.index], axis=0)
thr = threshold_otsu(np.log1p(f0mean.max(0)))
for p in range(al.N_PLANES):
pm = np.log1p(f0mean[p]) > thr
if pm.sum() > 100:
ref.append((ndimage.distance_transform_edt(pm)[pm] * al.PIXEL_UM * al.BIN <= 5 + al.GLOM_MIN_UM / 2).mean())
print("reference: fraction of AL bins within 10 um of the border (a 10 um region centred there comes within 5 um), mean over planes:", round(float(np.mean(ref)), 3))
print("by odor: regions, overlapping another odor's, touching the border")
print(odor_dec.groupby("odor").agg(n=("plane", "size"), overlap=("overlap_other_odor", lambda s: int((s > 0).sum())),
border=("edge_um", lambda s: int((s <= al.PIXEL_UM * al.BIN).sum()))).to_string())
glomerulus-sized regions at -0.2 or below in >=2 trials of one odor: 762 ; of these also in the vehicle trial: 168 ; odor-specific: 594
odor-specific regions per ant and odor:
odor 6MHO heptanol heptanone hexanol
ant
ant1-m4-d21-y2022 24 9 6 3
ant1-m4-d26-y2022 33 1 10 7
ant1-m4-d27-y2022 39 40 5 7
ant2-m4-d20-y2022 0 6 0 2
ant2-m4-d21-y2022 2 17 15 7
ant2-m4-d26-y2022 36 23 15 2
ant2-m4-d27-y2022 2 9 7 6
ant3-m4-d21-y2022 6 10 3 0
ant3-m4-d26-y2022 1 0 11 1
ant3-m4-d27-y2022 11 18 18 7
ant4-m4-d20-y2022 17 8 4 15
ant4-m4-d26-y2022 1 41 11 33
ant5-m4-d26-y2022 22 16 4 3
diameters (um): median 12.4 range 10.0 to 33.6
regions of 10 to 20 um: 540
most negative odor-specific region:
ant odor plane diameter_um mean_value in_vehicle edge_um
ant4-m4-d26-y2022 hexanol 3 22.844 -0.452 False 1.155
ant4-m4-d26-y2022 hexanol 9 22.394 -0.440 False 1.155
ant4-m4-d26-y2022 heptanol 0 18.346 -0.440 False 1.155
ant4-m4-d26-y2022 hexanol 8 24.427 -0.437 False 1.155
ant4-m4-d20-y2022 6MHO 3 24.704 -0.429 False 1.155
ants with at least one: 13 of 13
odor-specific regions overlapping a negative region of another odor (same ant and plane): 384 of 594 ; by half or more: 302 distance of the region's nearest bin to the AL border (um): median 1.2 ; regions touching the border (within one bin, 1.2 um): 487 ; within 5 um: 564 of 594
reference: fraction of AL bins within 10 um of the border (a 10 um region centred there comes within 5 um), mean over planes: 0.83
by odor: regions, overlapping another odor's, touching the border
n overlap border
odor
6MHO 194 105 161
heptanol 198 131 165
heptanone 109 72 76
hexanol 93 76 85
Result: not reproduced. 594 regions at least 10 um across (540 of them 10 to 20 um) reach -0.2 or below in at least two trials of one odor without doing so in the vehicle trial, in all 13 ants; the most negative has a mean of -0.452 (ant4-m4-d26-y2022, hexanol, plane 3). Where they lie points to motion, the source the paper names for small non-specific decreases: 487 of the 594 touch the AL border (within one 1.2 um bin) and 564 lie within 5 um of it, against 83% of the AL area expected for a 10 um region placed at random; and 384 overlap a negative region of another odor in the same plane (302 by half or more). The criterion counts every such region, and its vehicle comparison rests on one vehicle trial per ant.
fig4-large-lasting-responses¶
Fluorescence increases to alarm pheromones are often large (1 to 2-fold change) and outlast the 5 s odor pulse.
Criterion: over responding ROI-trials (M2) at each ant's higher concentration, the median ROI peak fold change lies in 0.9 to 2.2, under each ROI value reading (i) and (ii); and in more than half of those ROI-trials dF/F0 at the first volume after the pulse ends (8.4 s, volume 8) is still above the baseline mean plus 3 SD. A responding ROI-trial is an ROI region whose value in that trial is at least 0.2 (counting y). The trace is the ROI's mean raw fluorescence in the plane of its depth.
rows = []
for ant in alarm_ants:
g = sel[(sel.ant == ant) & sel.odor.isin(al.PHEROMONES)]
for d in "ab":
for r_ in rois[ant, d]:
for i in g.index:
vi_, vii_ = al.roi_values(maps, [r_], i)
fr = maps[i]["frames"][r_["depth"]].astype(np.float32) # volumes 1-5 and 8
f = fr[:, r_["mask"]].mean(1)
dff = f / f[:5].mean() - 1
lasting = dff[5] > dff[:5].mean() + 3 * dff[:5].std(ddof=1)
rows.append({"ant": ant, "drawing": d, "odor": sel.loc[i].odor, "vi": vi_[0], "vii": vii_[0], "lasting": lasting})
lr = pd.DataFrame(rows)
out = []
for d in "ab":
for v in ("vi", "vii"):
sub = lr[(lr.drawing == d) & (lr[v] >= al.THRESHOLD)]
out.append({"drawing": d, "value": v[1:], "responding ROI-trials": len(sub), "median peak": round(sub[v].median(), 3),
"IQR": (round(sub[v].quantile(0.25), 3), round(sub[v].quantile(0.75), 3)),
"fraction in 1-2": round(sub[v].between(1, 2).mean(), 3),
"fraction lasting past 8 s": round(sub.lasting.mean(), 3), "ants": sub.ant.nunique(),
"median in 0.9-2.2": bool(0.9 <= sub[v].median() <= 2.2), "lasting > half": bool(sub.lasting.mean() > 0.5)})
lrt = pd.DataFrame(out)
print(lrt.to_string(index=False))
print("\nBy ant (drawing b, value i):")
sub = lr[(lr.drawing == "b") & (lr.vi >= al.THRESHOLD)]
print(sub.groupby("ant").agg(n=("vi", "size"), median=("vi", "median"), lasting=("lasting", "mean")).round(3).to_string())
drawing value responding ROI-trials median peak IQR fraction in 1-2 fraction lasting past 8 s ants median in 0.9-2.2 lasting > half
a i 1159 0.377 (0.278, 0.544) 0.063 0.619 13 False True
a ii 2460 0.573 (0.336, 1.114) 0.189 0.439 13 False False
b i 445 0.453 (0.307, 0.638) 0.088 0.703 13 False True
b ii 1086 1.016 (0.53, 1.736) 0.327 0.413 13 True False
By ant (drawing b, value i):
n median lasting
ant
ant1-m4-d21-y2022 31 0.393 0.742
ant1-m4-d26-y2022 37 0.659 0.946
ant1-m4-d27-y2022 140 0.417 0.350
ant2-m4-d20-y2022 13 0.453 0.769
ant2-m4-d21-y2022 15 0.313 0.800
ant2-m4-d26-y2022 63 0.479 0.810
ant2-m4-d27-y2022 10 0.354 1.000
ant3-m4-d21-y2022 16 0.333 1.000
ant3-m4-d26-y2022 17 0.425 1.000
ant3-m4-d27-y2022 14 0.411 0.714
ant4-m4-d20-y2022 14 0.430 0.857
ant4-m4-d26-y2022 57 0.642 0.912
ant5-m4-d26-y2022 18 0.334 0.889
Result: not reproduced. No reading meets both parts. With glomerulus-sized ROIs (b) the median peak fold change of responding ROI-trials is 0.453 with the mean value (445 ROI-trials, 70.3% still elevated at 8.4 s) and 1.016 with the maximum (1,086 ROI-trials, 41.3% elevated): the first median is below 0.9 and the second reading fails the lasting part. With drawing a the medians are 0.377 and 0.573. Under the four readings 6.3 to 32.7% of responding ROI-trials lie within 1 to 2.
fig5ab-focal-glomeruli¶
PGb lies in the anterior ventral hemi-lobe next to a gap without glomeruli, about halfway between the dorsal and ventral AL surfaces, with two neighbouring alarm-responsive glomeruli in the same plane; PGb and 6G were found in 13 of 13 ants and PGa in 11 of 13.
Criterion: identification by M4 on each ant's higher-concentration ROIs (M2, each reading). Reproduced when the counts are within tolerance (12 or 13, 12 or 13, 10 to 12) and, in most ants with PGb, its centre plane lies in the middle third of the AL depth (from the top plane to the deepest plane with AL fluorescence). "Adjacent" (M4) is read as centres at most 20 um apart; the deepest plane with AL fluorescence as the deepest where the mean baseline fluorescence inside the AL mask is at least 25% of the brightest plane's (10% and 50% shown).
focal, idrows = {}, []
for ant in alarm_ants:
hi = sel[(sel.ant == ant) & sel.odor.isin(al.PHEROMONES)]
f0prof = np.mean([maps[i]["f0"][:, al_masks[ant]].mean(1) for i in sel[sel.ant == ant].index], axis=0)
deepest = {q: int(np.max(np.where(f0prof >= q * f0prof.max())[0])) for q in (0.1, 0.25, 0.5)}
for d, v, c in al.READINGS:
f = al.identify_focal(maps, rois[ant, d], hi, v, c)
focal[ant, d + v + c] = f
row = {"ant": ant, "reading": d + v + c, "PGb": f["PGb"] is not None, "6G": f["6G"] is not None,
"PGa": f["PGa"] is not None, "PGb candidates": f["n_pgb_candidates"]}
if f["PGb"] is not None:
p = f["depth"]["PGb"]
row.update({"PGb plane": p, "AL deepest plane": deepest[0.25], "PGb depth fraction": round(p / deepest[0.25], 2)})
for q in (0.1, 0.25, 0.5):
row[f"middle third ({int(q * 100)}%)"] = deepest[q] / 3 <= p <= 2 * deepest[q] / 3
row["6G plane"] = f["depth"].get("6G")
row["PGa plane"] = f["depth"].get("PGa")
idrows.append(row)
ident = pd.DataFrame(idrows)
summ = ident.groupby("reading").agg(PGb=("PGb", "sum"), G6=("6G", "sum"), PGa=("PGa", "sum"),
middle_third=("middle third (25%)", "sum"),
middle_third_10=("middle third (10%)", "sum"), middle_third_50=("middle third (50%)", "sum"))
summ["counts within tolerance"] = (summ.PGb >= 12) & (summ.G6 >= 12) & summ.PGa.between(10, 12)
summ["PGb in middle third in most"] = summ.middle_third > summ.PGb / 2
print(summ.to_string())
print("\nPer ant, reading bix:")
print(ident[ident.reading == "bix"].drop(columns=["reading"]).to_string(index=False))
MAIN = "bix"
# A second identification for the traces: the reading that identifies PGa in the most ants (then PGb and 6G)
ALT = (summ.PGa * 1000 + summ.PGb * 10 + summ.G6).idxmax()
print("\nsingle-glomerulus traces are computed under", MAIN, "(the adopted M2 reading) and", ALT,
"(the reading that identifies PGa in the most ants):", summ.loc[ALT, ["PGb", "G6", "PGa"]].to_dict())
PGb G6 PGa middle_third middle_third_10 middle_third_50 counts within tolerance PGb in middle third in most
reading
aiix 12 10 3 3 11 1 False False
aiiy 12 10 4 3 11 0 False False
aix 7 4 2 1 7 0 False False
aiy 8 3 2 2 7 1 False False
biix 10 9 2 2 10 1 False False
biiy 11 9 2 3 10 2 False False
bix 5 2 0 1 5 0 False False
biy 5 1 0 1 5 0 False False
Per ant, reading bix:
ant PGb 6G PGa PGb candidates PGb plane AL deepest plane PGb depth fraction middle third (10%) middle third (25%) middle third (50%) 6G plane PGa plane
ant1-m4-d21-y2022 True True False 1 19.0 25.0 0.76 True False False 20.0 NaN
ant1-m4-d26-y2022 True True False 1 17.0 22.0 0.77 True False False 18.0 NaN
ant1-m4-d27-y2022 False False False 0 NaN NaN NaN NaN NaN NaN NaN NaN
ant2-m4-d20-y2022 False False False 0 NaN NaN NaN NaN NaN NaN NaN NaN
ant2-m4-d21-y2022 False False False 0 NaN NaN NaN NaN NaN NaN NaN NaN
ant2-m4-d26-y2022 True False False 2 18.0 28.0 0.64 True True False NaN NaN
ant2-m4-d27-y2022 False False False 0 NaN NaN NaN NaN NaN NaN NaN NaN
ant3-m4-d21-y2022 False False False 0 NaN NaN NaN NaN NaN NaN NaN NaN
ant3-m4-d26-y2022 False False False 0 NaN NaN NaN NaN NaN NaN NaN NaN
ant3-m4-d27-y2022 False False False 0 NaN NaN NaN NaN NaN NaN NaN NaN
ant4-m4-d20-y2022 False False False 0 NaN NaN NaN NaN NaN NaN NaN NaN
ant4-m4-d26-y2022 True False False 1 17.0 24.0 0.71 True False False NaN NaN
ant5-m4-d26-y2022 True False False 1 20.0 26.0 0.77 True False False NaN NaN
single-glomerulus traces are computed under bix (the adopted M2 reading) and aiiy (the reading that identifies PGa in the most ants): {'PGb': 12, 'G6': 10, 'PGa': 4}
Result: not reproduced. No reading gives all three counts within tolerance: PGb is identified in 5 to 12 of 13 ants, 6G in 1 to 10 and PGa in 0 to 4 (bix 5, 2, 0; aiix 12, 10, 3). With the deepest plane at 25% of the brightest plane's AL fluorescence, PGb lies in the middle third of the depth in 1 to 3 ants per reading (bix 1 of 5, the others at 0.71 to 0.77 of the depth); with a 10% rule, in 5 to 11.
fig5d-stereotyped-positions¶
PGa is always anterior (12.9 +/- 1.9 SD um) and slightly lateral (5.1 +/- 2.9 um) to PGb, and 6G always lateral to PGb (13.1 +/- 2.6 um) at a similar anterior-posterior position (0.6 +/- 2.2 um).
Criterion: one plane per ant (PGb's), PGb at (0, 0); centres (a) the centroid of each glomerulus's pixels of at least 0.2 to its defining odor in that plane, inside its ROI, or (b) its peak pixel; the plane's map from (c) one higher-concentration trial (the first) or (d) the mean over that ant's higher-concentration trials. Reproduced when, under every reading, the four means are within tolerance, PGa is anterior and 6G lateral in every ant, and each SD is below 10 um. Glomeruli are identified by M4 under the main M2 reading (b, i, x: glomerulus-sized ROIs, ImageJ's mean, the Methods' counting); the other M2 readings are shown as variants. M4 identifies PGa and 6G by their direction from PGb's ROI, but the centres here are taken again on the defining odors' maps in PGb's plane, so the "every ant" conditions are tested, not given by construction.
DEFINING = {"PGb": ["heptanone", "heptanol", "hexanol"], "6G": ["6MHO"], "PGa": ["heptanol", "hexanol"]}
def centres(ant, f, rd_centre, rd_plane, d):
hi = sel[(sel.ant == ant) & sel.odor.isin(al.PHEROMONES)]
p = f["depth"]["PGb"]
masks = al.group_masks(rois[ant, d])
out = {}
for name, odors in DEFINING.items():
if f[name] is None:
continue
idx = [hi[hi.odor == o].index for o in odors]
if rd_plane == "c":
m = np.mean([maps[ix[0]]["planes"][p] for ix in idx], axis=0)
else:
m = np.mean([maps[i]["planes"][p] for ix in idx for i in ix], axis=0)
roi = ndimage.binary_dilation(masks[f[name]], iterations=2)
if rd_centre == "a":
px = roi & (m >= al.THRESHOLD)
if not px.any():
px = roi
out[name] = al.to_full(ndimage.center_of_mass(np.where(px, m, 0)))
else:
out[name] = al.to_full(np.unravel_index(np.argmax(np.where(roi, m, -np.inf)), m.shape))
return out
pos = []
for reading_m2 in [d + v + c for d, v, c in al.READINGS]:
for ant in alarm_ants:
f = focal[ant, reading_m2]
if f["PGb"] is None:
continue
for rc in "ab":
for rp in "cd":
cs = centres(ant, f, rc, rp, reading_m2[0])
for name in ("PGa", "6G"):
if name in cs:
a, l = al.offsets_um(cs["PGb"], cs[name])
pos.append({"M2": reading_m2, "centre": rc, "plane": rp, "ant": ant, "glom": name, "anterior": a, "lateral": l})
pos = pd.DataFrame(pos)
EXPECTED = {("PGa", "anterior"): (12.9, 1.9), ("PGa", "lateral"): (5.1, 2.9), ("6G", "lateral"): (13.1, 2.6), ("6G", "anterior"): (0.6, 2.2)}
def judge_pos(p):
rows = []
for (m2, rc, rp), g in p.groupby(["M2", "centre", "plane"]):
row = {"M2": m2, "reading": rc + rp}
ok = True
for (glom, axis), (val, tol) in EXPECTED.items():
s = g[g.glom == glom][axis]
row[f"{glom} {axis}"] = f"{s.mean():.1f} +/- {s.std():.1f} (n={len(s)})"
ok &= bool(abs(s.mean() - val) <= tol and s.std() < 10) if len(s) > 1 else False
row["PGa anterior in all"] = bool((g[g.glom == "PGa"].anterior > 0).all())
row["6G lateral in all"] = bool((g[g.glom == "6G"].lateral > 0).all())
row["met"] = ok and row["PGa anterior in all"] and row["6G lateral in all"]
rows.append(row)
return pd.DataFrame(rows)
jp = judge_pos(pos)
print("Main M2 reading (bix):")
print(jp[jp.M2 == MAIN].to_string(index=False))
print("\nPer ant, bix, centre a, plane d (um):")
print(pos[(pos.M2 == MAIN) & (pos.centre == "a") & (pos.plane == "d")].pivot_table(index="ant", columns="glom", values=["anterior", "lateral"]).round(1).to_string())
print("\nReadings where a direction condition fails (PGa anterior, 6G lateral in every ant):")
print(jp[~(jp["PGa anterior in all"] & jp["6G lateral in all"])][["M2", "reading", "PGa anterior in all", "6G lateral in all"]].to_string(index=False))
print("\nOther M2 readings:")
print(jp[jp.M2 != MAIN].to_string(index=False))
# Orientation check: baseline fluorescence in PGb's plane for one ant where all three are identified
# under ALT, with the centres marked (rows: up is lateral; columns: right is anterior, as the files say)
ant_o = next((a for a in alarm_ants if all(focal[a, ALT][n] is not None for n in ("PGb", "6G", "PGa"))), None)
if ant_o is not None:
f_o = focal[ant_o, ALT]
p_o = f_o["depth"]["PGb"]
img = np.mean([maps[i]["f0"][p_o] for i in sel[sel.ant == ant_o].index], axis=0)
fig, ax = plt.subplots(1, 2, figsize=(9, 4.5))
ax[0].imshow(img, cmap="gray")
hi = sel[(sel.ant == ant_o) & sel.odor.isin(al.PHEROMONES)]
ax[1].imshow(np.mean([maps[i]["planes"][p_o] for i in hi[hi.odor.isin(["heptanol", "hexanol"])].index], axis=0), cmap="magma", vmin=0, vmax=1)
for a_ in ax:
for n, col in (("PGb", "c"), ("6G", "lime"), ("PGa", "y")):
r_, c_ = f_o["centroid"][n]
a_.plot(c_ / al.BIN, r_ / al.BIN, "o", mfc="none", mec=col, ms=8)
a_.text(c_ / al.BIN + 4, r_ / al.BIN, n, color=col, fontsize=8)
a_.set_xlabel("column (anterior to the right)"); a_.set_ylabel("row (lateral up)")
ax[0].set_title(f"{ant_o}, plane {p_o}: baseline F0"); ax[1].set_title("heptanol and hexanol peak map (0 to 1)")
plt.tight_layout(); plt.show()
print(ant_o, "plane", p_o, "centres (row, column, full-resolution pixels):",
{n: tuple(round(float(x), 1) for x in f_o["centroid"][n]) for n in ("PGb", "6G", "PGa")})
print("binned F0 image range", round(float(img.min()), 1), "to", round(float(img.max()), 1))
Main M2 reading (bix):
M2 reading PGa anterior PGa lateral 6G lateral 6G anterior PGa anterior in all 6G lateral in all met
bix ac nan +/- nan (n=0) nan +/- nan (n=0) 14.2 +/- 2.8 (n=2) 0.1 +/- 4.0 (n=2) True True False
bix ad nan +/- nan (n=0) nan +/- nan (n=0) 12.9 +/- 1.3 (n=2) 1.6 +/- 6.6 (n=2) True True False
bix bc nan +/- nan (n=0) nan +/- nan (n=0) 15.6 +/- 7.4 (n=2) 1.7 +/- 2.5 (n=2) True True False
bix bd nan +/- nan (n=0) nan +/- nan (n=0) 13.3 +/- 4.1 (n=2) 3.5 +/- 4.9 (n=2) True True False
Per ant, bix, centre a, plane d (um):
anterior lateral
glom 6G 6G
ant
ant1-m4-d21-y2022 -3.0 12.0
ant1-m4-d26-y2022 6.3 13.8
Readings where a direction condition fails (PGa anterior, 6G lateral in every ant):
M2 reading PGa anterior in all 6G lateral in all
aiix bd True False
aiiy ad True False
aiiy bc False True
aiiy bd True False
aiy ad True False
aiy bd True False
biix ac True False
biix bd False True
biiy ac True False
biiy bd False True
Other M2 readings:
M2 reading PGa anterior PGa lateral 6G lateral 6G anterior PGa anterior in all 6G lateral in all met
aiix ac 15.7 +/- 2.6 (n=3) 0.6 +/- 6.5 (n=3) 12.5 +/- 3.2 (n=10) -1.3 +/- 6.1 (n=10) True True False
aiix ad 15.7 +/- 2.6 (n=3) 2.1 +/- 5.4 (n=3) 12.2 +/- 3.7 (n=10) -1.6 +/- 6.2 (n=10) True True False
aiix bc 18.5 +/- 0.0 (n=3) 0.4 +/- 10.7 (n=3) 12.8 +/- 6.2 (n=10) 1.0 +/- 5.2 (n=10) True True False
aiix bd 16.9 +/- 2.7 (n=3) 3.9 +/- 8.7 (n=3) 11.0 +/- 6.8 (n=10) -0.2 +/- 5.2 (n=10) True False False
aiiy ac 12.2 +/- 7.6 (n=4) 1.9 +/- 10.0 (n=4) 11.9 +/- 3.3 (n=10) -1.4 +/- 6.3 (n=10) True True False
aiiy ad 12.3 +/- 6.5 (n=4) 0.5 +/- 13.6 (n=4) 11.5 +/- 5.1 (n=10) -1.7 +/- 6.3 (n=10) True False False
aiiy bc 11.3 +/- 10.4 (n=4) 3.8 +/- 10.4 (n=4) 11.8 +/- 5.3 (n=10) 0.7 +/- 6.0 (n=10) False True False
aiiy bd 12.4 +/- 8.2 (n=4) 2.6 +/- 14.0 (n=4) 10.2 +/- 7.7 (n=10) -0.6 +/- 5.8 (n=10) True False False
aix ac 17.1 +/- 1.1 (n=2) 2.5 +/- 8.0 (n=2) 10.9 +/- 4.8 (n=4) 2.6 +/- 3.7 (n=4) True True False
aix ad 17.2 +/- 0.7 (n=2) 4.7 +/- 4.4 (n=2) 11.3 +/- 2.1 (n=4) 2.8 +/- 4.2 (n=4) True True False
aix bc 18.5 +/- 0.0 (n=2) 3.5 +/- 13.1 (n=2) 10.4 +/- 7.4 (n=4) 4.3 +/- 3.8 (n=4) True True False
aix bd 18.5 +/- 0.0 (n=2) 6.9 +/- 9.8 (n=2) 10.4 +/- 4.7 (n=4) 4.3 +/- 3.0 (n=4) True True False
aiy ac 17.1 +/- 1.1 (n=2) 2.5 +/- 8.0 (n=2) 11.6 +/- 4.2 (n=3) -2.0 +/- 11.3 (n=3) True True False
aiy ad 17.2 +/- 0.7 (n=2) 4.7 +/- 4.4 (n=2) 7.6 +/- 8.0 (n=3) -0.2 +/- 10.6 (n=3) True False False
aiy bc 18.5 +/- 0.0 (n=2) 3.5 +/- 13.1 (n=2) 12.3 +/- 7.7 (n=3) -1.5 +/- 14.0 (n=3) True True False
aiy bd 18.5 +/- 0.0 (n=2) 6.9 +/- 9.8 (n=2) 8.5 +/- 8.8 (n=3) -0.4 +/- 10.7 (n=3) True False False
biix ac 10.3 +/- 8.5 (n=2) 8.8 +/- 1.0 (n=2) 11.3 +/- 5.6 (n=9) -0.4 +/- 3.6 (n=9) True False False
biix ad 10.8 +/- 8.4 (n=2) 9.1 +/- 1.9 (n=2) 11.9 +/- 5.1 (n=9) 0.2 +/- 3.4 (n=9) True True False
biix bc 13.3 +/- 7.4 (n=2) 9.8 +/- 4.1 (n=2) 13.1 +/- 6.9 (n=9) -0.9 +/- 5.4 (n=9) True True False
biix bd 9.2 +/- 13.1 (n=2) 7.5 +/- 9.0 (n=2) 10.9 +/- 5.4 (n=9) 0.1 +/- 5.0 (n=9) False True False
biiy ac 10.3 +/- 8.5 (n=2) 8.8 +/- 1.0 (n=2) 11.3 +/- 5.6 (n=9) -0.4 +/- 3.6 (n=9) True False False
biiy ad 10.8 +/- 8.4 (n=2) 9.1 +/- 1.9 (n=2) 11.9 +/- 5.1 (n=9) 0.2 +/- 3.4 (n=9) True True False
biiy bc 13.3 +/- 7.4 (n=2) 9.8 +/- 4.1 (n=2) 13.1 +/- 6.9 (n=9) -0.9 +/- 5.4 (n=9) True True False
biiy bd 9.2 +/- 13.1 (n=2) 7.5 +/- 9.0 (n=2) 10.9 +/- 5.4 (n=9) 0.1 +/- 5.0 (n=9) False True False
biy ac nan +/- nan (n=0) nan +/- nan (n=0) 16.2 +/- nan (n=1) 2.9 +/- nan (n=1) True True False
biy ad nan +/- nan (n=0) nan +/- nan (n=0) 13.8 +/- nan (n=1) 6.3 +/- nan (n=1) True True False
biy bc nan +/- nan (n=0) nan +/- nan (n=0) 20.8 +/- nan (n=1) 3.5 +/- nan (n=1) True True False
biy bd nan +/- nan (n=0) nan +/- nan (n=0) 16.2 +/- nan (n=1) 6.9 +/- nan (n=1) True True False
ant1-m4-d26-y2022 plane 17 centres (row, column, full-resolution pixels): {'PGb': (272.0, 319.5), '6G': (221.1, 334.6), 'PGa': (253.5, 375.8)}
binned F0 image range 4.4 to 6195.8
Result: not reproduced. No reading meets the criterion, so the readings do not disagree on the outcome. Under bix no PGa is identified, and 6G (2 ants) lies 12.9 to 15.6 um lateral and 0.1 to 3.5 um anterior of PGb on average. Under the other readings PGa (2 to 4 ants) lies 9.2 to 18.5 um anterior of PGb on average (expected 12.9 +/- 1.9) with SDs up to 13.1 um, and 0.4 to 9.8 um lateral with SDs up to 14.0 um; 6G lies 7.6 to 20.8 um lateral (1 to 10 ants) with SDs of 1.3 to 8.8 um. M4 picks PGa and 6G by their direction from PGb, but the centres taken again here do not always keep it: PGa is not anterior in every ant under aiiy bc, biix bd and biiy bd, and 6G is not lateral in every ant under aiix bd, aiiy ad and bd, aiy ad and bd, biix ac and biiy ac. In the baseline image of ant1-m4-d26-y2022, PGa lies to the right of PGb (anterior) and 6G above it (lateral), the arrangement of the paper's Figure 5B under the files' orientation.
Single-glomerulus traces (M3)¶
For each ant where PGb was identified under the main M2 reading (bix) or under the reading that
identifies PGa in the most ants (ALT, printed above), every pheromone trial at both concentrations,
the vehicle trial and the 144 s recordings are read as row bands around the focal glomeruli, on PGb's,
6G's and PGa's planes +-2. Each plane's band is stabilized as the maps are (phase correlation against
the baseline mean), as the claims file's M3 assumes; the unstabilized disc trace is kept to show what
this changes. The centre plane of each glomerulus is chosen per trial among its higher-concentration
depth +-1, (a) where its area of at least 0.2 is largest or (b) where its disc's peak fold change is
highest. The ROI is the claims file's 15 um disc on the centre ("disc") or the region of at least 0.2
there ("region"). Every claim below is shown under both identifications, both plane readings and
both ROIs. Volume times are (index - 1) x 1.205 s plus the centre plane's offset within the volume
scan (PLANE_DT x plane, from the files' starting_time).
from concurrent.futures import ThreadPoolExecutor
m3_trials = alarm[alarm.odor.isin(al.PHEROMONES + ["vehicle"])]
IDS = [MAIN] + ([ALT] if ALT != MAIN else [])
jobs = []
for ant in alarm_ants:
fs = {k: focal[ant, k] for k in IDS if focal[ant, k]["PGb"] is not None}
if not fs:
continue
rows_band = al.band_for([c for f in fs.values() for c in f["centroid"].values()])
for idx, r in m3_trials[m3_trials.ant == ant].iterrows():
jobs.append((idx, r, rows_band, fs))
print(len(jobs), "trials for M3, from", len({j[1].ant for j in jobs}), "ants")
t = time.time()
def run(job):
idx, r, rows_band, fs = job
return idx, al.trial_m3(r.blobs, r.nvol, rows_band, fs)
with ThreadPoolExecutor(6) as ex:
m3 = dict(ex.map(run, jobs))
print(f"M3 in {time.time() - t:.0f} s")
trace_rows, shift_rows = [], []
for idx, res in m3.items():
r = m3_trials.loc[idx]
for p, s_ in res["_shift"].items():
shift_rows.append({"ant": r.ant, "trial": r.trial, "odor": r.odor, "nvol": r.nvol, "plane": p,
"max shift px": int(np.abs(s_).max()), "shifts": s_})
for ident in IDS:
for name, rr in res.get(ident, {}).items():
for (rd, roi), v in rr.items():
trace_rows.append({"ident": ident, "ant": r.ant, "trial": r.trial, "odor": r.odor, "conc": r.conc, "nvol": r.nvol,
"glom": name, "plane_reading": rd, "roi": roi, "plane": v["plane"], "peak": v["peak"],
"roi_px": v["roi_px"], "t0": v["plane"] * PLANE_DT, "trace": v["trace"]})
tr_all = pd.DataFrame(trace_rows)
shifts_m3 = pd.DataFrame(shift_rows)
tr = tr_all[tr_all.roi != "raw"]
print(tr.groupby(["ident", "glom", "plane_reading", "roi"]).size().unstack(["plane_reading", "roi"]).to_string())
print("ants with traces:", tr.groupby(["ident", "glom"]).ant.nunique().unstack().to_string())
print("region ROI size (pixels; the disc has", int(tr[tr.roi == "disc"].roi_px.iloc[0]), "): median",
int(tr[tr.roi == "region"].roi_px.median()), "; trials where no disc pixel reached 0.2, so the disc was used:",
int((tr[tr.roi == "region"].roi_px.values == tr[tr.roi == "disc"].roi_px.values).sum()), "of", int((tr.roi == "region").sum()))
305 trials for M3, from 12 ants
M3 in 630 s
plane_reading a b
roi disc region disc region
ident glom
aiiy 6G 255 255 255 255
PGa 102 102 102 102
PGb 305 305 305 305
bix 6G 51 51 51 51
PGb 129 129 129 129
ants with traces: glom 6G PGa PGb
ident
aiiy 10.0 4.0 12.0
bix 2.0 NaN 5.0
region ROI size (pixels; the disc has 2119 ): median 565 ; trials where no disc pixel reached 0.2, so the disc was used: 618 of 1684
# Clock check: the odor-evoked rise follows the stimulus at 3 s in pheromone trials and is absent in the
# vehicle trials (PGb, MAIN identification, plane reading b, disc, 40-volume trials at the higher concentration)
t40 = np.arange(40) * al.VOLUME_S
sub = tr[(tr.ident == MAIN) & (tr.glom == "PGb") & (tr.plane_reading == "b") & (tr.roi == "disc") & (tr.nvol == 40)]
odor_tr = np.stack(sub[sub.odor.isin(["heptanone", "heptanol", "hexanol"]) & (sub.conc == sub.ant.map(conc_pairs.map(max)))].trace)
veh_tr = np.stack(sub[sub.odor == "vehicle"].trace)
print("volume time (s): ", np.round(t40[:14], 1).tolist())
print("PGb mean dF/F0, odor: ", np.round(odor_tr.mean(0)[:14].astype(float), 3).tolist(), "n =", len(odor_tr))
print("PGb mean dF/F0, vehicle:", np.round(veh_tr.mean(0)[:14].astype(float), 3).tolist(), "n =", len(veh_tr))
diff = odor_tr.mean(0) - veh_tr.mean(0)
print("first volume where odor minus vehicle exceeds 0.1:", int(np.argmax(diff > 0.1)) + 1, "at", round(t40[np.argmax(diff > 0.1)], 1), "s")
print("odor minus vehicle over volumes 1-3 (before 3 s):", np.round(diff[:3].astype(float), 3).tolist())
# Motion in the row bands: the shift of each volume against the baseline mean, and what stabilization changes
mx = shifts_m3.groupby(["ant", "trial"])["max shift px"].max()
print("\nlargest shift per trial in the M3 bands (pixels; 0.289 um each): median", mx.median(), "max", mx.max(),
"; trials over 10 pixels:", int((mx > 10).sum()), "of", len(mx))
fig, ax = plt.subplots(1, 2, figsize=(11, 3))
ex = shifts_m3[shifts_m3.nvol == 40].sort_values("max shift px")
for k in list(range(0, len(ex), max(1, len(ex) // 3)))[:3] + [len(ex) - 1]:
s_ = ex.iloc[k]
ax[0].plot(np.arange(1, 41), np.hypot(*s_.shifts.T) * al.PIXEL_UM, label=f"{s_.ant[:9]} t{s_.trial} {s_.odor} plane {s_.plane}")
ax[0].axvspan(3.5, 6.5, color="0.9"); ax[0].set_xlabel("volume"); ax[0].set_ylabel("shift (um)"); ax[0].legend(fontsize=6)
st = tr_all[(tr_all.ident == MAIN) & (tr_all.glom == "PGb") & (tr_all.plane_reading == "b") & tr_all.roi.isin(["disc", "raw"])]
pv = st.pivot_table(index=["ant", "trial"], columns="roi", values="peak")
print("PGb peak fold change (MAIN, plane b, disc), stabilized against not: median absolute difference",
round(float((pv.disc - pv.raw).abs().median()), 3), "; largest", round(float((pv.disc - pv.raw).abs().max()), 3),
"; r =", round(float(np.corrcoef(pv.disc, pv.raw)[0, 1]), 3), "; trials on different sides of 0.2:",
int(((pv.disc >= 0.2) != (pv.raw >= 0.2)).sum()), "of", len(pv))
k_ = (pv.disc - pv.raw).abs().idxmax()
for roi, sty in (("disc", "k"), ("raw", "r--")):
ax[1].plot(t40, st[(st.ant == k_[0]) & (st.trial == k_[1]) & (st.roi == roi)].trace.iloc[0][:40], sty, label=f"{roi} ({'stabilized' if roi == 'disc' else 'not stabilized'})")
ax[1].set_title(f"PGb, {k_[0][:9]} trial {k_[1]}: the largest difference", fontsize=8); ax[1].set_xlabel("s"); ax[1].legend(fontsize=7)
plt.tight_layout(); plt.show()
volume time (s): [0.0, 1.2, 2.4, 3.6, 4.8, 6.0, 7.2, 8.4, 9.6, 10.8, 12.0, 13.3, 14.5, 15.7] PGb mean dF/F0, odor: [0.006, -0.009, -0.013, -0.009, 0.026, 0.198, 0.39, 0.622, 0.771, 0.808, 0.833, 0.84, 0.811, 0.792] n = 45 PGb mean dF/F0, vehicle: [-0.002, -0.013, 0.049, 0.003, -0.036, -0.031, -0.048, -0.065, -0.095, -0.072, -0.096, -0.117, -0.135, -0.107] n = 5 first volume where odor minus vehicle exceeds 0.1: 6 at 6.0 s odor minus vehicle over volumes 1-3 (before 3 s): [0.008, 0.004, -0.061]
largest shift per trial in the M3 bands (pixels; 0.289 um each): median 10.0 max 44 ; trials over 10 pixels: 142 of 305 PGb peak fold change (MAIN, plane b, disc), stabilized against not: median absolute difference 0.035 ; largest 0.591 ; r = 0.98 ; trials on different sides of 0.2: 3 of 129
fig5c-s6-tuning¶
PGb, PGa and 6G each respond to a unique combination of pheromones, with several pheromone and glomerulus pairs giving no response; PGb responds to heptanone, heptanol and hexanol and not to 6MHO, PGa to heptanol and hexanol, and 6G to 6MHO.
Criterion: peak fold change per trial from M3, all concentrations; readings (a) a linear model with individual as a fixed factor, (b) a linear mixed model with a random intercept per ant; concentration log10-transformed. A combination responds when the prediction at 48% is at least 0.2 with its 95% CI above 0, and gives no response when the 95% CI at 48% includes 0. Reproduced when the six named combinations respond, PGb to 6MHO does not, and the three glomeruli's sets of responding pheromones all differ.
x48 = np.log10(0.48)
fits = []
for ident in IDS:
for roi in ("disc", "region"):
for rd in "ab":
for glom in ("PGb", "PGa", "6G"):
for od in al.PHEROMONES:
d_ = tr[(tr.ident == ident) & (tr.roi == roi) & (tr.glom == glom) & (tr.odor == od) & (tr.plane_reading == rd) & (tr.nvol == 40)].copy()
d_["lc"] = np.log10(d_.conc)
row = {"ident": ident, "roi": roi, "plane": rd, "glom": glom, "odor": od, "n trials": len(d_), "n ants": d_.ant.nunique()}
if d_.ant.nunique() < 2:
fits.append(row)
continue
# (a) fixed ant effects, sum coding: intercept + slope*x is the prediction for the average ant
fa = smf.ols("peak ~ lc + C(ant, Sum)", d_).fit()
xv = np.zeros(len(fa.params)); xv[0] = 1; xv[list(fa.params.index).index("lc")] = x48
pa = float(xv @ fa.params); sa = float(np.sqrt(xv @ fa.cov_params().values @ xv))
q = stats.t.ppf(0.975, fa.df_resid)
row.update({"(a) pred": pa, "(a) lo": pa - q * sa, "(a) hi": pa + q * sa})
try:
fb = smf.mixedlm("peak ~ lc", d_, groups=d_.ant).fit(reml=True)
fe = fb.fe_params; cv = fb.cov_params().loc[fe.index, fe.index].values
xb = np.array([1, x48]); pb = float(xb @ fe); sb = float(np.sqrt(xb @ cv @ xb))
row.update({"(b) pred": pb, "(b) lo": pb - 1.96 * sb, "(b) hi": pb + 1.96 * sb, "(b) converged": bool(fb.converged)})
except Exception as e: # noqa: BLE001
row.update({"(b) pred": np.nan, "(b) lo": np.nan, "(b) hi": np.nan, "error": str(e)[:60]})
fits.append(row)
fits = pd.DataFrame(fits)
for m in ("(a)", "(b)"):
fits[f"{m} responds"] = (fits[f"{m} pred"] >= 0.2) & (fits[f"{m} lo"] > 0)
fits[f"{m} no response"] = (fits[f"{m} lo"] <= 0) & (fits[f"{m} hi"] >= 0)
print("fits with fewer than 2 ants (not fitted):", int(fits["(a) pred"].isna().sum()), "of", len(fits), "; mixed-model fits that failed:",
int(fits["error"].notna().sum()) if "error" in fits else 0, "; mixed-model fits not converged:", int((fits["(b) converged"] == False).sum()))
print(fits.drop(columns=[c for c in ["error"] if c in fits]).round(3).to_string(index=False))
NAMED = [("PGb", "heptanone"), ("PGb", "heptanol"), ("PGb", "hexanol"), ("PGa", "heptanol"), ("PGa", "hexanol"), ("6G", "6MHO")]
tun = []
for ident in IDS:
for roi in ("disc", "region"):
for rd in "ab":
for m in ("(a)", "(b)"):
f_ = fits[(fits.ident == ident) & (fits.roi == roi) & (fits.plane == rd)].set_index(["glom", "odor"]).reindex(
pd.MultiIndex.from_product([["PGb", "PGa", "6G"], al.PHEROMONES]))
f_[[f"{m} responds", f"{m} no response"]] = f_[[f"{m} responds", f"{m} no response"]].fillna(False).astype(bool)
named = [bool(f_.loc[k, f"{m} responds"]) for k in NAMED]
pgb_mho = bool(f_.loc[("PGb", "6MHO"), f"{m} no response"])
sets = {g: frozenset(o for o in al.PHEROMONES if f_.loc[(g, o), f"{m} responds"]) for g in ("PGb", "PGa", "6G")}
distinct = len(set(sets.values())) == 3
tun.append({"ident": ident, "roi": roi, "plane": rd, "model": m, "named responding": f"{sum(named)}/6",
"which": "".join("1" if x else "0" for x in named), "PGb-6MHO none": pgb_mho,
"PGb set": ",".join(sorted(sets["PGb"])), "PGa set": ",".join(sorted(sets["PGa"])),
"6G set": ",".join(sorted(sets["6G"])), "sets differ": distinct,
"met": all(named) and pgb_mho and distinct})
tun = pd.DataFrame(tun)
print("\n'which' marks the six named combinations in the order", NAMED)
print(tun.to_string(index=False))
fits with fewer than 2 ants (not fitted): 16 of 96 ; mixed-model fits that failed: 0 ; mixed-model fits not converged: 9
ident roi plane glom odor n trials n ants (a) pred (a) lo (a) hi (b) pred (b) lo (b) hi (b) converged (a) responds (a) no response (b) responds (b) no response
bix disc a PGb heptanone 30 5 0.851 0.742 0.960 0.849 0.583 1.115 True True False True False
bix disc a PGb heptanol 30 5 0.678 0.570 0.786 0.676 0.395 0.957 True True False True False
bix disc a PGb hexanol 30 5 0.734 0.653 0.816 0.733 0.453 1.014 True True False True False
bix disc a PGb 6MHO 30 5 -0.087 -0.141 -0.032 -0.084 -0.148 -0.021 True False False False False
bix disc a PGa heptanone 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix disc a PGa heptanol 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix disc a PGa hexanol 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix disc a PGa 6MHO 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix disc a 6G heptanone 12 2 -0.178 -0.229 -0.126 -0.178 -0.221 -0.136 False False False False False
bix disc a 6G heptanol 12 2 -0.159 -0.185 -0.132 -0.160 -0.205 -0.114 True False False False False
bix disc a 6G hexanol 12 2 -0.093 -0.131 -0.055 -0.095 -0.154 -0.035 True False False False False
bix disc a 6G 6MHO 12 2 0.335 0.213 0.456 0.328 0.166 0.491 True True False True False
bix disc b PGb heptanone 30 5 0.883 0.774 0.992 0.881 0.569 1.194 True True False True False
bix disc b PGb heptanol 30 5 0.684 0.594 0.773 0.683 0.391 0.974 True True False True False
bix disc b PGb hexanol 30 5 0.849 0.760 0.938 0.848 0.492 1.205 True True False True False
bix disc b PGb 6MHO 30 5 -0.078 -0.128 -0.028 -0.076 -0.140 -0.012 True False False False False
bix disc b PGa heptanone 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix disc b PGa heptanol 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix disc b PGa hexanol 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix disc b PGa 6MHO 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix disc b 6G heptanone 12 2 -0.148 -0.200 -0.096 -0.151 -0.208 -0.094 False False False False False
bix disc b 6G heptanol 12 2 -0.110 -0.155 -0.066 -0.115 -0.155 -0.074 False False False False False
bix disc b 6G hexanol 12 2 -0.063 -0.112 -0.014 -0.065 -0.149 0.019 True False False False True
bix disc b 6G 6MHO 12 2 0.359 0.242 0.476 0.355 0.140 0.570 True True False True False
bix region a PGb heptanone 30 5 1.785 1.661 1.909 1.789 1.542 2.036 True True False True False
bix region a PGb heptanol 30 5 1.617 1.416 1.818 1.620 1.218 2.023 True True False True False
bix region a PGb hexanol 30 5 1.575 1.418 1.731 1.575 1.392 1.757 True True False True False
bix region a PGb 6MHO 30 5 0.211 -0.058 0.480 0.219 -0.104 0.542 True False True False True
bix region a PGa heptanone 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix region a PGa heptanol 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix region a PGa hexanol 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix region a PGa 6MHO 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix region a 6G heptanone 12 2 0.248 -0.666 1.162 0.315 -0.609 1.239 True False True False True
bix region a 6G heptanol 12 2 -0.159 -0.185 -0.132 -0.160 -0.205 -0.114 True False False False False
bix region a 6G hexanol 12 2 0.665 -0.229 1.560 0.676 -0.062 1.414 True False True False True
bix region a 6G 6MHO 12 2 0.991 0.573 1.409 0.991 0.646 1.336 True True False True False
bix region b PGb heptanone 30 5 1.797 1.655 1.938 1.799 1.544 2.055 True True False True False
bix region b PGb heptanol 30 5 1.621 1.464 1.778 1.622 1.237 2.008 True True False True False
bix region b PGb hexanol 30 5 1.722 1.574 1.870 1.721 1.411 2.032 True True False True False
bix region b PGb 6MHO 30 5 0.290 0.022 0.558 0.313 0.018 0.607 True True False True False
bix region b PGa heptanone 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix region b PGa heptanol 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix region b PGa hexanol 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix region b PGa 6MHO 0 0 NaN NaN NaN NaN NaN NaN NaN False False False False
bix region b 6G heptanone 12 2 0.020 -0.660 0.700 0.075 -0.560 0.710 False False True False True
bix region b 6G heptanol 12 2 -0.126 -0.391 0.139 -0.144 -0.417 0.129 True False True False True
bix region b 6G hexanol 12 2 0.252 -0.542 1.047 0.263 -0.393 0.918 False False True False True
bix region b 6G 6MHO 12 2 1.058 0.655 1.460 1.056 0.724 1.389 False True False True False
aiiy disc a PGb heptanone 72 12 0.596 0.518 0.673 0.605 0.498 0.711 True True False True False
aiiy disc a PGb heptanol 72 12 0.327 0.271 0.384 0.330 0.266 0.393 True True False True False
aiiy disc a PGb hexanol 72 12 0.528 0.472 0.585 0.534 0.433 0.635 True True False True False
aiiy disc a PGb 6MHO 72 12 -0.084 -0.119 -0.049 -0.079 -0.113 -0.044 False False False False False
aiiy disc a PGa heptanone 24 4 0.028 -0.031 0.087 0.020 -0.054 0.094 True False True False True
aiiy disc a PGa heptanol 24 4 0.274 0.197 0.351 0.272 0.017 0.528 True True False True False
aiiy disc a PGa hexanol 24 4 0.167 0.104 0.229 0.164 0.003 0.324 True False False False False
aiiy disc a PGa 6MHO 24 4 -0.059 -0.102 -0.015 -0.069 -0.115 -0.022 False False False False False
aiiy disc a 6G heptanone 60 10 -0.150 -0.172 -0.127 -0.150 -0.190 -0.110 True False False False False
aiiy disc a 6G heptanol 60 10 -0.110 -0.128 -0.092 -0.111 -0.148 -0.074 True False False False False
aiiy disc a 6G hexanol 60 10 -0.095 -0.118 -0.071 -0.094 -0.136 -0.051 True False False False False
aiiy disc a 6G 6MHO 60 10 0.280 0.225 0.335 0.279 0.187 0.371 True True False True False
aiiy disc b PGb heptanone 72 12 0.622 0.545 0.699 0.630 0.532 0.729 True True False True False
aiiy disc b PGb heptanol 72 12 0.332 0.282 0.383 0.335 0.276 0.394 True True False True False
aiiy disc b PGb hexanol 72 12 0.598 0.545 0.652 0.601 0.494 0.707 True True False True False
aiiy disc b PGb 6MHO 72 12 -0.076 -0.108 -0.044 -0.071 -0.106 -0.036 True False False False False
aiiy disc b PGa heptanone 24 4 0.025 -0.034 0.084 0.018 -0.056 0.091 True False True False True
aiiy disc b PGa heptanol 24 4 0.258 0.183 0.334 0.257 0.008 0.507 True True False True False
aiiy disc b PGa hexanol 24 4 0.191 0.122 0.261 0.187 0.019 0.356 True False False False False
aiiy disc b PGa 6MHO 24 4 -0.064 -0.092 -0.036 -0.066 -0.104 -0.029 True False False False False
aiiy disc b 6G heptanone 60 10 -0.130 -0.153 -0.107 -0.130 -0.165 -0.096 True False False False False
aiiy disc b 6G heptanol 60 10 -0.085 -0.105 -0.065 -0.086 -0.120 -0.053 True False False False False
aiiy disc b 6G hexanol 60 10 -0.074 -0.098 -0.050 -0.073 -0.113 -0.033 True False False False False
aiiy disc b 6G 6MHO 60 10 0.279 0.221 0.337 0.279 0.179 0.379 True True False True False
aiiy region a PGb heptanone 72 12 1.412 1.240 1.583 1.427 1.214 1.640 True True False True False
aiiy region a PGb heptanol 72 12 1.357 1.137 1.577 1.330 1.095 1.564 True True False True False
aiiy region a PGb hexanol 72 12 1.503 1.379 1.626 1.505 1.248 1.762 True True False True False
aiiy region a PGb 6MHO 72 12 0.375 0.138 0.613 0.340 0.075 0.605 False True False True False
aiiy region a PGa heptanone 24 4 0.915 0.609 1.222 0.902 0.405 1.398 True True False True False
aiiy region a PGa heptanol 24 4 2.313 1.848 2.779 2.331 1.157 3.504 True True False True False
aiiy region a PGa hexanol 24 4 1.115 0.585 1.645 1.144 0.472 1.816 True True False True False
aiiy region a PGa 6MHO 24 4 0.001 -0.351 0.353 -0.011 -0.333 0.311 True False True False True
aiiy region a 6G heptanone 60 10 0.655 0.365 0.946 0.623 0.287 0.960 True True False True False
aiiy region a 6G heptanol 60 10 0.328 0.133 0.522 0.316 0.058 0.574 True True False True False
aiiy region a 6G hexanol 60 10 0.725 0.415 1.035 0.660 0.346 0.974 True True False True False
aiiy region a 6G 6MHO 60 10 0.729 0.598 0.860 0.735 0.520 0.949 True True False True False
aiiy region b PGb heptanone 72 12 1.431 1.251 1.612 1.454 1.263 1.645 True True False True False
aiiy region b PGb heptanol 72 12 1.399 1.208 1.591 1.376 1.138 1.614 True True False True False
aiiy region b PGb hexanol 72 12 1.532 1.412 1.653 1.534 1.286 1.782 True True False True False
aiiy region b PGb 6MHO 72 12 0.465 0.211 0.720 0.403 0.131 0.676 True True False True False
aiiy region b PGa heptanone 24 4 0.943 0.654 1.232 0.935 0.354 1.516 True True False True False
aiiy region b PGa heptanol 24 4 1.816 1.394 2.238 1.829 0.955 2.704 True True False True False
aiiy region b PGa hexanol 24 4 1.172 0.775 1.569 1.171 0.467 1.876 True True False True False
aiiy region b PGa 6MHO 24 4 0.010 -0.127 0.147 0.011 -0.368 0.390 True False True False True
aiiy region b 6G heptanone 60 10 0.343 0.091 0.594 0.322 0.047 0.597 True True False True False
aiiy region b 6G heptanol 60 10 0.200 0.010 0.389 0.228 0.038 0.418 True False False True False
aiiy region b 6G hexanol 60 10 0.699 0.355 1.042 0.614 0.271 0.957 True True False True False
aiiy region b 6G 6MHO 60 10 0.780 0.647 0.913 0.785 0.564 1.007 True True False True False
'which' marks the six named combinations in the order [('PGb', 'heptanone'), ('PGb', 'heptanol'), ('PGb', 'hexanol'), ('PGa', 'heptanol'), ('PGa', 'hexanol'), ('6G', '6MHO')]
ident roi plane model named responding which PGb-6MHO none PGb set PGa set 6G set sets differ met
bix disc a (a) 4/6 111001 False heptanol,heptanone,hexanol 6MHO True False
bix disc a (b) 4/6 111001 False heptanol,heptanone,hexanol 6MHO True False
bix disc b (a) 4/6 111001 False heptanol,heptanone,hexanol 6MHO True False
bix disc b (b) 4/6 111001 False heptanol,heptanone,hexanol 6MHO True False
bix region a (a) 4/6 111001 True heptanol,heptanone,hexanol 6MHO True False
bix region a (b) 4/6 111001 True heptanol,heptanone,hexanol 6MHO True False
bix region b (a) 4/6 111001 False 6MHO,heptanol,heptanone,hexanol 6MHO True False
bix region b (b) 4/6 111001 False 6MHO,heptanol,heptanone,hexanol 6MHO True False
aiiy disc a (a) 5/6 111101 False heptanol,heptanone,hexanol heptanol 6MHO True False
aiiy disc a (b) 5/6 111101 False heptanol,heptanone,hexanol heptanol 6MHO True False
aiiy disc b (a) 5/6 111101 False heptanol,heptanone,hexanol heptanol 6MHO True False
aiiy disc b (b) 5/6 111101 False heptanol,heptanone,hexanol heptanol 6MHO True False
aiiy region a (a) 6/6 111111 False 6MHO,heptanol,heptanone,hexanol heptanol,heptanone,hexanol 6MHO,heptanol,heptanone,hexanol False False
aiiy region a (b) 6/6 111111 False 6MHO,heptanol,heptanone,hexanol heptanol,heptanone,hexanol 6MHO,heptanol,heptanone,hexanol False False
aiiy region b (a) 6/6 111111 False 6MHO,heptanol,heptanone,hexanol heptanol,heptanone,hexanol 6MHO,heptanone,hexanol True False
aiiy region b (b) 6/6 111111 False 6MHO,heptanol,heptanone,hexanol heptanol,heptanone,hexanol 6MHO,heptanol,heptanone,hexanol False False
Result: partially reproduced, resting on the identification and the disc ROI. With the 15 um disc, PGb responds at 48% to heptanone, heptanol and hexanol under both identifications, both plane readings and both models (plane a, model a: bix 0.851, 0.678, 0.734; aiiy 0.596, 0.327, 0.528), and 6G responds to 6MHO (0.335 and 0.280). PGa is not identified under bix. Under aiiy (4 ants) it responds to heptanol (0.258 to 0.274) and its hexanol prediction (0.164 to 0.191, CI above 0) falls short of 0.2, so 5 of the 6 named combinations respond. PGb's prediction for 6MHO is negative with its CI below 0 (-0.087 and -0.084), which the criterion counts as neither a response nor no response. With the region of at least 0.2, which takes in neighbouring glomeruli within 17.5 um of the centre, PGb responds to 6MHO in some fits and under aiiy the three sets of responding pheromones differ in only one of four fits. No reading meets the whole criterion. The aiiy identification uses the ROI's maximum and drawing a, the reading set aside for the counts of figs5a-sparse-median-6, and 356 of drawing a's 436 regions are under 10 um across; it is used here only because it is the one that finds PGa in the most ants. For the region ROI, 618 of the 1,684 traces had no disc pixel of at least 0.2 and fell back to the disc, so the region and disc readings coincide for those traces.
fig5c-6g-slow¶
Responses in PGb and PGa peak and then decline close to baseline by the end of the 48 s recording, while 6G responses to 6MHO are very slow, with a plateau of tens of seconds.
Criterion: mean M3 traces for heptanone in PGb, heptanol and hexanol in PGb and PGa, 6MHO in 6G; readings (a) 48% (8 ants) or (b) all concentrations pooled. Reproduced when the PGb and PGa mean traces over the last five volumes are below 0.2 and below a quarter of their peak, while the 6G trace stays at or above half its peak for at least 20 s.
COMBOS = [("PGb", "heptanone"), ("PGb", "heptanol"), ("PGb", "hexanol"), ("PGa", "heptanol"), ("PGa", "hexanol"), ("6G", "6MHO")]
rows = []
for ident in IDS:
for roi in ("disc", "region"):
for rd in "ab":
for conc_reading in ("48%", "pooled"):
for glom, od in COMBOS:
s = tr[(tr.ident == ident) & (tr.roi == roi) & (tr.glom == glom) & (tr.odor == od) & (tr.plane_reading == rd) & (tr.nvol == 40)]
if conc_reading == "48%":
s = s[s.conc == 0.48]
if not len(s):
continue
m = np.stack(s.trace).mean(0)
pk = m.max()
end = m[-5:].mean()
above = m >= pk / 2
# longest run of volumes at or above half the peak
run_, best = 0, 0
for a in above:
run_ = run_ + 1 if a else 0
best = max(best, run_)
rows.append({"ident": ident, "roi": roi, "plane": rd, "conc": conc_reading, "glom": glom, "odor": od, "n": len(s),
"ants": s.ant.nunique(), "peak": round(pk, 3), "peak volume": int(m.argmax()) + 1, "last 5 mean": round(end, 3),
"end/peak": round(end / pk, 3), "s at >= half peak": round(best * al.VOLUME_S, 1)})
slow = pd.DataFrame(rows)
print(slow.to_string(index=False))
slow_j = []
for (ident, roi, rd, cr), g in slow.groupby(["ident", "roi", "plane", "conc"]):
fast = g[g.glom != "6G"]
six = g[g.glom == "6G"]
ok_fast = bool(((fast["last 5 mean"] < 0.2) & (fast["end/peak"] < 0.25)).all())
ok_six = bool((six["s at >= half peak"] >= 20).all()) if len(six) else False
slow_j.append({"ident": ident, "roi": roi, "plane": rd, "conc": cr, "PGa tested": bool((fast.glom == "PGa").any()),
"PGb/PGa near baseline": ok_fast, "6G >= half peak for >= 20 s": ok_six, "met": ok_fast and ok_six})
slow_j = pd.DataFrame(slow_j)
print(slow_j.to_string(index=False))
ident roi plane conc glom odor n ants peak peak volume last 5 mean end/peak s at >= half peak bix disc a 48% PGb heptanone 15 5 0.894 12 0.044 0.049 20.5 bix disc a 48% PGb heptanol 15 5 0.762 15 0.113 0.148 22.9 bix disc a 48% PGb hexanol 15 5 0.784 11 -0.105 -0.134 18.1 bix disc a 48% 6G 6MHO 6 2 0.590 23 0.458 0.776 36.1 bix disc a pooled PGb heptanone 30 5 0.617 9 -0.095 -0.154 16.9 bix disc a pooled PGb heptanol 30 5 0.442 12 -0.035 -0.079 20.5 bix disc a pooled PGb hexanol 30 5 0.657 10 -0.134 -0.203 15.7 bix disc a pooled 6G 6MHO 12 2 0.442 21 0.311 0.705 36.1 bix disc b 48% PGb heptanone 15 5 0.922 12 0.040 0.044 20.5 bix disc b 48% PGb heptanol 15 5 0.765 15 0.116 0.151 22.9 bix disc b 48% PGb hexanol 15 5 0.912 11 -0.068 -0.075 16.9 bix disc b 48% 6G 6MHO 6 2 0.633 24 0.521 0.823 36.1 bix disc b pooled PGb heptanone 30 5 0.663 9 -0.088 -0.132 16.9 bix disc b pooled PGb heptanol 30 5 0.459 12 -0.025 -0.054 20.5 bix disc b pooled PGb hexanol 30 5 0.735 10 -0.108 -0.147 16.9 bix disc b pooled 6G 6MHO 12 2 0.497 21 0.377 0.758 36.1 bix region a 48% PGb heptanone 15 5 1.855 10 0.289 0.156 22.9 bix region a 48% PGb heptanol 15 5 1.806 15 0.472 0.261 24.1 bix region a 48% PGb hexanol 15 5 1.682 11 0.035 0.021 19.3 bix region a 48% 6G 6MHO 6 2 1.392 20 1.105 0.794 37.3 bix region a pooled PGb heptanone 30 5 1.709 10 0.169 0.099 20.5 bix region a pooled PGb heptanol 30 5 1.447 15 0.331 0.228 22.9 bix region a pooled PGb hexanol 30 5 1.706 10 0.052 0.030 19.3 bix region a pooled 6G 6MHO 12 2 1.376 24 1.157 0.841 38.6 bix region b 48% PGb heptanone 15 5 1.875 10 0.285 0.152 24.1 bix region b 48% PGb heptanol 15 5 1.805 15 0.466 0.258 24.1 bix region b 48% PGb hexanol 15 5 1.843 11 0.059 0.032 19.3 bix region b 48% 6G 6MHO 6 2 1.574 24 1.289 0.819 37.3 bix region b pooled PGb heptanone 30 5 1.687 9 0.144 0.085 20.5 bix region b pooled PGb heptanol 30 5 1.502 15 0.339 0.226 24.1 bix region b pooled PGb hexanol 30 5 1.776 11 0.066 0.037 19.3 bix region b pooled 6G 6MHO 12 2 1.524 24 1.273 0.835 37.3 aiiy disc a 48% PGb heptanone 24 8 0.719 10 0.022 0.031 20.5 aiiy disc a 48% PGb heptanol 24 8 0.447 15 -0.014 -0.032 21.7 aiiy disc a 48% PGb hexanol 24 8 0.644 11 -0.131 -0.203 14.5 aiiy disc a 48% PGa heptanol 9 3 0.328 19 0.046 0.140 24.1 aiiy disc a 48% PGa hexanol 9 3 0.129 9 -0.145 -1.123 7.2 aiiy disc a 48% 6G 6MHO 24 8 0.385 21 0.277 0.720 37.3 aiiy disc a pooled PGb heptanone 72 12 0.369 10 -0.120 -0.324 14.5 aiiy disc a pooled PGb heptanol 72 12 0.165 15 -0.136 -0.824 15.7 aiiy disc a pooled PGb hexanol 72 12 0.403 10 -0.165 -0.410 13.3 aiiy disc a pooled PGa heptanol 24 4 0.220 13 -0.015 -0.068 21.7 aiiy disc a pooled PGa hexanol 24 4 0.129 9 -0.145 -1.118 6.0 aiiy disc a pooled 6G 6MHO 60 10 0.351 20 0.250 0.712 37.3 aiiy disc b 48% PGb heptanone 24 8 0.712 12 0.006 0.009 20.5 aiiy disc b 48% PGb heptanol 24 8 0.446 15 -0.021 -0.047 20.5 aiiy disc b 48% PGb hexanol 24 8 0.700 11 -0.111 -0.159 14.5 aiiy disc b 48% PGa heptanol 9 3 0.295 14 0.036 0.122 22.9 aiiy disc b 48% PGa hexanol 9 3 0.141 10 -0.129 -0.917 8.4 aiiy disc b 48% 6G 6MHO 24 8 0.406 21 0.309 0.762 37.3 aiiy disc b pooled PGb heptanone 72 12 0.394 10 -0.100 -0.255 14.5 aiiy disc b pooled PGb heptanol 72 12 0.181 15 -0.133 -0.733 15.7 aiiy disc b pooled PGb hexanol 72 12 0.435 10 -0.147 -0.338 13.3 aiiy disc b pooled PGa heptanol 24 4 0.213 13 -0.017 -0.081 21.7 aiiy disc b pooled PGa hexanol 24 4 0.148 9 -0.149 -1.011 6.0 aiiy disc b pooled 6G 6MHO 60 10 0.363 21 0.268 0.740 36.1 aiiy region a 48% PGb heptanone 24 8 1.677 12 0.313 0.187 24.1 aiiy region a 48% PGb heptanol 24 8 1.549 15 0.396 0.256 24.1 aiiy region a 48% PGb hexanol 24 8 1.688 11 0.042 0.025 16.9 aiiy region a 48% PGa heptanol 9 3 2.941 14 1.178 0.401 28.9 aiiy region a 48% PGa hexanol 9 3 1.447 11 -0.071 -0.049 9.6 aiiy region a 48% 6G 6MHO 24 8 0.959 25 0.773 0.806 37.3 aiiy region a pooled PGb heptanone 72 12 1.473 11 0.169 0.115 18.1 aiiy region a pooled PGb heptanol 72 12 1.222 14 0.272 0.222 21.7 aiiy region a pooled PGb hexanol 72 12 1.506 10 0.055 0.037 15.7 aiiy region a pooled PGa heptanol 24 4 2.546 14 0.908 0.357 28.9 aiiy region a pooled PGa hexanol 24 4 1.372 11 -0.016 -0.011 10.8 aiiy region a pooled 6G 6MHO 60 10 0.936 25 0.716 0.764 38.6 aiiy region b 48% PGb heptanone 24 8 1.658 12 0.286 0.173 24.1 aiiy region b 48% PGb heptanol 24 8 1.567 15 0.385 0.246 24.1 aiiy region b 48% PGb hexanol 24 8 1.704 11 0.042 0.025 16.9 aiiy region b 48% PGa heptanol 9 3 2.452 11 0.850 0.347 26.5 aiiy region b 48% PGa hexanol 9 3 1.315 11 -0.025 -0.019 13.3 aiiy region b 48% 6G 6MHO 24 8 1.104 17 0.793 0.719 37.3 aiiy region b pooled PGb heptanone 72 12 1.449 11 0.161 0.111 18.1 aiiy region b pooled PGb heptanol 72 12 1.293 14 0.277 0.214 21.7 aiiy region b pooled PGb hexanol 72 12 1.471 11 0.029 0.020 16.9 aiiy region b pooled PGa heptanol 24 4 2.177 14 0.667 0.306 25.3 aiiy region b pooled PGa hexanol 24 4 1.270 9 -0.006 -0.005 10.8 aiiy region b pooled 6G 6MHO 60 10 0.987 25 0.731 0.741 38.6 ident roi plane conc PGa tested PGb/PGa near baseline 6G >= half peak for >= 20 s met aiiy disc a 48% True True True True aiiy disc a pooled True True True True aiiy disc b 48% True True True True aiiy disc b pooled True True True True aiiy region a 48% True False True False aiiy region a pooled True False True False aiiy region b 48% True False True False aiiy region b pooled True False True False bix disc a 48% False True True True bix disc a pooled False True True True bix disc b 48% False True True True bix disc b pooled False True True True bix region a 48% False False True False bix region a pooled False False True False bix region b 48% False False True False bix region b pooled False False True False
Result: partially reproduced, resting on the identification and the disc ROI. With the disc, the mean PGb traces for heptanone, heptanol and hexanol end (last five volumes) at -0.165 to 0.116, with end over peak at most 0.151, and the mean 6G trace to 6MHO stays at or above half its peak for 36.1 to 37.3 s, under both identifications, both plane readings and both concentration readings. PGa is identified only under aiiy, and its traces (9 trials at 48%, 24 pooled) end at -0.149 to 0.046; its hexanol trace peaks at only 0.129 to 0.148. With the region ROI the PGb heptanol trace ends at 0.272 to 0.472, above 0.2, and the criterion fails. The PGa part rests on 3 or 4 ants under one identification, and with no clear hexanol response in PGa it passes trivially. The aiiy identification uses the ROI's maximum and drawing a, the reading set aside for the counts of figs5a-sparse-median-6, and 356 of drawing a's 436 regions are under 10 um across; it is used here only because it is the one that finds PGa in the most ants. For the region ROI, 618 of the 1,684 traces had no disc pixel of at least 0.2 and fell back to the disc, so the region and disc readings coincide for those traces.
figs7a-6g-extended¶
In 144 s recordings, 6G responses to 6MHO plateau for about 30 s and return to baseline about 80 s after odor presentation, sometimes taking more than 100 s.
Criterion: single 144 s trials from five ants at 48%, M3 traces of 6G, and their mean. Return to baseline as the first volume after the peak where the mean trace falls (a) below baseline mean plus 3 SD or (b) below 0.2; plateau as the time the mean trace stays at or above (c) 90% or (d) 80% of its peak; odor presentation as (e) 3 s or (f) 8 s. Reproduced when, under one reading, both values are within tolerance (30 +/- 3 s, 80 +/- 8 s) and at least one trial takes more than 100 s.
t120 = np.arange(120) * al.VOLUME_S
def plateau_return(tr_, t_):
pk = int(np.argmax(tr_))
base = tr_[al.BASE]
out = {}
for name, frac in (("c", 0.9), ("d", 0.8)):
above = tr_ >= frac * tr_[pk]
a0 = pk
while a0 > 0 and above[a0 - 1]:
a0 -= 1
a1 = pk
while a1 < len(tr_) - 1 and above[a1 + 1]:
a1 += 1
out[name] = (a1 - a0 + 1) * al.VOLUME_S
for name, bound in (("a", base.mean() + 3 * base.std(ddof=1)), ("b", 0.2)):
below = np.where(tr_[pk:] < bound)[0]
out[name] = t_[pk + below[0]] if len(below) else np.nan
return out
res7 = []
for ident in IDS:
long = tr[(tr.ident == ident) & (tr.nvol == 120) & (tr.glom == "6G")]
print(ident, ": 144 s trials with 6G identified:", long.ant.nunique() // 1, "ants of", alarm[alarm.nvol == 120].ant.nunique(),
"with a 144 s recording; trials", long[(long.roi == "disc") & (long.plane_reading == "a")].shape[0])
for roi in ("disc", "region"):
for rd in "ab":
s = long[(long.plane_reading == rd) & (long.roi == roi)]
if not len(s):
continue
t_ = t120 + float(s.t0.mean())
mean = np.stack(s.trace).mean(0)
pm = plateau_return(mean, t_)
per = [plateau_return(x, t120 + t0) for x, t0 in zip(s.trace, s.t0)]
for ret in "ab":
for pl in "cd":
for on, t_on in (("e", 3.0), ("f", 8.0)):
plateau = pm[pl]
back = pm[ret] - t_on
per_back = [p[ret] - t_on for p in per]
res7.append({"ident": ident, "roi": roi, "plane": rd, "reading": ret + pl + on, "n trials": len(s), "plateau s": round(plateau, 1),
"return s": round(back, 1), "per-trial return s": [round(x, 1) for x in per_back],
"any > 100 s": bool(np.nanmax(per_back) > 100) if np.isfinite(per_back).any() else False,
"within tolerance": bool(abs(plateau - 30) <= 3 and abs(back - 80) <= 8)})
print(f" {roi}, plane {rd}: mean 6G trace peak {mean.max():.3f} at {t_[mean.argmax()]:.1f} s; range of the mean trace {mean.min():.3f} to {mean.max():.3f}")
res7 = pd.DataFrame(res7)
res7["met"] = res7["within tolerance"] & res7["any > 100 s"]
print(res7.to_string(index=False))
print("readings meeting the criterion:", int(res7.met.sum()), "of", len(res7))
print("\nTrials returning more than 100 s after odor presentation (onset reading e, plateau reading c):")
for _, r_ in res7[res7.reading.str.endswith("ce")].iterrows():
over = [x for x in r_["per-trial return s"] if np.isfinite(x) and x > 100]
print(f" {r_.ident}, {r_.roi}, plane {r_.plane}, return {r_.reading[0]}: {len(over)} of {r_['n trials']}", [float(x) for x in over])
long = tr[(tr.nvol == 120) & (tr.glom == "6G") & (tr.roi == "disc")]
if len(long):
fig, ax = plt.subplots(1, len(IDS), figsize=(5 * len(IDS), 3), squeeze=False)
for k, ident in enumerate(IDS):
s = long[(long.ident == ident) & (long.plane_reading == "a")]
if not len(s):
continue
for x in s.trace:
ax[0, k].plot(t120, x, lw=0.7)
ax[0, k].plot(t120, np.stack(s.trace).mean(0), "k", lw=2)
ax[0, k].axvspan(3, 8, color="0.9"); ax[0, k].set_title(f"6G, 6MHO 48%, 144 s, {ident}, disc, plane a", fontsize=8); ax[0, k].set_xlabel("s")
plt.tight_layout(); plt.show()
bix : 144 s trials with 6G identified: 1 ants of 5 with a 144 s recording; trials 1 disc, plane a: mean 6G trace peak 0.627 at 27.1 s; range of the mean trace -0.153 to 0.627 disc, plane b: mean 6G trace peak 0.627 at 27.1 s; range of the mean trace -0.153 to 0.627 region, plane a: mean 6G trace peak 2.435 at 27.1 s; range of the mean trace -0.233 to 2.435 region, plane b: mean 6G trace peak 2.435 at 27.1 s; range of the mean trace -0.233 to 2.435 aiiy : 144 s trials with 6G identified: 5 ants of 5 with a 144 s recording; trials 5 disc, plane a: mean 6G trace peak 0.252 at 33.1 s; range of the mean trace -0.122 to 0.252 disc, plane b: mean 6G trace peak 0.281 at 33.1 s; range of the mean trace -0.108 to 0.281 region, plane a: mean 6G trace peak 0.828 at 27.0 s; range of the mean trace -0.105 to 0.828 region, plane b: mean 6G trace peak 0.822 at 27.1 s; range of the mean trace -0.100 to 0.822 ident roi plane reading n trials plateau s return s per-trial return s any > 100 s within tolerance met bix disc a ace 1 9.6 91.6 [91.6] False False False bix disc a acf 1 9.6 86.6 [86.6] False False False bix disc a ade 1 22.9 91.6 [91.6] False False False bix disc a adf 1 22.9 86.6 [86.6] False False False bix disc a bce 1 9.6 83.2 [83.2] False False False bix disc a bcf 1 9.6 78.2 [78.2] False False False bix disc a bde 1 22.9 83.2 [83.2] False False False bix disc a bdf 1 22.9 78.2 [78.2] False False False bix disc b ace 1 9.6 91.6 [91.6] False False False bix disc b acf 1 9.6 86.6 [86.6] False False False bix disc b ade 1 22.9 91.6 [91.6] False False False bix disc b adf 1 22.9 86.6 [86.6] False False False bix disc b bce 1 9.6 83.2 [83.2] False False False bix disc b bcf 1 9.6 78.2 [78.2] False False False bix disc b bde 1 22.9 83.2 [83.2] False False False bix disc b bdf 1 22.9 78.2 [78.2] False False False bix region a ace 1 7.2 91.6 [91.6] False False False bix region a acf 1 7.2 86.6 [86.6] False False False bix region a ade 1 15.7 91.6 [91.6] False False False bix region a adf 1 15.7 86.6 [86.6] False False False bix region a bce 1 7.2 110.9 [110.9] True False False bix region a bcf 1 7.2 105.9 [105.9] True False False bix region a bde 1 15.7 110.9 [110.9] True False False bix region a bdf 1 15.7 105.9 [105.9] True False False bix region b ace 1 7.2 91.6 [91.6] False False False bix region b acf 1 7.2 86.6 [86.6] False False False bix region b ade 1 15.7 91.6 [91.6] False False False bix region b adf 1 15.7 86.6 [86.6] False False False bix region b bce 1 7.2 110.9 [110.9] True False False bix region b bcf 1 7.2 105.9 [105.9] True False False bix region b bde 1 15.7 110.9 [110.9] True False False bix region b bdf 1 15.7 105.9 [105.9] True False False aiiy disc a ace 5 2.4 56.6 [91.6, 48.3, 33.8, 1.1, 105.7] True False False aiiy disc a acf 5 2.4 51.6 [86.6, 43.3, 28.8, -3.9, 100.7] True False False aiiy disc a ade 5 4.8 56.6 [91.6, 48.3, 33.8, 1.1, 105.7] True False False aiiy disc a adf 5 4.8 51.6 [86.6, 43.3, 28.8, -3.9, 100.7] True False False aiiy disc a bce 5 2.4 32.5 [83.2, 43.5, 27.8, 1.1, 105.7] True False False aiiy disc a bcf 5 2.4 27.5 [78.2, 38.5, 22.8, -3.9, 100.7] True False False aiiy disc a bde 5 4.8 32.5 [83.2, 43.5, 27.8, 1.1, 105.7] True False False aiiy disc a bdf 5 4.8 27.5 [78.2, 38.5, 22.8, -3.9, 100.7] True False False aiiy disc b ace 5 3.6 56.6 [91.6, 47.1, 44.7, 1.1, 105.7] True False False aiiy disc b acf 5 3.6 51.6 [86.6, 42.1, 39.7, -3.9, 100.7] True False False aiiy disc b ade 5 8.4 56.6 [91.6, 47.1, 44.7, 1.1, 105.7] True False False aiiy disc b adf 5 8.4 51.6 [86.6, 42.1, 39.7, -3.9, 100.7] True False False aiiy disc b bce 5 3.6 38.5 [83.2, 51.9, 27.8, 1.1, 105.7] True False False aiiy disc b bcf 5 3.6 33.5 [78.2, 46.9, 22.8, -3.9, 100.7] True False False aiiy disc b bde 5 8.4 38.5 [83.2, 51.9, 27.8, 1.1, 105.7] True False False aiiy disc b bdf 5 8.4 33.5 [78.2, 46.9, 22.8, -3.9, 100.7] True False False aiiy region a ace 5 7.2 75.8 [91.6, 31.4, 35.0, 1.1, 105.7] True False False aiiy region a acf 5 7.2 70.8 [86.6, 26.4, 30.0, -3.9, 100.7] True False False aiiy region a ade 5 15.7 75.8 [91.6, 31.4, 35.0, 1.1, 105.7] True False False aiiy region a adf 5 15.7 70.8 [86.6, 26.4, 30.0, -3.9, 100.7] True False False aiiy region a bce 5 7.2 91.5 [110.9, 44.7, 45.8, 1.1, 105.7] True False False aiiy region a bcf 5 7.2 86.5 [105.9, 39.7, 40.8, -3.9, 100.7] True False False aiiy region a bde 5 15.7 91.5 [110.9, 44.7, 45.8, 1.1, 105.7] True False False aiiy region a bdf 5 15.7 86.5 [105.9, 39.7, 40.8, -3.9, 100.7] True False False aiiy region b ace 5 7.2 83.1 [91.6, 38.7, 19.4, 1.1, 105.7] True False False aiiy region b acf 5 7.2 78.1 [86.6, 33.7, 14.4, -3.9, 100.7] True False False aiiy region b ade 5 15.7 83.1 [91.6, 38.7, 19.4, 1.1, 105.7] True False False aiiy region b adf 5 15.7 78.1 [86.6, 33.7, 14.4, -3.9, 100.7] True False False aiiy region b bce 5 7.2 91.5 [110.9, 45.9, 35.0, 1.1, 105.7] True False False aiiy region b bcf 5 7.2 86.5 [105.9, 40.9, 30.0, -3.9, 100.7] True False False aiiy region b bde 5 15.7 91.5 [110.9, 45.9, 35.0, 1.1, 105.7] True False False aiiy region b bdf 5 15.7 86.5 [105.9, 40.9, 30.0, -3.9, 100.7] True False False readings meeting the criterion: 0 of 64 Trials returning more than 100 s after odor presentation (onset reading e, plateau reading c): bix, disc, plane a, return a: 0 of 1 [] bix, disc, plane a, return b: 0 of 1 [] bix, disc, plane b, return a: 0 of 1 [] bix, disc, plane b, return b: 0 of 1 [] bix, region, plane a, return a: 0 of 1 [] bix, region, plane a, return b: 1 of 1 [110.9] bix, region, plane b, return a: 0 of 1 [] bix, region, plane b, return b: 1 of 1 [110.9] aiiy, disc, plane a, return a: 1 of 5 [105.7] aiiy, disc, plane a, return b: 1 of 5 [105.7] aiiy, disc, plane b, return a: 1 of 5 [105.7] aiiy, disc, plane b, return b: 1 of 5 [105.7] aiiy, region, plane a, return a: 1 of 5 [105.7] aiiy, region, plane a, return b: 2 of 5 [110.9, 105.7] aiiy, region, plane b, return a: 1 of 5 [105.7] aiiy, region, plane b, return b: 2 of 5 [110.9, 105.7]
Result: not reproduced. Under aiiy 6G is identified in all 5 ants with a 144 s recording (under bix in 1). With the disc the mean trace peaks at 0.252 to 0.281 at 33.1 s, stays at or above 90% of its peak for 2.4 to 3.6 s and at or above 80% for 4.8 to 8.4 s (expected about 30 s), and returns 27.5 to 56.6 s after odor presentation (expected 80 +/- 8 s). With the region ROI the return (70.8 to 91.5 s) is near the expected value but the plateau is 7.2 to 15.7 s. One trial returns more than 100 s after odor presentation under the disc (100.7 to 105.7 s, by onset reading); under the region ROI with return reading b two trials do (105.9 to 110.9 s and 100.7 to 105.7 s). For the region ROI, 618 of the 1,684 traces had no disc pixel of at least 0.2 and fell back to the disc, so the region and disc readings coincide for those traces. None of the 64 combinations of identification, ROI, plane and criterion reading meets the criterion.
figs7c-temporal-lme¶
Onset and peak times differ by pheromone and glomerulus; for PGb and PGa responses to heptanol and hexanol, onset time depends on pheromone, glomerulus and their interaction, and time to maximum on pheromone and glomerulus but not their interaction.
Criterion: M3 traces at 48%; onset and time to maximum (Methods); traces that never exceed the bound excluded. Model per parameter: pheromone, glomerulus and their interaction, random effect for trial nested in ant (lme). p-values read as (a) sequential tests in the order the Methods list (here likelihood-ratio tests of nested ML fits, as statsmodels has no anova for mixed models), (b) marginal tests (Wald z of sum-coded effects), (c) the coefficient tests of a treatment-coded summary. Reproduced when, under every reading, the five effects have p < 0.05 and the time-to-maximum interaction has p of 0.05 or more; partially reproduced when three or four of the five hold.
lat = []
for _, r in tr[(tr.conc == 0.48) & (tr.nvol == 40) & tr.glom.isin(["PGb", "PGa"]) & tr.odor.isin(["heptanol", "hexanol"])].iterrows():
on, mx = al.onset_and_max(r.trace, t40 + r.t0)
lat.append({"ident": r.ident, "roi": r.roi, "ant": r.ant, "trial": f"{r.ant}-{r.trial}", "odor": r.odor, "glom": r.glom,
"plane_reading": r.plane_reading, "onset": on, "tmax": mx})
lat = pd.DataFrame(lat)
print("traces per identification, ROI, plane reading and glomerulus:")
print(lat.groupby(["ident", "roi", "plane_reading", "glom"]).size().unstack().to_string())
print("excluded (never above the bound):")
print(lat.groupby(["ident", "roi", "plane_reading", "glom"]).onset.apply(lambda s: int(s.isna().sum())).unstack().to_string())
print(lat.dropna().groupby(["ident", "roi", "plane_reading", "glom", "odor"])[["onset", "tmax"]].agg(["mean", "std", "size"]).round(2).to_string())
def lme_p(d_, y):
d_ = d_.dropna(subset=[y]).copy()
vc = {"trial": "0 + C(trial)"}
out = {}
# (c) treatment coding, coefficient z-tests
fc = smf.mixedlm(f"{y} ~ C(odor) * C(glom)", d_, groups=d_.ant, vc_formula=vc).fit(reml=True)
out["converged"] = bool(fc.converged)
pv = fc.pvalues
out["c"] = {"pheromone": pv[[k for k in pv.index if k.startswith("C(odor)") and ":" not in k][0]],
"glomerulus": pv[[k for k in pv.index if k.startswith("C(glom)") and ":" not in k][0]],
"interaction": pv[[k for k in pv.index if ":" in k][0]]}
# (b) sum coding, marginal z-tests
fb = smf.mixedlm(f"{y} ~ C(odor, Sum) * C(glom, Sum)", d_, groups=d_.ant, vc_formula=vc).fit(reml=True)
out["converged_b"] = bool(fb.converged)
pv = fb.pvalues
out["b"] = {"pheromone": pv[[k for k in pv.index if k.startswith("C(odor") and ":" not in k][0]],
"glomerulus": pv[[k for k in pv.index if k.startswith("C(glom") and ":" not in k][0]],
"interaction": pv[[k for k in pv.index if ":" in k][0]]}
# (a) sequential likelihood-ratio tests (ML)
forms = ["1", "C(odor)", "C(odor) + C(glom)", "C(odor) * C(glom)"]
fa = [smf.mixedlm(f"{y} ~ {f}", d_, groups=d_.ant, vc_formula=vc).fit(reml=False) for f in forms]
out["converged_a"] = bool(all(f_.converged for f_ in fa))
ll = [f_.llf for f_ in fa]
out["a"] = {k: float(stats.chi2.sf(2 * (ll[i + 1] - ll[i]), 1)) for i, k in enumerate(["pheromone", "glomerulus", "interaction"])}
return out
rows, untestable = [], []
for (ident, roi, rd), d_ in lat.groupby(["ident", "roi", "plane_reading"]):
for y in ("onset", "tmax"):
levels = d_.dropna(subset=[y]).glom.nunique()
if levels < 2:
untestable.append(f"{ident}/{roi}/plane {rd}/{y}: only {sorted(d_.dropna(subset=[y]).glom.unique())} has traces above the bound, so the glomerulus factor has one level and the model cannot be fitted")
continue
try:
ps = lme_p(d_, y)
except Exception as e: # noqa: BLE001
untestable.append(f"{ident}/{roi}/plane {rd}/{y}: fit failed ({e})")
continue
conv = {"c": ps.pop("converged"), "b": ps.pop("converged_b"), "a": ps.pop("converged_a")}
for reading, p in ps.items():
rows.append({"converged": conv[reading], **{"ident": ident, "roi": roi, "plane": rd, "p reading": reading, "parameter": y, **{k: round(float(v), 4) for k, v in p.items()}}})
for u in untestable:
print("NOT TESTED:", u)
for ident in IDS:
if not (lat.ident == ident).any() or (lat[lat.ident == ident].glom == "PGa").sum() == 0:
print(f"NOT TESTED under {ident}: no PGa traces, so the claim cannot be tested under this identification")
lme = pd.DataFrame(rows, columns=["ident", "roi", "plane", "p reading", "parameter", "pheromone", "glomerulus", "interaction", "converged"])
print(lme.to_string(index=False))
print("fits converged (all four ML fits for reading a):", int(lme.converged.sum()), "of", len(lme))
lme_j = []
for (ident, roi, rd, pr), g in lme.groupby(["ident", "roi", "plane", "p reading"]):
if set(g.parameter) != {"onset", "tmax"}:
continue
on = g[g.parameter == "onset"].iloc[0]; mx = g[g.parameter == "tmax"].iloc[0]
holds = [on.pheromone < 0.05, on.glomerulus < 0.05, on.interaction < 0.05, mx.pheromone < 0.05, mx.glomerulus < 0.05]
lme_j.append({"ident": ident, "roi": roi, "plane": rd, "p reading": pr, "significant of 5": sum(holds),
"which": "".join("1" if h else "0" for h in holds), "tmax interaction p >= 0.05": bool(mx.interaction >= 0.05)})
lme_j = pd.DataFrame(lme_j)
print("\n'which' marks onset pheromone, glomerulus, interaction, time-to-maximum pheromone, glomerulus")
print(lme_j.to_string(index=False))
traces per identification, ROI, plane reading and glomerulus:
glom PGa PGb
ident roi plane_reading
aiiy disc a 18.0 48.0
b 18.0 48.0
region a 18.0 48.0
b 18.0 48.0
bix disc a NaN 30.0
b NaN 30.0
region a NaN 30.0
b NaN 30.0
excluded (never above the bound):
glom PGa PGb
ident roi plane_reading
aiiy disc a 9.0 1.0
b 8.0 1.0
region a 2.0 0.0
b 3.0 0.0
bix disc a NaN 0.0
b NaN 0.0
region a NaN 0.0
b NaN 0.0
onset tmax
mean std size mean std size
ident roi plane_reading glom odor
aiiy disc a PGa heptanol 6.79 5.11 5 16.18 4.13 5
hexanol 5.43 0.71 4 7.84 0.67 4
PGb heptanol 7.00 2.40 23 14.07 2.55 23
hexanol 4.95 0.89 24 9.97 2.13 24
b PGa heptanol 12.04 14.14 6 19.87 10.32 6
hexanol 5.12 1.16 4 7.84 0.71 4
PGb heptanol 6.73 1.52 23 14.07 2.35 23
hexanol 4.80 0.92 24 10.07 2.17 24
region a PGa heptanol 4.22 1.26 8 17.47 8.91 8
hexanol 5.71 0.85 8 8.42 1.40 8
PGb heptanol 5.41 1.30 24 15.05 2.48 24
hexanol 4.10 0.72 24 10.37 1.96 24
b PGa heptanol 9.03 12.55 8 18.06 9.53 8
hexanol 4.97 0.78 7 8.93 1.13 7
PGb heptanol 5.35 0.94 24 15.34 3.24 24
hexanol 4.20 1.51 24 10.27 1.83 24
bix disc a PGb heptanol 6.21 1.21 15 13.92 2.50 15
hexanol 4.44 0.78 15 9.74 2.03 15
b PGb heptanol 6.37 1.17 15 14.00 2.49 15
hexanol 4.28 0.79 15 10.15 2.14 15
region a PGb heptanol 5.17 0.84 15 14.65 2.35 15
hexanol 4.03 0.60 15 10.38 1.88 15
b PGb heptanol 5.24 0.74 15 13.76 2.42 15
hexanol 4.28 1.88 15 9.91 1.54 15
NOT TESTED: bix/disc/plane a/onset: only ['PGb'] has traces above the bound, so the glomerulus factor has one level and the model cannot be fitted NOT TESTED: bix/disc/plane a/tmax: only ['PGb'] has traces above the bound, so the glomerulus factor has one level and the model cannot be fitted NOT TESTED: bix/disc/plane b/onset: only ['PGb'] has traces above the bound, so the glomerulus factor has one level and the model cannot be fitted NOT TESTED: bix/disc/plane b/tmax: only ['PGb'] has traces above the bound, so the glomerulus factor has one level and the model cannot be fitted NOT TESTED: bix/region/plane a/onset: only ['PGb'] has traces above the bound, so the glomerulus factor has one level and the model cannot be fitted NOT TESTED: bix/region/plane a/tmax: only ['PGb'] has traces above the bound, so the glomerulus factor has one level and the model cannot be fitted NOT TESTED: bix/region/plane b/onset: only ['PGb'] has traces above the bound, so the glomerulus factor has one level and the model cannot be fitted NOT TESTED: bix/region/plane b/tmax: only ['PGb'] has traces above the bound, so the glomerulus factor has one level and the model cannot be fitted NOT TESTED under bix: no PGa traces, so the claim cannot be tested under this identification ident roi plane p reading parameter pheromone glomerulus interaction converged aiiy disc a c onset 0.3562 0.8432 0.6663 True aiiy disc a b onset 0.0337 0.8733 0.6663 True aiiy disc a a onset 0.0011 0.8981 0.6548 True aiiy disc a c tmax 0.0000 0.0379 0.0103 True aiiy disc a b tmax 0.0000 0.7596 0.0103 True aiiy disc a a tmax 0.0000 0.7255 0.0113 True aiiy disc b c onset 0.0173 0.0101 0.1178 True aiiy disc b b onset 0.0055 0.0775 0.1178 True aiiy disc b a onset 0.0171 0.0415 0.1088 True aiiy disc b c tmax 0.0000 0.0009 0.0027 True aiiy disc b b tmax 0.0000 0.1875 0.0027 True aiiy disc b a tmax 0.0000 0.0818 0.0028 True aiiy region a c onset 0.0128 0.0198 0.0000 True aiiy region a b onset 0.7938 0.5211 0.0000 False aiiy region a a onset 0.0384 0.5346 0.0000 False aiiy region a c tmax 0.0000 0.0785 0.0294 True aiiy region a b tmax 0.0000 0.7567 0.0294 True aiiy region a a tmax 0.0000 0.7694 0.0278 True aiiy region b c onset 0.0796 0.0440 0.2729 True aiiy region b b onset 0.0493 0.0934 0.2729 True aiiy region b a onset 0.0964 0.0793 0.2594 True aiiy region b c tmax 0.0000 0.1011 0.0900 True aiiy region b b tmax 0.0000 0.5665 0.0900 True aiiy region b a tmax 0.0000 0.5044 0.0826 True fits converged (all four ML fits for reading a): 22 of 24 'which' marks onset pheromone, glomerulus, interaction, time-to-maximum pheromone, glomerulus ident roi plane p reading significant of 5 which tmax interaction p >= 0.05 aiiy disc a a 2 10010 False aiiy disc a b 2 10010 False aiiy disc a c 2 00011 False aiiy disc b a 3 11010 False aiiy disc b b 2 10010 False aiiy disc b c 4 11011 False aiiy region a a 3 10110 False aiiy region a b 2 00110 False aiiy region a c 4 11110 False aiiy region b a 1 00010 True aiiy region b b 2 10010 True aiiy region b c 2 01010 True
Result: not reproduced, resting on the identification. Under bix no PGa is identified, so the models cannot be fitted (the glomerulus factor has one level; listed as NOT TESTED above). Under aiiy, the identification that finds PGa in the most ants (4 of 13, 3 of them at 48%), the models fit with 18 PGa and 48 PGb traces at 48% (with the disc 8 or 9 PGa traces never exceed the bound and are excluded, with the region 2 or 3). Across the 12 combinations of ROI, plane and p-value reading, 1 to 4 of the 5 effects have p < 0.05, and the time-to-maximum interaction has p < 0.05 in 9, where the paper finds none (p = 0.88). One of the 24 sets of fits did not converge: the ML fits of reading a for onset with the region ROI and plane a; the other 23 converged. The evidence is weak: PGa comes from 3 ants at 48%, against the paper's 8. The aiiy identification uses the ROI's maximum and drawing a, the reading set aside for the counts of figs5a-sparse-median-6, and 356 of drawing a's 436 regions are under 10 um across; it is used here only because it is the one that finds PGa in the most ants.
Summary¶
| Claim | Outcome |
|---|---|
| fig1cd-new-pheromones-leave-nest | blocked (data_missing) |
| fig1e-categorical-response | blocked (data_missing) |
| fig1f-hexanol-like-native | blocked (data_missing) |
| fig1f-mho-differs | blocked (data_missing) |
| fig1f-vehicle-differs | blocked (data_missing) |
| table1-transgenesis | blocked (data_missing) |
| fig2b-s1a-transgene-fluorescence | blocked (data_missing) |
| fig2cd-gcamp-in-osns | blocked (other), elsewhere |
| figs1-expression-outside-al | blocked (other), elsewhere |
| figs2ab-glomerulus-counts | blocked (not_runnable), elsewhere |
| fig2e-t7-ammc | blocked (other), elsewhere |
| fig2f-ablation | blocked (other), elsewhere |
| ablation-al-volume-trend | blocked (not_runnable), elsewhere |
| figs2cf-gcamp-ants-behavior | blocked (data_missing) |
| figs3-single-insertion | blocked (not_runnable), elsewhere |
| fig3f-general-odorants-unique | not reproduced |
| fig3fg-hexanone-broad | reproduced |
| figs4a-bilateral-symmetry | partially reproduced |
| fig4b-vehicle-no-response | not reproduced |
| fig4-no-decreases | not reproduced |
| fig4-large-lasting-responses | not reproduced |
| figs5a-sparse-median-6 | reproduced (assumed) |
| figs5a-concentration-more-glomeruli | reproduced (assumed) |
| figs5b-shared-glomerulus | not reproduced |
| figs5c-mho-rarely-overlaps | not reproduced |
| fig5ab-focal-glomeruli | not reproduced |
| fig5c-s6-tuning | partially reproduced (assumed) |
| fig5c-6g-slow | partially reproduced (assumed) |
| figs7a-6g-extended | not reproduced |
| figs7c-temporal-lme | not reproduced (assumed) |
| fig5d-stereotyped-positions | not reproduced |
| fig6-shared-panic-pathway | not reproduced, imaging half |
"(assumed)" marks an outcome that rests on the adopted reading of a method the paper leaves open. The paper drew its ROIs by eye. Of the eight M2 readings of the claims file, the outcomes of figs5a-sparse-median-6 and figs5a-concentration-more-glomeruli rest on the ROI's mean value (ImageJ's default measure) with the Methods' trial-mean rule, under which both ROI drawings meet each criterion. For figs5a-sparse-median-6, aiy, aiix and aiiy fail and biix and biiy meet it; for figs5a-concentration-more-glomeruli, aiix, aiiy and biix fail. figs5c-mho-rarely-overlaps is not reproduced: its criterion asks for every reading, and the four readings with the ROI's maximum fail. The single-glomerulus claims rest on the focal glomeruli identified under bix (the adopted reading, which finds PGb in 5 of 13 ants, 6G in 2 and PGa in none) or under aiiy (the reading that finds PGa in the most ants, 4, which uses the ROI's maximum and drawing a, the reading set aside for the counts), and on the 15 um disc ROI; the claims file's other ROI, the region of at least 0.2, takes in neighbouring glomeruli and is shown everywhere.
How the analysis differs from the paper's: Fiji's Image Stabilizer is replaced by whole-pixel phase correlation on each plane, for the maps and for the single-glomerulus row bands; per-plane maps are binned 4 x 4 (1.16 um); ROIs are drawn by rule inside an AL mask, not by eye, as regions of at least 0.2 that recur in two trials of an odor (drawing a keeps every such region, however small, as the claims file has it; drawing b keeps those of glomerulus size); a recurrence is an overlap of at least half the smaller region; the Methods' exclusion of trials with large motion artifacts has no rule, and a variant leaves out the one trial where most of the AL is at least 0.2.
What was found about the dataset: it holds all three imaging experiments the paper describes (6 general odorant ants with the five analysed odorants, 3 bilateral ants, 13 alarm ants at the stated concentration pairs, and five 144 s recordings), one file per plane and trial, 14,685 files, 1.26 TB of uncompressed float64. No file or trial is deposited twice. The files give orientation, pixel size and ages; the stimulus timing is only in free text. The planes' starting times step by 0.0335 s (1.105 s per 33-plane volume), while the imaging rate field says 0.83 volumes/s (1.205 s) and the paper 27.5 frames/s (1.2 s per volume).
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:
print(f"notebook ran in {(time.time() - T0) / 60:.1f} min")
notebook ran in 24.9 min