General odorants and bilateral 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. 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 general odorant experiment (6 ants, Figure 3F-G) and the bilateral imaging
(3 ants, Figure S4A). The alarm pheromone claims are in alarm_pheromones.ipynb.
| Claim | Here |
|---|---|
| fig3f-general-odorants-unique | tested |
| fig3fg-hexanone-broad | tested |
| figs4a-bilateral-symmetry | tested |
| all other claims | alarm_pheromones.ipynb, or blocked (see the attempt record) |
import time
import numpy as np
import pandas as pd
from scipy import ndimage
import al_imaging as al
pd.set_option("display.width", 200)
pd.set_option("display.max_columns", 40)
T0 = time.time()
print("dandiset", al.DANDISET_ID, "version", al.DANDISET_VERSION)
dandiset 000467 version 0.261004.1655
assets = al.list_assets()
trials = al.trials(assets)
alarm_ants = sorted(trials[trials.odor == "heptanone"].ant.unique())
gen_ants = sorted(trials[trials.odor == "3-hexanone"].ant.unique())
bil_ants = sorted(set(trials.ant) - set(alarm_ants) - set(gen_ants))
gen = trials[trials.ant.isin(gen_ants)]
bil = trials[trials.ant.isin(bil_ants)]
print("general odorant ants:", gen_ants)
print("bilateral ants:", bil_ants)
general odorant ants: ['ant1-m10-d11-y2022', 'ant1-m10-d6-y2022', 'ant2-m10-d11-y2022', 'ant3-m10-d11-y2022', 'ant3-m10-d6-y2022', 'ant5-m10-d11-y2022'] bilateral ants: ['ant2-m2-d10-y2023', 'ant3-m2-d10-y2023', 'ant4-m2-d10-y2023']
print("General odorant trials per ant, odor and concentration (planes per trial", sorted(gen.n_planes.unique()),
", volumes", sorted(gen.nvol.unique()), "):")
print(gen.groupby(["ant", "odor"]).size().unstack(fill_value=0).to_string())
print(gen.groupby(["odor", "conc"]).size().to_string())
print("\nBilateral trials:")
print(bil.groupby(["ant", "odor", "conc"]).size().unstack(fill_value=0).to_string())
meta = pd.DataFrame([{"ant": ant, **{k: v for k, v in al.file_metadata(trials[trials.ant == ant].iloc[0].blobs[16]).items()
if k in ("shape", "offset", "imaging_rate", "grid_spacing", "age", "location")}}
for ant in gen_ants + bil_ants])
print(meta.to_string(index=False))
General odorant trials per ant, odor and concentration (planes per trial [np.int64(33)] , volumes [np.int64(40)] ):
odor 3-hexanone ethanol ethylpyrazine isopropanol propionic acid vehicle
ant
ant1-m10-d11-y2022 3 3 3 3 3 1
ant1-m10-d6-y2022 3 3 3 3 0 1
ant2-m10-d11-y2022 3 3 3 3 3 1
ant3-m10-d11-y2022 3 3 3 3 3 1
ant3-m10-d6-y2022 3 3 3 3 0 1
ant5-m10-d11-y2022 3 3 3 3 3 1
odor conc
3-hexanone 0.48 18
ethanol 0.48 18
ethylpyrazine 0.48 18
isopropanol 0.48 18
propionic acid 0.48 12
vehicle 1.00 6
Bilateral trials:
conc 0.48 1.00
ant odor
ant2-m2-d10-y2023 hexanol 3 0
vehicle 0 1
ant3-m2-d10-y2023 hexanol 3 0
vehicle 0 1
ant4-m2-d10-y2023 hexanol 3 0
vehicle 0 1
ant shape offset imaging_rate grid_spacing location age ant1-m10-d11-y2022 (40, 512, 512) 3464 0.83 (0.288867, 0.288867, 5.0) right antennal lobe, up is lateral, right is anterior P60D ant1-m10-d6-y2022 (40, 512, 512) 3464 0.83 (0.288867, 0.288867, 5.0) right antennal lobe, up is lateral, right is anterior P55D ant2-m10-d11-y2022 (40, 512, 512) 3464 0.83 (0.288867, 0.288867, 5.0) right antennal lobe, up is lateral, right is anterior P60D ant3-m10-d11-y2022 (40, 512, 512) 3464 0.83 (0.288867, 0.288867, 5.0) right antennal lobe, up is lateral, right is anterior P60D ant3-m10-d6-y2022 (40, 512, 512) 3464 0.83 (0.288867, 0.288867, 5.0) right antennal lobe, up is lateral, right is anterior P55D ant5-m10-d11-y2022 (40, 512, 512) 3464 0.83 (0.288867, 0.288867, 5.0) right antennal lobe, up is lateral, right is anterior P60D ant2-m2-d10-y2023 (40, 512, 512) 3464 0.83 (0.577733, 0.577733, 5.0) bilateral imaging of both antennal lobes, right is anterior P91D ant3-m2-d10-y2023 (40, 512, 512) 3464 0.83 (0.577733, 0.577733, 5.0) bilateral imaging of both antennal lobes, right is anterior P91D ant4-m2-d10-y2023 (40, 512, 512) 3464 0.83 (0.577733, 0.577733, 5.0) bilateral imaging of both antennal lobes, right is anterior P91D
The general odorant files hold five odorants and the vehicle, the five the paper analyses; the paper says 7 to 9 odorants were presented, so the others were not deposited. Ethylpyrazine, ethanol, isopropanol and 3-hexanone were given to all six ants and propionic acid to the four of 2022-10-11. The bilateral files have a pixel size of 0.578 um (1X zoom), as the paper states, and their location says "bilateral imaging of both antennal lobes, right is anterior".
t = time.time()
gmaps = al.trial_maps(gen[gen.nvol == 40])
print(f"general odorant maps: {len(gen)} trials in {time.time() - t:.0f} s")
t = time.time()
bmaps = al.trial_maps(bil)
print(f"bilateral maps: {len(bil)} trials in {time.time() - t:.0f} s")
general odorant maps: 90 trials in 355 s
bilateral maps: 12 trials in 46 s
for name, mp, tt in (("general", gmaps, gen), ("bilateral", bmaps, bil)):
sh = pd.Series({i: mp[i]["shift"].max() for i in tt.index})
print(f"{name}: largest frame shift per trial median {sh.median()} px, max {sh.max()} px; baseline F0 range "
f"{min(mp[i]['f0'].min() for i in tt.index):.0f} to {max(mp[i]['f0'].max() for i in tt.index):.0f}; "
f"saturated baseline pixels, max fraction {max(mp[i]['saturated'].max() for i in tt.index):.4f}")
dups = [(a, b) for ant, g in tt.groupby("ant") for k, a in enumerate(g.index) for b in g.index[k + 1:]
if np.array_equal(mp[a]["f0"], mp[b]["f0"])]
print(f"{name}: pairs of trials with identical baseline volumes: {dups if dups else 'none'}")
general: largest frame shift per trial median 12.0 px, max 40 px; baseline F0 range 0 to 8191; saturated baseline pixels, max fraction 0.0111 general: pairs of trials with identical baseline volumes: none bilateral: largest frame shift per trial median 4.0 px, max 8 px; baseline F0 range 0 to 8106; saturated baseline pixels, max fraction 0.0019 bilateral: pairs of trials with identical baseline volumes: none
fig3f-general-odorants-unique¶
Each general odorant activates a unique combination of glomeruli, and the same odorant activates similar AL regions in different individuals.
Criterion (the reader's rule): within each ant, the M2 sets of responding ROIs differ between every pair of the odorants that ant was given; across ants, the centroid of each odorant's responding pixels (at least 0.2, trial-averaged map), in coordinates scaled to the AL's bounding box in raw fluorescence, lies closer to the same odorant's centroids in the other ants than to other odorants' centroids, in more than half of the ant and odorant cases. Reproduced when both hold under every M2 reading. "Closer" is read as: the mean distance to the same odorant's centroids in the other ants is smaller than the mean distance to each other odorant's centroids in the other ants.
ODS = ["3-hexanone", "ethylpyrazine", "ethanol", "isopropanol", "propionic acid"]
gmask = {ant: al.al_mask(gmaps, gen[gen.ant == ant].index) for ant in gen_ants}
grois, gresp = {}, {}
rows = []
for ant in gen_ants:
g = gen[(gen.ant == ant) & gen.odor.isin(ODS)]
for d in "ab":
grois[ant, d] = al.draw_rois(gmaps, g, d, gmask[ant])
for d, v, c in al.READINGS:
R = {}
for od in sorted(g.odor.unique()):
R[od] = al.responding(gmaps, grois[ant, d], g[g.odor == od].index, v, c)[0]
ods = list(R)
pairs = [(a, b) for k, a in enumerate(ods) for b in ods[k + 1:]]
same = [(a, b) for a, b in pairs if np.array_equal(R[a], R[b])]
rows.append({"ant": ant, "reading": d + v + c, "ROIs": al.n_groups(grois[ant, d]),
**{od: int(R[od].sum()) for od in ods}, "pairs": len(pairs), "identical pairs": len(same),
"identical": same})
uni = pd.DataFrame(rows)
print(uni.drop(columns=["identical"]).to_string(index=False))
within = uni.groupby("reading")["identical pairs"].sum()
print("\nidentical odorant pairs summed over ants, per reading:", within.to_dict())
# across ants: centroids in AL bounding-box coordinates
cent = []
for ant in gen_ants:
m = gmask[ant]
rr, cc = np.where(m)
box = (rr.min(), rr.max(), cc.min(), cc.max())
g = gen[gen.ant == ant]
for od in ODS:
idx = g[g.odor == od].index
if not len(idx):
continue
mean = np.mean([al._bin(gmaps[i]["proj"], al.BIN) for i in idx], axis=0)
px = (mean >= al.THRESHOLD) & m
if not px.any():
cent.append({"ant": ant, "odor": od, "y": np.nan, "x": np.nan, "n_px": 0})
continue
cy, cx = ndimage.center_of_mass(px)
cent.append({"ant": ant, "odor": od, "y": (cy - box[0]) / (box[1] - box[0]), "x": (cx - box[2]) / (box[3] - box[2]), "n_px": int(px.sum())})
cent = pd.DataFrame(cent)
print(cent.round(3).to_string(index=False))
cases = []
for _, r in cent.dropna().iterrows():
others = cent[(cent.ant != r.ant)].dropna()
dist = {od: np.hypot(g.y - r.y, g.x - r.x).mean() for od, g in others.groupby("odor")}
if r.odor not in dist:
continue
cases.append({"ant": r.ant, "odor": r.odor, "same": dist[r.odor], "nearest other": min(v for k, v in dist.items() if k != r.odor),
"closer to same": dist[r.odor] < min(v for k, v in dist.items() if k != r.odor)})
cases = pd.DataFrame(cases)
print(cases.round(3).to_string(index=False))
print(f"\nacross ants: {int(cases['closer to same'].sum())} of {len(cases)} ant and odorant cases closer to the same odorant")
print("criterion met (within ants, no identical pair) under readings:", [k for k, v in within.items() if v == 0])
ant reading ROIs 3-hexanone ethanol ethylpyrazine isopropanol propionic acid pairs identical pairs
ant1-m10-d11-y2022 aix 8 8 4 7 0 1.0 10 0
ant1-m10-d11-y2022 aiy 8 8 3 3 1 1.0 10 1
ant1-m10-d11-y2022 aiix 8 8 8 8 4 6.0 10 3
ant1-m10-d11-y2022 aiiy 8 8 7 6 4 7.0 10 1
ant1-m10-d11-y2022 bix 46 40 27 34 0 1.0 10 0
ant1-m10-d11-y2022 biy 46 43 11 7 0 1.0 10 0
ant1-m10-d11-y2022 biix 46 46 45 45 29 42.0 10 1
ant1-m10-d11-y2022 biiy 46 46 44 44 27 32.0 10 0
ant1-m10-d6-y2022 aix 31 5 0 3 0 NaN 6 1
ant1-m10-d6-y2022 aiy 31 6 1 4 4 NaN 6 0
ant1-m10-d6-y2022 aiix 31 14 10 15 11 NaN 6 0
ant1-m10-d6-y2022 aiiy 31 20 10 17 13 NaN 6 0
ant1-m10-d6-y2022 bix 9 3 0 1 0 NaN 6 1
ant1-m10-d6-y2022 biy 9 2 0 1 0 NaN 6 1
ant1-m10-d6-y2022 biix 9 9 7 7 5 NaN 6 0
ant1-m10-d6-y2022 biiy 9 9 7 8 5 NaN 6 0
ant2-m10-d11-y2022 aix 15 5 0 1 2 0.0 10 1
ant2-m10-d11-y2022 aiy 15 3 0 1 1 1.0 10 0
ant2-m10-d11-y2022 aiix 15 10 2 9 7 6.0 10 0
ant2-m10-d11-y2022 aiiy 15 7 3 9 6 6.0 10 0
ant2-m10-d11-y2022 bix 17 6 0 1 0 0.0 10 3
ant2-m10-d11-y2022 biy 17 3 0 1 0 0.0 10 3
ant2-m10-d11-y2022 biix 17 17 7 17 13 10.0 10 1
ant2-m10-d11-y2022 biiy 17 15 5 9 10 10.0 10 0
ant3-m10-d11-y2022 aix 19 10 1 5 2 0.0 10 0
ant3-m10-d11-y2022 aiy 19 9 3 2 4 1.0 10 0
ant3-m10-d11-y2022 aiix 19 13 8 13 15 12.0 10 0
ant3-m10-d11-y2022 aiiy 19 12 9 12 15 9.0 10 0
ant3-m10-d11-y2022 bix 37 19 0 10 0 0.0 10 3
ant3-m10-d11-y2022 biy 37 21 0 2 6 0.0 10 1
ant3-m10-d11-y2022 biix 37 37 30 37 37 36.0 10 3
ant3-m10-d11-y2022 biiy 37 33 28 35 36 27.0 10 0
ant3-m10-d6-y2022 aix 9 9 0 2 2 NaN 6 1
ant3-m10-d6-y2022 aiy 9 9 0 3 3 NaN 6 0
ant3-m10-d6-y2022 aiix 9 9 3 4 5 NaN 6 0
ant3-m10-d6-y2022 aiiy 9 9 3 4 6 NaN 6 0
ant3-m10-d6-y2022 bix 18 16 0 8 10 NaN 6 0
ant3-m10-d6-y2022 biy 18 17 0 9 10 NaN 6 0
ant3-m10-d6-y2022 biix 18 18 6 17 18 NaN 6 1
ant3-m10-d6-y2022 biiy 18 18 6 18 18 NaN 6 3
ant5-m10-d11-y2022 aix 30 12 2 4 11 2.0 10 0
ant5-m10-d11-y2022 aiy 30 11 6 3 11 4.0 10 0
ant5-m10-d11-y2022 aiix 30 16 16 14 21 20.0 10 0
ant5-m10-d11-y2022 aiiy 30 16 17 13 18 19.0 10 0
ant5-m10-d11-y2022 bix 44 35 0 12 27 3.0 10 0
ant5-m10-d11-y2022 biy 44 34 1 12 19 3.0 10 0
ant5-m10-d11-y2022 biix 44 43 44 42 44 44.0 10 3
ant5-m10-d11-y2022 biiy 44 43 42 43 44 44.0 10 1
identical odorant pairs summed over ants, per reading: {'aiix': 3, 'aiiy': 1, 'aix': 3, 'aiy': 1, 'biix': 9, 'biiy': 4, 'bix': 7, 'biy': 5}
ant odor y x n_px
ant1-m10-d11-y2022 3-hexanone 0.502 0.504 3235
ant1-m10-d11-y2022 ethylpyrazine 0.446 0.457 2341
ant1-m10-d11-y2022 ethanol 0.398 0.457 1517
ant1-m10-d11-y2022 isopropanol 0.314 0.343 16
ant1-m10-d11-y2022 propionic acid 0.454 0.531 131
ant1-m10-d6-y2022 3-hexanone 0.482 0.518 551
ant1-m10-d6-y2022 ethylpyrazine 0.434 0.634 199
ant1-m10-d6-y2022 ethanol 0.425 0.277 10
ant1-m10-d6-y2022 isopropanol 0.394 0.369 21
ant2-m10-d11-y2022 3-hexanone 0.577 0.514 1021
ant2-m10-d11-y2022 ethylpyrazine 0.455 0.665 199
ant2-m10-d11-y2022 ethanol 0.462 0.313 3
ant2-m10-d11-y2022 isopropanol 0.596 0.502 37
ant2-m10-d11-y2022 propionic acid 0.375 0.609 45
ant3-m10-d11-y2022 3-hexanone 0.586 0.485 2043
ant3-m10-d11-y2022 ethylpyrazine 0.361 0.507 866
ant3-m10-d11-y2022 ethanol 0.476 0.379 13
ant3-m10-d11-y2022 isopropanol 0.294 0.382 408
ant3-m10-d11-y2022 propionic acid 0.300 0.468 240
ant3-m10-d6-y2022 3-hexanone 0.571 0.488 2660
ant3-m10-d6-y2022 ethylpyrazine 0.525 0.505 864
ant3-m10-d6-y2022 ethanol NaN NaN 0
ant3-m10-d6-y2022 isopropanol 0.554 0.489 999
ant5-m10-d11-y2022 3-hexanone 0.605 0.514 2237
ant5-m10-d11-y2022 ethylpyrazine 0.507 0.548 711
ant5-m10-d11-y2022 ethanol 0.519 0.510 247
ant5-m10-d11-y2022 isopropanol 0.535 0.463 1392
ant5-m10-d11-y2022 propionic acid 0.583 0.475 494
ant odor same nearest other closer to same ant1-m10-d11-y2022 3-hexanone 0.072 0.105 True ant1-m10-d11-y2022 ethylpyrazine 0.138 0.125 False ant1-m10-d11-y2022 ethanol 0.145 0.143 False ant1-m10-d11-y2022 isopropanol 0.196 0.177 False ant1-m10-d11-y2022 propionic acid 0.139 0.093 False ant1-m10-d6-y2022 3-hexanone 0.089 0.085 False ant1-m10-d6-y2022 ethylpyrazine 0.126 0.150 True ant1-m10-d6-y2022 ethanol 0.150 0.209 True ant1-m10-d6-y2022 isopropanol 0.159 0.112 False ant2-m10-d11-y2022 3-hexanone 0.051 0.136 True ant2-m10-d11-y2022 ethylpyrazine 0.146 0.193 True ant2-m10-d11-y2022 ethanol 0.120 0.157 True ant2-m10-d11-y2022 isopropanol 0.201 0.055 False ant2-m10-d11-y2022 propionic acid 0.173 0.132 False ant3-m10-d11-y2022 3-hexanone 0.055 0.128 True ant3-m10-d11-y2022 ethylpyrazine 0.149 0.141 False ant3-m10-d11-y2022 ethanol 0.107 0.132 True ant3-m10-d11-y2022 isopropanol 0.201 0.184 False ant3-m10-d11-y2022 propionic acid 0.203 0.193 False ant3-m10-d6-y2022 3-hexanone 0.050 0.161 True ant3-m10-d6-y2022 ethylpyrazine 0.127 0.053 False ant3-m10-d6-y2022 isopropanol 0.167 0.051 False ant5-m10-d11-y2022 3-hexanone 0.067 0.183 True ant5-m10-d11-y2022 ethylpyrazine 0.110 0.070 False ant5-m10-d11-y2022 ethanol 0.182 0.049 False ant5-m10-d11-y2022 isopropanol 0.156 0.059 False ant5-m10-d11-y2022 propionic acid 0.223 0.053 False across ants: 10 of 27 ant and odorant cases closer to the same odorant criterion met (within ants, no identical pair) under readings: []
Result: not reproduced. Within ants, identical odorant pairs occur under every reading (1 to 9 summed over the 6 ants; bix 7). Across ants, 10 of 27 ant and odorant cases lie closer to the same odorant in the other ants than to any other odorant, fewer than half; 3-hexanone does so in 5 of its 6 ants.
fig3fg-hexanone-broad¶
The breadth of responses varies across general odorants; most activate a few glomeruli, while 3-hexanone activates large regions of the ventral and medial AL.
Criterion: breadth is the area of pixels of at least 0.2 in each ant's trial-averaged M1 map. Reproduced when 3-hexanone has the largest area of the five odorants in every ant where it was tested, and its median area across ants is at least twice that of each other odorant. The area is counted inside the AL mask (pixels outside the AL are background).
area = []
for ant in gen_ants:
g = gen[gen.ant == ant]
for od in ODS:
idx = g[g.odor == od].index
if not len(idx):
continue
mean = np.mean([gmaps[i]["proj"] for i in idx], axis=0)
mfull = np.kron(gmask[ant], np.ones((al.BIN, al.BIN), bool))
area.append({"ant": ant, "odor": od, "area_um2": float(((mean >= al.THRESHOLD) & mfull).sum() * al.PIXEL_UM ** 2),
"area_all_um2": float((mean >= al.THRESHOLD).sum() * al.PIXEL_UM ** 2)})
area = pd.DataFrame(area)
tab = area.pivot_table(index="ant", columns="odor", values="area_um2").round(0)
print(tab.to_string())
largest = tab.idxmax(axis=1)
print("odorant with the largest area per ant:", largest.to_dict())
med = tab.median()
print("median area (um2):", med.round(0).to_dict())
ratio = {od: round(med["3-hexanone"] / med[od], 2) for od in ODS if od != "3-hexanone"}
print("median 3-hexanone area over each other odorant's median:", ratio)
print("3-hexanone largest in every ant:", bool((largest == "3-hexanone").all()), "; ratios all >= 2:", all(v >= 2 for v in ratio.values()))
print("\nVariant, the whole field of view (no AL mask):")
tab2 = area.pivot_table(index="ant", columns="odor", values="area_all_um2").round(0)
print(tab2.to_string())
print("largest per ant:", tab2.idxmax(axis=1).to_dict(), "; median ratios:",
{od: round(tab2.median()["3-hexanone"] / tab2.median()[od], 2) for od in ODS if od != "3-hexanone"})
odor 3-hexanone ethanol ethylpyrazine isopropanol propionic acid
ant
ant1-m10-d11-y2022 4221.0 1989.0 2979.0 57.0 243.0
ant1-m10-d6-y2022 801.0 37.0 305.0 54.0 NaN
ant2-m10-d11-y2022 1385.0 14.0 351.0 84.0 76.0
ant3-m10-d11-y2022 2702.0 72.0 1246.0 695.0 442.0
ant3-m10-d6-y2022 3567.0 5.0 1163.0 1310.0 NaN
ant5-m10-d11-y2022 2917.0 515.0 1022.0 1862.0 858.0
odorant with the largest area per ant: {'ant1-m10-d11-y2022': '3-hexanone', 'ant1-m10-d6-y2022': '3-hexanone', 'ant2-m10-d11-y2022': '3-hexanone', 'ant3-m10-d11-y2022': '3-hexanone', 'ant3-m10-d6-y2022': '3-hexanone', 'ant5-m10-d11-y2022': '3-hexanone'}
median area (um2): {'3-hexanone': 2810.0, 'ethanol': 54.0, 'ethylpyrazine': 1092.0, 'isopropanol': 390.0, 'propionic acid': 342.0}
median 3-hexanone area over each other odorant's median: {'ethylpyrazine': np.float64(2.57), 'ethanol': np.float64(51.55), 'isopropanol': np.float64(7.21), 'propionic acid': np.float64(8.2)}
3-hexanone largest in every ant: True ; ratios all >= 2: True
Variant, the whole field of view (no AL mask):
odor 3-hexanone ethanol ethylpyrazine isopropanol propionic acid
ant
ant1-m10-d11-y2022 4320.0 2001.0 3032.0 58.0 255.0
ant1-m10-d6-y2022 807.0 38.0 311.0 56.0 NaN
ant2-m10-d11-y2022 1391.0 19.0 362.0 85.0 77.0
ant3-m10-d11-y2022 2785.0 73.0 1252.0 701.0 445.0
ant3-m10-d6-y2022 3845.0 5.0 1181.0 1313.0 NaN
ant5-m10-d11-y2022 2927.0 546.0 1031.0 1898.0 920.0
largest per ant: {'ant1-m10-d11-y2022': '3-hexanone', 'ant1-m10-d6-y2022': '3-hexanone', 'ant2-m10-d11-y2022': '3-hexanone', 'ant3-m10-d11-y2022': '3-hexanone', 'ant3-m10-d6-y2022': '3-hexanone', 'ant5-m10-d11-y2022': '3-hexanone'} ; median ratios: {'ethylpyrazine': np.float64(2.58), 'ethanol': np.float64(51.46), 'isopropanol': np.float64(7.27), 'propionic acid': np.float64(8.16)}
Result: reproduced. 3-hexanone has the largest area of at least 0.2 in all 6 ants (median 2,810 um2), 2.57 times the median of ethylpyrazine (1,092 um2) and 7.2 to 51.6 times those of the other three odorants. Without the AL mask it is also the largest in every ant, and the ratio to ethylpyrazine is 2.58.
figs4a-bilateral-symmetry¶
Responses to 4-methyl-3-hexanol are bilaterally symmetrical in the two ALs imaged together.
Criterion (the reader's rule): in each of the three bilateral ants (1X zoom, hexanol 48%), M1 maps split at the midline between the ALs, one side mirrored. Reproduced when in every ant both ALs have responding ROIs (M2) and their counts differ by at most one, and the mirrored map of responding pixels of one AL and the map of the other correlate better than the second does with itself shifted by half its width. The two ALs lie one above the other in the images, so the midline is a row, placed at the least fluorescent row between 30% and 70% of the height, and each AL is the largest region of baseline fluorescence on its side; the mirror is a flip of rows. The files do not say which AL is the left one.
PX1X = 0.57773327
rows = []
for ant in bil_ants:
g = bil[bil.ant == ant]
from skimage.filters import threshold_otsu
f0 = np.mean([bmaps[i]["f0"].max(0) for i in g.index], axis=0)
mask = np.log1p(f0) > threshold_otsu(np.log1p(f0))
prof = ndimage.uniform_filter1d(np.where(mask, f0, 0).sum(1), 5)
lo, hi = int(0.3 * len(prof)), int(0.7 * len(prof))
mid = lo + int(np.argmin(prof[lo:hi]))
comps = []
for half in (slice(0, mid), slice(mid, f0.shape[0])):
part = np.zeros_like(mask)
part[half] = mask[half]
lab, n = ndimage.label(part)
sizes = ndimage.sum(np.ones_like(f0), lab, range(1, n + 1))
comps.append(ndimage.binary_fill_holes(lab == 1 + int(np.argmax(sizes))))
sizes = np.array([c.sum() for c in comps])
w = min(mid, f0.shape[0] - mid)
hx = g[g.odor == "hexanol"]
resp_px = np.mean([al._bin(bmaps[i]["proj"], al.BIN) for i in hx.index], axis=0) >= al.THRESHOLD
top = resp_px[mid - w:mid][::-1]
bottom = resp_px[mid:mid + w]
r_mirror = np.corrcoef(top.ravel(), bottom.ravel())[0, 1]
half = bottom.shape[1] // 2
r_shift = np.corrcoef(bottom.ravel(), np.roll(bottom, half, axis=1).ravel())[0, 1]
row = {"ant": ant, "midline row (binned)": mid, "AL areas (bins)": [int(x) for x in sizes],
"r mirrored": round(r_mirror, 3), "r shifted half width": round(r_shift, 3)}
for d, v, c in al.READINGS:
ns = []
for comp in comps:
rr = al.draw_rois(bmaps, hx, d, comp, pixel_um=PX1X)
ns.append(al.responding(bmaps, rr, hx.index, v, c)[1])
row[d + v + c] = tuple(round(x, 1) for x in ns)
rows.append(row)
sym = pd.DataFrame(rows)
print(sym.to_string(index=False))
for d, v, c in al.READINGS:
k = d + v + c
ok_counts = all(min(x) > 0 and abs(x[0] - x[1]) <= 1 for x in sym[k])
ok_corr = bool((sym["r mirrored"] > sym["r shifted half width"]).all())
print(f"reading {k}: counts on both sides and within one in every ant {ok_counts}; mirrored correlation above shifted in every ant {ok_corr}; met {ok_counts and ok_corr}")
ant midline row (binned) AL areas (bins) r mirrored r shifted half width aix aiy aiix aiiy bix biy biix biiy ant2-m2-d10-y2023 60 [1577, 1851] 0.143 -0.015 (5.0, 8.0) (5.7, 7.7) (7.0, 9.0) (7.3, 8.7) (4.0, 5.0) (3.7, 4.7) (4.0, 5.0) (4.0, 5.0) ant3-m2-d10-y2023 56 [1351, 1356] 0.185 -0.024 (4.0, 5.0) (3.7, 4.7) (5.0, 8.0) (4.7, 8.0) (4.0, 8.0) (3.3, 7.3) (4.0, 8.0) (4.0, 8.0) ant4-m2-d10-y2023 61 [1430, 1430] 0.072 -0.008 (3.0, 4.0) (3.7, 3.3) (4.0, 5.0) (4.3, 4.7) (1.0, 3.0) (1.3, 2.3) (2.0, 3.0) (1.7, 3.0) reading aix: counts on both sides and within one in every ant False; mirrored correlation above shifted in every ant True; met False reading aiy: counts on both sides and within one in every ant False; mirrored correlation above shifted in every ant True; met False reading aiix: counts on both sides and within one in every ant False; mirrored correlation above shifted in every ant True; met False reading aiiy: counts on both sides and within one in every ant False; mirrored correlation above shifted in every ant True; met False reading bix: counts on both sides and within one in every ant False; mirrored correlation above shifted in every ant True; met False reading biy: counts on both sides and within one in every ant False; mirrored correlation above shifted in every ant True; met False reading biix: counts on both sides and within one in every ant False; mirrored correlation above shifted in every ant True; met False reading biiy: counts on both sides and within one in every ant False; mirrored correlation above shifted in every ant True; met False
Result: partially reproduced. In all three ants both ALs have responding ROIs under every reading, and the mirrored maps of responding pixels correlate better (r = 0.072 to 0.185) than the half-width shift (r = -0.024 to -0.008). The ROI counts of the two ALs differ by more than one in at least one ant under every reading (under bix 4 and 5, 4 and 8, 1 and 3).
Summary¶
The outcomes of all claims, the differences from the paper's method and the findings about the dataset are summarised in alarm_pheromones.ipynb and the attempt record. Here: fig3f-general-odorants-unique: not reproduced; fig3fg-hexanone-broad: reproduced; figs4a-bilateral-symmetry: partially reproduced.
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 6.9 min