Reproducing Kim et al. (2022) from dandiset 000489¶

Paper. Kim V, Semenov I, Kiester AS, Keppler MA, Ibey BL, Bixler JN, Pakhomov AG (2022). Action spectra and mechanisms of (in)efficiency of bipolar electric pulses at electroporation. Bioelectrochemistry 149:108319. doi:10.1016/j.bioelechem.2022.108319. The claims file was written from the accepted author manuscript in PubMed Central (PMC9729435), which holds the main text, the Methods and the figure legends; the paper has no supplement and links no code.

Dandiset. DANDI:000489, version 0.230518.1811 (doi:10.48324/dandi.000489/0.230518.1811). The paper cites no version. This is the only published version, and the draft holds the same 18 files (same paths, sizes and blobs; the check is printed below), so the most recent published version is pinned.

What the dandiset holds. One stitched two-channel image (MemBrite and YO-PRO-1) of the whole glass bottom of each of 17 dishes exposed to 5 pulses at 1 Hz, unipolar 600 ns or bipolar 600+600 ns with a second phase of 25, 50, 75 or 100%: the experiment of Fig. 2. An 18th file is a sample image named for Fig. 1F. The experiments of Figs. 3 to 6 (pulse number, duration, the aa' line, repetition rate), the strobe imaging of Fig. 7 and the thermometry of Fig. 1E are not in it. Those of Figs. 3 to 7 are in four other dandisets that name the paper (000548, 000549, 000550 and 000448; listed in the data check). They are separate dandisets and separate targets of the registry, and the claims file does not name them as a source for this one, so they are not used. The files hold no ROI, no field values, no pixel size and no footprint values: everything is measured here from the images, and the field of each ROI is recomputed from the electrode geometry of Methods 2.3 (see bipolar.py).

Method. Electrode footprints are found as the dark discs of the YP image. ROI of 200 um are tiled from the midpoint of the electrode centres along bb', the perpendicular to the electrode axis, out to 3 mm on each side. In each ROI, YP = YP_ROI x MB_0 / MB_ROI - YP_0 (Methods 2.5), with YP_0 and MB_0 from the footprints. The file sub-Fig2-0-03 is left out of the main analysis because its YP channel is a copy of another dish's (data check). Dandiset 000548 holds the YP image of that dish; it is checked, not used.

Claim Figure Status
fig2a-uptake-rises-with-field 2A tested
fig2a-vertical-scales 2A tested
fig2b-exponential-same-slope 2B tested
fig2c-max-inhibition-50-75 2C tested
fig2c-inhibition-flat-above-8-7 2C tested
fig2c-weaker-at-6-7 2C analysed; blocked: other (unipolar mean at background at 6.7 kV/cm)
fig3-uptake-linear-in-pulse-number, fig3d-cancellation-independent-of-number 3 blocked: other (data in another dandiset of the paper)
fig4ac-ns-bipolar-20-25pct, fig4d-1200ns-tenfold, fig4eg-us-bipolar-30-50pct, fig4h-us-stronger-at-low-field 4 blocked: other (data in another dandiset of the paper)
fig5-ns-symmetric-along-aa, fig5-us-anode-stronger 5 blocked: other (data in another dandiset of the paper)
fig6ad-unipolar-833khz-enhanced, fig6be-bipolar-khz-less-than-1hz, fig6cf-833khz-thirtyfold, fig6cf-cancellation-stronger-at-high-field 6 blocked: other (data in another dandiset of the paper)
fig7-charging-time-constant, fig7-bipolar-overshoot, fig7-summation-without-buildup 7 blocked: other (data in another dandiset of the paper)
fig1e-heating 1E blocked: data missing
In [1]:
import time
import warnings

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import requests
from scipy import optimize, stats

import bipolar as bp

pd.set_option("display.width", 200)
pd.set_option("display.max_columns", 40)
pd.set_option("display.max_rows", 200)
warnings.filterwarnings("ignore", category=RuntimeWarning)

started = time.time()
dishes = bp.load_all()
print(f"loaded {len(dishes)} files from dandiset {bp.DANDISET_ID} version {bp.DANDISET_VERSION} "
      f"in {time.time() - started:.0f} s")
loaded 18 files from dandiset 000489 version 0.230518.1811 in 558 s

Data check¶

Each check of the metadata against the values is stated under its output.

In [2]:
meta = bp.dandiset_metadata()
draft = bp.dandiset_metadata("draft")
print("Title:", meta["name"])
print("Description:", meta["description"])
print("Related resource:", [(r["relation"], r["url"]) for r in meta.get("relatedResource", [])])
draft_assets = sorted((a["path"], a["size"], a.get("blob")) for a in
    requests.get(f"{bp.API}/dandisets/{bp.DANDISET_ID}/versions/draft/assets/?page_size=100").json()["results"])
print("Draft holds the same files (path, size, blob) as", bp.DANDISET_VERSION, ":",
      draft_assets == sorted((a["path"], a["size"], a["blob"]) for a in bp.assets()))
print("Metadata fields that differ between draft and published:",
      sorted(k for k in set(meta) | set(draft) if meta.get(k) != draft.get(k)))

info = pd.DataFrame([{
    "subject": d["subject"], "amplitude_%": d["amplitude"], "acquisition": ",".join(d["meta"]["acquisition"]),
    "session_id": d["meta"]["session_id"], "date": d["meta"]["session_start"][:16],
    "shape": d["meta"]["shape"], "dtype": d["meta"]["dtype"], "unit": d["meta"]["unit"],
    "conversion": d["meta"]["conversion"], "rate": d["meta"]["rate"],
    "other_groups": d["meta"]["processing"] + d["meta"]["analysis"] + d["meta"]["stimulus"]} for d in dishes])
print("\nFiles:", len(info))
print(info.to_string(index=False))
print("\nDishes per second phase amplitude (Fig. 2 files):")
print(info["amplitude_%"].value_counts().sort_index().to_string())
print("\nSeries description (first Fig. 2 file):", dishes[1]["meta"]["description"])
print("Session description (first Fig. 2 file):", dishes[1]["meta"]["session_description"])
Title: The impact of the second phase amplitude (% to the first phase) on the electroporation efficiency (measured as YP emission)
Description: Cells were electroporated by a train of 5 unipolar 600 ns pulses (0 percent) or bipolar 600 + 600 ns pulses (25, 50, 75, 100 percent) at 1 Hz. 25, 50, 75, and 100% represent the second pulse's amplitude in relation to the first pulse's amplitude. 0% represents a unipolar pulse, where no second pulse was applied. The electrodes were two parallel stainless-steel cylinders, 1.5 mm in diameter and 3 mm apart. Supported by NIH 1R21EY034258
Related resource: [('dcite:IsPublishedIn', 'https://doi.org/10.1016/j.bioelechem.2022.108319')]
Draft holds the same files (path, size, blob) as 0.230518.1811 : True
Metadata fields that differ between draft and published: ['citation', 'doi', 'id', 'manifestLocation', 'url', 'version']

Files: 18
        subject  amplitude_%           acquisition               session_id             date             shape  dtype unit  conversion  rate other_groups
      sub-Fig1F          NaN Channel1 and Channel2            BPAE_staining 2021-09-01T19:13 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
  sub-Fig2-0-03          0.0             Fig2_0_03   BPAE_bipolar_0 percent 2021-07-29T13:00 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
  sub-Fig2-0-06          0.0             Fig2_0_06   BPAE_bipolar_0 percent 2021-07-29T13:46 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
  sub-Fig2-0-07          0.0             Fig2_0_07   BPAE_bipolar_0 percent 2021-07-29T14:00 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
sub-Fig2-100-03        100.0           Fig2_100_03 BPAE_bipolar_100 percent 2021-08-24T15:18 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
sub-Fig2-100-07        100.0           Fig2_100_07 BPAE_bipolar_100 percent 2021-08-24T16:16 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
sub-Fig2-100-08        100.0           Fig2_100_08 BPAE_bipolar_100 percent 2021-08-24T16:31 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
 sub-Fig2-25-04         25.0               Fig2_25  BPAE_bipolar_25 percent 2021-07-26T17:25 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
 sub-Fig2-25-10         25.0               Fig2_25  BPAE_bipolar_25 percent 2021-07-29T16:24 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
 sub-Fig2-25-14         25.0               Fig2_25  BPAE_bipolar_25 percent 2021-07-29T17:22 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
 sub-Fig2-50-06         50.0            Fig2_50_06  BPAE_bipolar_50 percent 2021-07-26T18:52 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
 sub-Fig2-50-10         50.0            Fig2_50_10  BPAE_bipolar_50 percent 2021-07-26T19:52 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
 sub-Fig2-50-12         50.0            Fig2_50_12  BPAE_bipolar_50 percent 2021-07-26T20:21 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
 sub-Fig2-50-13         50.0            Fig2_50_13  BPAE_bipolar_50 percent 2021-07-29T17:07 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
 sub-Fig2-50-18         50.0            Fig2_50_18  BPAE_bipolar_50 percent 2021-07-29T18:06 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
 sub-Fig2-75-02         75.0            Fig2_75_02  BPAE_bipolar_75 percent 2021-08-24T15:01 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
 sub-Fig2-75-05         75.0            Fig2_75_05  BPAE_bipolar_75 percent 2021-08-24T15:47 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []
 sub-Fig2-75-10         75.0            Fig2_75_10  BPAE_bipolar_75 percent 2021-08-24T17:00 (22323, 22323, 2) uint16 n.a.         1.0   2.0           []

Dishes per second phase amplitude (Fig. 2 files):
amplitude_%
0.0      3
25.0     3
50.0     5
75.0     3
100.0    3

Series description (first Fig. 2 file): Channel1 ex/em 350/460nm and ex/em 480/535nm. All cells labeled with MemBrite dye (Channel 1), permeabilized cells visualized by membrane-impermeable fluorescence dye YOPRO-1, Dark circles with no electroporated cells are the footprints of electrodes. An image was obtained by scanning a whole well from a six-well plate, and a total of 144 images (12x12 grid) were captured. These images were then automatically stitched together to create a composite image.
Session description (first Fig. 2 file): Cells were electroporated by a train of 5 unipolar 600 ns pulses (no second pulse(0%)) at 1 Hz.The electrodes were two parallel stainless-steel cylinders, 1.5 mm in diameter and 3 mm apart

The dandiset holds 18 files: one stitched image of the whole glass bottom per dish for the 17 dishes of Fig. 2 (0% second phase: 3, 25%: 3, 50%: 5, 75%: 3, 100%: 3, inside the paper's "3-8" exposures per protocol) and one sample image named for Fig. 1F. Each file holds one ImageSeries whose data is a single two-channel image stored as (22323, 22323, 2) with a "rate" of 2 Hz: as an ImageSeries the first axis is time, so the file reads as 22323 frames of 22323 x 2 pixels. The image has no pixel size, unit ("n.a."), electrode position, ROI, field map, pulse voltage or footprint values; nothing is in processing, analysis or stimulus. The series description says the image was stitched from 144 images (12 x 12) of "a whole well from a six-well plate"; the paper says 216 images of a 35 mm dish (Methods 2.1 and 2.5). The acquisition name is not unique (the three 25% files all name it Fig2_25). There are no time series and no trial table, so the check of a shared clock does not apply: each file is one image taken after the exposure.

In [3]:
fig, axes = plt.subplots(3, 6, figsize=(18, 9))
for ax, d in zip(axes.flat, dishes):
    im = d["overview"][bp.YP]
    lo, hi = np.percentile(im[im > 0], [5, 99.5])
    ax.imshow(im, vmin=lo, vmax=hi, cmap="gray")
    if "footprints" in d:
        for c, r in zip(d["footprints"]["centres"], d["footprints"]["radii"]):
            ax.add_patch(plt.Circle((c[1] / bp.BLOCK[1], c[0] / bp.BLOCK[0]), r / 24, fill=False, color="r"))
        for roi in d["measure"]["rois"]:
            ax.plot(roi["col"] / bp.BLOCK[1], roi["row"] / bp.BLOCK[0], ".", color="c", ms=2)
    ax.set_title(d["label"], fontsize=9)
    ax.set_xticks([]); ax.set_yticks([])
plt.suptitle("YP channel (block means of 25 x 23 px); red: footprints found; cyan: ROI centres along bb'")
plt.tight_layout()
plt.show()
print("Display range per panel: 5th to 99.5th percentile of the non-zero block means.")

ov = {d["subject"]: d["overview"] for d in dishes}
f1f = ov["sub-Fig1F"]
print("sub-Fig1F: channel 0 identical to channel 1:", bool(np.array_equal(f1f[0], f1f[1])))
No description has been provided for this image
Display range per panel: 5th to 99.5th percentile of the non-zero block means.
sub-Fig1F: channel 0 identical to channel 1: True
In [4]:
# Duplicated recordings: identical overviews of either channel across files.
subjects = sorted(ov)
dups = []
for i, a in enumerate(subjects):
    for b in subjects[i + 1:]:
        for ca in (bp.MB, bp.YP):
            for cb in (bp.MB, bp.YP):
                if np.array_equal(ov[a][ca], ov[b][cb]):
                    dups.append((a, ["MB", "YP"][ca], b, ["MB", "YP"][cb]))
print("Identical channel overviews across files:", dups)

# Which file each YP image belongs to: phase correlation of the fine structure of a file's MemBrite
# and YP channels (cells, debris), on the central 9.6 x 9.6 mm. Same scan: a sharp peak at zero shift.
from scipy.ndimage import gaussian_filter
def fine(x):
    x = np.log1p(x[150:750, 150:810])
    return x - gaussian_filter(x, 8)
def peak(a, b):
    A, B = np.fft.fft2(fine(a)), np.fft.fft2(fine(b))
    R = A * np.conj(B)
    r = np.real(np.fft.ifft2(R / (np.abs(R) + 1e-9)))
    return r[0, 0], np.unravel_index(np.argmax(r), r.shape)
rows = []
for s in subjects:
    if s == "sub-Fig1F":
        continue
    p, where = peak(ov[s][bp.MB], ov[s][bp.YP])
    rows.append({"subject": s, "MB_vs_own_YP_peak": round(p, 3), "argmax_at_zero_shift": where == (0, 0)})
reg = pd.DataFrame(rows)
print(reg.to_string(index=False))
others = [peak(ov[a][bp.MB], ov[b][bp.YP])[0] for a in subjects for b in subjects
          if a != b and "Fig1F" not in a + b][:60]
print(f"Baseline, MB of one dish against YP of another (60 pairs): {np.mean(others):.3f} "
      f"(range {np.min(others):.3f} to {np.max(others):.3f})")
print("MB of sub-Fig2-25-04 against YP of sub-Fig2-0-03:",
      round(peak(ov["sub-Fig2-25-04"][bp.MB], ov["sub-Fig2-0-03"][bp.YP])[0], 3))
Identical channel overviews across files: [('sub-Fig2-0-03', 'YP', 'sub-Fig2-25-04', 'YP')]
        subject  MB_vs_own_YP_peak  argmax_at_zero_shift
  sub-Fig2-0-03              0.028                  True
  sub-Fig2-0-06              0.287                  True
  sub-Fig2-0-07              0.321                  True
sub-Fig2-100-03              0.348                  True
sub-Fig2-100-07              0.328                  True
sub-Fig2-100-08              0.324                  True
 sub-Fig2-25-04              0.454                  True
 sub-Fig2-25-10              0.310                  True
 sub-Fig2-25-14              0.333                  True
 sub-Fig2-50-06              0.379                  True
 sub-Fig2-50-10              0.355                  True
 sub-Fig2-50-12              0.363                  True
 sub-Fig2-50-13              0.349                  True
 sub-Fig2-50-18              0.377                  True
 sub-Fig2-75-02              0.373                  True
 sub-Fig2-75-05              0.361                  True
 sub-Fig2-75-10              0.335                  True
Baseline, MB of one dish against YP of another (60 pairs): 0.018 (range 0.007 to 0.031)
MB of sub-Fig2-25-04 against YP of sub-Fig2-0-03: 0.454

The YP channel of sub-Fig2-0-03 (unipolar) is identical to the YP channel of sub-Fig2-25-04 (25% bipolar), while their MemBrite channels differ. In every other file the MemBrite and YP channels share fine structure (a sharp phase-correlation peak at zero shift); in sub-Fig2-0-03 they do not, at the level of two unrelated dishes, while in sub-Fig2-25-04 they do. The YP image stored in sub-Fig2-0-03 is therefore that of dish 25-04, and the unipolar YP image of dish 0-03 is not in the dandiset. Each recording is counted once: the main analysis leaves sub-Fig2-0-03 out, so unipolar pulses have 2 dishes. The sub-Fig1F file holds the same image in both channels.

In [5]:
# Other dandisets that name this paper (relation IsPublishedIn), read for their file lists and metadata.
SIBLINGS = {"000548": "0.230519.1825", "000549": "0.230519.2043", "000550": "0.230520.1609", "000448": "0.230421.1844"}
sib = {}
for ds, v in SIBLINGS.items():
    m = bp.dandiset_metadata(v, ds)
    rel = [r["url"] for r in m.get("relatedResource", []) if r.get("relation") == "dcite:IsPublishedIn"]
    sib[ds] = bp.assets(ds, v)
    print(f"{ds} version {v}: {m['name']!r}; {len(sib[ds])} files; IsPublishedIn {rel}")
    if ds != "000448":
        cond = pd.Series([a["path"].split("/")[0][4:].rsplit("-", 1)[0] for a in sib[ds]]).value_counts().sort_index()
        print("   files per condition:", cond.to_dict())
desc = pd.DataFrame(bp.session_descriptions("000448", SIBLINGS["000448"]))
desc["pulse_shape"] = desc.session_description.str.extract(r"Pulse_shape: (\w+)")[0]
desc["cell"] = desc.session_description.str.extract(r"\((cell\d+)\)")[0]
print("   000448 cells by pulse shape (each recorded forward and reverse):",
      desc.groupby("pulse_shape").cell.nunique().to_dict(), "; files:", len(desc))
print("   000448 session description of the first file:", desc.session_description.iloc[0])

# The 5-pulse dishes of 000548 against the Fig. 2 dishes of 000489 with the same dish number.
size489 = {a["path"].split("/")[0]: a["size"] for a in bp.assets()}
rows = []
for a in sib["000548"]:
    s = a["path"].split("/")[0]
    if "Uni5-" in s or "Bi5-" in s:
        twin = f"sub-Fig2-{'0' if 'Uni5' in s else '50'}-{s.rsplit('-', 1)[1]}"
        rows.append({"000548": s, "000489": twin, "bytes_000548": a["size"], "bytes_000489": size489.get(twin),
                     "difference": a["size"] - size489[twin] if twin in size489 else None})
print("\n5-pulse files of 000548 and the 000489 file with the same dish number:")
print(pd.DataFrame(rows).to_string(index=False))

# Dish 0-03 as deposited in 000548, read for this check only.
dish_0_03_fig3 = bp.Dish(next(a for a in sib["000548"] if a["path"].startswith("sub-Fig3-Uni5-03/")))
f3 = dish_0_03_fig3.overview()
print("\nsub-Fig3-Uni5-03 (000548) against sub-Fig2-0-03 (000489):")
print("  MemBrite overviews identical:", bool(np.array_equal(f3[bp.MB], ov["sub-Fig2-0-03"][bp.MB])),
      "; YP overviews identical:", bool(np.array_equal(f3[bp.YP], ov["sub-Fig2-0-03"][bp.YP])))
p, where = peak(f3[bp.MB], f3[bp.YP])
print(f"  MB against its own YP: peak {p:.3f} at shift {tuple(int(w) for w in where)} "
      f"(000489's 0-03: {reg.set_index('subject').loc['sub-Fig2-0-03', 'MB_vs_own_YP_peak']:.3f})")
print(f"  its YP against the YP of 000489's 0-03 (= 25-04): {peak(f3[bp.YP], ov['sub-Fig2-0-03'][bp.YP])[0]:.3f}")
print("  session start:", dish_0_03_fig3.meta["session_start"], "; 000489 0-03:",
      next(d for d in dishes if d["subject"] == "sub-Fig2-0-03")["meta"]["session_start"])
print("  file session description:", dish_0_03_fig3.meta["session_description"])
fp = bp.find_footprints(f3[bp.YP])
print("  footprints found in its YP image: centres", fp["centres"].round().tolist(), "px apart",
      f"{np.hypot(*(fp['centres'][1] - fp['centres'][0])) * bp.UM_PER_PX / 1000:.2f} mm, diameters",
      (2 * fp["radii"] * bp.UM_PER_PX / 1000).round(2).tolist(), "mm (nominal 4.5 and 1.5 mm)")
000548 version 0.230519.1825: 'Effect of the number of pulses on electroporation by unipolar and 50 % bipolar pulses'; 19 files; IsPublishedIn ['https://doi.org/10.1016/j.bioelechem.2022.108319']
   files per condition: {'Fig3-Bi10': 1, 'Fig3-Bi15': 3, 'Fig3-Bi5': 5, 'Fig3-Uni10': 4, 'Fig3-Uni15': 3, 'Fig3-Uni5': 3}
000549 version 0.230519.2043: 'Effect of the pulse duration on electroporation by unipolar and 50% bipolar pulses'; 26 files; IsPublishedIn ['https://doi.org/10.1016/j.bioelechem.2022.108319']
   files per condition: {'Fig4-5-Bi100us': 3, 'Fig4-5-Bi1200ns': 3, 'Fig4-5-Bi300ns': 3, 'Fig4-5-Bi500us': 2, 'Fig4-5-Bi50us': 2, 'Fig4-5-Uni100us': 3, 'Fig4-5-Uni1200ns': 2, 'Fig4-5-Uni300ns': 3, 'Fig4-5-Uni500us': 2, 'Fig4-5-Uni50us': 3}
000550 version 0.230520.1609: 'Pulse repetition rate'; 17 files; IsPublishedIn ['https://doi.org/10.1016/j.bioelechem.2022.108319']
   files per condition: {'Fig6-5xBi-0,83MHz': 3, 'Fig6-5xBi-100kHz': 3, 'Fig6-5xBi-10kHz': 3, 'Fig6-5xUni-0,83MHz': 2, 'Fig6-5xUni-100kHz': 3, 'Fig6-5xUni-10kHz': 3}
000448 version 0.230421.1844: 'Time kinetics of the membrane potential at the cathode- and anode-facing poles of a cell induced by a train of 5 pulses at 833 kHz'; 18 files; IsPublishedIn ['https://doi.org/10.1016/j.bioelechem.2022.108319']
   000448 cells by pulse shape (each recorded forward and reverse): {'bipolar': 4, 'unipolar': 5} ; files: 18
   000448 session description of the first file: Pulse_shape: bipolar, Recording: forward (cell09), Cell_line: CHO, Pulse_duration: 600ns, Number_of_pulses: 5p, Pulse_repetition_rate: 833kHz, Electric_field: 0.3kV/cm, Dye: FluoVolt, Recording_type: strobe imaging
5-pulse files of 000548 and the 000489 file with the same dish number:
          000548         000489  bytes_000548  bytes_000489  difference
 sub-Fig3-Bi5-06 sub-Fig2-50-06    1047439718    1047437078        2640
 sub-Fig3-Bi5-10 sub-Fig2-50-10    1032098503    1032095863        2640
 sub-Fig3-Bi5-12 sub-Fig2-50-12    1039265891    1039263251        2640
 sub-Fig3-Bi5-13 sub-Fig2-50-13    1016157823    1016155183        2640
 sub-Fig3-Bi5-18 sub-Fig2-50-18    1035530035    1035527395        2640
sub-Fig3-Uni5-03  sub-Fig2-0-03    1037476805    1021672026    15804779
sub-Fig3-Uni5-06  sub-Fig2-0-06    1022353847    1022351207        2640
sub-Fig3-Uni5-07  sub-Fig2-0-07    1035590084    1035587444        2640
sub-Fig3-Uni5-03 (000548) against sub-Fig2-0-03 (000489):
  MemBrite overviews identical: True ; YP overviews identical: False
  MB against its own YP: peak 0.301 at shift (0, 0) (000489's 0-03: 0.028)
  its YP against the YP of 000489's 0-03 (= 25-04): 0.022
  session start: 2021-07-29T13:00:26-04:00 ; 000489 0-03: 2021-07-29T13:00:26-04:00
  file session description: Cells were electroporated by a train of 5 600 ns unipolar pulses at 1 Hz.The electrodes were two parallel stainless-steel cylinders, 1.5 mm in diameter and 3 mm apart
  footprints found in its YP image: centres [[10325.0, 6967.0], [10642.0, 12818.0]] px apart 3.81 mm, diameters [1.97, 0.71] mm (nominal 4.5 and 1.5 mm)

Four other dandisets name this paper: 000548 (pulse number, Fig. 3), 000549 (pulse duration, Figs. 4 and 5), 000550 (repetition rate, Fig. 6) and 000448 (strobe imaging, Fig. 7, with fewer cells than the paper's 18 unipolar and 17 bipolar). Each is a separate dandiset and a separate target of the registry, and the claims file does not name them as a source for this one, so the claims on Figs. 3 to 7 are not tested here (their notes say where the data are). The 5-pulse dishes of 000548 carry the dish numbers of the Fig. 2 unipolar and 50% dishes, and all but one are 2640 bytes larger than the 000489 file of the same dish. The exception is unipolar dish 03: its MemBrite channel in 000548 is identical to 000489's, and its YP channel registers with that MemBrite channel, so 000548 holds the YP image of dish 0-03 that 000489 lacks. It is not added to the analysis: it is outside this dandiset, and in it the footprint finder takes a cell-free patch next to the right electrode for the footprint (the distance and diameters printed above are off the nominal 4.5 and 1.5 mm), so the dish could not be measured as the others are.

In [6]:
geo = pd.DataFrame([{
    "dish": d["label"], "date": d["meta"]["session_start"][:10],
    "centre_distance_mm": d["measure"]["separation_px"] * bp.UM_PER_PX / 1000,
    "footprint_diam_left_mm": 2 * d["footprints"]["radii"][0] * bp.UM_PER_PX / 1000,
    "footprint_diam_right_mm": 2 * d["footprints"]["radii"][1] * bp.UM_PER_PX / 1000,
    "axis_angle_deg": np.degrees(np.arctan2(*(d["footprints"]["centres"][1] - d["footprints"]["centres"][0]))) - 90,
    "YP0_left": d["measure"]["footprints"][0][bp.YP], "YP0_right": d["measure"]["footprints"][1][bp.YP],
    "MB0_left": d["measure"]["footprints"][0][bp.MB], "MB0_right": d["measure"]["footprints"][1][bp.MB],
} for d in dishes if "measure" in d])
print(geo.round(3).to_string(index=False))
print(f"\nAt {bp.UM_PER_PX} um per pixel: electrode centres {geo.centre_distance_mm.median():.2f} mm apart "
      f"(median; {geo.centre_distance_mm.min():.2f} to {geo.centre_distance_mm.max():.2f}); nominal "
      f"{bp.CENTRE_DISTANCE_MM} mm (1.5 mm diameter, 3 mm side to side). Footprint diameter (equal-area disc of "
      f"the dark region) median {np.median(geo[['footprint_diam_left_mm', 'footprint_diam_right_mm']]):.2f} mm; "
      f"electrode diameter 1.5 mm.")
print("Glass bottom diameter in the overview of sub-Fig2-50-10 (rows with signal, mm):",
      round((ov["sub-Fig2-50-10"][bp.YP].max(axis=1) > 0).sum() * bp.BLOCK[0] * bp.UM_PER_PX / 1000, 1))
print("\nDates of the dishes by amplitude:")
print(pd.crosstab(geo.dish.str.split("%").str[0].astype(int), geo.date).to_string())
   dish       date  centre_distance_mm  footprint_diam_left_mm  footprint_diam_right_mm  axis_angle_deg  YP0_left  YP0_right  MB0_left  MB0_right
  0%-03 2021-07-29               4.424                   1.524                    1.372         -97.861   278.259    261.126   204.294    211.784
  0%-06 2021-07-29               4.574                   1.542                    1.469         -91.058   238.476    253.200   186.069    206.630
  0%-07 2021-07-29               4.407                   1.395                    1.830         -97.622   282.863    365.376   205.171    208.163
100%-03 2021-08-24               4.668                   1.462                    1.612         -98.807   203.200    223.699   263.435    248.783
100%-07 2021-08-24               4.475                   1.503                    1.370         -97.216   215.394    203.637   236.847    248.715
100%-08 2021-08-24               4.490                   1.491                    1.322         -94.412   173.861    177.206   256.165    255.640
 25%-04 2021-07-26               4.424                   1.524                    1.372         -97.861   278.259    261.126   292.414    348.203
 25%-10 2021-07-29               4.572                   1.559                    1.644         -96.215   298.277    323.455   262.909    264.122
 25%-14 2021-07-29               4.818                   1.776                    1.667         -99.244   271.609    322.510   238.430    227.290
 50%-06 2021-07-26               4.693                   1.502                    1.625         -92.601   261.867    288.769   303.543    334.647
 50%-10 2021-07-26               4.540                   1.388                    1.511         -96.329   221.971    228.593   279.720    277.547
 50%-12 2021-07-26               4.469                   1.445                    1.427         -93.880   264.208    257.333   295.726    300.290
 50%-13 2021-07-29               4.678                   1.481                    1.814         -94.555   287.848    308.233   244.877    234.085
 50%-18 2021-07-29               4.900                   1.590                    1.845         -96.286   339.104    322.269   261.295    271.510
 75%-02 2021-08-24               4.469                   1.474                    1.358         -95.310   226.254    235.476   242.875    255.433
 75%-05 2021-08-24               4.444                   1.409                    1.357         -92.436   218.664    214.804   260.129    258.572
 75%-10 2021-08-24               4.603                   1.388                    1.389         -92.769   223.664    235.276   305.821    267.073

At 0.65 um per pixel: electrode centres 4.54 mm apart (median; 4.41 to 4.90); nominal 4.5 mm (1.5 mm diameter, 3 mm side to side). Footprint diameter (equal-area disc of the dark region) median 1.49 mm; electrode diameter 1.5 mm.
Glass bottom diameter in the overview of sub-Fig2-50-10 (rows with signal, mm): 14.5

Dates of the dishes by amplitude:
date  2021-07-26  2021-07-29  2021-08-24
dish                                    
0              0           3           0
25             1           2           0
50             3           2           0
75             0           0           3
100            0           0           3

The pixel size is not in the files. At 0.65 um per pixel (the 6.5 um pixel of the ORCA-Flash4 camera through the 10x objective, Methods 2.5) the distance between the footprint centres and the footprint diameter agree with the electrode geometry of Methods 2.2, so the scale is consistent with the data; the variant that sets the scale from each dish's electrode distance is shown with every claim. The dishes come from three days: all unipolar dishes from 2021-07-29, and all 75% and 100% dishes from 2021-08-24, a day with no unipolar dish.

In [7]:
# Field model (Methods 2.3): two cylinders in the plane, 5.75 kV on the first phase.
print(f"Model field at the centre: {bp.field_kv_cm(0, 0):.2f} kV/cm (paper, Fig. 2C: strongest field 15.1 kV/cm)")
d0 = next(d for d in dishes if d["subject"] == "sub-Fig2-50-10")
prof = pd.DataFrame([{"k": r["k"], "distance_mm": r["distance_mm"], "ROI_mean_field": r["field"],
                      "point_field": bp.field_kv_cm(0, r["distance_mm"])} for r in d0["measure"]["rois"] if r["k"] >= 0])
print(prof.round(2).to_string(index=False))
grad = [(bp.field_kv_cm(0, max(r - 0.1, 0)) - bp.field_kv_cm(0, r + 0.1)) / bp.field_kv_cm(0, r) for r in prof.distance_mm]
print(f"Largest field change across one 200 um ROI along bb': {max(grad) * 100:.1f}% (paper, Methods 2.5: <10%)")

T = {}
keep = [d for d in dishes if "measure" in d and d["subject"] != "sub-Fig2-0-03"]
T["main"] = bp.roi_table(keep)
T["top side"] = bp.roi_table(keep, side="top")
T["bottom side"] = bp.roi_table(keep, side="bottom")
T["scale from electrodes"] = bp.roi_table(keep, which="measure_scaled")
T["left footprint"] = bp.roi_table(keep, footprint="left")
T["right footprint"] = bp.roi_table(keep, footprint="right")
main = T["main"]
# The model's field at the centre is 15.38 kV/cm against the paper's 15.1: fields scaled to match it,
# which moves the ROI taken as nearest each named field (6.7, 8.7, 15.1 kV/cm).
T["field scaled to 15.1 at centre"] = main.assign(field=main.field * 15.1 / bp.field_kv_cm(0, 0))
print("\nYP index (Methods 2.5) per dish along bb', main reading (both sides averaged), j = ROI index from the centre:")
print(main.pivot_table(index="j", columns="dish", values="YP").round(0).to_string())
print("\nRange of the YP index over all dishes and ROI:", round(main.YP.min(), 1), "to", round(main.YP.max(), 1))
raw = pd.DataFrame([{"dish": d["label"], "k": r["k"], "YP_roi": r["YP_roi"], "MB_roi": r["MB_roi"]}
                    for d in keep for r in d["measure"]["rois"]])
print("Raw ROI means (camera counts, uint16): YP", round(raw.YP_roi.min()), "to", round(raw.YP_roi.max()),
      "; MB", round(raw.MB_roi.min()), "to", round(raw.MB_roi.max()))
print("ROI whose MemBrite mean is more than twice the dish median (debris or bright objects):",
      raw.groupby("dish").apply(lambda g: g[g.MB_roi > 2 * g.MB_roi.median()][["k", "MB_roi"]].round(0).values.tolist()).to_dict())
Model field at the centre: 15.38 kV/cm (paper, Fig. 2C: strongest field 15.1 kV/cm)
 k  distance_mm  ROI_mean_field  point_field
 0          0.0           15.38        15.38
 1          0.2           15.24        15.24
 2          0.4           14.85        14.85
 3          0.6           14.24        14.24
 4          0.8           13.46        13.46
 5          1.0           12.58        12.58
 6          1.2           11.65        11.65
 7          1.4           10.72        10.71
 8          1.6            9.80         9.80
 9          1.8            8.94         8.94
10          2.0            8.14         8.14
11          2.2            7.41         7.41
12          2.4            6.75         6.74
13          2.6            6.15         6.15
14          2.8            5.61         5.61
15          3.0            5.13         5.13
Largest field change across one 200 um ROI along bb': 9.4% (paper, Methods 2.5: <10%)

YP index (Methods 2.5) per dish along bb', main reading (both sides averaged), j = ROI index from the centre:
dish  0%-06  0%-07  100%-03  100%-07  100%-08  25%-04  25%-10  25%-14  50%-06  50%-10  50%-12  50%-13  50%-18  75%-02  75%-05  75%-10
j                                                                                                                                    
0     727.0  335.0    682.0    788.0    791.0    75.0   789.0   359.0   153.0   305.0   263.0   189.0   118.0   192.0   167.0   322.0
1     640.0  500.0    504.0    863.0    916.0    90.0   622.0   603.0   197.0   308.0   186.0   132.0   170.0   226.0   213.0   279.0
2     785.0  357.0    433.0    725.0    744.0    94.0   483.0   601.0   213.0   251.0   222.0   170.0   157.0   187.0   181.0   333.0
3     731.0  325.0    349.0    539.0    531.0    77.0   532.0   422.0   162.0   233.0   186.0    73.0   180.0   163.0   154.0   301.0
4     526.0  212.0    248.0    601.0    518.0    54.0   372.0   428.0   151.0   177.0   141.0   105.0   144.0   156.0   161.0   219.0
5     656.0  163.0    203.0    486.0    414.0    69.0   328.0   239.0   132.0   236.0   133.0   139.0    74.0   166.0    98.0   185.0
6     325.0   73.0    213.0    281.0    369.0    69.0   262.0   310.0    99.0   130.0   160.0    65.0    88.0   108.0    89.0   145.0
7     194.0   89.0    127.0    199.0    243.0    66.0   248.0   203.0    80.0    80.0   131.0    77.0    88.0   105.0    72.0   122.0
8     115.0   50.0    154.0    145.0    146.0    57.0   237.0   205.0    62.0    68.0   101.0    52.0    57.0    60.0    80.0   106.0
9      83.0   12.0    114.0    124.0     91.0    40.0   108.0   137.0    67.0    35.0    68.0    36.0    64.0    51.0    73.0    99.0
10     55.0   11.0     41.0    133.0     87.0    51.0    68.0    95.0    49.0    26.0    43.0    35.0    42.0    32.0    46.0    85.0
11     58.0  -17.0     44.0     85.0     73.0    44.0    41.0    49.0    34.0    29.0    23.0    -8.0    34.0    38.0    39.0    53.0
12     11.0  -28.0     50.0     55.0     45.0    12.0    49.0    59.0    35.0    23.0    56.0    16.0    43.0    24.0    33.0    29.0
13      2.0  -29.0     47.0     25.0     49.0    37.0    50.0    28.0    26.0    28.0    34.0     2.0    33.0    45.0    18.0    39.0
14      6.0  -77.0     42.0     51.0     29.0    33.0    68.0    37.0    31.0     3.0    26.0     3.0    25.0    13.0    24.0    45.0
15     28.0  -57.0     35.0     46.0     13.0    25.0    91.0    -6.0    38.0   -15.0    35.0    10.0    28.0    10.0    21.0    34.0

Range of the YP index over all dishes and ROI: -77.0 to 916.1
Raw ROI means (camera counts, uint16): YP 216 to 1722 ; MB 187 to 1260
ROI whose MemBrite mean is more than twice the dish median (debris or bright objects): {'0%-06': [], '0%-07': [], '100%-03': [], '100%-07': [], '100%-08': [], '25%-04': [], '25%-10': [], '25%-14': [], '50%-06': [], '50%-10': [[14.0, 1260.0]], '50%-12': [], '50%-13': [], '50%-18': [], '75%-02': [], '75%-05': [], '75%-10': [[5.0, 611.0]]}
In [8]:
def means(t):
    return (t.groupby(["amplitude", "j"]).agg(field=("field", "mean"), YP=("YP", "mean"),
            se=("YP", "sem"), n=("YP", "size")).reset_index())

M = means(main)
fig, axes = plt.subplots(1, 5, figsize=(20, 3.6))
for ax, (amp, g) in zip(axes, M.groupby("amplitude")):
    ax.errorbar(g.field, g.YP, g.se, fmt="o-", ms=3)
    for dish, h in main[main.amplitude == amp].groupby("dish"):
        ax.plot(h.field, h.YP, ".", ms=3, alpha=0.5, label=dish)
    ax.set_title(f"{amp}% second phase (n = {g.n.max()})")
    ax.set_xlabel("field (kV/cm)"); ax.legend(fontsize=6)
axes[0].set_ylabel("YP (a.u.)")
plt.tight_layout(); plt.show()
print("Mean YP (a.u.) per amplitude, by ROI field:")
print(M.pivot(index="j", columns="amplitude", values="YP").round(1).assign(
    field=M.groupby("j").field.mean().round(2)).to_string())
print("All points are within the axes.")
No description has been provided for this image
Mean YP (a.u.) per amplitude, by ROI field:
amplitude      0     25     50     75    100  field
j                                                  
0          530.7  407.7  205.4  226.9  753.8  15.38
1          570.4  438.3  198.3  239.5  761.0  15.24
2          571.1  392.8  202.7  233.6  634.0  14.85
3          527.8  343.8  166.7  205.9  472.7  14.24
4          369.0  284.8  143.8  178.6  455.6  13.46
5          409.6  212.1  142.9  149.6  367.5  12.58
6          199.1  213.9  108.7  114.0  287.7  11.65
7          141.4  172.2   90.8   99.6  189.8  10.72
8           82.2  166.4   67.9   82.0  148.6   9.80
9           47.7   94.9   54.0   74.3  109.6   8.94
10          32.8   71.0   38.8   54.4   86.7   8.14
11          20.3   44.4   22.1   43.6   67.2   7.41
12          -8.6   39.7   34.5   29.0   50.0   6.75
13         -13.3   38.4   24.7   33.9   40.4   6.15
14         -35.6   46.2   17.6   27.2   40.5   5.61
15         -14.7   36.7   19.2   21.8   31.4   5.13
All points are within the axes.

Claims on data the dandiset does not hold (Figs. 1E, 3 to 7) are blocked and listed in the summary. The six claims on Fig. 2 follow, each under every reading that the claims file names. For the ROI placement, reading (a), the two ROI at the same distance on both sides of bb' averaged, decides the outcomes, as an assumption: Methods 2.3 and Fig. 1C draw bb' as one line through the centre, on which the field is symmetric, and the paper names no side. Reading (b), one side only, is two readings, top and bottom, and the paper gives no way to choose between them; both are shown, and an outcome that either changes is marked as resting on the assumption. The other variants of the data are the scale set from the electrode distance, each footprint alone for YP_0 and MB_0, and the model field scaled so that the centre is at the paper's 15.1 kV/cm.

In [9]:
def nearest_j(t, value):
    f = t.groupby("j").field.mean()
    return int((f - value).abs().idxmin())

def ttest(a, b, paired):
    a, b = np.asarray(a, float), np.asarray(b, float)
    if paired:
        return stats.ttest_rel(a, b).pvalue
    return stats.ttest_ind(a, b).pvalue

def dish_values(t, amp, j):
    return t[(t.amplitude == amp) & (t.j == j)].set_index("dish").YP

def ratio_table(t):
    """Bipolar YP per dish over the unipolar mean at the same ROI (Fig. 2C legend: unipolar as 100%)."""
    uni = t[t.amplitude == 0].groupby("j").YP.mean()
    r = t[t.amplitude > 0].copy()
    r["pct"] = 100 * r.YP / r.j.map(uni)
    return r

def run_variants(fn):
    rows = []
    for name, t in T.items():
        out = fn(t, verbose=False)
        rows.append({"data": name, **{k: v for k, v in out.items() if not isinstance(v, (pd.DataFrame, dict))}})
    print(pd.DataFrame(rows).to_string(index=False))

AMPS = [0, 25, 50, 75, 100]

fig2a-uptake-rises-with-field¶

For unipolar 600 ns pulses and for bipolar 600+600 ns pulses with every second phase amplitude, a train of 5 pulses at 1 Hz evoked YP uptake that increased with the local electric field strength.

Criterion: per amplitude, dish YP at the ROI with the strongest field against the ROI with the weakest field, two-sided t-test (a) paired across dishes and (b) unpaired; reproduced when YP is higher at the strongest field with p < 0.05 for all five amplitudes, partially for three or four; methods_underspecified when (a) and (b) differ. Spearman correlation of mean YP with field reported.

In [10]:
def claim_fig2a(t, verbose=True, jmin=None):
    jmax = int(t.groupby("j").field.mean().idxmax())
    if jmin is None:
        jmin = int(t.groupby("j").field.mean().idxmin())
    rows = []
    m = means(t)
    for amp in AMPS:
        hi, lo = dish_values(t, amp, jmax), dish_values(t, amp, jmin)
        g = m[m.amplitude == amp]
        rows.append({"amplitude": amp, "n": len(hi), "YP_strongest": hi.mean(), "YP_weakest": lo.mean(),
                     "p_paired": ttest(hi, lo.loc[hi.index], True), "p_unpaired": ttest(hi, lo, False),
                     "spearman_rho": stats.spearmanr(g.field, g.YP)[0], "spearman_p": stats.spearmanr(g.field, g.YP)[1]})
    r = pd.DataFrame(rows)
    def outcome(col):
        k = int(((r.YP_strongest > r.YP_weakest) & (r[col] < 0.05)).sum())
        return k, "reproduced" if k == 5 else "partially_reproduced" if k >= 3 else "not_reproduced"
    (ka, oa), (kb, ob) = outcome("p_paired"), outcome("p_unpaired")
    if verbose:
        print(f"Strongest field ROI j = {jmax}, weakest j = {jmin}")
        print(r.round(4).to_string(index=False))
    return {"amplitudes_paired": ka, "paired": oa, "amplitudes_unpaired": kb, "unpaired": ob,
            "outcome": oa if oa == ob else "methods_underspecified"}

res = claim_fig2a(main)
print(res)
print("\nUnder every variant of the data:")
run_variants(claim_fig2a)
# How far the ROI extend is a choice (bipolar.N_ROI_SIDE): the weakest ROI taken nearer the centre.
F = main.groupby("j").field.mean()
print("\nWeakest ROI taken nearer the centre (main reading):")
print(pd.DataFrame([{"weakest_j": j, "distance_mm": j * bp.ROI_UM / 1000, "field": round(F[j], 2),
                     **claim_fig2a(main, verbose=False, jmin=j)} for j in (10, 12, 15)]).to_string(index=False))
Strongest field ROI j = 0, weakest j = 15
 amplitude  n  YP_strongest  YP_weakest  p_paired  p_unpaired  spearman_rho  spearman_p
         0  2      530.7258    -14.6514    0.1747      0.1126        0.9824         0.0
        25  3      407.6941     36.6996    0.1860      0.1512        0.9765         0.0
        50  5      205.4304     19.2461    0.0106      0.0008        0.9853         0.0
        75  3      226.9046     21.7559    0.0407      0.0135        0.9882         0.0
       100  3      753.7553     31.3755    0.0030      0.0000        0.9941         0.0
{'amplitudes_paired': 3, 'paired': 'partially_reproduced', 'amplitudes_unpaired': 3, 'unpaired': 'partially_reproduced', 'outcome': 'partially_reproduced'}

Under every variant of the data:
                          data  amplitudes_paired               paired  amplitudes_unpaired             unpaired                outcome
                          main                  3 partially_reproduced                    3 partially_reproduced   partially_reproduced
                      top side                  3 partially_reproduced                    3 partially_reproduced   partially_reproduced
                   bottom side                  2       not_reproduced                    3 partially_reproduced methods_underspecified
         scale from electrodes                  3 partially_reproduced                    3 partially_reproduced   partially_reproduced
                left footprint                  2       not_reproduced                    3 partially_reproduced methods_underspecified
               right footprint                  3 partially_reproduced                    3 partially_reproduced   partially_reproduced
field scaled to 15.1 at centre                  3 partially_reproduced                    3 partially_reproduced   partially_reproduced

Weakest ROI taken nearer the centre (main reading):
 weakest_j  distance_mm  field  amplitudes_paired               paired  amplitudes_unpaired             unpaired                outcome
        10          2.0   8.14                  3 partially_reproduced                    3 partially_reproduced   partially_reproduced
        12          2.4   6.75                  2       not_reproduced                    3 partially_reproduced methods_underspecified
        15          3.0   5.13                  3 partially_reproduced                    3 partially_reproduced   partially_reproduced

Result: partially reproduced, resting on an assumption. Mean YP rises with field for every amplitude (Spearman rho 0.98 to 0.99 over the 16 ROI). By the criterion's test of the strongest against the weakest ROI, YP is higher at p < 0.05 for 50%, 75% and 100% under both readings (paired p 0.011, 0.041, 0.003; unpaired p 0.0008, 0.014, < 0.0001), but not for unipolar (2 dishes, paired p 0.17, unpaired p 0.11) or 25% (3 dishes, paired p 0.19, unpaired p 0.15): 3 of 5 amplitudes under both. The same holds on the top side, with the scale from the electrodes, with the right footprint and with the field scaled to 15.1 kV/cm at the centre. On the bottom side alone, or with the left footprint alone, the paired test passes for 2 amplitudes and the unpaired for 3, which the criterion would call methods_underspecified; the main reading averages both sides and both footprints. How far the ROI extend is a choice: with the weakest ROI at 2.0 mm (8.14 kV/cm) the outcome is the same, and at 2.4 mm (6.75 kV/cm, the weakest field the paper names) the paired test passes for 2 amplitudes and the unpaired for 3.

fig2a-vertical-scales¶

Although the panels of Fig. 2A look similar, their vertical scale goes from 1200 a.u. for unipolar pulses down to only 250 a.u. for bipolar pulses with a 75% second phase.

Criterion: reproduced when the largest mean plus s.e.m. over ROI is at most 1200 a.u. for unipolar and at most 250 a.u. for 75% bipolar pulses, and the largest unipolar mean exceeds 250 a.u. The files hold uint16 camera counts with conversion 1.0 and no stated rescaling; they are taken as the units the paper measured (an assumption: nothing in the files says how the paper's a.u. relate to them). The ratio of the two largest means is reported either way.

In [11]:
def claim_scales(t, verbose=True):
    m = means(t)
    u, s = m[m.amplitude == 0], m[m.amplitude == 75]
    top_u, top_s = (u.YP + u.se).max(), (s.YP + s.se).max()
    ok = top_u <= 1200 and top_s <= 250 and u.YP.max() > 250
    out = {"max_mean+sem_unipolar": round(top_u, 1), "max_mean_unipolar": round(u.YP.max(), 1),
           "max_mean+sem_75pct": round(top_s, 1), "max_mean_75pct": round(s.YP.max(), 1),
           "ratio_of_max_means": round(u.YP.max() / s.YP.max(), 2),
           "outcome": "reproduced" if ok else "not_reproduced"}
    if verbose:
        for amp in AMPS:
            g = m[m.amplitude == amp]
            print(f"{amp:>3}%: largest mean {g.YP.max():7.1f}, largest mean + s.e.m. {(g.YP + g.se).max():7.1f} a.u.")
    return out

print(claim_scales(main))
print("\nUnder every variant of the data:")
run_variants(claim_scales)
  0%: largest mean   571.1, largest mean + s.e.m.   785.4 a.u.
 25%: largest mean   438.3, largest mean + s.e.m.   615.2 a.u.
 50%: largest mean   205.4, largest mean + s.e.m.   239.9 a.u.
 75%: largest mean   239.5, largest mean + s.e.m.   283.2 a.u.
100%: largest mean   761.0, largest mean + s.e.m.   890.4 a.u.
{'max_mean+sem_unipolar': np.float64(785.4), 'max_mean_unipolar': np.float64(571.1), 'max_mean+sem_75pct': np.float64(283.2), 'max_mean_75pct': np.float64(239.5), 'ratio_of_max_means': np.float64(2.38), 'outcome': 'not_reproduced'}

Under every variant of the data:
                          data  max_mean+sem_unipolar  max_mean_unipolar  max_mean+sem_75pct  max_mean_75pct  ratio_of_max_means        outcome
                          main                  785.4              571.1               283.2           239.5                2.38 not_reproduced
                      top side                  730.9              530.7               275.1           236.3                2.25 not_reproduced
                   bottom side                 1010.1              725.7               342.8           269.4                2.69 not_reproduced
         scale from electrodes                  774.7              574.1               284.0           240.3                2.39 not_reproduced
                left footprint                  738.7              568.6               311.3           250.4                2.27 not_reproduced
               right footprint                  832.0              576.3               255.2           228.7                2.52 not_reproduced
field scaled to 15.1 at centre                  785.4              571.1               283.2           239.5                2.38 not_reproduced

Result: not reproduced, resting on an assumption. Taking the files' camera counts as the paper's a.u., the largest unipolar mean is 571.1 a.u. (mean plus s.e.m. 785.4, within 1200, and above 250), but the 75% largest mean plus s.e.m. is 283.2 a.u., above 250 (largest mean 239.5). The 75% value exceeds 250 under every variant of the data (255.2 to 342.8). The ratio of the largest unipolar to the largest 75% mean is 2.38; the paper's axis limits differ by 1200/250.

fig2b-exponential-same-slope¶

YP uptake increased exponentially with the field strength, and the second phase did not change the slope of the exponential fits on a semi-log scale.

Criterion: mean YP per ROI fitted with YP = A exp(kE), leaving out ROI below a cutoff of 6, 7 or 8 kV/cm; fits (a) nonlinear least squares on linear YP and (b) linear regression of log YP on field. Exponential adequate when its residual sum of squares on linear YP is below a straight line's for at least four of five amplitudes. Slopes compared with the unipolar slope by (i) per-dish fits and an unpaired t-test and (ii) the fits to the means with their standard errors. Reproduced when no bipolar slope differs at p < 0.05 and the exponential is adequate; not reproduced when any slope differs; methods_underspecified when the readings disagree. Under (b), points with mean (or dish) YP at or below zero cannot be logged and are dropped; the count is printed.

In [12]:
def fit_exp(E, Y, how):
    """k, se(k), A, residual sum of squares on linear YP, points used."""
    E, Y = np.asarray(E, float), np.asarray(Y, float)
    if how == "nls":
        p0 = np.polyfit(E[Y > 0], np.log(Y[Y > 0]), 1) if (Y > 0).sum() >= 2 else [0.3, 0]
        try:
            popt, pcov = optimize.curve_fit(lambda e, a, k: a * np.exp(k * e), E, Y,
                                            p0=[np.exp(p0[1]), p0[0]], maxfev=20000)
        except RuntimeError:
            return np.nan, np.nan, np.nan, np.nan, len(E)
        return popt[1], np.sqrt(pcov[1, 1]), popt[0], ((Y - popt[0] * np.exp(popt[1] * E)) ** 2).sum(), len(E)
    ok = Y > 0
    if ok.sum() < 3:
        return np.nan, np.nan, np.nan, np.nan, int(ok.sum())
    lr = stats.linregress(E[ok], np.log(Y[ok]))
    A = np.exp(lr.intercept)
    return lr.slope, lr.stderr, A, ((Y - A * np.exp(lr.slope * E)) ** 2).sum(), int(ok.sum())

def claim_fig2b(t, verbose=True):
    m = means(t)
    readings, details = [], []
    for cut in (6, 7, 8):
        mm = m[m.field >= cut]
        tt = t[t.field >= cut]
        for how in ("nls", "log"):
            fits = {}
            adequate = 0
            for amp in AMPS:
                g = mm[mm.amplitude == amp]
                k, se, A, rss, used = fit_exp(g.field, g.YP, how)
                lin = np.polyfit(g.field, g.YP, 1)
                rss_lin = ((g.YP - np.polyval(lin, g.field)) ** 2).sum()
                adequate += bool(rss < rss_lin)
                per_dish = {dish: fit_exp(h.field, h.YP, how)[0] for dish, h in tt[tt.amplitude == amp].groupby("dish")}
                fits[amp] = (k, se, used, len(g), per_dish)
                details.append({"cutoff": cut, "fit": how, "amplitude": amp, "k": k, "se_k": se,
                                "k_ratio_to_unipolar": np.nan, "points_used": f"{used}/{len(g)}",
                                "rss_exp": rss, "rss_line": rss_lin,
                                "per_dish_k": ", ".join(f"{v:.3f}" for v in per_dish.values())})
            ku, seu, nu, _, du = fits[0]
            for test in ("per-dish", "means"):
                differs = []
                for amp in AMPS[1:]:
                    k, se, n, _, dd = fits[amp]
                    if test == "per-dish":
                        a = [v for v in dd.values() if np.isfinite(v)]; b = [v for v in du.values() if np.isfinite(v)]
                        p = stats.ttest_ind(a, b).pvalue if len(a) > 1 and len(b) > 1 else np.nan
                    else:
                        tstat = (k - ku) / np.hypot(se, seu)
                        p = 2 * stats.t.sf(abs(tstat), n + nu - 4)
                    differs.append((amp, p))
                any_diff = any(p < 0.05 for _, p in differs)
                # The criterion: reproduced only when no slope differs and the exponential is adequate.
                outcome = "reproduced" if not any_diff and adequate >= 4 else "not_reproduced"
                readings.append({"cutoff": cut, "fit": how, "test": test, "exp_better_than_line": adequate,
                                 **{f"p_{a}": round(p, 4) for a, p in differs}, "slope_differs": any_diff,
                                 "outcome": outcome})
    R = pd.DataFrame(readings)
    Dt = pd.DataFrame(details)
    for (cut, how), g in Dt.groupby(["cutoff", "fit"]):
        Dt.loc[g.index, "k_ratio_to_unipolar"] = g.k / g[g.amplitude == 0].k.iloc[0]
    outs = set(R.outcome)
    if verbose:
        print(Dt.round(4).to_string(index=False))
        print()
        print(R.to_string(index=False))
    return {"readings_slope_differs": f"{int(R.slope_differs.sum())}/{len(R)}",
            "readings_exp_adequate": f"{int((R.exp_better_than_line >= 4).sum())}/{len(R)}",
            "outcomes": "/".join(sorted(outs)),
            "outcome": outs.pop() if len(outs) == 1 else "methods_underspecified"}

print(claim_fig2b(main))
print("\nUnder every variant of the data:")
run_variants(claim_fig2b)
 cutoff fit  amplitude      k   se_k  k_ratio_to_unipolar points_used     rss_exp   rss_line                        per_dish_k
      6 nls          0 0.2933 0.0375               1.0000       14/14  49642.6008 46568.4338                      0.258, 0.394
      6 nls         25 0.2247 0.0137               0.7661       14/14   6162.3232 11366.6358               0.099, 0.266, 0.213
      6 nls         50 0.2052 0.0125               0.6994       14/14   1491.6475  1880.6859 0.186, 0.258, 0.179, 0.231, 0.164
      6 nls         75 0.2004 0.0092               0.6831       14/14   1144.4085  2563.7553               0.194, 0.194, 0.209
      6 nls        100 0.2900 0.0132               0.9885       14/14   9257.7498 79107.0826               0.323, 0.276, 0.289
      6 log          0 0.4175 0.0288               1.0000       12/14 120177.1812 46568.4338                      0.503, 0.497
      6 log         25 0.2661 0.0158               0.6374       14/14  12885.3303 11366.6358               0.124, 0.303, 0.280
      6 log         50 0.2389 0.0147               0.5721       14/14   2654.4084  1880.6859 0.210, 0.304, 0.212, 0.333, 0.174
      6 log         75 0.2226 0.0095               0.5332       14/14   1885.3366  2563.7553               0.222, 0.216, 0.235
      6 log        100 0.3108 0.0073               0.7444       14/14  12883.6076 79107.0826               0.282, 0.327, 0.326
      7 nls          0 0.2809 0.0383               1.0000       12/12  42705.0999 31099.9485                      0.245, 0.382
      7 nls         25 0.2174 0.0146               0.7738       12/12   5221.5483  7658.9587               0.081, 0.262, 0.203
      7 nls         50 0.2018 0.0144               0.7182       12/12   1421.8082   605.1427 0.180, 0.255, 0.178, 0.220, 0.164
      7 nls         75 0.1939 0.0093               0.6902       12/12    850.3384  1595.4499               0.192, 0.186, 0.200
      7 nls        100 0.2874 0.0148               1.0230       12/12   8955.2048 53524.3089               0.329, 0.270, 0.285
      7 log          0 0.4175 0.0288               1.0000       12/12 118419.1096 31099.9485                      0.358, 0.497
      7 log         25 0.2504 0.0197               0.5996       12/12   8684.6857  7658.9587               0.081, 0.305, 0.249
      7 log         50 0.2459 0.0192               0.5890       12/12   3109.2087   605.1427 0.201, 0.322, 0.230, 0.204, 0.182
      7 log         75 0.2074 0.0072               0.4968       12/12   1065.6243  1595.4499               0.231, 0.188, 0.207
      7 log        100 0.2996 0.0080               0.7175       12/12   9917.1748 53524.3089               0.301, 0.288, 0.319
      8 nls          0 0.2744 0.0400               1.0000       11/11  40480.9597 23422.8067                      0.239, 0.374
      8 nls         25 0.2099 0.0140               0.7648       11/11   4043.6012  7286.2761               0.080, 0.255, 0.192
      8 nls         50 0.1926 0.0128               0.7018       11/11    935.7607   576.6908 0.174, 0.251, 0.164, 0.197, 0.158
      8 nls         75 0.1903 0.0098               0.6936       11/11    756.8031  1290.2527               0.188, 0.183, 0.196
      8 nls        100 0.2860 0.0160               1.0425       11/11   8862.9459 41895.8414               0.329, 0.268, 0.283
      8 log          0 0.3953 0.0315               1.0000       11/11  93686.3676 23422.8067                      0.362, 0.497
      8 log         25 0.2250 0.0166               0.5691       11/11   4656.3923  7286.2761               0.080, 0.271, 0.213
      8 log         50 0.2170 0.0126               0.5489       11/11   1386.5423   576.6908 0.187, 0.326, 0.188, 0.204, 0.171
      8 log         75 0.1995 0.0070               0.5048       11/11    840.6989  1290.2527               0.234, 0.180, 0.193
      8 log        100 0.2956 0.0093               0.7478       11/11   9373.2869 41895.8414               0.291, 0.286, 0.320

 cutoff fit     test  exp_better_than_line   p_25   p_50   p_75  p_100  slope_differs        outcome
      6 nls per-dish                     4 0.2000 0.0468 0.0894 0.6181           True not_reproduced
      6 nls    means                     4 0.0985 0.0352 0.0241 0.9333           True not_reproduced
      6 log per-dish                     2 0.0357 0.0042 0.0000 0.0022           True not_reproduced
      6 log    means                     2 0.0001 0.0000 0.0000 0.0016           True not_reproduced
      7 nls per-dish                     3 0.2218 0.0548 0.0996 0.7578          False not_reproduced
      7 nls    means                     3 0.1367 0.0674 0.0391 0.8768           True not_reproduced
      7 log per-dish                     2 0.1215 0.0152 0.0273 0.0991           True not_reproduced
      7 log    means                     2 0.0001 0.0001 0.0000 0.0008           True not_reproduced
      8 nls per-dish                     3 0.2135 0.0494 0.1026 0.8303           True not_reproduced
      8 nls    means                     3 0.1453 0.0671 0.0562 0.7899          False not_reproduced
      8 log per-dish                     3 0.0724 0.0154 0.0256 0.0887           True not_reproduced
      8 log    means                     3 0.0001 0.0001 0.0000 0.0071           True not_reproduced
{'readings_slope_differs': '10/12', 'readings_exp_adequate': '2/12', 'outcomes': 'not_reproduced', 'outcome': 'not_reproduced'}

Under every variant of the data:
                          data readings_slope_differs readings_exp_adequate                  outcomes                outcome
                          main                  10/12                  2/12            not_reproduced         not_reproduced
                      top side                   9/12                 10/12 not_reproduced/reproduced methods_underspecified
                   bottom side                   6/12                  0/12            not_reproduced         not_reproduced
         scale from electrodes                   8/12                  2/12 not_reproduced/reproduced methods_underspecified
                left footprint                  12/12                  2/12            not_reproduced         not_reproduced
               right footprint                   7/12                  2/12 not_reproduced/reproduced methods_underspecified
field scaled to 15.1 at centre                   9/12                  2/12            not_reproduced         not_reproduced

Result: not reproduced, resting on an assumption. The criterion calls the claim reproduced when no bipolar slope differs from the unipolar slope at p < 0.05 and the exponential beats a straight line for at least four of five amplitudes, and not reproduced when a slope differs. None of the 12 readings (cutoff 6, 7, 8 kV/cm; least squares or log-linear fit; per-dish or means test) meets both parts. In 10 a bipolar slope differs; in the other 2 (least squares, per-dish test at 7 kV/cm and means test at 8 kV/cm) no slope differs but the exponential beats a straight line for only 3 of 5 amplitudes. The exponential is adequate in 2 of the 12 readings. The log-linear fits put every bipolar slope below the unipolar one (ratios 0.50 to 0.75, all four bipolar slopes differing at p < 0.01 in the means test); the least-squares fits give ratios 0.68 to 1.04. Under the bottom side, the left footprint and the scaled field every reading also gives not reproduced; on the top side, with the scale from the electrodes and with the right footprint some readings give reproduced, so the outcome rests on the main reading of the ROI, the footprints and the scale.

fig2c-max-inhibition-50-75¶

The strongest inhibition of YP uptake, to about 20% of the uptake after unipolar pulses, was reached with a second phase of 50 to 75% of the first (p < 0.01).

Criterion: mean YP per amplitude as percent of the unipolar mean at each ROI from 8.7 to 15.1 kV/cm (the ROI nearest each named field and those between, opening comment). Reproduced when the lower of the 50% and 75% values is below the 25% and 100% values at every such field, the percent is within 18 to 22% under reading (a) the mean over those fields for the better of 50 and 75%, or (b) the lowest percent at any of them, and the 50 or 75% dish values differ by an unpaired two-sided t-test at p < 0.01 under (i) against unipolar or (ii) against the 25% and 100% dishes, on raw YP or on dish values divided by the unipolar mean. Partially reproduced when ordering and test hold but the percent is outside tolerance under every reading. Where the test is made is not said: it is run at every ROI of the range (holding when p < 0.01 at all of them) and, as a second reading, on each dish's mean over the range. At a single ROI, dividing every dish by the same unipolar mean leaves the t-test unchanged, so raw and normalised values differ only in the pooled reading.

In [13]:
def claim_fig2c_max(t, verbose=True):
    j_hi, j_lo = nearest_j(t, 15.1), nearest_j(t, 8.7)
    js = list(range(min(j_hi, j_lo), max(j_hi, j_lo) + 1))
    m = means(t)
    pv = m.pivot(index="j", columns="amplitude", values="YP").loc[js]
    pct = 100 * pv.div(pv[0], axis=0)
    best = 50 if pct[50].mean() <= pct[75].mean() else 75
    order = bool(((pct[[50, 75]].min(axis=1) < pct[25]) & (pct[[50, 75]].min(axis=1) < pct[100])).all())
    val_a = pct[best].mean()
    val_b = pct[[50, 75]].min().min()
    in_tol = {"a": 18 <= val_a <= 22, "b": 18 <= val_b <= 22}
    uni = t[t.amplitude == 0].groupby("j").YP.mean()
    tests, per_roi = [], []
    F = t.groupby("j").field.mean()
    for amp in (50, 75):
        # (i) against unipolar, (ii) against 25% and 100%, at every ROI of the range
        for reading, other in (("i", [0]), ("ii", [25, 100])):
            pj = {j: ttest(dish_values(t, amp, j), pd.concat([dish_values(t, o, j) for o in other]), False) for j in js}
            per_roi += [{"amplitude": amp, "comparison": reading, "j": j, "field": round(F[j], 2), "p": p}
                        for j, p in pj.items()]
            ps = list(pj.values())
            sub = t[t.j.isin(js)].copy()
            sub["norm"] = sub.YP / sub.j.map(uni)
            for col in ("YP", "norm"):
                a = sub[sub.amplitude == amp].groupby("dish")[col].mean()
                b = sub[sub.amplitude.isin(other)].groupby("dish")[col].mean()
                tests.append({"amplitude": amp, "comparison": reading, "values": col,
                              "p_every_ROI_max": max(ps), "ROI_p<0.01": sum(p < 0.01 for p in ps),
                              "p_at_8.7": pj[j_lo], "p_at_15.1": pj[j_hi],
                              "p_pooled_dish_means": ttest(a, b, False)})
    Tt = pd.DataFrame(tests)
    # Where the test is made: at every ROI of the range, on each dish's mean over the range, at both
    # named fields (8.7 and 15.1 kV/cm), or at one ROI of the range at least.
    where = {"every ROI": bool((Tt.p_every_ROI_max < 0.01).any()),
             "pooled": bool((Tt.p_pooled_dish_means < 0.01).any()),
             "both named fields": bool(((Tt["p_at_8.7"] < 0.01) & (Tt["p_at_15.1"] < 0.01)).any()),
             "one ROI or more": bool((Tt["ROI_p<0.01"] > 0).any())}
    def outcome(test_ok):
        if order and test_ok and (in_tol["a"] or in_tol["b"]):
            return "reproduced"
        if order and test_ok:
            return "partially_reproduced"
        return "not_reproduced"
    outs = {k: outcome(v) for k, v in where.items()}
    if verbose:
        print(f"ROI used: j = {js} (fields {t.groupby('j').field.mean().loc[js].round(2).tolist()} kV/cm)")
        print("Percent of the unipolar mean:")
        print(pct.round(1).to_string())
        print(f"Better of 50 and 75%: {best}%; (a) mean over the range {val_a:.1f}%; (b) lowest {val_b:.1f}%")
        print("Ordering (lower of 50/75 below 25 and 100 at every ROI):", order)
        print(Tt.round(4).to_string(index=False))
        print("p per ROI (unpaired), 50 and 75% against (i) unipolar and (ii) 25 and 100%:")
        print(pd.DataFrame(per_roi).pivot_table(index=["j", "field"], columns=["amplitude", "comparison"],
                                                values="p").round(4).to_string())
    return {"best": best, "pct_a": round(val_a, 1), "pct_b": round(val_b, 1), "ordering": order,
            **{f"test_{k}": v for k, v in where.items()},
            "outcome": " / ".join(f"{k}: {v}" for k, v in outs.items())}

print(claim_fig2c_max(main))
print("\nUnder every variant of the data:")
run_variants(claim_fig2c_max)
ROI used: j = [1, 2, 3, 4, 5, 6, 7, 8, 9] (fields [15.24, 14.85, 14.24, 13.46, 12.58, 11.65, 10.72, 9.8, 8.94] kV/cm)
Percent of the unipolar mean:
amplitude    0      25     50     75     100
j                                           
1          100.0   76.8   34.8   42.0  133.4
2          100.0   68.8   35.5   40.9  111.0
3          100.0   65.1   31.6   39.0   89.6
4          100.0   77.2   39.0   48.4  123.5
5          100.0   51.8   34.9   36.5   89.7
6          100.0  107.4   54.6   57.3  144.5
7          100.0  121.8   64.2   70.4  134.2
8          100.0  202.5   82.7   99.8  180.8
9          100.0  199.0  113.2  155.7  229.9
Better of 50 and 75%: 50%; (a) mean over the range 54.5%; (b) lowest 31.6%
Ordering (lower of 50/75 below 25 and 100 at every ROI): True
 amplitude comparison values  p_every_ROI_max  ROI_p<0.01  p_at_8.7  p_at_15.1  p_pooled_dish_means
        50          i     YP           0.7935           1    0.7935     0.0018               0.0401
        50          i   norm           0.7935           1    0.7935     0.0018               0.1300
        50         ii     YP           0.0721           0    0.0186     0.0163               0.0166
        50         ii   norm           0.0721           0    0.0186     0.0163               0.0107
        75          i     YP           0.9957           0    0.4670     0.0109               0.1815
        75          i   norm           0.9957           0    0.4670     0.0109               0.4074
        75         ii     YP           0.2492           0    0.2492     0.0820               0.0915
        75         ii   norm           0.2492           0    0.2492     0.0820               0.0831
p per ROI (unpaired), 50 and 75% against (i) unipolar and (ii) 25 and 100%:
amplitude       50              75        
comparison       i      ii       i      ii
j field                                   
1 15.24     0.0018  0.0163  0.0109  0.0820
2 14.85     0.0254  0.0195  0.1447  0.0990
3 14.24     0.0265  0.0187  0.1432  0.1127
4 13.46     0.0457  0.0321  0.2098  0.1492
5 12.58     0.1105  0.0721  0.2585  0.1706
6 11.65     0.2666  0.0172  0.4428  0.0644
7 10.72     0.1802  0.0243  0.4080  0.1045
8 9.80      0.5541  0.0126  0.9957  0.0859
9 8.94      0.7935  0.0186  0.4670  0.2492
{'best': 50, 'pct_a': np.float64(54.5), 'pct_b': np.float64(31.6), 'ordering': True, 'test_every ROI': False, 'test_pooled': False, 'test_both named fields': False, 'test_one ROI or more': True, 'outcome': 'every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced'}

Under every variant of the data:
                          data  best  pct_a  pct_b  ordering  test_every ROI  test_pooled  test_both named fields  test_one ROI or more                                                                                                                              outcome
                          main    50   54.5   31.6      True           False        False                   False                  True       every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced
                      top side    50  107.9   32.7      True           False        False                   False                  True       every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced
                   bottom side    50   46.1   24.3      True           False        False                   False                  True       every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced
         scale from electrodes    50   50.1   29.8      True           False        False                   False                  True       every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced
                left footprint    50   49.1   31.5      True           False        False                   False                  True       every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced
               right footprint    50   63.2   31.7      True           False         True                   False                  True every ROI: not_reproduced / pooled: partially_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced
field scaled to 15.1 at centre    50   52.9   31.6      True           False        False                   False                  True       every ROI: not_reproduced / pooled: not_reproduced / both named fields: not_reproduced / one ROI or more: partially_reproduced

Result: not reproduced, resting on an assumption. Over the ROI from 8.94 to 15.24 kV/cm the 50% second phase inhibits most, and the lower of the 50% and 75% values is below the 25% and 100% values at every ROI, so the ordering holds. The inhibition is weaker than the paper's: 54.5% of the unipolar mean on average over the range (reading a) and 31.6% at its lowest, at 14.24 kV/cm (reading b), both outside 18 to 22%, so the claim is at most partially reproduced. Where the p < 0.01 test is made is not given, and four readings are run. At every ROI of the range it fails for both amplitudes (largest p for 50%, 0.79 against unipolar and 0.072 against 25 and 100%; for 75%, 0.996 and 0.249); on dish means over the range it fails (smallest p 0.011, 50% against 25 and 100%, normalised values); at both named fields it fails (50% against unipolar, p 0.79 at 8.94 kV/cm and 0.0018 at 15.24 kV/cm). It holds only at one ROI or more: 50% against unipolar at 15.24 kV/cm (p 0.0018) is the only one of the 36 tests (2 amplitudes, 2 comparisons, 9 ROI) below 0.01, and that reading would give partially reproduced. At the ROI of strongest inhibition (14.24 kV/cm) p is 0.027 against unipolar and 0.019 against 25 and 100%. The outcome takes the paper's p < 0.01 to apply over the range of fields where it reports the inhibition, not to one ROI chosen from 36 tests; this is an assumption. With the right footprint alone the pooled test reaches p < 0.01 (partially reproduced); with the field scaled to 15.1 kV/cm at the centre the range starts at the centre ROI and the mean percent is 52.9%.

fig2c-inhibition-flat-above-8-7¶

Between 8.7 and 15.1 kV/cm the degree of inhibition by the second phase did not depend on the field strength.

Criterion (a claim of absence): bipolar YP of each dish divided by the unipolar mean at the same ROI; for each bipolar amplitude, the values at 8.7 against 15.1 kV/cm by a two-sided t-test at p < 0.05, (a) paired across dishes and (b) unpaired. Reproduced when no amplitude differs under both; not reproduced when one does under both; methods_underspecified when (a) and (b) disagree. Every pair of ROI in the range is shown as a variant, with the size of each difference.

In [14]:
def claim_fig2c_flat(t, verbose=True):
    r = ratio_table(t)
    j_hi, j_lo = nearest_j(t, 15.1), nearest_j(t, 8.7)
    rows = []
    for amp in (25, 50, 75, 100):
        a = r[(r.amplitude == amp) & (r.j == j_lo)].set_index("dish").pct
        b = r[(r.amplitude == amp) & (r.j == j_hi)].set_index("dish").pct
        rows.append({"amplitude": amp, "pct_at_8.7": a.mean(), "pct_at_15.1": b.mean(),
                     "difference": a.mean() - b.mean(),
                     "p_paired": ttest(a, b.loc[a.index], True), "p_unpaired": ttest(a, b, False)})
    R = pd.DataFrame(rows)
    pa, pb = bool((R.p_paired < 0.05).any()), bool((R.p_unpaired < 0.05).any())
    outcome = ("reproduced" if not pa and not pb else "not_reproduced" if pa and pb
               else "methods_underspecified")
    if verbose:
        print(f"8.7 kV/cm: ROI j = {j_lo}; 15.1 kV/cm: ROI j = {j_hi}")
        print(R.round(4).to_string(index=False))
        js = list(range(min(j_hi, j_lo), max(j_hi, j_lo) + 1))
        pairs = []
        for amp in (25, 50, 75, 100):
            for i, x in enumerate(js):
                for y in js[i + 1:]:
                    a = r[(r.amplitude == amp) & (r.j == x)].set_index("dish").pct
                    b = r[(r.amplitude == amp) & (r.j == y)].set_index("dish").pct
                    pairs.append({"amplitude": amp, "paired_sig": ttest(a, b.loc[a.index], True) < 0.05,
                                  "unpaired_sig": ttest(a, b, False) < 0.05})
        P = pd.DataFrame(pairs)
        print("Variant, every pair of ROI in the range: pairs with p < 0.05 per amplitude")
        print(P.groupby("amplitude").agg(pairs=("paired_sig", "size"), paired=("paired_sig", "sum"),
                                         unpaired=("unpaired_sig", "sum")).to_string())
    return {"paired_any_sig": pa, "unpaired_any_sig": pb, "outcome": outcome}

print(claim_fig2c_flat(main))
print("\nUnder every variant of the data:")
run_variants(claim_fig2c_flat)
8.7 kV/cm: ROI j = 9; 15.1 kV/cm: ROI j = 1
 amplitude  pct_at_8.7  pct_at_15.1  difference  p_paired  p_unpaired
        25    199.0029      76.8311    122.1718    0.0640      0.1435
        50    113.1618      34.7623     78.3995    0.0124      0.0016
        75    155.6884      41.9911    113.6974    0.0494      0.0175
       100    229.8687     133.4092     96.4594    0.1142      0.0349
Variant, every pair of ROI in the range: pairs with p < 0.05 per amplitude
           pairs  paired  unpaired
amplitude                         
25            36       2         0
50            36      23        18
75            36      15        11
100           36       4        10
{'paired_any_sig': True, 'unpaired_any_sig': True, 'outcome': 'not_reproduced'}

Under every variant of the data:
                          data  paired_any_sig  unpaired_any_sig        outcome
                          main            True              True not_reproduced
                      top side            True              True not_reproduced
                   bottom side            True              True not_reproduced
         scale from electrodes            True              True not_reproduced
                left footprint            True              True not_reproduced
               right footprint            True              True not_reproduced
field scaled to 15.1 at centre            True              True not_reproduced

Result: not reproduced. The percent of the unipolar mean rises from the ROI at 15.24 kV/cm to the ROI at 8.94 kV/cm for every amplitude (25%: 76.8 to 199.0; 50%: 34.8 to 113.2; 75%: 42.0 to 155.7; 100%: 133.4 to 229.9). The difference is significant for 50% and 75% under both the paired (p 0.012, 0.049) and the unpaired test (p 0.0016, 0.018), and for 100% unpaired (p 0.035). Over all 36 pairs of ROI in the range, 23 (paired) and 18 (unpaired) differ for 50%. Every variant of the data gives the same outcome.

fig2c-weaker-at-6-7¶

Inhibition was weaker at 6.7 kV/cm than at the higher field strengths, with p < 0.01 for the 50% bipolar pulses and p < 0.05 for the 100% bipolar pulses.

Criterion: bipolar YP per dish as percent of the unipolar mean at the same ROI. For 50% and 100%, the ROI nearest 6.7 kV/cm against the higher fields (ROI from 8.7 to 15.1 kV/cm), (i) against each higher ROI or (ii) against the higher ROI pooled, either sufficing; each (a) paired and (b) unpaired, methods_underspecified when they differ. Reproduced when the percent at 6.7 kV/cm is higher than at every higher field, with p < 0.01 for 50% and p < 0.05 for 100%; partially reproduced when it is higher at every field but the tests hold for one amplitude only. The criterion divides by the unipolar mean at the same ROI. Where that mean is not above background (here: not more than twice its s.e.m. above zero), the percent is a ratio to background, not a measure of inhibition, and the claim cannot be tested; the per-dish unipolar values are printed.

In [15]:
def claim_fig2c_67(t, verbose=True):
    r = ratio_table(t)
    j67, j_hi, j_lo = nearest_j(t, 6.7), nearest_j(t, 15.1), nearest_j(t, 8.7)
    js = list(range(min(j_hi, j_lo), max(j_hi, j_lo) + 1))
    uni = t[t.amplitude == 0].groupby("j").YP.mean()
    rows, verdict = [], {}
    for amp, thr in ((50, 0.01), (100, 0.05)):
        x = r[(r.amplitude == amp) & (r.j == j67)].set_index("dish").pct
        higher_every = all(x.mean() > r[(r.amplitude == amp) & (r.j == j)].pct.mean() for j in js)
        for paired in (True, False):
            p_each = []
            for j in js:
                y = r[(r.amplitude == amp) & (r.j == j)].set_index("dish").pct
                p_each.append(ttest(x, y.loc[x.index], True) if paired else ttest(x, y, False))
            pool = r[(r.amplitude == amp) & r.j.isin(js)]
            if paired:
                p_pool = ttest(x, pool.groupby("dish").pct.mean().loc[x.index], True)
            else:
                p_pool = ttest(x, pool.pct, False)
            holds = higher_every and (max(p_each) < thr or p_pool < thr)
            verdict[(amp, paired)] = (higher_every, holds)
            rows.append({"amplitude": amp, "test": "paired" if paired else "unpaired",
                         "pct_at_6.7": x.mean(), "pct_higher_fields_mean": pool.pct.mean(),
                         "higher_than_every_higher_ROI": higher_every,
                         "p_each_max": max(p_each), f"each_p<thr": f"{sum(p < thr for p in p_each)}/{len(p_each)}",
                         "p_pooled": p_pool, "holds": holds})
    R = pd.DataFrame(rows)
    def outcome(paired):
        h50, h100 = verdict[(50, paired)], verdict[(100, paired)]
        if h50[1] and h100[1]:
            return "reproduced"
        if h50[0] and h100[0] and (h50[1] or h100[1]):
            return "partially_reproduced"
        return "not_reproduced"
    oa, ob = outcome(True), outcome(False)
    if verbose:
        print(f"6.7 kV/cm: ROI j = {j67} ({t.groupby('j').field.mean().loc[j67]:.2f} kV/cm); higher fields j = {js}")
        print(f"Unipolar mean YP at j = {j67}: {uni.loc[j67]:.1f}; at the higher ROI: {uni.loc[js].round(1).tolist()}")
        print(R.round(4).to_string(index=False))
    # The percent of the unipolar mean measures inhibition only where the unipolar mean is above
    # background; here taken as more than twice its s.e.m. above zero.
    u67 = dish_values(t, 0, j67)
    background = bool(u67.mean() <= 2 * u67.sem())
    return {"field_6.7_ROI": round(t.groupby("j").field.mean().loc[j67], 2), "uni_mean_6.7": round(u67.mean(), 1),
            "uni_sem_6.7": round(u67.sem(), 1), "uni_at_background": background, "paired": oa, "unpaired": ob,
            "outcome": "blocked (unipolar mean at background)" if background
            else oa if oa == ob else "methods_underspecified"}

print(claim_fig2c_67(main))
print("\nUnder every variant of the data:")
run_variants(claim_fig2c_67)
# The denominator: unipolar YP per dish at the ROI nearest 6.7 kV/cm and the ROI either side of it.
rows = []
for name, t in T.items():
    j67 = nearest_j(t, 6.7)
    for j in (j67 - 1, j67, j67 + 1):
        u = dish_values(t, 0, j)
        rows.append({"data": name, "j": j, "field": round(t[t.j == j].field.mean(), 2),
                     **{d: round(v, 1) for d, v in u.items()}, "mean": round(u.mean(), 1), "sem": round(u.sem(), 1)})
print("\nUnipolar YP per dish (a.u.) around the ROI nearest 6.7 kV/cm (the middle row of each variant):")
print(pd.DataFrame(rows).to_string(index=False))
6.7 kV/cm: ROI j = 12 (6.75 kV/cm); higher fields j = [1, 2, 3, 4, 5, 6, 7, 8, 9]
Unipolar mean YP at j = 12: -8.6; at the higher ROI: [570.4, 571.1, 527.8, 369.0, 409.6, 199.1, 141.4, 82.2, 47.7]
 amplitude     test  pct_at_6.7  pct_higher_fields_mean  higher_than_every_higher_ROI  p_each_max each_p<thr  p_pooled  holds
        50   paired   -399.1128                 54.4785                         False      0.0068        9/9    0.0061  False
        50 unpaired   -399.1128                 54.4785                         False      0.0008        9/9    0.0000  False
       100   paired   -578.7826                137.4000                         False      0.0049        9/9    0.0028  False
       100 unpaired   -578.7826                137.4000                         False      0.0001        9/9    0.0000  False
{'field_6.7_ROI': np.float64(6.75), 'uni_mean_6.7': np.float64(-8.6), 'uni_sem_6.7': np.float64(19.6), 'uni_at_background': True, 'paired': 'not_reproduced', 'unpaired': 'not_reproduced', 'outcome': 'blocked (unipolar mean at background)'}

Under every variant of the data:
                          data  field_6.7_ROI  uni_mean_6.7  uni_sem_6.7  uni_at_background         paired       unpaired                               outcome
                          main           6.75          -8.6         19.6               True not_reproduced not_reproduced blocked (unipolar mean at background)
                      top side           6.75         -32.1         35.9               True not_reproduced not_reproduced blocked (unipolar mean at background)
                   bottom side           6.75          14.8          3.3              False     reproduced     reproduced                            reproduced
         scale from electrodes           6.75          -7.4         15.7               True not_reproduced not_reproduced blocked (unipolar mean at background)
                left footprint           6.75           7.9          3.0              False     reproduced     reproduced                            reproduced
               right footprint           6.75         -25.2         42.2               True not_reproduced not_reproduced blocked (unipolar mean at background)
field scaled to 15.1 at centre           6.63          -8.6         19.6               True not_reproduced not_reproduced blocked (unipolar mean at background)

Unipolar YP per dish (a.u.) around the ROI nearest 6.7 kV/cm (the middle row of each variant):
                          data  j  field  0%-06  0%-07  mean  sem
                          main 11   7.41   58.0  -17.3  20.3 37.7
                          main 12   6.75   11.0  -28.2  -8.6 19.6
                          main 13   6.15    2.0  -28.7 -13.3 15.3
                      top side 11   7.41   51.5  -46.4   2.6 49.0
                      top side 12   6.75    3.7  -68.0 -32.1 35.9
                      top side 13   6.15   -7.4  -51.8 -29.6 22.2
                   bottom side 11   7.41   64.5   11.7  38.1 26.4
                   bottom side 12   6.75   18.2   11.5  14.8  3.3
                   bottom side 13   6.15   11.4   -5.5   3.0  8.5
         scale from electrodes 11   7.41   40.9  -16.9  12.0 28.9
         scale from electrodes 12   6.75    8.3  -23.1  -7.4 15.7
         scale from electrodes 13   6.15   -2.5  -25.1 -13.8 11.3
                left footprint 11   7.41   49.5   21.7  35.6 13.9
                left footprint 12   6.75    4.9   10.9   7.9  3.0
                left footprint 13   6.15   -3.6   10.4   3.4  7.0
               right footprint 11   7.41   66.6  -56.4   5.1 61.5
               right footprint 12   6.75   17.0  -67.4 -25.2 42.2
               right footprint 13   6.15    7.6  -67.8 -30.1 37.7
field scaled to 15.1 at centre 11   7.28   58.0  -17.3  20.3 37.7
field scaled to 15.1 at centre 12   6.63   11.0  -28.2  -8.6 19.6
field scaled to 15.1 at centre 13   6.04    2.0  -28.7 -13.3 15.3

Result: blocked (other), under the main reading of the ROI and the footprints. At the ROI nearest 6.7 kV/cm (6.75 kV/cm) the unipolar mean YP is -8.6 a.u. with an s.e.m. of 19.6 (the two unipolar dishes give 11.0 and -28.2), not above background. The percent of the unipolar mean there (-399% for 50%, -579% for 100%) is a ratio to background, not a measure of inhibition, so the claim cannot be tested on these two dishes under the main reading. On the top side, with the right footprint, with the scale from the electrodes and with the field scaled to 15.1 kV/cm at the centre the unipolar mean is also at background. On the bottom side alone (14.8 a.u., s.e.m. 3.3) or with the left footprint alone (7.9 a.u., s.e.m. 3.0) it is more than twice its s.e.m. above zero and the claim is reproduced under both pairings, with a denominator below 3% of the largest unipolar mean of the main reading (571.1 a.u.).

The 600 ns exposure of Figs. 4B and 5B on the Fig. 2 dishes (no outcome)¶

The claims on Figs. 4 and 5 use the dishes of the pulse duration experiment, which are not in the dandiset, and are blocked. The Fig. 2 dishes with 0% and 50% second phase had the same exposure as the 600 ns pair of Fig. 4 (5 pulses at 1 Hz, 5.75 kV first phase, 50% second phase; Results 3.1 and 3.3), but they are other dishes. This section applies the measures of fig4ac-ns-bipolar-20-25pct (the ratio of the fits of bipolar and unipolar mean YP, over fields above 7 or 8 kV/cm, with exponential, 2nd degree polynomial or the better fit per curve) and of fig5-ns-symmetric-along-aa (ROI along aa', the two sides compared at each distance) to them, to show what part of those claims the dandiset could speak to. It decides no outcome. Which electrode was the anode is not in the files, so the sides are named left and right.

In [16]:
def fit_curve(E, Y, kind):
    E, Y = np.asarray(E, float), np.asarray(Y, float)
    if kind == "poly2":
        p = np.polyfit(E, Y, 2)
        return (lambda e: np.polyval(p, e)), ((Y - np.polyval(p, E)) ** 2).sum()
    k, se, A, rss, used = fit_exp(E, Y, "nls")
    return (lambda e: A * np.exp(k * e)), rss

rows = []
for cut in (7, 8):
    g = M[(M.field >= cut) & M.amplitude.isin([0, 50])]
    E = np.sort(g.groupby("j").field.mean().values)
    fits = {}
    for amp in (0, 50):
        h = g[g.amplitude == amp]
        fits[amp] = {kind: fit_curve(h.field, h.YP, kind) for kind in ("exp", "poly2")}
        best = min(fits[amp].items(), key=lambda kv: kv[1][1])
        fits[amp]["best"] = best[1]
    for kind in ("exp", "poly2", "best"):
        ratio = 100 * fits[50][kind][0](E) / fits[0][kind][0](E)
        rows.append({"cutoff": cut, "fit": kind, "ROI": len(E), "ratio_min_%": ratio.min(),
                     "ratio_max_%": ratio.max(), "ratio_mean_%": ratio.mean(),
                     "inside_18_27.5_everywhere": bool(((ratio >= 18) & (ratio <= 27.5)).all())})
print("Ratio of the 50% bipolar fit to the unipolar fit, 600 ns, Fig. 2 dishes (fig4ac measure):")
print(pd.DataFrame(rows).round(1).to_string(index=False))
r = ratio_table(main)
for cut in (7, 8):
    js = sorted(main[main.field >= cut].j.unique())
    lo_j, hi_j = max(js), min(js)
    a = r[(r.amplitude == 50) & (r.j == lo_j)].set_index("dish").pct
    b = r[(r.amplitude == 50) & (r.j == hi_j)].set_index("dish").pct
    print(f"cutoff {cut}: dish percent at the lowest included field ({main.groupby('j').field.mean()[lo_j]:.2f} kV/cm) "
          f"{a.mean():.1f}, at the highest ({main.groupby('j').field.mean()[hi_j]:.2f}) {b.mean():.1f}; "
          f"paired p = {ttest(a, b.loc[a.index], True):.4f}")

aa = []
for d in keep:
    feet = d["measure"]["footprints"]
    yp0, mb0 = np.mean([f[bp.YP] for f in feet]), np.mean([f[bp.MB] for f in feet])
    for roi in d["measure"]["rois_aa"]:
        aa.append({"dish": d["label"], "amplitude": d["amplitude"], "k": roi["k"], "field": roi["field"],
                   "YP": bp.yp_index(roi["YP_roi"], roi["MB_roi"], yp0, mb0)})
aa = pd.DataFrame(aa)
rows = []
for amp in (0, 50):
    for j in range(1, bp.N_ROI_AA + 1):
        L = aa[(aa.amplitude == amp) & (aa.k == -j)].set_index("dish").YP
        R = aa[(aa.amplitude == amp) & (aa.k == j)].set_index("dish").YP
        rows.append({"amplitude": amp, "distance_mm": j * bp.ROI_UM / 1000,
                     "field": aa[aa.k == j].field.mean(), "left": L.mean(), "right": R.mean(),
                     "left_over_right": L.mean() / R.mean(),
                     "p_paired": ttest(L, R.loc[L.index], True), "p_unpaired": ttest(L, R, False)})
S = pd.DataFrame(rows)
print("\nYP along aa', left against right side at the same distance (fig5 measure), 600 ns, Fig. 2 dishes:")
print(S.round(4).to_string(index=False))
print("Distances with p < 0.05, paired / unpaired:",
      S.groupby("amplitude").apply(lambda g: (int((g.p_paired < 0.05).sum()), int((g.p_unpaired < 0.05).sum()))).to_dict())
print(f"Range of YP along aa': {aa.YP.min():.1f} to {aa.YP.max():.1f} a.u.; fields {aa.field.min():.2f} to {aa.field.max():.2f} kV/cm")
Ratio of the 50% bipolar fit to the unipolar fit, 600 ns, Fig. 2 dishes (fig4ac measure):
 cutoff   fit  ROI  ratio_min_%  ratio_max_%  ratio_mean_%  inside_18_27.5_everywhere
      7   exp   12         34.5         64.9          46.7                      False
      7 poly2   12       -457.4        150.6          13.3                      False
      7  best   12       -457.4        150.6          13.3                      False
      8   exp   11         34.4         62.2          45.3                      False
      8 poly2   11         34.8       1537.4         184.2                      False
      8  best   11         34.8       1537.4         184.2                      False
cutoff 7: dish percent at the lowest included field (7.41 kV/cm) 108.8, at the highest (15.38) 38.7; paired p = 0.1531
cutoff 8: dish percent at the lowest included field (8.14 kV/cm) 118.6, at the highest (15.38) 38.7; paired p = 0.0099

YP along aa', left against right side at the same distance (fig5 measure), 600 ns, Fig. 2 dishes:
 amplitude  distance_mm   field     left    right  left_over_right  p_paired  p_unpaired
         0          0.2 15.5151 611.8128 573.4728           1.0669    0.5259      0.7411
         0          0.4 15.9449 593.1983 553.6832           1.0714    0.8637      0.8249
         0          0.6 16.7167 479.6662 498.9566           0.9613    0.2406      0.6420
         0          0.8 17.9322 745.5819 620.3965           1.2018    0.0920      0.1381
         0          1.0 19.7822 625.4799 699.2402           0.8945    0.5009      0.7464
         0          1.2 22.6386 817.5989 999.3741           0.8181    0.2119      0.1093
        50          0.2 15.5151 203.2038 223.9233           0.9075    0.6084      0.7120
        50          0.4 15.9449 270.4474 208.3476           1.2981    0.0822      0.3966
        50          0.6 16.7167 275.9588 233.7003           1.1808    0.0293      0.5757
        50          0.8 17.9322 278.9468 304.7569           0.9153    0.6915      0.6849
        50          1.0 19.7822 315.0202 359.8747           0.8754    0.5335      0.6535
        50          1.2 22.6386 414.1029 407.1447           1.0171    0.9249      0.9502
Distances with p < 0.05, paired / unpaired: {0: (0, 0), 50: (1, 0)}
Range of YP along aa': 37.8 to 1360.1 a.u.; fields 15.38 to 22.64 kV/cm
In [17]:
print(f"Notebook ran in {(time.time() - started) / 60:.1f} min")
Notebook ran in 11.6 min

Summary¶

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.

Claim Outcome
fig2a-uptake-rises-with-field partially reproduced (assumed)
fig2a-vertical-scales not reproduced (assumed)
fig2b-exponential-same-slope not reproduced (assumed)
fig2c-max-inhibition-50-75 not reproduced (assumed)
fig2c-inhibition-flat-above-8-7 not reproduced
fig2c-weaker-at-6-7 blocked: other, unipolar mean at background at 6.7 kV/cm (main reading)
15 claims on Figs. 3, 4, 5, 6 and 7 blocked: other, data in dandisets 000548, 000549, 000550 and 000448
fig1e-heating blocked: data missing

The claims on Figs. 4 and 5 are blocked, as their dishes are not in the dandiset. On the Fig. 2 dishes, which had the 600 ns exposure of Fig. 4, the measures of those claims give a ratio of the bipolar to the unipolar fit of 34.4 to 64.9% with exponential fits (fig4ac-ns-bipolar-20-25pct expects 18 to 27.5%). With polynomial fits the ratio crosses zero above 7 kV/cm (minimum -457.4%) and runs from 34.8 to 1537.4% above 8 kV/cm, and YP on the two sides of aa' differs at p < 0.05 at no distance under both pairings. These are other dishes than the paper's, and decide no outcome.

How the analysis differs from the paper's. The paper measured fluorescence in MetaMorph in ROI it placed by hand, and took the field of each ROI from a finite element model in Sim4Life. Here the electrode footprints are found automatically, 200 um ROI are tiled contiguously along bb' on both sides of the centre and averaged by distance, YP_0 and MB_0 are averaged over both footprints, the pixel size is taken as 0.65 um (camera pixel over the 10x objective), and the field comes from the closed-form solution for two cylinders in the plane, which gives 15.38 kV/cm at the centre against the paper's 15.1. The dish sub-Fig2-0-03 is left out because its YP image belongs to another dish, so unipolar pulses have 2 dishes, not 3. Each claim is shown under one side only, under a scale set from the electrode distance, under each footprint alone, and with the field scaled to the paper's 15.1 kV/cm at the centre.

What was found about the dandiset.

  • It holds only the Fig. 2 experiment (17 dishes) and a Fig. 1F sample image. Figs. 1E and 3 to 7 have no data in it. Those of Figs. 3 to 7 are in dandisets 000548, 000549, 000550 and 000448, which name the same paper; nothing in 000489 points to them.
  • The YP channel of sub-Fig2-0-03 (unipolar) is identical to that of sub-Fig2-25-04 (25%) and does not register with its own MemBrite channel; the unipolar YP image of dish 0-03 is missing. Dandiset 000548 holds it: its file for that dish has the same MemBrite channel and a YP channel that registers with it.
  • The sub-Fig1F file holds the same image in both channels.
  • The image is stored as an ImageSeries of shape (22323, 22323, 2) with a rate of 2 Hz, so its first axis reads as time. There is no pixel size, no electrode position, no ROI, no field per ROI, no footprint values and no pulse voltage.
  • The series description says 144 images of a well of a six-well plate; the paper says 216 images of a 35 mm dish. The three 25% files share the acquisition name Fig2_25.
  • All unipolar dishes were imaged on 2021-07-29 and all 75% and 100% dishes on 2021-08-24, with no unipolar dish that day. The 100% dishes show more uptake than the unipolar dishes (761.0 against 571.1 a.u. at most), which the paper's Fig. 2C, where every bipolar pulse inhibits, does not show.
  • Pixel scale and geometry are consistent with the paper: at 0.65 um per pixel the electrode centres are 4.54 mm apart (nominal 4.5) and the footprints 1.49 mm across (nominal 1.5).