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)
In [1]:
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
In [2]:
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']
In [3]:
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".

In [4]:
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
In [5]:
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.

In [6]:
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).

In [7]:
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.

In [8]:
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:

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