Reproducing Silkuniene et al. (2025) from dandiset 001047¶
Paper. Silkuniene G, Silkunas M, Pakhomova ON, Semenov I, Pakhomov AG (2025). MHz compression of pulse packets facilitates remote focusing of electroporation. Bioelectrochemistry 166, 109016. doi:10.1016/j.bioelechem.2025.109016. The claims file was written from the NIH author manuscript in PubMed Central (PMC12331426): main text, Methods and figure legends; there is no supplement, and the figures were not seen.
Dandiset. DANDI:001047, version 0.250701.1609. The paper cites no version (its data statement says "Data will be made available on request"). This is the most recent published version. It has the same 88 assets and size as the draft and as 0.241211.1856; the first version, 0.240610.2022, held 54 assets (the CT26 files; the CHO files were added in December 2024). The pinned version is the one that names the paper (IsPublishedIn).
What the dandiset holds. Stitched single-channel images of cell monolayers after pulsed-field exposure: CT26 cells at packet rates of 1 Hz (code 013) and 0.2 MHz (code 011), CANCAN and control protocols, burst labels x1, x2, x3 and x5, YO-PRO-1 (FITC), Hoechst taken before the other images (DAPI) and propidium (Cy3); and CHO cells, one burst label, YP and Hoechst. There are no images at 1 kHz or 10 kHz, no 300- or 900-ns pulses, no electrode elevations, no thermochromic film images and no CHO series over burst numbers.
Burst labels. The paper's rate experiments used 2 bursts (x4). The files give the burst number only as a label. Read as repeats after the first burst (adopted, see "Methods common to the claims"), x1 is 2 bursts at both rates; read as the total, 2 bursts exists only at 1 Hz (x2).
| Claim | Tested here |
|---|---|
| fig2-heating | Blocked: no film images |
| fig3a-centre-enhanced | Tested at the two rates present |
| fig3bc-rate-suppresses-off-target | Tested |
| fig3-lorentzian-profile | Tested at the two rates present |
| fig3-tail-deviation-falls-with-rate | Tested at the two rates present |
| fig4b-centre-no-rate-effect | Tested at the two rates present |
| fig4b-near-electrode-decline | Tested at the two rates present |
| fig4b-40-fold-ratio | Tested |
| fig4c-fwhm-falls-with-rate | Blocked: no 1 kHz or 10 kHz images (values at 1 Hz and 0.2 MHz shown) |
| fig4c-log-trend | Blocked: no 1 kHz or 10 kHz images |
| fig5-raised-electrodes-weaken-centre | Blocked: no file gives an electrode elevation |
| fig5ab-yp-spot-slightly-larger-than-pr | Tested, assuming electrodes touching the monolayer |
| fig5b-gaussian-pr-lorentzian-yp | Tested, assuming electrodes touching the monolayer |
| fig5c-nuclear-yp-centre | Blocked: images at half the camera's resolution, Hoechst taken before exposure |
| fig6b, fig6c, fig6d (pulse duration) | Blocked: all files are 600-ns pulses |
| fig7be, fig7cf | Blocked: no CHO series over burst numbers (CT26 half tested and shown) |
| fig7-sharpest-one-burst | Blocked: 1 burst (x4) is not among the labels under the adopted reading (shown under the other) |
import re
import time
import warnings
from concurrent.futures import ProcessPoolExecutor
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy import stats
from scipy.optimize import OptimizeWarning, curve_fit
import cancan as cc
pd.set_option("display.width", 220)
pd.set_option("display.max_columns", 40)
pd.set_option("display.max_rows", 300)
warnings.filterwarnings("ignore", category=RuntimeWarning)
warnings.filterwarnings("ignore", category=OptimizeWarning)
started = time.time()
version = cc.version_metadata()
assets = cc.list_assets()
with ProcessPoolExecutor(16) as ex:
metas = list(ex.map(cc.read_metadata, assets))
files = []
for m in metas:
p = cc.parse(m)
files.append({**{k: p[k] for k in ("file", "cells", "channel", "phase", "pixel_um")}, "asset_id": m["asset_id"],
"rows": m["shape"][1], "cols": m["shape"][2], "resolution_attr": m["resolution_attr"],
"start": m["start"][:16], "areas": p["areas"], "desc": m["desc"], "size_mb": m["size"] / 1e6})
files = pd.DataFrame(files).sort_values("file").reset_index(drop=True)
print(f"dandiset {cc.DANDISET_ID} version {cc.DANDISET_VERSION}: {len(assets)} assets, "
f"{sum(a['size'] for a in assets) / 1e9:.2f} GB; metadata read in {time.time() - started:.0f} s")
dandiset 001047 version 0.250701.1609: 88 assets, 17.25 GB; metadata read in 6 s
Data check¶
Each check of the metadata against the values is stated under its output.
print("Title:", version["name"])
print("Description:", version["description"])
print("Species:", [s["name"] for s in version["assetsSummary"]["species"]], "; subjects:", version["assetsSummary"]["numberOfSubjects"])
print()
print("Files by cell line, channel and phase (CT26 Hoechst set to 'pre' from the acquisition times below):")
print(files.groupby(["cells", "channel", "phase"]).size().to_string())
print("\nStated resolution attribute of the image data, all files:", sorted(files.resolution_attr.unique()))
print("CHO pixel size stated in the OME metadata (um):", sorted(files[files.cells == "CHO"].pixel_um.unique()))
print("\nImage shapes (rows x cols) and areas per image:")
files["n_areas"] = files.areas.map(len)
print(files.groupby(["cells", "rows", "cols", "n_areas", "start"]).size().to_string())
ct24 = files[(files.cells == "CT26") & files.start.str.startswith("2024-05-24")]
print("\nCT26 images of May 24 by channel: acquisition times, number of files, and whether the channels share the same set of",
"image shapes and subject pairs (one image of each channel per plate pair)")
print(ct24.groupby("channel").agg(start=("start", lambda s: sorted(set(s))), files=("file", "size")).to_string())
key = lambda g: sorted((r.rows, r.cols, tuple(a["subject"] for a in r.areas)) for r in g.itertuples())
print("same shapes and subject pairs in DAPI, FITC and Cy3:", key(ct24[ct24.channel == "DAPI"]) == key(ct24[ct24.channel == "FITC"]) == key(ct24[ct24.channel == "Cy3"]))
print("assumed pixel size of these images (um):", sorted(ct24.pixel_um.unique()))
d0 = files[files.cells == "CT26"].desc.iloc[0]
d1 = files[files.cells == "CHO"].desc.iloc[0]
for d in (d0, d1):
i = d.find("four")
print("\nProtocol text in a", "CT26" if d is d0 else "CHO", "file:", d[i - 40:i + 420])
i = d0.find("Upon completion")
print("\nBurst repetitions in the CT26 protocol text:", d0[i:i + 190])
reps = sorted({m for d in files[files.cells == "CT26"].desc
for m in re.findall(r"(\d{3}\w*?_x\d): 9 packets of pulses delivered at \S+ frequency, [^,]+, (Unknown repetitions|repeated \d+ times)", d)})
print("Repetitions stated per CT26 subject:", reps)
Title: Optimizing NG-CanCan Protocol Delivery with MHz Compression to Minimize Near-Electrode Ablation
Description: This study advanced the Next Generation (NG) CanCan protocols by implementing MHz compression for electric pulse packet delivery. Packets were delivered at 0.2 MHz, and the compression significantly reduced near-electrode cell ablation compared to delivery at 1 Hz, while preserving effects at the center. The experiments used a four-electrode array, with YoPro-1 to evaluate cell permeabilization and Propidium Iodide to assess cell death two hours post-delivery. This work was supported by NIH grant 1R21EY034258.
Species: ['Mus musculus - House mouse', 'Cricetulus griseus - Cricetulus aureus'] ; subjects: 29
Files by cell line, channel and phase (CT26 Hoechst set to 'pre' from the acquisition times below):
cells channel phase
CHO DAPI pre 17
FITC post 17
CT26 Cy3 post 12
DAPI pre 12
FITC post 30
Stated resolution attribute of the image data, all files: [np.float64(0.1), np.float64(96.0)]
CHO pixel size stated in the OME metadata (um): [np.float64(1.6250000000000002)]
Image shapes (rows x cols) and areas per image:
cells rows cols n_areas start
CHO 8019 8462 1 2024-12-11T13:29 6
2024-12-11T13:30 10
2024-12-11T13:31 11
2024-12-11T13:32 7
CT26 8043 14198 2 2024-05-24T10:35 1
2024-05-24T12:49 1
2024-05-24T14:24 1
16849 2 2024-05-24T10:35 2
2024-05-24T12:49 2
2024-05-24T14:24 2
17105 2 2024-05-24T10:35 2
2024-05-24T12:49 2
2024-05-24T14:24 2
17362 2 2024-05-24T10:35 4
2024-05-24T12:49 4
2024-05-24T14:24 4
18816 2 2024-05-24T10:35 1
2024-05-24T12:49 1
2024-05-24T14:24 1
19585 2 2024-05-24T10:35 1
2024-05-24T12:49 1
2024-05-24T14:24 1
19927 2 2024-05-24T10:35 1
2024-05-24T12:49 1
2024-05-24T14:24 1
9490 18123 2 2024-05-23T13:35 11
21870 21289 1 2024-05-23T14:13 7
CT26 images of May 24 by channel: acquisition times, number of files, and whether the channels share the same set of image shapes and subject pairs (one image of each channel per plate pair)
start files
channel
Cy3 [2024-05-24T14:24] 12
DAPI [2024-05-24T10:35] 12
FITC [2024-05-24T12:49] 12
same shapes and subject pairs in DAPI, FITC and Cy3: True
assumed pixel size of these images (um): [np.float64(3.25)]
Protocol text in a CT26 file: elow.Experiments were conducted using a four stainless steel electrode setup. The distance between adjacent electrodes was 3.8 mm. The CanCan exposure protocol consisted of packets of pulses delivered from four electrodes (channels). The first pulse (600 ns) in each packet was delivered from one electrode (e.g., electrode 1) at a 3.2 kV amplitude. Subsequent pulses (600 ns) were delivered simultaneously from two electrodes (electrodes 2 and 4) at 75% of th
Protocol text in a CHO file: nt): Experiments were conducted using a four-electrode stainless steel setup with an inter-electrode distance of 3.8 mm. The CanCan exposure protocol involved delivering packets of 600 ns pulses from four electrodes. Initially, a single 600 ns pulse (3.2 kV) was applied from one electrode (e.g., electrode 1). Subsequently, 600 ns pulses at 75% amplitude were delivered simultaneously from two electrodes (e.g., electrodes 2 and 4), followed by pulses at the
Burst repetitions in the CT26 protocol text: Upon completion of the protocol, it was either repeated 2, 3, or 5 times at a 1 Hz frequency or switched to another starting active electrode (e.g., electrode 2). Sequences were repeated in
Repetitions stated per CT26 subject: [('011CC_x1', 'Unknown repetitions'), ('011CC_x3', 'Unknown repetitions'), ('011CC_x5', 'Unknown repetitions'), ('011C_x5', 'repeated 5 times'), ('011Control_x1', 'Unknown repetitions'), ('011Control_x3', 'Unknown repetitions'), ('011Control_x5', 'Unknown repetitions'), ('013CC_x1', 'Unknown repetitions'), ('013CC_x2', 'Unknown repetitions'), ('013CC_x5', 'Unknown repetitions'), ('013Control_x1', 'Unknown repetitions'), ('013Control_x2', 'Unknown repetitions'), ('013Control_x5', 'Unknown repetitions')]
The dandiset's description and every file's protocol text say the electrodes were 3.8 mm apart and the first pulse was 3.2 kV, with later pulses at 75, 50 and 25% of it. The paper used a 15.3-mm array, a 7.2-kV first pulse and 16-pulse packets falling by 12.5% per phase (Methods 2.2). The CT26 images show the footprints of the paper's array, not a 3.8-mm one (checked below), so the geometry in the text does not describe these images. The voltage cannot be checked from images: if the CT26 wells were exposed at 3.2 kV, they are not the paper's 7.2-kV experiments.
Burst number. Every CT26 subject but one says "Unknown repetitions"; the burst number is only in the subject label (x1, x2, x3, x5). The protocol text says that "upon completion of the protocol, it was either repeated 2, 3, or 5 times", and the one subject with its own count (011C_x5) says "repeated 5 times" for label x5. The label is therefore the number of times the protocol was repeated. Whether that counts the first application is not said (see "Methods common to the claims").
Subject 011C_x5 has protocol code "C" and "Unknown protocol type", the text that every control subject carries, so it is taken as a control.
Pixel size. The resolution attribute of the image data is 96 in the CT26 files and 0.1 in the CHO
files; neither is a pixel size (the CHO OME metadata give 1.625 um), and the CT26 files state none.
Hoechst timing. On May 24 the 12 Hoechst (DAPI), 12 YP (FITC) and 12 propidium (Cy3) images share the same image shapes and subject pairs, one image of each per plate pair, and were acquired at 10:35, 12:49 and 14:24. The Hoechst images were taken before the YP images, consistent with the paper's Hoechst images before exposure (Methods 2.4); there is no Hoechst image taken after the propidium image.
area_rows = []
for _, f in files.iterrows():
for a in f.areas:
area_rows.append({"file": f.file, "cells": f.cells, "channel": f.channel, "phase": f.phase, **a})
areas_meta = pd.DataFrame(area_rows)
print("Exposure areas (one well each) by cell line, channel, rate code, protocol and burst label:")
print(areas_meta.groupby(["cells", "channel", "rate", "protocol", "label"]).size().unstack("channel").fillna(0).astype(int).to_string())
cho = files[files.cells == "CHO"]
print(f"\nCHO images: {len(cho)} files, {cho.rows.iloc[0]} x {cho.cols.iloc[0]} pixels at 1.625 um = "
f"{cho.rows.iloc[0] * 1.625e-3:.1f} x {cho.cols.iloc[0] * 1.625e-3:.1f} mm; "
f"wells: {areas_meta[areas_meta.cells == 'CHO'].subject.nunique()}")
print("CHO burst labels:", sorted(areas_meta[areas_meta.cells == "CHO"].label.unique()))
Exposure areas (one well each) by cell line, channel, rate code, protocol and burst label:
channel Cy3 DAPI FITC
cells rate protocol label
CHO 011 CANCAN r1 0 6 6
013 CANCAN r1 0 6 6
control r1 0 5 5
CT26 011 CANCAN x1 2 2 5
x3 2 2 2
x5 2 2 6
control x1 2 2 6
x3 2 2 2
x5 2 2 6
013 CANCAN x1 2 2 6
x2 2 2 2
x5 2 2 5
control x1 2 2 6
x2 2 2 2
x5 2 2 5
CHO images: 34 files, 8019 x 8462 pixels at 1.625 um = 13.0 x 13.8 mm; wells: 17
CHO burst labels: ['r1']
CHO: 17 wells (1 Hz CANCAN 6, 1 Hz control 5, 0.2 MHz CANCAN 6), each imaged before (Hoechst) and after (YP), all with one burst label. The images span 13.0 x 13.8 mm. The paper's electrode centres lie 6.4 mm from the array centre along each axis, inside the half-extents of these images (6.5 and 6.9 mm), but the 2.8-mm footprints reach 7.8 mm and are clipped, and the 7-mm ROIs on the diagonals reach the corners only if the array is centred to within about 0.1 mm. With one burst label the CHO images cannot hold the Figure 7 series, so they are not analysed further. The paper reports no CHO experiment at 1 Hz.
ct = files[(files.cells == "CT26") & files.channel.isin(["FITC", "Cy3"])].reset_index(drop=True)
t0 = time.time()
loaded = cc.load_many([(r.asset_id, r.pixel_um) for r in ct.itertuples()])
print(f"streamed {len(ct)} CT26 images ({ct.size_mb.sum() / 1e3:.1f} GB) in {time.time() - t0:.0f} s")
imgs, recs = {}, []
for r, (img, (vmin, vmax)) in zip(ct.itertuples(), loaded):
for a, (A, off) in zip(r.areas, cc.split_areas(img, len(r.areas))):
c, how, score = cc.locate(A)
key = (r.file, a["side"])
imgs[key] = A
recs.append({"file": r.file, "side": a["side"], "channel": r.channel, "rate": cc.RATE[a["rate"]],
"protocol": a["protocol"], "label": a["label"], "pixel_um": r.pixel_um, "raw_min": vmin,
"raw_max": vmax, "rows": A.shape[0], "cols": A.shape[1], "cy": c[0], "cx": c[1], "located_by": how,
"match": score})
areas = pd.DataFrame(recs)
print("raw pixel values (uint16, a.u.) over all CT26 images:", areas.raw_min.min(), "to", areas.raw_max.max())
print("\nAreas by channel, rate, protocol, burst label and how the array centre was found:")
print(areas.groupby(["channel", "rate", "protocol", "label", "located_by"]).size().unstack("located_by").fillna(0).astype(int).to_string())
streamed 42 CT26 images (11.5 GB) in 141 s
raw pixel values (uint16, a.u.) over all CT26 images: 0 to 65442
Areas by channel, rate, protocol, burst label and how the array centre was found:
located_by centroid footprints
channel rate protocol label
Cy3 0.2 MHz CANCAN x1 1 1
x3 1 1
x5 1 1
control x1 1 1
x3 0 2
x5 0 2
1 Hz CANCAN x1 2 0
x2 0 2
x5 1 1
control x1 0 2
x2 0 2
x5 0 2
FITC 0.2 MHz CANCAN x1 3 2
x3 1 1
x5 1 5
control x1 1 5
x3 0 2
x5 1 5
1 Hz CANCAN x1 3 3
x2 0 2
x5 2 3
control x1 1 5
x2 0 2
x5 2 3
Pixel size and array geometry. The footprint match uses the paper's geometry at the pixel sizes taken in the helper (1.625 um for whole-well images, 3.25 um for two-well images). If those were wrong, the best-matching electrode spacing would differ from the paper's. Below, the spacing is searched from 5 to 8 mm on the areas where the footprints were found.
rows = []
for r in areas[areas.located_by == "footprints"].itertuples():
A = imgs[(r.file, r.side)]
sc = [(cc.match_footprints(A, hs)[0], hs) for hs in np.arange(5.0, 8.01, 0.2)]
rows.append({"file": r.file, "side": r.side, "pixel_um": r.pixel_um, "best_half_side_mm": max(sc)[1]})
geo = pd.DataFrame(rows)
print("Best-matching half side of the electrode square (mm), by pixel size taken; the paper's geometry gives",
round(cc.HALF_SIDE_MM, 2))
print(geo.groupby("pixel_um").best_half_side_mm.describe().round(2).to_string())
for px, g in geo.groupby("pixel_um"):
m = g.best_half_side_mm.mean()
print(f"pixel size {px} um: mean best half side {m:.2f} mm, {100 * (m / cc.HALF_SIDE_MM - 1):+.0f}% from the paper's geometry; "
f"the pixel size that would give the paper's geometry is {px * cc.HALF_SIDE_MM / m:.2f} um")
w = files[(files.rows == 21870)]
print(f"\nWhole-well images: {w.rows.iloc[0]} x {w.cols.iloc[0]} pixels = "
f"{w.rows.iloc[0] * 1.625e-3:.1f} x {w.cols.iloc[0] * 1.625e-3:.1f} mm at 1.625 um (a 6-well plate well is about 35 mm)")
Best-matching half side of the electrode square (mm), by pixel size taken; the paper's geometry gives 6.4
count mean std min 25% 50% 75% max
pixel_um
1.625 3.0 7.00 0.53 6.4 6.8 7.2 7.3 7.4
3.250 52.0 6.11 0.32 5.2 6.0 6.2 6.4 6.6
pixel size 1.625 um: mean best half side 7.00 mm, +9% from the paper's geometry; the pixel size that would give the paper's geometry is 1.49 um
pixel size 3.25 um: mean best half side 6.11 mm, -4% from the paper's geometry; the pixel size that would give the paper's geometry is 3.40 um
Whole-well images: 21870 x 21289 pixels = 35.5 x 34.6 mm at 1.625 um (a 6-well plate well is about 35 mm)
o = cc.HALF_SIDE_MM / cc.PX_MM
ncol = 7
fig, axs = plt.subplots(int(np.ceil(len(areas) / ncol)), ncol, figsize=(21, 3.1 * np.ceil(len(areas) / ncol)))
for ax, (i, r) in zip(axs.flat, areas.iterrows()):
A = imgs[(r.file, r.side)]
lo, hi = np.percentile(A, [2, 99.5])
ax.imshow(A, vmin=lo, vmax=hi, cmap="gray")
for dy in (-o, o):
for dx in (-o, o):
ax.add_patch(plt.Circle((r.cx + dx, r.cy + dy), 1.4 / cc.PX_MM, fill=False, color="r" if r.located_by == "footprints" else "y"))
ax.plot(r.cx, r.cy, "c+", ms=10)
ax.set_title(f"{i}: {r.file[4:28]} {r.side} {r.channel}\n{r.rate} {r.protocol} {r.label} {r.located_by} {r.match:.3f}", fontsize=8)
ax.axis("off")
for ax in axs.flat[len(areas):]:
ax.axis("off")
plt.tight_layout()
plt.show()
print("panels shown:", len(areas), "of", len(areas), "areas")
panels shown: 77 of 77 areas
Each panel is one exposure area at 26-um pixels, scaled between its 2nd and 99.5th percentiles; the circles are the four footprints placed at the array centre found (red: footprints matched, yellow: centroid of the stained pattern). All 77 areas are shown. Where an area has almost no stain, neither method has anything to find, and its profile is near zero whatever the centre.
Six areas have a centre that is visibly misplaced, judged by eye from these panels:
011CC-x1_obj-1drpw33 L (Cy3; the footprints are not visible and the circles sit on unrelated
texture), 011CC-x5_obj-96qypk L (FITC; the circles are offset from the stained square),
011Control-x5_obj-f5ffop W (FITC, a whole-well image; the circles do not sit on the footprints) and
011Control-x5_obj-kvqdb2 R (Cy3; faint stain, centroid with a weak match),
013Control-x1_obj-1pihpj7 R (panel 62, FITC, 1 Hz CANCAN; centroid with a weak match, the upper
circles sit at the image edge above the stain) and 013Control-x1_obj-6stwj8 W (panel 63, FITC,
1 Hz control, a whole-well image; centroid with a weak match, the circles are offset up and left
from the stained square). The last two were found in the second verification round. All six are
kept in the analysis and marked flagged. The propidium claims, where two of them enter, are also
shown without them, and so are the x1 rate claims, where the last two enter (after
fig4b-near-electrode-decline). The other two flagged areas are in x5 groups, which decide no
outcome.
Labels against the data. A single time-point image has no clock to check; what can be checked is that the condition labels, and the left and right sides, match what the conditions do. The paper's control has its lowest YP at the centre (Results 3.1), so the centre relative to 6 mm should be lower in control areas than in CANCAN areas of the same rate and burst label.
P, PC = cc.POS_WHOLE_MM, cc.POS_CONTIG_MM
prof = {}
for i, r in areas.iterrows():
A = imgs[(r.file, r.side)]
c = (r.cy, r.cx)
bg, bgmin = cc.background(A, c)
areas.loc[i, "bg"], areas.loc[i, "bg_min"] = bg, bgmin
prof[(r.file, r.side)] = {"diag_whole": cc.profiles(A, c, P, cc.DIAGONALS), "diag_contig": cc.profiles(A, c, PC, cc.DIAGONALS),
"par_whole": cc.profiles(A, c, P, cc.PARALLEL), "par_contig": cc.profiles(A, c, PC, cc.PARALLEL)}
areas["usable"] = [not np.isnan(prof[(r.file, r.side)]["diag_whole"]).any() and not np.isnan(prof[(r.file, r.side)]["par_whole"]).any()
for r in areas.itertuples()]
print("Areas dropped because an ROI line leaves the image (centre placed near an edge):", int((~areas.usable).sum()), "of", len(areas))
print(areas[~areas.usable][["file", "side", "channel", "rate", "protocol", "label", "located_by", "cy", "cx"]].to_string(index=False))
def area_profile(key, kind="diag", o1="a", o2="a", bgkind="bg"):
pos = P if o1 == "a" else PC
raw = prof[key][f"{kind}_{'whole' if o1 == 'a' else 'contig'}"]
bg = areas.set_index(["file", "side"]).loc[key, bgkind]
return cc.pool(raw - bg, pos, o2)
def at(pos, y, x):
return y[np.argmin(np.abs(pos - x))]
fitc = areas[(areas.channel == "FITC") & areas.usable].copy()
for i, r in fitc.iterrows():
pos, y = area_profile((r.file, r.side))
fitc.loc[i, "y0"], fitc.loc[i, "y6"], fitc.loc[i, "y7"] = at(pos, y, 0), at(pos, y, 6), at(pos, y, 7)
print("\nBackground-subtracted YP (a.u.) at 0, 6 and 7 mm, and background, by group (mean over areas):")
print(fitc.groupby(["label", "rate", "protocol"])[["y0", "y6", "y7", "bg"]].agg(["mean", "count"]).round(1).to_string())
fitc["centre_minus_6mm"] = fitc.y0 - fitc.y6
fitc["image"] = np.where(fitc.pixel_um == 1.625, "whole-well", "two-well")
print("\nBackground (a.u., mean of the four footprints) by image type, YP areas:")
print(fitc.groupby("image").bg.describe().round(1).to_string())
print("\nBackground by image type and burst label (mean, n):")
print(fitc.groupby(["label", "image"]).bg.agg(["mean", "count"]).round(1).unstack("image").to_string())
print("\nCentre minus 6 mm, CANCAN against control in the same image (paired areas):")
pairs = fitc.groupby("file").filter(lambda g: len(g) == 2 and set(g.protocol) == {"CANCAN", "control"})
pp = pairs.pivot_table(index="file", columns="protocol", values="centre_minus_6mm")
print(pp.round(1).to_string())
print("CANCAN above control in", int((pp.CANCAN > pp.control).sum()), "of", len(pp), "images")
print("two-sided binomial test of that count against one half: p =", round(stats.binomtest(int((pp.CANCAN > pp.control).sum()), len(pp)).pvalue, 2))
Areas dropped because an ROI line leaves the image (centre placed near an edge): 3 of 77
file side channel rate protocol label located_by cy cx
sub-011CC-x1_obj-47st27 L FITC 0.2 MHz CANCAN x1 centroid 553.444422 10.261077
sub-013Control-x1_obj-1g50ez6 R Cy3 1 Hz CANCAN x1 centroid 261.038438 439.686945
sub-013Control-x5_obj-15l9v1y R Cy3 0.2 MHz CANCAN x1 centroid 495.849322 240.432677
Background-subtracted YP (a.u.) at 0, 6 and 7 mm, and background, by group (mean over areas):
y0 y6 y7 bg
mean count mean count mean count mean count
label rate protocol
x1 0.2 MHz CANCAN -1.4 4 5.7 4 3.3 4 199.4 4
control 88.6 6 795.4 6 571.6 6 482.4 6
1 Hz CANCAN 1.0 6 120.9 6 260.9 6 375.2 6
control 401.7 6 941.6 6 664.6 6 482.2 6
x2 1 Hz CANCAN 516.4 2 886.9 2 464.4 2 214.4 2
control 956.1 2 3130.0 2 2790.8 2 294.6 2
x3 0.2 MHz CANCAN 352.7 2 74.7 2 144.4 2 227.5 2
control 1026.1 2 1911.3 2 1726.2 2 282.7 2
x5 0.2 MHz CANCAN 2275.3 6 1751.0 6 1212.3 6 864.6 6
control 2132.3 6 1769.5 6 1688.6 6 1263.1 6
1 Hz CANCAN 1576.2 5 1700.9 5 1047.7 5 1002.1 5
control 1099.0 5 1793.2 5 1311.8 5 933.0 5
Background (a.u., mean of the four footprints) by image type, YP areas:
count mean std min 25% 50% 75% max
image
two-well 45.0 331.6 205.8 179.8 213.6 259.3 314.0 1121.4
whole-well 7.0 2628.0 1376.6 1061.4 1711.1 1755.8 3798.3 4560.0
Background by image type and burst label (mean, n):
mean count
image two-well whole-well two-well whole-well
label
x1 229.1 1494.5 19.0 3.0
x2 254.5 NaN 4.0 NaN
x3 255.1 NaN 4.0 NaN
x5 473.9 3478.1 18.0 4.0
Centre minus 6 mm, CANCAN against control in the same image (paired areas):
protocol CANCAN control
file
sub-011CC-x1_obj-1jakd50 -3.1 -77.6
sub-011CC-x3_obj-1us8p8 525.8 -1069.8
sub-011CC-x5_obj-96qypk 14.8 529.9
sub-011CC-x5_obj-ryt1td 1789.4 -695.5
sub-011Control-x1_obj-1kbxt87 -242.4 -239.0
sub-011Control-x1_obj-ejqkbf -17.3 -604.2
sub-011Control-x1_obj-s8wtbt -13.1 -2.9
sub-011Control-x5_obj-ljdh5h 2123.6 250.6
sub-011Control-x5_obj-mzbjav 52.9 103.5
sub-013CC-x1_obj-1ldbf6s -182.4 -2517.5
sub-013CC-x1_obj-1mph1c3 18.1 -473.2
sub-013CC-x2_obj-2xp3mi -708.8 -2953.2
sub-013CC-x5_obj-1dsf0i2 -1092.2 -886.6
sub-013CC-x5_obj-e3sltv -394.7 -3.1
sub-013Control-x1_obj-1pihpj7 -10.8 -174.2
sub-013Control-x1_obj-l3r6gc -92.7 -1347.2
sub-013Control-x5_obj-16wml2b -14.2 865.5
sub-013Control-x5_obj-oezvi9 5.2 -2977.6
CANCAN above control in 11 of 18 images
two-sided binomial test of that count against one half: p = 0.48
Background-subtracted YP is in the camera's arbitrary units, as in the paper. Whole-well images (taken on May 23 at 14:13) have backgrounds of 1061 to 4560 a.u. (mean 2628, 7 YP areas), against 180 to 1121 (mean 332, 45 YP areas) in the two-well images, about 8 times higher on average and 6.5 times at x1 alone (1495 against 229), so their a.u. are not on the same scale as the others; they are kept, as each is one well, and the near- electrode claim is also shown on two-well images only.
The label check is not informative: CANCAN is above control in 11 of 18 paired images, a count a coin would give (binomial p = 0.48). By eye (panels above), the 1 Hz control areas show a ring with a dark centre and the 1 Hz CANCAN areas four lobes at the electrodes, matching their labels, but this was not measured.
The pixel-size check above is conditional on the pixel sizes assumed. For the two-well images the best half side is 6.11 mm, 4% below the paper's 6.40 mm; for the three whole-well images located by the footprints it is 7.00 mm, 9% above, which would mean a pixel of about 1.49 um. That was not followed up; distances in the seven whole-well areas may be off by up to 9%.
Duplicates. The protocol asks for recordings deposited more than once. Every pair of exposure areas in the same channel, whatever their condition and burst label, was compared by the peak of the normalised cross-correlation of their high-passed images (16 x 16 mm around the array centre), under translation, flips and a half turn (a well imaged twice would correlate at the scale of single cells).
from scipy import fft
from scipy.ndimage import gaussian_filter
def hp(A):
B = np.log1p(np.clip(A - np.percentile(A, 5), 0, None))
B = gaussian_filter(B - gaussian_filter(B, 8), 1)
return (B - B.mean()) / B.std()
h = int(8 / cc.PX_MM)
N = 4 * h # zero padding, so that the circular correlation equals the linear one
def crop(r):
A = imgs[(r.file, r.side)]
cy, cx = int(r.cy), int(r.cx)
return A[max(cy - h, 0):cy + h, max(cx - h, 0):cx + h]
def spectra(A):
"""FFTs of the high-passed crop and of its flips and half turn."""
return [fft.rfft2(hp(B), s=(N, N), workers=4) for B in (A, A[::-1], A[:, ::-1], A[::-1, ::-1])], A.size
def xcorr(sa, sb):
"""Peak normalised cross-correlation of area a against area b, its flips and its half turn."""
(Fa, na), (Fb, nb) = sa, sb
return max(float(fft.irfft2(Fa[0] * np.conj(F), s=(N, N), workers=4).max()) for F in Fb) / np.sqrt(na * nb)
t0 = time.time()
res = []
for ch, g in areas.groupby("channel"):
g = list(g.itertuples())
sp = [spectra(crop(r)) for r in g]
for i in range(len(g)):
for j in range(i + 1, len(g)):
res.append((xcorr(sp[i], sp[j]), g[i], g[j]))
res.sort(key=lambda t: -t[0])
same = [t for t in res if (t[1].rate, t[1].protocol, t[1].label) == (t[2].rate, t[2].protocol, t[2].label)]
print(f"pairs compared: {len(res)} (every pair in the same channel, any condition and label), {len(same)} of them in "
f"the same condition; {time.time() - t0:.0f} s")
print("highest correlations:")
for v, a, b in res[:5]:
print(round(v, 3), a.file, a.side, a.rate, a.protocol, a.label, "|", b.file, b.side, b.rate, b.protocol, b.label)
print("highest within the same condition:", round(same[0][0], 3))
# positive control: an area against itself shifted by 3 pixels with independent noise
A = crop(next(areas.itertuples()))
noisy = np.roll(A, 3, axis=1) + np.random.default_rng(0).normal(0, A.std() * 0.5, A.shape)
print("positive control, one area against a shifted, noisy copy of itself:", round(xcorr(spectra(A), spectra(noisy)), 3))
pairs compared: 1654 (every pair in the same channel, any condition and label), 121 of them in the same condition; 68 s highest correlations: 0.039 sub-011C-x5 L 0.2 MHz control x5 | sub-011Control-x5_obj-ljdh5h L 0.2 MHz control x5 0.038 sub-013CC-x2_obj-2xp3mi R 1 Hz control x2 | sub-013Control-x5_obj-oezvi9 L 1 Hz control x5 0.038 sub-011CC-x3_obj-1us8p8 R 0.2 MHz control x3 | sub-013Control-x5_obj-oezvi9 L 1 Hz control x5 0.036 sub-011Control-x5_obj-ljdh5h L 0.2 MHz control x5 | sub-013Control-x5_obj-oezvi9 L 1 Hz control x5 0.035 sub-011CC-x3_obj-1us8p8 R 0.2 MHz control x3 | sub-013CC-x2_obj-2xp3mi R 1 Hz control x2 highest within the same condition: 0.039
positive control, one area against a shifted, noisy copy of itself: 0.703
No well appears twice: over all 1654 pairs in the same channel, 121 of them in the same condition, the highest peak correlation is 0.039, against 0.703 for a shifted, noisy copy of one area.
Methods common to the claims¶
Every profile is the background-subtracted mean in 406 x 325 um ROIs along the diagonals (F1, F2), with the readings of the claims file: ROIs at whole-mm distances (O1 a) or contiguous from the centre (O1 b), and the two diagonals averaged at each signed position (O2 a) or the four half-diagonals at each distance (O2 b). Groups are means +/- s.e. over exposure areas (F3).
Burst number. The paper's rate experiments (set S1) and the propidium experiments (S2) used 2 bursts (x4), and its burst series (Figure 7) went up to 6 bursts (x4). The files' text says the protocol "was either repeated 2, 3, or 5 times" (above). Two readings of the labels:
- repeats after the first (adopted): x1, x2, x3 and x5 are 2, 3, 4 and 6 bursts (x4). The text says the protocol was repeated "upon completion", which counts the repeats after the first application; under this reading x1 is the paper's 2 bursts (x4) at both rates and x5 its largest number, 6 bursts (x4), the point where Figure 7 shows saturation.
- total number of bursts: x1, x2, x3 and x5 are 1, 2, 3 and 5. Then 2 bursts exists only at 1 Hz (x2), and the rate comparison at 2 bursts cannot be made. The tables show 1 Hz x2 against 0.2 MHz x1 and x3 (1 and 3 bursts), the two numbers either side of 2.
The rate claims are decided at x1 against x1, an assumption recorded in the attempt. Pairs: x1|x1
(adopted), x2|x1 and x2|x3 (1 Hz label | 0.2 MHz label, total reading, brackets only), and
x5|x5 (6 bursts under the adopted reading, shown as a variant; it decides nothing). The controls
are those of the same label at both rates (the paper's control rate is not given; per-rate controls
are shown as a variant).
Error-bar gaps (F8). "A gap exceeding the length of the error bars" does not say which length when
the two s.e. arms differ. Every table shows three readings: the gap exceeds the longer arm
(long, adopted: the gap exceeds each error bar, the literal reading), the mean of the two arms
(mean) or the shorter arm (short).
READINGS = [(o1, o2) for o1 in "ab" for o2 in "ab"]
RATES = ["1 Hz", "0.2 MHz"]
# (1 Hz label, 0.2 MHz label) compared for the set S1 claims, see the text above
PAIRS = {"x1|x1": ("x1", "x1"), "x2|x1": ("x2", "x1"), "x2|x3": ("x2", "x3"), "x5|x5": ("x5", "x5")}
PAIR_NOTE = {"x1|x1": "2 v 2 bursts, adopted", "x2|x1": "2 v 1 bursts, total reading", "x2|x3": "2 v 3 bursts, total reading",
"x5|x5": "6 v 6 bursts, variant"}
# areas whose centre is visibly misplaced in the panels above (see the text there)
FLAGGED = {("sub-011CC-x1_obj-1drpw33", "L"), ("sub-011CC-x5_obj-96qypk", "L"), ("sub-011Control-x5_obj-f5ffop", "W"),
("sub-011Control-x5_obj-kvqdb2", "R"), ("sub-013Control-x1_obj-1pihpj7", "R"), ("sub-013Control-x1_obj-6stwj8", "W")}
assert FLAGGED <= set(zip(areas.file, areas.side))
areas["flagged"] = [(f, s) in FLAGGED for f, s in zip(areas.file, areas.side)]
def group_profile(sel, kind="diag", o1="a", o2="a", bgkind="bg"):
ys = [area_profile((r.file, r.side), kind, o1, o2, bgkind)[1] for r in sel.itertuples()]
pos = P if o1 == "a" else PC
ys = np.array(ys)
return pos, np.nanmean(ys, 0), np.nanstd(ys, 0, ddof=1) / np.sqrt(np.isfinite(ys).sum(0)), ys
def sel(rate=None, protocol="CANCAN", label="x1", channel="FITC", drop_flagged=False):
s = areas[(areas.channel == channel) & (areas.label == label) & areas.usable]
if protocol:
s = s[s.protocol == protocol]
if rate:
s = s[s.rate == rate]
if drop_flagged:
s = s[~s.flagged]
return s
def sel_labels(protocol, labmap, channel="FITC"):
"""Areas of one protocol at a burst label per rate, e.g. {"1 Hz": "x2"}."""
return pd.concat([sel(rate, protocol, lab, channel) for rate, lab in labmap.items()])
groups = [("1 Hz", "CANCAN", "x1"), ("0.2 MHz", "CANCAN", "x1"), ("1 Hz", "CANCAN", "x2"), ("0.2 MHz", "CANCAN", "x3"),
("1 Hz", "CANCAN", "x5"), ("0.2 MHz", "CANCAN", "x5"), ("1 Hz", "control", "x1"), ("0.2 MHz", "control", "x1"),
("1 Hz", "control", "x2"), ("0.2 MHz", "control", "x3"), ("1 Hz", "control", "x5"), ("0.2 MHz", "control", "x5")]
print("Mean diagonal YP profiles (a.u., background subtracted, O1 a, O2 a) at -7..7 mm, and their s.e.:")
for rate, prot, lab in groups:
pos, m, se, ys = group_profile(sel(rate, prot, lab))
print(f"{lab} {rate:8s} {prot:7s} n={len(ys)} mean:", np.round(m).astype(int).tolist())
print(f"{' ' * 29}s.e.:", np.round(se).astype(int).tolist())
print("\n0.2 MHz CANCAN x1 areas: raw (not background subtracted) diagonal profile, mean of the two diagonals (a.u.) at -7..7 mm,"
" the background (mean of four footprints) and the darkest footprint")
for r in sel("0.2 MHz", "CANCAN", "x1").itertuples():
raw = np.nanmean(prof[(r.file, r.side)]["diag_whole"], 0)
print(f" {r.file} {r.side} ({r.located_by}): raw {np.round(raw).astype(int).tolist()}; background {r.bg:.0f}, darkest footprint {r.bg_min:.0f}")
panels = [("x1, 2 bursts under the adopted reading", [("1 Hz", "x1"), ("0.2 MHz", "x1")], {"1 Hz": "x1", "0.2 MHz": "x1"}),
("total reading: 1 Hz x2 (2 bursts), 0.2 MHz x1 (1) and x3 (3)", [("1 Hz", "x2"), ("0.2 MHz", "x1"), ("0.2 MHz", "x3")], {"1 Hz": "x2"}),
("x5, 6 bursts under the adopted reading (variant)", [("1 Hz", "x5"), ("0.2 MHz", "x5")], {"1 Hz": "x5", "0.2 MHz": "x5"})]
fig, axs = plt.subplots(1, 3, figsize=(20, 4.5))
for ax, (title, sets, ctl) in zip(axs, panels):
for (rate, lab), col in zip(sets, ["C0", "C3", "C1"]):
pos, m, se, ys = group_profile(sel(rate, "CANCAN", lab))
ax.errorbar(pos, m, se, color=col, marker="o", label=f"CANCAN {rate} {lab} (n={len(ys)})")
pos, m, se, ys = group_profile(sel_labels("control", ctl))
ax.errorbar(pos, m, se, color="k", marker="s", ls="--", label=f"control {', '.join(f'{k} {v}' for k, v in ctl.items())} (n={len(ys)})")
ax.set_title(title, fontsize=10)
ax.set_xlabel("distance from centre (mm)")
ax.set_ylabel("YP, background subtracted (a.u.)")
ax.legend(fontsize=8)
plt.tight_layout()
plt.show()
print("every plotted point lies within the axes")
Mean diagonal YP profiles (a.u., background subtracted, O1 a, O2 a) at -7..7 mm, and their s.e.:
x1 1 Hz CANCAN n=6 mean: [87, 184, 104, -6, -46, 31, 23, 1, 85, -26, 1, 30, 126, 121, 261]
s.e.: [54, 90, 89, 52, 43, 28, 32, 22, 93, 22, 44, 35, 55, 66, 211]
x1 0.2 MHz CANCAN n=4 mean: [7, 13, 9, 11, 12, 14, 22, -1, -1, 4, -1, 32, 1, 6, 3]
s.e.: [5, 6, 7, 10, 8, 9, 8, 6, 6, 4, 4, 28, 2, 4, 3]
x2 1 Hz CANCAN n=2 mean: [538, 576, 724, 424, 251, 202, 536, 516, 621, 258, 207, 448, 769, 887, 464]
s.e.: [37, 406, 481, 90, 25, 17, 193, 167, 191, 40, 26, 200, 442, 506, 249]
x3 0.2 MHz CANCAN n=2 mean: [105, 102, 181, 141, 58, 83, 208, 353, 331, 222, 142, 134, 88, 75, 144]
s.e.: [56, 36, 55, 45, 7, 33, 177, 254, 173, 8, 18, 34, 44, 6, 71]
x5 1 Hz CANCAN n=5 mean: [513, 645, 1238, 1704, 1524, 1671, 1583, 1576, 1729, 1211, 1590, 1254, 1736, 1701, 1048]
s.e.: [432, 716, 453, 488, 363, 583, 415, 465, 401, 521, 446, 328, 359, 459, 442]
x5 0.2 MHz CANCAN n=6 mean: [622, 1317, 1627, 1335, 1302, 2228, 2820, 2275, 2847, 2468, 1840, 1954, 1927, 1751, 1212]
s.e.: [329, 495, 574, 425, 410, 842, 944, 922, 969, 841, 613, 662, 647, 713, 712]
x1 1 Hz control n=6 mean: [275, 850, 758, 796, 634, 650, 504, 402, 459, 309, 443, 765, 954, 942, 665]
s.e.: [159, 324, 238, 244, 307, 506, 426, 345, 385, 195, 141, 220, 344, 444, 292]
x1 0.2 MHz control n=6 mean: [272, 442, 791, 383, 284, 177, 51, 89, 39, 156, 296, 486, 637, 795, 572]
s.e.: [117, 191, 361, 192, 134, 68, 97, 39, 78, 47, 119, 201, 269, 358, 292]
x2 1 Hz control n=2 mean: [2340, 2688, 2792, 2521, 2111, 1475, 1239, 956, 1266, 1454, 2037, 2429, 2950, 3130, 2791]
s.e.: [512, 1099, 1188, 39, 428, 3, 199, 7, 348, 169, 328, 629, 724, 772, 719]
x3 0.2 MHz control n=2 mean: [1520, 1404, 1412, 1499, 1197, 1452, 1334, 1026, 1314, 1385, 1633, 1740, 1885, 1911, 1726]
s.e.: [401, 316, 346, 221, 467, 33, 150, 16, 232, 265, 292, 412, 67, 201, 610]
x5 1 Hz control n=5 mean: [1593, 1645, 1434, 1342, 1216, 1375, 1196, 1099, 1507, 1478, 1311, 1386, 1034, 1793, 1312]
s.e.: [517, 537, 521, 441, 407, 510, 569, 400, 584, 611, 593, 860, 697, 904, 625]
x5 0.2 MHz control n=6 mean: [1430, 1325, 1525, 1791, 1920, 2119, 1956, 2132, 2508, 1943, 1933, 2382, 2103, 1769, 1689]
s.e.: [811, 683, 593, 630, 587, 578, 464, 489, 788, 527, 690, 627, 611, 605, 704]
0.2 MHz CANCAN x1 areas: raw (not background subtracted) diagonal profile, mean of the two diagonals (a.u.) at -7..7 mm, the background (mean of four footprints) and the darkest footprint
sub-011CC-x1_obj-1jakd50 L (footprints): raw [208, 224, 223, 232, 226, 231, 202, 196, 192, 199, 202, 203, 195, 199, 190]; background 193, darkest footprint 184
sub-011Control-x1_obj-ejqkbf R (centroid): raw [213, 223, 214, 217, 214, 215, 263, 196, 199, 217, 209, 331, 216, 214, 216]; background 216, darkest footprint 184
sub-011Control-x1_obj-s8wtbt R (footprints): raw [197, 188, 188, 189, 194, 193, 197, 181, 187, 194, 184, 186, 186, 195, 187]; background 180, darkest footprint 175
sub-013Control-x5_obj-oezvi9 R (centroid): raw [207, 213, 209, 203, 211, 214, 224, 218, 214, 205, 200, 205, 204, 213, 218]; background 209, darkest footprint 201
every plotted point lies within the axes
fig3a-centre-enhanced¶
Every CANCAN protocol enhanced electroporation at the array centre, while in the control (single unipolar pulse per packet) YP uptake decreased gradually toward the centre.
Criterion: for each CANCAN rate the mean diagonal profile has its maximum at x = 0; for the control, the mean at 0 is below the means at 6 and 7 mm by a gap larger than the error bars (F8). Only 1 Hz and 0.2 MHz are in the dataset. Shown for 2 bursts under both label readings (x1, and 1 Hz x2) and for x5.
# condition sets: the burst label at each rate that is 2 bursts (x4) under each reading, and the x5 variant
SETS = {"x1 (adopted)": {"1 Hz": "x1", "0.2 MHz": "x1"}, "x2 (total reading)": {"1 Hz": "x2"},
"x5 (variant)": {"1 Hz": "x5", "0.2 MHz": "x5"}}
rows = []
for sname, labmap in SETS.items():
for o1, o2 in READINGS:
for rate, lab in labmap.items():
pos, m, se, ys = group_profile(sel(rate, "CANCAN", lab), o1=o1, o2=o2)
rows.append({"set": sname, "O1": o1, "O2": o2, "group": f"CANCAN {rate} {lab}", "n": len(ys),
"argmax_mm": round(float(pos[np.nanargmax(m)]), 2), "y0": m[np.argmin(np.abs(pos))],
"max": np.nanmax(m), "holds": bool(abs(pos[np.nanargmax(m)]) < 1e-9)})
pos, m, se, ys = group_profile(sel_labels("control", labmap), o1=o1, o2=o2)
i0, i6, i7 = (np.argmin(np.abs(pos - x)) for x in (0, 6, 7))
r = {"set": sname, "O1": o1, "O2": o2, "group": "control", "n": len(ys), "y0": m[i0], "y6": m[i6], "y7": m[i7],
"se0": se[i0], "se6": se[i6], "se7": se[i7]}
for arm in cc.ARMS:
g6 = cc.gap_significant(m[i6], se[i6], m[i0], se[i0], arm)
g7 = cc.gap_significant(m[i7], se[i7], m[i0], se[i0], arm)
r[f"holds_{arm}"] = bool(m[i0] < m[i6] and m[i0] < m[i7] and g6[0] and g7[0])
r["holds"] = r["holds_long"]
rows.append(r)
t3a = pd.DataFrame(rows)
print(t3a.round(1).to_string(index=False))
print("\nCANCAN maximum at x = 0 under every reading, by set and group:")
print(t3a[t3a.group != "control"].groupby(["set", "group"]).holds.all().to_string())
print("\ncontrol centre below 6 and 7 mm with a gap (F8) under every O1/O2 reading, by set and F8 reading:")
print(t3a[t3a.group == "control"].groupby("set")[[f"holds_{a}" for a in cc.ARMS]].all().to_string())
for lab in ("x1", "x5"):
for rate in RATES:
pos, m, se, ys = group_profile(sel(rate, "control", lab))
print(f"variant, control of {rate} only, {lab}: n={len(ys)}, y0={m[7]:.0f}+/-{se[7]:.0f}, y6={m[13]:.0f}+/-{se[13]:.0f}, y7={m[14]:.0f}+/-{se[14]:.0f}")
set O1 O2 group n argmax_mm y0 max holds y6 y7 se0 se6 se7 holds_long holds_mean holds_short
x1 (adopted) a a CANCAN 1 Hz x1 6 7.0 1.0 260.9 False NaN NaN NaN NaN NaN NaN NaN NaN
x1 (adopted) a a CANCAN 0.2 MHz x1 4 4.0 -1.4 32.0 False NaN NaN NaN NaN NaN NaN NaN NaN
x1 (adopted) a a control 12 NaN 245.2 NaN False 868.5 618.1 172.1 272.6 197.1 False False False
x1 (adopted) a b CANCAN 1 Hz x1 6 -7.0 1.0 174.0 False NaN NaN NaN NaN NaN NaN NaN NaN
x1 (adopted) a b CANCAN 0.2 MHz x1 4 -4.0 -1.4 21.4 False NaN NaN NaN NaN NaN NaN NaN NaN
x1 (adopted) a b control 12 NaN 245.2 NaN False 757.4 445.8 172.1 222.2 141.3 False False False
x1 (adopted) b a CANCAN 1 Hz x1 6 6.5 1.0 222.0 False NaN NaN NaN NaN NaN NaN NaN NaN
x1 (adopted) b a CANCAN 0.2 MHz x1 4 4.1 -1.4 37.2 False NaN NaN NaN NaN NaN NaN NaN NaN
x1 (adopted) b a control 12 NaN 245.2 NaN False 807.1 692.5 172.1 256.7 215.6 False False False
x1 (adopted) b b CANCAN 1 Hz x1 6 -6.5 1.0 213.2 False NaN NaN NaN NaN NaN NaN NaN NaN
x1 (adopted) b b CANCAN 0.2 MHz x1 4 -4.1 -1.4 24.4 False NaN NaN NaN NaN NaN NaN NaN NaN
x1 (adopted) b b control 12 NaN 245.2 NaN False 688.6 510.8 172.1 213.3 150.9 False False False
x2 (total reading) a a CANCAN 1 Hz x2 2 6.0 516.4 886.9 False NaN NaN NaN NaN NaN NaN NaN NaN
x2 (total reading) a a control 2 NaN 956.1 NaN True 3130.0 2790.8 7.1 772.2 718.7 True True True
x2 (total reading) a b CANCAN 1 Hz x2 2 -5.0 516.4 746.5 False NaN NaN NaN NaN NaN NaN NaN NaN
x2 (total reading) a b control 2 NaN 956.1 NaN True 2909.0 2565.4 7.1 935.5 615.3 True True True
x2 (total reading) b a CANCAN 1 Hz x2 2 5.3 516.4 997.7 False NaN NaN NaN NaN NaN NaN NaN NaN
x2 (total reading) b a control 2 NaN 956.1 NaN False 3144.3 2919.4 7.1 819.8 1024.0 False True True
x2 (total reading) b b CANCAN 1 Hz x2 2 -5.3 516.4 902.1 False NaN NaN NaN NaN NaN NaN NaN NaN
x2 (total reading) b b control 2 NaN 956.1 NaN True 2971.8 2676.4 7.1 950.8 847.7 True True True
x5 (variant) a a CANCAN 1 Hz x5 5 5.0 1576.2 1735.7 False NaN NaN NaN NaN NaN NaN NaN NaN
x5 (variant) a a CANCAN 0.2 MHz x5 6 1.0 2275.3 2847.4 False NaN NaN NaN NaN NaN NaN NaN NaN
x5 (variant) a a control 11 NaN 1662.6 NaN False 1780.3 1517.3 347.6 498.5 458.2 False False False
x5 (variant) a b CANCAN 1 Hz x5 5 -1.0 1576.2 1656.1 False NaN NaN NaN NaN NaN NaN NaN NaN
x5 (variant) a b CANCAN 0.2 MHz x5 6 -1.0 2275.3 2833.8 False NaN NaN NaN NaN NaN NaN NaN NaN
x5 (variant) a b control 11 NaN 1662.6 NaN False 1625.4 1510.7 347.6 439.5 452.6 False False False
x5 (variant) b a CANCAN 1 Hz x5 5 -2.4 1576.2 1866.1 False NaN NaN NaN NaN NaN NaN NaN NaN
x5 (variant) b a CANCAN 0.2 MHz x5 6 -0.4 2275.3 3068.5 False NaN NaN NaN NaN NaN NaN NaN NaN
x5 (variant) b a control 11 NaN 1662.6 NaN False 1801.7 1494.3 347.6 540.8 484.3 False False False
x5 (variant) b b CANCAN 1 Hz x5 5 -3.6 1576.2 1789.2 False NaN NaN NaN NaN NaN NaN NaN NaN
x5 (variant) b b CANCAN 0.2 MHz x5 6 -0.4 2275.3 3053.7 False NaN NaN NaN NaN NaN NaN NaN NaN
x5 (variant) b b control 11 NaN 1662.6 NaN False 1698.5 1458.5 347.6 459.3 470.9 False False False
CANCAN maximum at x = 0 under every reading, by set and group:
set group
x1 (adopted) CANCAN 0.2 MHz x1 False
CANCAN 1 Hz x1 False
x2 (total reading) CANCAN 1 Hz x2 False
x5 (variant) CANCAN 0.2 MHz x5 False
CANCAN 1 Hz x5 False
control centre below 6 and 7 mm with a gap (F8) under every O1/O2 reading, by set and F8 reading:
holds_long holds_mean holds_short
set
x1 (adopted) False False False
x2 (total reading) False True True
x5 (variant) False False False
variant, control of 1 Hz only, x1: n=6, y0=402+/-345, y6=942+/-444, y7=665+/-292
variant, control of 0.2 MHz only, x1: n=6, y0=89+/-39, y6=795+/-358, y7=572+/-292
variant, control of 1 Hz only, x5: n=5, y0=1099+/-400, y6=1793+/-904, y7=1312+/-625
variant, control of 0.2 MHz only, x5: n=6, y0=2132+/-489, y6=1769+/-605, y7=1689+/-704
Result: not reproduced. At x1 (2 bursts, adopted) no CANCAN profile peaks at the centre under any reading: the 1 Hz profile peaks at 6.5 to 7 mm from it, and the 0.2 MHz wells have essentially no stain (centre -1.4 a.u., maximum 21 to 37 a.u., peak at 4 to 4.1 mm). Under the total reading, the 1 Hz x2 CANCAN profile (2 bursts) also peaks off centre, at 5 to 6 mm, so the CANCAN part fails under both readings of the labels. The pooled x1 control is lower at the centre (245 a.u.) than at 6 mm (868) and 7 mm (618) under O1 a, O2 a, but without a gap beyond the error bars under any F8 reading. Without the flagged 1 Hz control area 013Control-x1_obj-6stwj8 W (panel 63), whose centre sits on the stain, the control part holds under every reading (centre 74 +/- 23 against 990 +/- 268 and 668 +/- 209 a.u., n = 11, gap beyond the longer arm), but the CANCAN part still fails (the five other 1 Hz areas peak at 6.5 to 7 mm), so the outcome is the same. In the x5 variant no CANCAN profile peaks at 0 (0.2 MHz peaks at 0.4 to 1 mm from it).
fig3bc-rate-suppresses-off-target¶
Raising the packet rate from 1 Hz to 0.2 MHz suppressed electroporation better both near the electrodes (diagonal profile) and at the perimeter between electrodes (profile parallel to the sides).
Criterion: mean YP at 0.2 MHz below 1 Hz by a gap larger than the error bars at 6 mm, at 7 mm (diagonal) and at the perimeter (parallel profile: (a) outermost ROIs, (b) mean of ROIs at 4 mm or more); two-sided t tests reported. The parallel profile is measured on every area (the paper measured it in one set).
def per_area(selection, kind, o1, o2, xs=None, beyond=None):
out = []
for r in selection.itertuples():
pos, y = area_profile((r.file, r.side), kind, o1, o2)
if beyond is not None:
out.append(np.nanmean(y[np.abs(pos) >= beyond]))
else:
out.append(np.nanmean([at(pos, y, x) for x in xs]))
return np.array(out)
def compare(hi, lo):
"""1 Hz values against 0.2 MHz values: means, s.e., the F8 gap under each reading, t test."""
m1, s1 = hi.mean(), hi.std(ddof=1) / np.sqrt(len(hi))
m2, s2 = lo.mean(), lo.std(ddof=1) / np.sqrt(len(lo))
r = {"1 Hz": m1, "se_1Hz": s1, "0.2 MHz": m2, "se_02": s2, "gap": cc.gap_significant(m1, s1, m2, s2)[1]}
for arm in cc.ARMS:
r[f"holds_{arm}"] = bool(m2 < m1 and cc.gap_significant(m1, s1, m2, s2, arm)[0])
r["t_p"] = stats.ttest_ind(hi, lo).pvalue
r["n"] = f"{len(hi)}/{len(lo)}"
return r
rows = []
for pname, (l1, l2) in PAIRS.items():
for o1, o2 in READINGS:
outer = P.max() if o1 == "a" else PC.max()
tests = {"6 mm": ("diag", dict(xs=[6, -6] if o2 == "a" else [6])), "7 mm": ("diag", dict(xs=[7, -7] if o2 == "a" else [7])),
"perimeter (a) outermost": ("par", dict(xs=[outer, -outer])), "perimeter (b) >= 4 mm": ("par", dict(beyond=4))}
for name, (kind, kw) in tests.items():
hi = per_area(sel("1 Hz", "CANCAN", l1), kind, o1, o2, **kw)
lo = per_area(sel("0.2 MHz", "CANCAN", l2), kind, o1, o2, **kw)
rows.append({"pair": pname, "O1": o1, "O2": o2, "where": name, **compare(hi, lo)})
t3bc = pd.DataFrame(rows)
print("pairs (1 Hz label | 0.2 MHz label):", PAIR_NOTE)
print(t3bc.round(3).to_string(index=False))
H = [f"holds_{a}" for a in cc.ARMS]
print("\nholds at 6 mm, 7 mm and the perimeter under every O1/O2 reading, by pair and F8 reading:")
print(t3bc.groupby("pair")[H].all().to_string())
print("\nholds under every O1/O2 reading, by pair, place and F8 reading:")
print(t3bc.groupby(["pair", "where"])[H].all().to_string())
pairs (1 Hz label | 0.2 MHz label): {'x1|x1': '2 v 2 bursts, adopted', 'x2|x1': '2 v 1 bursts, total reading', 'x2|x3': '2 v 3 bursts, total reading', 'x5|x5': '6 v 6 bursts, variant'}
pair O1 O2 where 1 Hz se_1Hz 0.2 MHz se_02 gap holds_long holds_mean holds_short t_p n
x1|x1 a a 6 mm 152.545 76.180 9.110 3.820 63.435 False True True 0.171 6/4
x1|x1 a a 7 mm 173.955 130.916 4.932 2.860 35.247 False False True 0.332 6/4
x1|x1 a a perimeter (a) outermost 18.781 9.331 0.629 1.553 7.268 False True True 0.160 6/4
x1|x1 a a perimeter (b) >= 4 mm -10.064 20.634 6.808 2.957 -6.719 False False False 0.533 6/4
x1|x1 a b 6 mm 152.545 76.180 9.110 3.820 63.435 False True True 0.171 6/4
x1|x1 a b 7 mm 173.955 130.916 4.932 2.860 35.247 False False True 0.332 6/4
x1|x1 a b perimeter (a) outermost 18.781 9.331 0.629 1.553 7.268 False True True 0.160 6/4
x1|x1 a b perimeter (b) >= 4 mm -10.064 20.634 6.808 2.957 -6.719 False False False 0.533 6/4
x1|x1 b a 6 mm 109.098 51.377 8.716 3.158 45.847 False True True 0.157 6/4
x1|x1 b a 7 mm 171.144 130.817 4.945 3.004 32.377 False False True 0.339 6/4
x1|x1 b a perimeter (a) outermost 9.284 17.519 4.915 1.362 -14.512 False False False 0.847 6/4
x1|x1 b a perimeter (b) >= 4 mm -6.117 18.234 4.984 1.627 -8.760 False False False 0.640 6/4
x1|x1 b b 6 mm 109.098 51.377 8.716 3.158 45.847 False True True 0.157 6/4
x1|x1 b b 7 mm 171.144 130.817 4.945 3.004 32.377 False False True 0.339 6/4
x1|x1 b b perimeter (a) outermost 9.284 17.519 4.915 1.362 -14.512 False False False 0.847 6/4
x1|x1 b b perimeter (b) >= 4 mm -6.117 18.234 4.984 1.627 -8.760 False False False 0.640 6/4
x2|x1 a a 6 mm 731.235 455.858 9.110 3.820 262.446 False True True 0.061 2/4
x2|x1 a a 7 mm 501.232 143.136 4.932 2.860 350.304 True True True 0.005 2/4
x2|x1 a a perimeter (a) outermost 398.715 137.798 0.629 1.553 258.734 True True True 0.009 2/4
x2|x1 a a perimeter (b) >= 4 mm 398.527 127.758 6.808 2.957 261.004 True True True 0.007 2/4
x2|x1 a b 6 mm 731.235 455.858 9.110 3.820 262.446 False True True 0.061 2/4
x2|x1 a b 7 mm 501.232 143.136 4.932 2.860 350.304 True True True 0.005 2/4
x2|x1 a b perimeter (a) outermost 398.715 137.798 0.629 1.553 258.734 True True True 0.009 2/4
x2|x1 a b perimeter (b) >= 4 mm 398.527 127.758 6.808 2.957 261.004 True True True 0.007 2/4
x2|x1 b a 6 mm 727.922 426.399 8.716 3.158 289.648 False True True 0.051 2/4
x2|x1 b a 7 mm 536.097 159.838 4.945 3.004 368.310 True True True 0.006 2/4
x2|x1 b a perimeter (a) outermost 371.577 88.355 4.915 1.362 276.945 True True True 0.002 2/4
x2|x1 b a perimeter (b) >= 4 mm 409.422 129.272 4.984 1.627 273.538 True True True 0.007 2/4
x2|x1 b b 6 mm 727.922 426.399 8.716 3.158 289.648 False True True 0.051 2/4
x2|x1 b b 7 mm 536.097 159.838 4.945 3.004 368.310 True True True 0.006 2/4
x2|x1 b b perimeter (a) outermost 371.577 88.355 4.915 1.362 276.945 True True True 0.002 2/4
x2|x1 b b perimeter (b) >= 4 mm 409.422 129.272 4.984 1.627 273.538 True True True 0.007 2/4
x2|x3 a a 6 mm 731.235 455.858 88.284 20.872 166.220 False False True 0.294 2/2
x2|x3 a a 7 mm 501.232 143.136 124.764 63.477 169.855 True True True 0.138 2/2
x2|x3 a a perimeter (a) outermost 398.715 137.798 43.363 21.126 196.428 True True True 0.126 2/2
x2|x3 a a perimeter (b) >= 4 mm 398.527 127.758 69.296 6.441 195.031 True True True 0.124 2/2
x2|x3 a b 6 mm 731.235 455.858 88.284 20.872 166.220 False False True 0.294 2/2
x2|x3 a b 7 mm 501.232 143.136 124.764 63.477 169.855 True True True 0.138 2/2
x2|x3 a b perimeter (a) outermost 398.715 137.798 43.363 21.126 196.428 True True True 0.126 2/2
x2|x3 a b perimeter (b) >= 4 mm 398.527 127.758 69.296 6.441 195.031 True True True 0.124 2/2
x2|x3 b a 6 mm 727.922 426.399 93.765 25.075 182.682 False False True 0.276 2/2
x2|x3 b a 7 mm 536.097 159.838 138.092 77.279 160.888 True True True 0.154 2/2
x2|x3 b a perimeter (a) outermost 371.577 88.355 40.653 11.536 231.033 True True True 0.065 2/2
x2|x3 b a perimeter (b) >= 4 mm 409.422 129.272 63.861 8.348 207.940 True True True 0.116 2/2
x2|x3 b b 6 mm 727.922 426.399 93.765 25.075 182.682 False False True 0.276 2/2
x2|x3 b b 7 mm 536.097 159.838 138.092 77.279 160.888 True True True 0.154 2/2
x2|x3 b b perimeter (a) outermost 371.577 88.355 40.653 11.536 231.033 True True True 0.065 2/2
x2|x3 b b perimeter (b) >= 4 mm 409.422 129.272 63.861 8.348 207.940 True True True 0.116 2/2
x5|x5 a a 6 mm 1173.111 529.386 1533.984 602.428 -770.942 False False False 0.670 5/6
x5|x5 a a 7 mm 780.419 421.736 917.323 468.543 -753.375 False False False 0.836 5/6
x5|x5 a a perimeter (a) outermost 1252.616 362.413 725.593 240.525 -75.915 False False False 0.243 5/6
x5|x5 a a perimeter (b) >= 4 mm 1498.726 371.002 1077.355 356.562 -306.193 False False False 0.436 5/6
x5|x5 a b 6 mm 1173.111 529.386 1533.984 602.428 -770.942 False False False 0.670 5/6
x5|x5 a b 7 mm 780.419 421.736 917.323 468.543 -753.375 False False False 0.836 5/6
x5|x5 a b perimeter (a) outermost 1252.616 362.413 725.593 240.525 -75.915 False False False 0.243 5/6
x5|x5 a b perimeter (b) >= 4 mm 1498.726 371.002 1077.355 356.562 -306.193 False False False 0.436 5/6
x5|x5 b a 6 mm 1110.648 495.637 1580.451 659.237 -685.071 False False False 0.596 5/6
x5|x5 b a 7 mm 822.537 471.989 971.352 516.542 -839.715 False False False 0.839 5/6
x5|x5 b a perimeter (a) outermost 1139.684 404.388 814.742 289.458 -368.905 False False False 0.520 5/6
x5|x5 b a perimeter (b) >= 4 mm 1414.524 379.173 1099.630 373.173 -437.452 False False False 0.571 5/6
x5|x5 b b 6 mm 1110.648 495.637 1580.451 659.237 -685.071 False False False 0.596 5/6
x5|x5 b b 7 mm 822.537 471.989 971.352 516.542 -839.715 False False False 0.839 5/6
x5|x5 b b perimeter (a) outermost 1139.684 404.388 814.742 289.458 -368.905 False False False 0.520 5/6
x5|x5 b b perimeter (b) >= 4 mm 1414.524 379.173 1099.630 373.173 -437.452 False False False 0.571 5/6
holds at 6 mm, 7 mm and the perimeter under every O1/O2 reading, by pair and F8 reading:
holds_long holds_mean holds_short
pair
x1|x1 False False False
x2|x1 False True True
x2|x3 False False True
x5|x5 False False False
holds under every O1/O2 reading, by pair, place and F8 reading:
holds_long holds_mean holds_short
pair where
x1|x1 6 mm False True True
7 mm False False True
perimeter (a) outermost False False False
perimeter (b) >= 4 mm False False False
x2|x1 6 mm False True True
7 mm True True True
perimeter (a) outermost True True True
perimeter (b) >= 4 mm True True True
x2|x3 6 mm False False True
7 mm True True True
perimeter (a) outermost True True True
perimeter (b) >= 4 mm True True True
x5|x5 6 mm False False False
7 mm False False False
perimeter (a) outermost False False False
perimeter (b) >= 4 mm False False False
Result: not reproduced. At x1 (2 bursts at both rates, adopted) YP is lower at 0.2 MHz than at 1 Hz at 6 mm (9.1 against 152.5 a.u.) and 7 mm (4.9 against 174.0), with a gap beyond the longer error bar at neither (t test p = 0.16 to 0.17 and 0.33 to 0.34); the mean of the parallel ROIs at 4 mm or more is higher at 0.2 MHz (6.8 against -10.1 a.u., O1 a), so the claim fails at x1 under every F8 reading. Under the total reading there is no 0.2 MHz 2-burst group; 1 Hz x2 (2 bursts) is above 0.2 MHz x3 (3 bursts) at 7 mm and at the perimeter with a gap beyond the longer error bar, and at 6 mm only under the shorter-arm reading (731 against 88 a.u., p = 0.28 to 0.29, n = 2 and 2). In the x5 variant 0.2 MHz is higher than 1 Hz at 6 and 7 mm. Without the flagged 1 Hz area (panel 62), the x1 gap exceeds the longer arm at 6 mm (183.6 against 9.1 a.u., p = 0.11) and not at 7 mm (p = 0.28), and perimeter (b) is still higher at 0.2 MHz, so the claim fails under every F8 reading.
fig3-lorentzian-profile¶
The decline of YP emission from the centre was bell-shaped and fitted a Lorentzian function over -4 to 4 mm nearly perfectly (R > 0.9, p < 0.001) for every packet rate.
Criterion: the Lorentzian (F4) fitted to the mean diagonal profile, and to the parallel profile,
over -4 to 4 mm for each rate; R is the Pearson correlation of fitted and measured values and p its
test. Reproduced when R > 0.9 with p < 0.001 for every rate and profile under every reading. A fit
whose width runs to its 100-mm bound has no peak to fit and is marked at_bound.
def at_bound(f):
"""A fit whose width ran to its bound (no peak to fit), or that failed."""
return f is None or f["w"] > 99 or f["w"] < 0.01
CANCAN_GROUPS = [("1 Hz", "x1"), ("0.2 MHz", "x1"), ("1 Hz", "x2"), ("0.2 MHz", "x3"), ("1 Hz", "x5"), ("0.2 MHz", "x5")]
fits = {}
rows = []
for rate, lab in CANCAN_GROUPS:
for kind in ("diag", "par"):
for o1, o2 in READINGS:
pos, m, se, ys = group_profile(sel(rate, "CANCAN", lab), kind, o1, o2)
f = cc.fit(cc.lorentz, pos, m, 4)
fits[(lab, rate, kind, o1, o2)] = (pos, m, f)
rows.append({"rate": rate, "label": lab, "profile": kind, "O1": o1, "O2": o2, "n": len(ys), "a": f["a"] if f else np.nan,
"FWHM_mm": f["w"] if f else np.nan, "at_bound": at_bound(f), "R": f["R"] if f else np.nan,
"p": f["p"] if f else np.nan, "points": f["n"] if f else 0, "holds": bool(f and f["R"] > 0.9 and f["p"] < 0.001)})
tl = pd.DataFrame(rows)
print(tl.round(4).to_string(index=False))
print("\nholds for both profiles under every reading, by rate and label:")
print(tl.groupby(["rate", "label"]).holds.all().to_string())
print("R range by rate and label:", tl.groupby(["rate", "label"]).R.agg(["min", "max"]).round(2).to_dict("index"))
print("fits that failed:", int(tl.a.isna().sum()), "; fits with the width at its bound:", int(tl.at_bound.sum()), "of", len(tl))
rate label profile O1 O2 n a FWHM_mm at_bound R p points holds
1 Hz x1 diag a a 6 29.5001 3.2670 False 0.3611 0.3396 9 False
1 Hz x1 diag a b 6 29.5098 3.2672 False 0.4919 0.1786 9 False
1 Hz x1 diag b a 6 35.6210 2.3905 False 0.5856 0.0084 19 False
1 Hz x1 diag b b 6 35.6218 2.3906 False 0.7241 0.0005 19 False
1 Hz x1 par a a 6 20.1599 1.6159 False 0.6182 0.0760 9 False
1 Hz x1 par a b 6 20.1206 1.6166 False 0.6761 0.0456 9 False
1 Hz x1 par b a 6 27.6879 1.3031 False 0.5943 0.0073 19 False
1 Hz x1 par b b 6 27.6884 1.3034 False 0.6796 0.0014 19 False
0.2 MHz x1 diag a a 4 10.1526 100.0000 True -0.4706 0.2011 9 False
0.2 MHz x1 diag a b 4 10.1526 100.0000 True -0.7382 0.0232 9 False
0.2 MHz x1 diag b a 4 7.6436 100.0000 True -0.0174 0.9437 19 False
0.2 MHz x1 diag b b 4 7.6436 100.0000 True -0.0377 0.8783 19 False
0.2 MHz x1 par a a 4 18.9058 100.0000 True -0.3959 0.2916 9 False
0.2 MHz x1 par a b 4 18.9058 100.0000 True -0.5313 0.1410 9 False
0.2 MHz x1 par b a 4 39.6416 100.0000 True -0.2332 0.3366 19 False
0.2 MHz x1 par b b 4 39.6416 100.0000 True -0.3078 0.1998 19 False
1 Hz x2 diag a a 2 488.7551 8.7478 False 0.3979 0.2889 9 False
1 Hz x2 diag a b 2 488.7547 8.7478 False 0.4044 0.2803 9 False
1 Hz x2 diag b a 2 611.1566 4.4383 False 0.8278 0.0000 19 False
1 Hz x2 diag b b 2 611.1565 4.4383 False 0.8523 0.0000 19 False
1 Hz x2 par a a 2 544.8924 5.3081 False 0.8386 0.0047 9 False
1 Hz x2 par a b 2 544.8920 5.3081 False 0.8986 0.0010 9 False
1 Hz x2 par b a 2 640.3301 3.8482 False 0.9269 0.0000 19 True
1 Hz x2 par b b 2 640.3301 3.8482 False 0.9448 0.0000 19 True
0.2 MHz x3 diag a a 2 332.4359 4.3475 False 0.8058 0.0087 9 False
0.2 MHz x3 diag a b 2 332.4359 4.3475 False 0.9286 0.0003 9 True
0.2 MHz x3 diag b a 2 346.2615 4.1326 False 0.7873 0.0001 19 False
0.2 MHz x3 diag b b 2 346.2613 4.1326 False 0.9690 0.0000 19 True
0.2 MHz x3 par a a 2 330.7986 3.8499 False 0.8103 0.0081 9 False
0.2 MHz x3 par a b 2 330.7985 3.8499 False 0.9982 0.0000 9 True
0.2 MHz x3 par b a 2 376.9244 3.2607 False 0.7898 0.0001 19 False
0.2 MHz x3 par b b 2 376.9243 3.2607 False 0.9742 0.0000 19 True
1 Hz x5 diag a a 5 1585.1086 29.1805 False 0.2288 0.5537 9 False
1 Hz x5 diag a b 5 1585.1086 29.1805 False 0.5082 0.1625 9 False
1 Hz x5 diag b a 5 1646.3241 100.0000 True -0.2283 0.3471 19 False
1 Hz x5 diag b b 5 1646.3241 100.0000 True -0.4103 0.0810 19 False
1 Hz x5 par a a 5 1687.6585 100.0000 True -0.2922 0.4455 9 False
1 Hz x5 par a b 5 1687.6585 100.0000 True -0.8893 0.0013 9 False
1 Hz x5 par b a 5 1684.6253 33.7459 False 0.1047 0.6697 19 False
1 Hz x5 par b b 5 1684.6237 33.7469 False 0.1942 0.4256 19 False
0.2 MHz x5 diag a a 6 2701.8052 8.9194 False 0.8025 0.0092 9 False
0.2 MHz x5 diag a b 6 2701.8052 8.9194 False 0.8666 0.0025 9 False
0.2 MHz x5 diag b a 6 3007.4224 7.0282 False 0.8769 0.0000 19 False
0.2 MHz x5 diag b b 6 3007.4223 7.0282 False 0.9244 0.0000 19 True
0.2 MHz x5 par a a 6 2671.6285 7.7338 False 0.9485 0.0001 9 True
0.2 MHz x5 par a b 6 2671.6288 7.7338 False 0.9634 0.0000 9 True
0.2 MHz x5 par b a 6 2758.1747 7.7281 False 0.8895 0.0000 19 False
0.2 MHz x5 par b b 6 2758.1747 7.7281 False 0.9591 0.0000 19 True
holds for both profiles under every reading, by rate and label:
rate label
0.2 MHz x1 False
x3 False
x5 False
1 Hz x1 False
x2 False
x5 False
R range by rate and label: {('0.2 MHz', 'x1'): {'min': -0.74, 'max': -0.02}, ('0.2 MHz', 'x3'): {'min': 0.79, 'max': 1.0}, ('0.2 MHz', 'x5'): {'min': 0.8, 'max': 0.96}, ('1 Hz', 'x1'): {'min': 0.36, 'max': 0.72}, ('1 Hz', 'x2'): {'min': 0.4, 'max': 0.94}, ('1 Hz', 'x5'): {'min': -0.89, 'max': 0.51}}
fits that failed: 0 ; fits with the width at its bound: 12 of 48
Result: not reproduced. At x1 (2 bursts, adopted) the 1 Hz fits give R from 0.36 to 0.72 (0.39 to 0.74 without the flagged area of panel 62) and the 0.2 MHz profiles have no central peak (all 8 fits at the 100-mm width bound, R from -0.74 to -0.02). Under the total reading the 1 Hz x2 fits give R from 0.40 to 0.94 and pass in 2 of 8 profile and reading combinations. No fit failed; 12 of 48 ran to the width bound. In the other groups, 0.2 MHz x3 passes in 4 of 8 combinations (R 0.79 to 1.00) and x5 in 4 of 8 (R 0.80 to 0.96); 1 Hz x5 gives R from -0.89 to 0.51.
fig3-tail-deviation-falls-with-rate¶
Beyond the fitted range, the data deviated from the Lorentzian fit less as the packet rate increased.
Criterion: fits of the previous claim extended to all distances; the deviation is the mean of
(data - fit) over ROIs beyond 4 mm, in (a) a.u. and (b) percent of the fitted peak a. Reproduced when
the deviation is largest at 1 Hz and smallest at 0.2 MHz, and its Spearman correlation with rate is
negative. With two rates the Spearman coefficient can only be +1 or -1 and has no test. A deviation
from a fit at its width bound is not a deviation from a bell-shaped fit, so holds also requires
that both fits have a peak (order_holds is the order alone).
rows = []
for pname, (l1, l2) in PAIRS.items():
for kind in ("diag", "par"):
for o1, o2 in READINGS:
dev, ok = {}, True
for rate, lab in (("1 Hz", l1), ("0.2 MHz", l2)):
pos, m, f = fits[(lab, rate, kind, o1, o2)]
far = np.abs(pos) > 4
d = np.nanmean(m[far] - cc.lorentz(pos[far], f["a"], f["w"])) if f else np.nan
dev[rate] = (d, 100 * d / f["a"] if f else np.nan)
ok = ok and not at_bound(f)
for u, k in (("a.u.", 0), ("% of a", 1)):
v = [dev[r][k] for r in RATES]
rho = stats.spearmanr([1, 208333], v)[0] if np.all(np.isfinite(v)) else np.nan
lit = bool(v[0] > v[1] and rho < 0)
rows.append({"pair": pname, "profile": kind, "O1": o1, "O2": o2, "unit": u, "dev_1Hz": v[0], "dev_0.2MHz": v[1],
"spearman": rho, "both_fits_have_a_peak": ok, "order_holds": lit, "holds": lit and ok})
tt = pd.DataFrame(rows)
print("pairs (1 Hz label | 0.2 MHz label):", PAIR_NOTE)
print(tt.round(2).to_string(index=False))
print("\nby pair, every reading and both profiles: order holds", tt.groupby("pair").order_holds.all().to_dict(),
"; both fits have a peak", tt.groupby("pair").both_fits_have_a_peak.all().to_dict(), "; holds", tt.groupby("pair").holds.all().to_dict())
pairs (1 Hz label | 0.2 MHz label): {'x1|x1': '2 v 2 bursts, adopted', 'x2|x1': '2 v 1 bursts, total reading', 'x2|x3': '2 v 3 bursts, total reading', 'x5|x5': '6 v 6 bursts, variant'}
pair profile O1 O2 unit dev_1Hz dev_0.2MHz spearman both_fits_have_a_peak order_holds holds
x1|x1 diag a a a.u. 145.03 -3.72 -1.0 False True False
x1|x1 diag a a % of a 491.64 -36.65 -1.0 False True False
x1|x1 diag a b a.u. 145.03 -3.72 -1.0 False True False
x1|x1 diag a b % of a 491.48 -36.65 -1.0 False True False
x1|x1 diag b a a.u. 110.59 1.98 -1.0 False True False
x1|x1 diag b a % of a 310.46 25.90 -1.0 False True False
x1|x1 diag b b a.u. 110.59 1.98 -1.0 False True False
x1|x1 diag b b % of a 310.45 25.90 -1.0 False True False
x1|x1 par a a a.u. 1.88 -16.04 -1.0 False True False
x1|x1 par a a % of a 9.34 -84.85 -1.0 False True False
x1|x1 par a b a.u. 1.88 -16.04 -1.0 False True False
x1|x1 par a b % of a 9.36 -84.85 -1.0 False True False
x1|x1 par b a a.u. -6.52 -34.14 -1.0 False True False
x1|x1 par b a % of a -23.53 -86.11 -1.0 False True False
x1|x1 par b b a.u. -6.52 -34.14 -1.0 False True False
x1|x1 par b b % of a -23.53 -86.11 -1.0 False True False
x2|x1 diag a a a.u. 486.76 -3.72 -1.0 False True False
x2|x1 diag a a % of a 99.59 -36.65 -1.0 False True False
x2|x1 diag a b a.u. 486.76 -3.72 -1.0 False True False
x2|x1 diag a b % of a 99.59 -36.65 -1.0 False True False
x2|x1 diag b a a.u. 551.72 1.98 -1.0 False True False
x2|x1 diag b a % of a 90.27 25.90 -1.0 False True False
x2|x1 diag b b a.u. 551.72 1.98 -1.0 False True False
x2|x1 diag b b % of a 90.27 25.90 -1.0 False True False
x2|x1 par a a a.u. 353.67 -16.04 -1.0 False True False
x2|x1 par a a % of a 64.91 -84.85 -1.0 False True False
x2|x1 par a b a.u. 353.67 -16.04 -1.0 False True False
x2|x1 par a b % of a 64.91 -84.85 -1.0 False True False
x2|x1 par b a a.u. 338.03 -34.14 -1.0 False True False
x2|x1 par b a % of a 52.79 -86.11 -1.0 False True False
x2|x1 par b b a.u. 338.03 -34.14 -1.0 False True False
x2|x1 par b b % of a 52.79 -86.11 -1.0 False True False
x2|x3 diag a a a.u. 486.76 75.57 -1.0 True True True
x2|x3 diag a a % of a 99.59 22.73 -1.0 True True True
x2|x3 diag a b a.u. 486.76 75.57 -1.0 True True True
x2|x3 diag a b % of a 99.59 22.73 -1.0 True True True
x2|x3 diag b a a.u. 551.72 71.36 -1.0 True True True
x2|x3 diag b a % of a 90.27 20.61 -1.0 True True True
x2|x3 diag b b a.u. 551.72 71.36 -1.0 True True True
x2|x3 diag b b % of a 90.27 20.61 -1.0 True True True
x2|x3 par a a a.u. 353.67 36.20 -1.0 True True True
x2|x3 par a a % of a 64.91 10.94 -1.0 True True True
x2|x3 par a b a.u. 353.67 36.20 -1.0 True True True
x2|x3 par a b % of a 64.91 10.94 -1.0 True True True
x2|x3 par b a a.u. 338.03 32.59 -1.0 True True True
x2|x3 par b a % of a 52.79 8.65 -1.0 True True True
x2|x3 par b b a.u. 338.03 32.59 -1.0 True True True
x2|x3 par b b % of a 52.79 8.65 -1.0 True True True
x5|x5 diag a a a.u. -207.49 429.91 1.0 True False False
x5|x5 diag a a % of a -13.09 15.91 1.0 True False False
x5|x5 diag a b a.u. -207.49 429.91 1.0 True False False
x5|x5 diag a b % of a -13.09 15.91 1.0 True False False
x5|x5 diag b a a.u. -450.49 665.55 1.0 False False False
x5|x5 diag b a % of a -27.36 22.13 1.0 False False False
x5|x5 diag b b a.u. -450.49 665.55 1.0 False False False
x5|x5 diag b b % of a -27.36 22.13 1.0 False False False
x5|x5 par a a a.u. -246.98 206.95 1.0 False False False
x5|x5 par a a % of a -14.63 7.75 1.0 False False False
x5|x5 par a b a.u. -246.98 206.95 1.0 False False False
x5|x5 par a b % of a -14.63 7.75 1.0 False False False
x5|x5 par b a a.u. -95.30 192.48 1.0 True False False
x5|x5 par b a % of a -5.66 6.98 1.0 True False False
x5|x5 par b b a.u. -95.31 192.48 1.0 True False False
x5|x5 par b b % of a -5.66 6.98 1.0 True False False
by pair, every reading and both profiles: order holds {'x1|x1': True, 'x2|x1': True, 'x2|x3': True, 'x5|x5': False} ; both fits have a peak {'x1|x1': False, 'x2|x1': False, 'x2|x3': True, 'x5|x5': False} ; holds {'x1|x1': False, 'x2|x1': False, 'x2|x3': True, 'x5|x5': False}
Result: not reproduced. At x1 (2 bursts, adopted) the deviation is larger at 1 Hz than at 0.2 MHz under every reading (diagonal, O1 a, 145.0 against -3.7 a.u.), but every 0.2 MHz fit has its width at the 100-mm bound, so there is no fitted peak for the data to deviate from; the order says only that the 0.2 MHz wells are unstained far out as everywhere else. Under the total reading there is no 0.2 MHz 2-burst group; 1 Hz x2 against 0.2 MHz x3 (3 bursts), both fits with a peak, gives the paper's order under every reading (diagonal, O1 a, 486.8 against 75.6 a.u.). In the x5 variant the order is reversed under every reading (-207.5 against 429.9 a.u.). With two rates the trend over rates cannot be tested.
fig4b-centre-no-rate-effect¶
YP uptake at the array centre showed no significant effect of packet rate.
A claim of absence, judged on the paper's test: two-sided Student's t tests of per-area YP at x = 0, Bonferroni-corrected. With two rates there is one comparison under both readings of the claims file; the paper's correction factors for its five rates (4 against 1 Hz, 10 for all pairs) are shown as variants. The 1 kHz and 10 kHz comparisons cannot be made.
rows = []
for pname, (l1, l2) in PAIRS.items():
for o2 in "ab":
v = {"1 Hz": per_area(sel("1 Hz", "CANCAN", l1), "diag", "a", o2, xs=[0]),
"0.2 MHz": per_area(sel("0.2 MHz", "CANCAN", l2), "diag", "a", o2, xs=[0])}
t = stats.ttest_ind(v["1 Hz"], v["0.2 MHz"])
rows.append({"pair": pname, "O2": o2, "mean_1Hz": v["1 Hz"].mean(), "se_1Hz": v["1 Hz"].std(ddof=1) / np.sqrt(len(v["1 Hz"])),
"mean_0.2MHz": v["0.2 MHz"].mean(), "se_0.2MHz": v["0.2 MHz"].std(ddof=1) / np.sqrt(len(v["0.2 MHz"])),
"ratio_02_over_1Hz": v["0.2 MHz"].mean() / v["1 Hz"].mean(), "t": t.statistic, "p (1 comparison)": t.pvalue,
"p x4": min(1, 4 * t.pvalue), "p x10": min(1, 10 * t.pvalue), "n": f"{len(v['1 Hz'])}/{len(v['0.2 MHz'])}"})
tc = pd.DataFrame(rows)
print("pairs (1 Hz label | 0.2 MHz label):", PAIR_NOTE)
print(tc.round(4).to_string(index=False))
print("\nno significant difference (p >= 0.05 before any correction), by pair:", (tc.groupby("pair")["p (1 comparison)"].min() >= 0.05).to_dict())
pairs (1 Hz label | 0.2 MHz label): {'x1|x1': '2 v 2 bursts, adopted', 'x2|x1': '2 v 1 bursts, total reading', 'x2|x3': '2 v 3 bursts, total reading', 'x5|x5': '6 v 6 bursts, variant'}
pair O2 mean_1Hz se_1Hz mean_0.2MHz se_0.2MHz ratio_02_over_1Hz t p (1 comparison) p x4 p x10 n
x1|x1 a 0.9753 21.7905 -1.4091 6.3316 -1.4448 0.0861 0.9335 1.0000 1.0000 6/4
x1|x1 b 0.9753 21.7905 -1.4091 6.3316 -1.4448 0.0861 0.9335 1.0000 1.0000 6/4
x2|x1 a 516.4462 167.3816 -1.4091 6.3316 -0.0027 5.0307 0.0073 0.0293 0.0733 2/4
x2|x1 b 516.4462 167.3816 -1.4091 6.3316 -0.0027 5.0307 0.0073 0.0293 0.0733 2/4
x2|x3 a 516.4462 167.3816 352.6762 253.5519 0.6829 0.5390 0.6438 1.0000 1.0000 2/2
x2|x3 b 516.4462 167.3816 352.6762 253.5519 0.6829 0.5390 0.6438 1.0000 1.0000 2/2
x5|x5 a 1576.1918 465.4956 2275.3182 921.7949 1.4436 -0.6342 0.5417 1.0000 1.0000 5/6
x5|x5 b 1576.1918 465.4956 2275.3182 921.7949 1.4436 -0.6342 0.5417 1.0000 1.0000 5/6
no significant difference (p >= 0.05 before any correction), by pair: {'x1|x1': True, 'x2|x1': False, 'x2|x3': True, 'x5|x5': True}
Result: partially reproduced. At x1 (2 bursts, adopted) centre YP does not differ between 1 Hz and 0.2 MHz (1.0 +/- 21.8 against -1.4 +/- 6.3 a.u., t = 0.09, p = 0.93, n = 6 and 4), so the paper's test finds no effect, but neither rate stains the centre at all, unlike the paper, where the centre is stained at every rate; and only one of the paper's comparisons can be made. Under the total reading, 1 Hz x2 (2 bursts, 516 a.u.) is above 0.2 MHz x1 (1 burst, p = 0.007, 0.03 with a factor of 4) and not different from 0.2 MHz x3 (3 bursts, 353 a.u., p = 0.64). In the x5 variant, 1576 against 2275 a.u., p = 0.54. Without the flagged 1 Hz area (panel 62), 4.4 +/- 26.4 against -1.4 +/- 6.3 a.u., p = 0.85 (n = 5 and 4).
fig4b-near-electrode-decline¶
In contrast to the centre, YP uptake at 6 and 7 mm from the centre (1.65 and 0.65 mm from the electrodes) declined steeply as the packet rate increased.
Criterion: a power function YP = c rate^k fitted at 6 and at 7 mm, to (a) group means or (b) per-area values; reproduced when k < 0 at both distances and the 0.2 MHz mean is below 1 Hz with a gap larger than the error bars at both. With two rates, the power fit to the means passes through both. A variant uses the two-well images only (the whole-well images are on another a.u. scale).
rates_hz = {"1 Hz": 1.0, "0.2 MHz": 208333.0}
def power(r, c, k):
return c * r ** k
whole_well = set(areas[areas.pixel_um == 1.625].file)
rows = []
for pname, (l1, l2) in list(PAIRS.items()) + [("x1|x1, two-well images only", ("x1", "x1"))]:
for o2 in "ab":
for x in (6, 7):
labs = {"1 Hz": l1, "0.2 MHz": l2}
ss = {r: sel(r, "CANCAN", labs[r]) for r in RATES}
if "two-well" in pname:
ss = {r: s[~s.file.isin(whole_well)] for r, s in ss.items()}
v = {r: per_area(ss[r], "diag", "a", o2, xs=[x, -x] if o2 == "a" else [x]) for r in RATES}
xs_m = np.array([rates_hz[r] for r in RATES]); ys_m = np.array([v[r].mean() for r in RATES])
xs_e = np.concatenate([[rates_hz[r]] * len(v[r]) for r in RATES]); ys_e = np.concatenate([v[r] for r in RATES])
ks = {}
for nm, xx, yy in (("means", xs_m, ys_m), ("per-area", xs_e, ys_e)):
try:
p_, _ = curve_fit(power, xx, yy, p0=[max(yy[0], 1), -0.1], maxfev=20000)
ks[nm] = p_[1]
except RuntimeError:
ks[nm] = np.nan
c = compare(v["1 Hz"], v["0.2 MHz"])
r = {"pair": pname, "O2": o2, "x_mm": x, "k_means": ks["means"], "k_per_area": ks["per-area"], **c}
for arm in cc.ARMS:
r[f"holds_{arm}"] = bool(ks["means"] < 0 and ks["per-area"] < 0 and c[f"holds_{arm}"])
rows.append(r)
tn = pd.DataFrame(rows)
print("pairs (1 Hz label | 0.2 MHz label):", PAIR_NOTE)
print(tn.round(4).to_string(index=False))
print("\nholds at 6 and 7 mm under every O2 and fit reading, by pair and F8 reading:")
print(tn.groupby("pair")[H].all().to_string())
print("\nby pair and distance:")
print(tn.groupby(["pair", "x_mm"])[H].all().to_string())
pairs (1 Hz label | 0.2 MHz label): {'x1|x1': '2 v 2 bursts, adopted', 'x2|x1': '2 v 1 bursts, total reading', 'x2|x3': '2 v 3 bursts, total reading', 'x5|x5': '6 v 6 bursts, variant'}
pair O2 x_mm k_means k_per_area 1 Hz se_1Hz 0.2 MHz se_02 gap holds_long holds_mean holds_short t_p n
x1|x1 a 6 -0.2301 -0.2301 152.5452 76.1802 9.1104 3.8201 63.4345 False True True 0.1706 6/4
x1|x1 a 7 -0.2909 -0.2909 173.9553 130.9160 4.9319 2.8601 35.2473 False False True 0.3319 6/4
x1|x1 b 6 -0.2301 -0.2301 152.5452 76.1802 9.1104 3.8201 63.4345 False True True 0.1706 6/4
x1|x1 b 7 -0.2909 -0.2909 173.9553 130.9160 4.9319 2.8601 35.2473 False False True 0.3319 6/4
x2|x1 a 6 -0.3581 -0.3581 731.2345 455.8577 9.1104 3.8201 262.4463 False True True 0.0609 2/4
x2|x1 a 7 -0.3773 -0.3773 501.2318 143.1360 4.9319 2.8601 350.3038 True True True 0.0048 2/4
x2|x1 b 6 -0.3581 -0.3581 731.2345 455.8577 9.1104 3.8201 262.4463 False True True 0.0609 2/4
x2|x1 b 7 -0.3773 -0.3773 501.2318 143.1360 4.9319 2.8601 350.3038 True True True 0.0048 2/4
x2|x3 a 6 -0.1726 -0.1726 731.2345 455.8577 88.2841 20.8724 166.2202 False False True 0.2942 2/2
x2|x3 a 7 -0.1136 -0.1136 501.2318 143.1360 124.7640 63.4768 169.8549 True True True 0.1381 2/2
x2|x3 b 6 -0.1726 -0.1726 731.2345 455.8577 88.2841 20.8724 166.2202 False False True 0.2942 2/2
x2|x3 b 7 -0.1136 -0.1136 501.2318 143.1360 124.7640 63.4768 169.8549 True True True 0.1381 2/2
x5|x5 a 6 0.0219 0.0219 1173.1112 529.3858 1533.9836 602.4284 -770.9420 False False False 0.6701 5/6
x5|x5 a 7 0.0132 0.0132 780.4187 421.7363 917.3230 468.5425 -753.3746 False False False 0.8361 5/6
x5|x5 b 6 0.0219 0.0219 1173.1112 529.3858 1533.9836 602.4284 -770.9420 False False False 0.6701 5/6
x5|x5 b 7 0.0132 0.0132 780.4187 421.7363 917.3230 468.5425 -753.3746 False False False 0.8361 5/6
x1|x1, two-well images only a 6 -0.1772 -0.1772 79.7968 27.6882 9.1104 3.8201 39.1781 True True True 0.0602 5/4
x1|x1, two-well images only a 7 -0.1802 -0.1802 44.8179 26.3400 4.9319 2.8601 10.6859 False False True 0.2250 5/4
x1|x1, two-well images only b 6 -0.1772 -0.1772 79.7968 27.6882 9.1104 3.8201 39.1781 True True True 0.0602 5/4
x1|x1, two-well images only b 7 -0.1802 -0.1802 44.8179 26.3400 4.9319 2.8601 10.6859 False False True 0.2250 5/4
holds at 6 and 7 mm under every O2 and fit reading, by pair and F8 reading:
holds_long holds_mean holds_short
pair
x1|x1 False False True
x1|x1, two-well images only False False True
x2|x1 False True True
x2|x3 False False True
x5|x5 False False False
by pair and distance:
holds_long holds_mean holds_short
pair x_mm
x1|x1 6 False True True
7 False False True
x1|x1, two-well images only 6 True True True
7 False False True
x2|x1 6 False True True
7 True True True
x2|x3 6 False False True
7 True True True
x5|x5 6 False False False
7 False False False
Result: not reproduced. At x1 (2 bursts, adopted) the power exponent is negative at 6 and 7 mm (k = -0.23 and -0.29) and the 0.2 MHz means are lower (9.1 against 152.5 and 4.9 against 174.0 a.u.), but the gap exceeds the longer error bar at neither distance (t test p = 0.17 and 0.33). The result rests on the reading of F8: under the mean-arm reading the gap holds at 6 mm only, and under the shorter-arm reading at both, which would make the claim reproduced. On two-well images only the gap exceeds the longer arm at 6 mm (79.8 against 9.1 a.u., p = 0.06) and not at 7 mm. Under the total reading, 1 Hz x2 against 0.2 MHz x3 has k < 0 at both distances, with a gap beyond the longer arm at 7 mm only. In the x5 variant k is positive (0.022 and 0.013). Without the flagged 1 Hz area (panel 62) the pattern is the same: k = -0.25 and -0.31, a gap beyond the longer arm at 6 mm (183.6 against 9.1 a.u., p = 0.11) and not at 7 mm (210.4 against 4.9, p = 0.28), and the claim holds only under the shorter-arm reading.
The x1 claims without the flagged areas¶
Two flagged areas are in x1 groups that decide outcomes: sub-013Control-x1_obj-1pihpj7 R (1 Hz
CANCAN) and sub-013Control-x1_obj-6stwj8 W (1 Hz control). A misplaced centre moves the 0, 6 and
7 mm ROIs off their places. Below, the tests of fig3a-centre-enhanced, fig3bc-rate-suppresses-off-target,
fig3-lorentzian-profile, fig4b-centre-no-rate-effect and fig4b-near-electrode-decline at x1 are
repeated without the flagged areas.
def x1(rate, protocol):
return sel(rate, protocol, "x1", drop_flagged=True)
print("flagged YP areas left out at x1:")
print(areas[areas.flagged & (areas.label == "x1") & (areas.channel == "FITC")][["file", "side", "rate", "protocol", "located_by", "match"]].to_string(index=False))
print("\nfig3a-centre-enhanced: CANCAN maximum, and the pooled control centre against 6 and 7 mm")
rows = []
for o1, o2 in READINGS:
for rate in RATES:
pos, m, se, ys = group_profile(x1(rate, "CANCAN"), o1=o1, o2=o2)
rows.append({"O1": o1, "O2": o2, "group": f"CANCAN {rate}", "n": len(ys), "argmax_mm": round(float(pos[np.nanargmax(m)]), 2),
"holds": bool(abs(pos[np.nanargmax(m)]) < 1e-9)})
pos, m, se, ys = group_profile(pd.concat([x1(r, "control") for r in RATES]), o1=o1, o2=o2)
i0, i6, i7 = (np.argmin(np.abs(pos - x)) for x in (0, 6, 7))
r = {"O1": o1, "O2": o2, "group": "control", "n": len(ys), "y0": m[i0], "se0": se[i0], "y6": m[i6], "se6": se[i6], "y7": m[i7], "se7": se[i7]}
for arm in cc.ARMS:
r[f"holds_{arm}"] = bool(m[i0] < m[i6] and m[i0] < m[i7] and cc.gap_significant(m[i6], se[i6], m[i0], se[i0], arm)[0]
and cc.gap_significant(m[i7], se[i7], m[i0], se[i0], arm)[0])
rows.append(r)
print(pd.DataFrame(rows).round(1).to_string(index=False))
print("\nfig3bc-rate-suppresses-off-target: 1 Hz against 0.2 MHz")
rows = []
for o1, o2 in READINGS:
outer = P.max() if o1 == "a" else PC.max()
tests = {"6 mm": ("diag", dict(xs=[6, -6] if o2 == "a" else [6])), "7 mm": ("diag", dict(xs=[7, -7] if o2 == "a" else [7])),
"perimeter (a) outermost": ("par", dict(xs=[outer, -outer])), "perimeter (b) >= 4 mm": ("par", dict(beyond=4))}
for name, (kind, kw) in tests.items():
rows.append({"O1": o1, "O2": o2, "where": name, **compare(per_area(x1("1 Hz", "CANCAN"), kind, o1, o2, **kw),
per_area(x1("0.2 MHz", "CANCAN"), kind, o1, o2, **kw))})
v3bc = pd.DataFrame(rows)
print(v3bc.round(3).to_string(index=False))
print("holds at every place under every O1/O2 reading, by F8 reading:", v3bc[H].all().to_dict())
print("by place:")
print(v3bc.groupby("where")[H].all().to_string())
print("\nfig3-lorentzian-profile: 1 Hz CANCAN fits")
rows = []
for kind in ("diag", "par"):
for o1, o2 in READINGS:
pos, m, se, ys = group_profile(x1("1 Hz", "CANCAN"), kind, o1, o2)
f = cc.fit(cc.lorentz, pos, m, 4)
rows.append({"profile": kind, "O1": o1, "O2": o2, "n": len(ys), "FWHM_mm": f["w"] if f else np.nan, "R": f["R"] if f else np.nan,
"p": f["p"] if f else np.nan, "holds": bool(f and f["R"] > 0.9 and f["p"] < 0.001)})
print(pd.DataFrame(rows).round(4).to_string(index=False))
print("\nfig4b-centre-no-rate-effect: centre YP")
for o2 in "ab":
a, b = (per_area(x1(r, "CANCAN"), "diag", "a", o2, xs=[0]) for r in RATES)
t = stats.ttest_ind(a, b)
print(f"O2 {o2}: 1 Hz {a.mean():.1f} +/- {a.std(ddof=1) / np.sqrt(len(a)):.1f} (n={len(a)}), 0.2 MHz {b.mean():.1f} +/- "
f"{b.std(ddof=1) / np.sqrt(len(b)):.1f} (n={len(b)}), t = {t.statistic:.2f}, p = {t.pvalue:.3f}")
print("\nfig4b-near-electrode-decline: 6 and 7 mm")
rows = []
for o2 in "ab":
for x in (6, 7):
v = {r: per_area(x1(r, "CANCAN"), "diag", "a", o2, xs=[x, -x] if o2 == "a" else [x]) for r in RATES}
ks = {}
for nm, xx, yy in (("means", np.array([rates_hz[r] for r in RATES]), np.array([v[r].mean() for r in RATES])),
("per-area", np.concatenate([[rates_hz[r]] * len(v[r]) for r in RATES]), np.concatenate([v[r] for r in RATES]))):
try:
ks[nm] = curve_fit(power, xx, yy, p0=[max(yy[0], 1), -0.1], maxfev=20000)[0][1]
except RuntimeError:
ks[nm] = np.nan
c = compare(v["1 Hz"], v["0.2 MHz"])
r = {"O2": o2, "x_mm": x, "k_means": ks["means"], "k_per_area": ks["per-area"], **c}
for arm in cc.ARMS:
r[f"holds_{arm}"] = bool(ks["means"] < 0 and ks["per-area"] < 0 and c[f"holds_{arm}"])
rows.append(r)
vn = pd.DataFrame(rows)
print(vn.round(4).to_string(index=False))
print("holds at 6 and 7 mm under every O2 and fit reading, by F8 reading:", vn[H].all().to_dict())
flagged YP areas left out at x1:
file side rate protocol located_by match
sub-013Control-x1_obj-1pihpj7 R 1 Hz CANCAN centroid 0.022294
sub-013Control-x1_obj-6stwj8 W 1 Hz control centroid 0.015468
fig3a-centre-enhanced: CANCAN maximum, and the pooled control centre against 6 and 7 mm
O1 O2 group n argmax_mm holds y0 se0 y6 se6 y7 se7 holds_long holds_mean holds_short
a a CANCAN 1 Hz 5 7.0 False NaN NaN NaN NaN NaN NaN NaN NaN NaN
a a CANCAN 0.2 MHz 4 4.0 False NaN NaN NaN NaN NaN NaN NaN NaN NaN
a a control 11 NaN NaN 74.4 22.9 989.6 267.5 667.8 209.0 True True True
a b CANCAN 1 Hz 5 -7.0 False NaN NaN NaN NaN NaN NaN NaN NaN NaN
a b CANCAN 0.2 MHz 4 -4.0 False NaN NaN NaN NaN NaN NaN NaN NaN NaN
a b control 11 NaN NaN 74.4 22.9 842.3 224.9 500.1 143.0 True True True
b a CANCAN 1 Hz 5 6.5 False NaN NaN NaN NaN NaN NaN NaN NaN NaN
b a CANCAN 0.2 MHz 4 4.1 False NaN NaN NaN NaN NaN NaN NaN NaN NaN
b a control 11 NaN NaN 74.4 22.9 922.3 251.3 740.7 230.2 True True True
b b CANCAN 1 Hz 5 -6.5 False NaN NaN NaN NaN NaN NaN NaN NaN NaN
b b CANCAN 0.2 MHz 4 -4.1 False NaN NaN NaN NaN NaN NaN NaN NaN NaN
b b control 11 NaN NaN 74.4 22.9 775.1 213.6 554.7 158.2 True True True
fig3bc-rate-suppresses-off-target: 1 Hz against 0.2 MHz
O1 O2 where 1 Hz se_1Hz 0.2 MHz se_02 gap holds_long holds_mean holds_short t_p n
a a 6 mm 183.613 85.190 9.110 3.820 85.492 True True True 0.114 5/4
a a 7 mm 210.393 154.003 4.932 2.860 48.598 False False True 0.278 5/4
a a perimeter (a) outermost 22.749 10.343 0.629 1.553 10.223 False True True 0.103 5/4
a a perimeter (b) >= 4 mm -13.903 24.830 6.808 2.957 -7.075 False False False 0.488 5/4
a b 6 mm 183.613 85.190 9.110 3.820 85.492 True True True 0.114 5/4
a b 7 mm 210.393 154.003 4.932 2.860 48.598 False False True 0.278 5/4
a b perimeter (a) outermost 22.749 10.343 0.629 1.553 10.223 False True True 0.103 5/4
a b perimeter (b) >= 4 mm -13.903 24.830 6.808 2.957 -7.075 False False False 0.488 5/4
b a 6 mm 131.454 56.654 8.716 3.158 62.926 True True True 0.098 5/4
b a 7 mm 206.541 154.241 4.945 3.004 44.352 False False True 0.287 5/4
b a perimeter (a) outermost 9.804 21.447 4.915 1.362 -17.921 False False False 0.847 5/4
b a perimeter (b) >= 4 mm -9.069 22.037 4.984 1.627 -9.611 False False False 0.592 5/4
b b 6 mm 131.454 56.654 8.716 3.158 62.926 True True True 0.098 5/4
b b 7 mm 206.541 154.241 4.945 3.004 44.352 False False True 0.287 5/4
b b perimeter (a) outermost 9.804 21.447 4.915 1.362 -17.921 False False False 0.847 5/4
b b perimeter (b) >= 4 mm -9.069 22.037 4.984 1.627 -9.611 False False False 0.592 5/4
holds at every place under every O1/O2 reading, by F8 reading: {'holds_long': False, 'holds_mean': False, 'holds_short': False}
by place:
holds_long holds_mean holds_short
where
6 mm True True True
7 mm False False True
perimeter (a) outermost False False False
perimeter (b) >= 4 mm False False False
fig3-lorentzian-profile: 1 Hz CANCAN fits
profile O1 O2 n FWHM_mm R p holds
diag a a 5 3.1720 0.3915 0.2974 False
diag a b 5 3.1716 0.5285 0.1435 False
diag b a 5 2.3795 0.6040 0.0062 False
diag b b 5 2.3791 0.7371 0.0003 False
par a a 5 1.6666 0.6372 0.0649 False
par a b 5 1.6669 0.6968 0.0370 False
par b a 5 1.3648 0.6158 0.0050 False
par b b 5 1.3649 0.7029 0.0008 False
fig4b-centre-no-rate-effect: centre YP
O2 a: 1 Hz 4.4 +/- 26.4 (n=5), 0.2 MHz -1.4 +/- 6.3 (n=4), t = 0.19, p = 0.853
O2 b: 1 Hz 4.4 +/- 26.4 (n=5), 0.2 MHz -1.4 +/- 6.3 (n=4), t = 0.19, p = 0.853
fig4b-near-electrode-decline: 6 and 7 mm
O2 x_mm k_means k_per_area 1 Hz se_1Hz 0.2 MHz se_02 gap holds_long holds_mean holds_short t_p n
a 6 -0.2452 -0.2452 183.6128 85.1901 9.1104 3.8201 85.4922 True True True 0.1140 5/4
a 7 -0.3065 -0.3065 210.3933 154.0029 4.9319 2.8601 48.5984 False False True 0.2779 5/4
b 6 -0.2452 -0.2452 183.6128 85.1901 9.1104 3.8201 85.4922 True True True 0.1140 5/4
b 7 -0.3065 -0.3065 210.3933 154.0029 4.9319 2.8601 48.5984 False False True 0.2779 5/4
holds at 6 and 7 mm under every O2 and fit reading, by F8 reading: {'holds_long': False, 'holds_mean': False, 'holds_short': True}
Result: no outcome changes. Without the two flagged x1 areas, fig3bc-rate-suppresses-off-target, fig3-lorentzian-profile, fig4b-centre-no-rate-effect and fig4b-near-electrode-decline give the same results under the same readings as with them. In fig3a-centre-enhanced the control part now holds under every reading, but the CANCAN part fails as before. The numbers are given under each claim.
fig4b-40-fold-ratio¶
At 0.2 MHz, YP uptake at the centre was 40 times that near the electrodes, despite the field there being about 4 times weaker (3.3 against about 14 kV/cm).
Criterion: the ratio of group-mean background-subtracted YP at x = 0 to that near the electrodes (O5: 6 or 7 mm), under O2 a or b; reproduced when within 36 to 44 under one reading. The field values come from the paper's simulation and are not tested. Shown for each 0.2 MHz label: x1 is 2 bursts under the adopted reading; x1 and x3 bracket 2 bursts under the total reading.
rows = []
for lab in ("x1", "x3", "x5"):
for o1, o2 in READINGS:
pos, m, se, ys = group_profile(sel("0.2 MHz", "CANCAN", lab), "diag", o1, o2)
y0 = at(pos, m, 0)
for near in (6, 7):
yn = at(pos, m, near) if o2 == "b" else np.mean([at(pos, m, near), at(pos, m, -near)])
rows.append({"label": lab, "bursts (adopted / total)": f"{cc.BURSTS['after_first'][lab]} / {cc.BURSTS['total'][lab]}",
"O1": o1, "O2": o2, "near_mm": near, "y0": y0, "y_near": yn, "ratio": y0 / yn if yn > 0 else np.nan, "n": len(ys)})
t40 = pd.DataFrame(rows)
print(t40.round(2).to_string(index=False))
print("\nany reading within 36 to 44, by label:", t40.groupby("label").ratio.apply(lambda r: bool(((r >= 36) & (r <= 44)).any())).to_dict())
print("range of ratios by label:", t40.groupby("label").ratio.agg(["min", "max"]).round(2).to_dict("index"))
print("\nvariant, background from the footprint with the lowest mean (O1 a, O2 a):")
for lab in ("x1", "x3", "x5"):
pos, m, se, ys = group_profile(sel("0.2 MHz", "CANCAN", lab), "diag", "a", "a", bgkind="bg_min")
y0, y6, y7 = at(pos, m, 0), np.mean([at(pos, m, 6), at(pos, m, -6)]), np.mean([at(pos, m, 7), at(pos, m, -7)])
print(f" {lab}: y0={y0:.1f}, y6={y6:.1f}, y7={y7:.1f}, ratios {y0 / y6:.2f} and {y0 / y7:.2f}")
label bursts (adopted / total) O1 O2 near_mm y0 y_near ratio n
x1 2 / 1 a a 6 -1.41 9.11 -0.15 4
x1 2 / 1 a a 7 -1.41 4.93 -0.29 4
x1 2 / 1 a b 6 -1.41 9.11 -0.15 4
x1 2 / 1 a b 7 -1.41 4.93 -0.29 4
x1 2 / 1 b a 6 -1.41 8.72 -0.16 4
x1 2 / 1 b a 7 -1.41 4.95 -0.28 4
x1 2 / 1 b b 6 -1.41 8.72 -0.16 4
x1 2 / 1 b b 7 -1.41 4.95 -0.28 4
x3 4 / 3 a a 6 352.68 88.28 3.99 2
x3 4 / 3 a a 7 352.68 124.76 2.83 2
x3 4 / 3 a b 6 352.68 88.28 3.99 2
x3 4 / 3 a b 7 352.68 124.76 2.83 2
x3 4 / 3 b a 6 352.68 93.77 3.76 2
x3 4 / 3 b a 7 352.68 138.09 2.55 2
x3 4 / 3 b b 6 352.68 93.77 3.76 2
x3 4 / 3 b b 7 352.68 138.09 2.55 2
x5 6 / 5 a a 6 2275.32 1533.98 1.48 6
x5 6 / 5 a a 7 2275.32 917.32 2.48 6
x5 6 / 5 a b 6 2275.32 1533.98 1.48 6
x5 6 / 5 a b 7 2275.32 917.32 2.48 6
x5 6 / 5 b a 6 2275.32 1580.45 1.44 6
x5 6 / 5 b a 7 2275.32 971.35 2.34 6
x5 6 / 5 b b 6 2275.32 1580.45 1.44 6
x5 6 / 5 b b 7 2275.32 971.35 2.34 6
any reading within 36 to 44, by label: {'x1': False, 'x3': False, 'x5': False}
range of ratios by label: {'x1': {'min': -0.29, 'max': -0.15}, 'x3': {'min': 2.55, 'max': 3.99}, 'x5': {'min': 1.44, 'max': 2.48}}
variant, background from the footprint with the lowest mean (O1 a, O2 a):
x1: y0=11.8, y6=22.4, y7=18.2, ratios 0.53 and 0.65
x3: y0=381.9, y6=117.5, y7=154.0, ratios 3.25 and 2.48
x5: y0=2536.9, y6=1795.5, y7=1178.9, ratios 1.41 and 2.15
Result: not reproduced. At x1 (2 bursts, adopted) the centre is below background (-1.4 a.u.) and the ratio is -0.29 to -0.15; with the darkest footprint as background it is 0.53 and 0.65. Under the total reading, where 2 bursts lies between x1 and x3, x3 gives 2.55 to 3.99. In the x5 variant, 1.44 to 2.48. No label and reading comes near 36 to 44.
fig4c-fwhm-falls-with-rate and fig4c-log-trend (blocked)¶
Both rest on the 1 kHz and 10 kHz groups, which the dataset does not hold. For the record, the per-area Lorentzian FWHM (fit over -4 to 4 mm, O1 a, O2 a) at the two rates present is printed; it decides no outcome.
rows = []
for rate, lab in CANCAN_GROUPS:
for r in sel(rate, "CANCAN", lab).itertuples():
pos, y = area_profile((r.file, r.side))
f = cc.fit(cc.lorentz, pos, y, 4)
rows.append({"rate": rate, "label": lab, "file": r.file, "side": r.side, "FWHM_mm": f["w"] if f else np.nan,
"R": f["R"] if f else np.nan, "at_bound": at_bound(f)})
fw = pd.DataFrame(rows)
print(fw.round(3).to_string(index=False))
print(fw.groupby(["rate", "label"]).agg(mean_FWHM=("FWHM_mm", "mean"), n=("FWHM_mm", "size"), at_bound=("at_bound", "sum")).round(2).to_string())
rate label file side FWHM_mm R at_bound
1 Hz x1 sub-011CC-x5_obj-1tqror5 R 0.001 -0.299 True
1 Hz x1 sub-013CC-x1_obj-1ldbf6s L 0.001 -0.301 True
1 Hz x1 sub-013CC-x1_obj-1mph1c3 L 3.889 0.691 False
1 Hz x1 sub-013CC-x1_obj-w3dn1f W 2.458 0.622 False
1 Hz x1 sub-013Control-x1_obj-1pihpj7 R 0.001 -0.335 True
1 Hz x1 sub-013Control-x1_obj-l3r6gc R 100.000 -0.000 True
0.2 MHz x1 sub-011CC-x1_obj-1jakd50 L 100.000 -0.466 True
0.2 MHz x1 sub-011Control-x1_obj-ejqkbf R 100.000 -0.480 True
0.2 MHz x1 sub-011Control-x1_obj-s8wtbt R 20.750 0.111 False
0.2 MHz x1 sub-013Control-x5_obj-oezvi9 R 2.056 0.711 False
1 Hz x2 sub-013CC-x2_obj-2xp3mi L 6.746 0.432 False
1 Hz x2 sub-013CC-x2_obj-ig09wu L 12.431 0.369 False
0.2 MHz x3 sub-011CC-x3_obj-1us8p8 L 2.764 0.966 False
0.2 MHz x3 sub-013CC-x2_obj-ig09wu R 100.000 -0.381 True
1 Hz x5 sub-011Control-x1_obj-1kbxt87 R 22.844 0.298 False
1 Hz x5 sub-013CC-x5_obj-1dsf0i2 L 100.000 -0.137 True
1 Hz x5 sub-013CC-x5_obj-1f0h9p1 W 6.257 0.369 False
1 Hz x5 sub-013CC-x5_obj-e3sltv L 83.193 0.017 False
1 Hz x5 sub-013Control-x5_obj-16wml2b R 100.000 -0.341 True
0.2 MHz x5 sub-011CC-x5_obj-1tqror5 L 9.103 0.822 False
0.2 MHz x5 sub-011CC-x5_obj-96qypk L 100.000 -0.650 True
0.2 MHz x5 sub-011CC-x5_obj-rvo7ll W 14.346 0.254 False
0.2 MHz x5 sub-011CC-x5_obj-ryt1td L 5.909 0.888 False
0.2 MHz x5 sub-011Control-x5_obj-ljdh5h R 7.565 0.788 False
0.2 MHz x5 sub-011Control-x5_obj-mzbjav R 8.043 0.645 False
mean_FWHM n at_bound
rate label
0.2 MHz x1 55.70 4 2
x3 51.38 2 1
x5 24.16 6 1
1 Hz x1 17.72 6 4
x2 9.59 2 0
x5 62.46 5 2
fig5ab-yp-spot-slightly-larger-than-pr¶
The central spot of YP staining (electroporation) was only slightly larger than the spot of Pr staining (cell killing).
Set S2 is 0.2 MHz, 2 bursts (x4), electrodes touching the monolayer or raised. The dataset gives no elevation; its 0.2 MHz CANCAN wells imaged for both YP and Pr (May 24) are taken as touching the monolayer (Methods 2.2, the array "positioned ... until touching the bottom of the culture dish"), an assumption the data cannot check. They carry burst labels x1, x3 and x5. Under the adopted reading x1 is 2 bursts, so x1 decides; under the total reading there is no 0.2 MHz 2-burst well. The x3, x5 and pooled wells are shown; pooling mixes burst numbers whose centre YP differs over 1000-fold, so it is not the S2 condition. Each is also shown without the flagged Pr areas (see the area panels). Criterion: FWHM of the Lorentzian fit to mean YP and of the Gaussian fit to mean Pr over -5 to 5 mm (FWHM = 2.4 sigma; 2.355 sigma as a variant); reproduced when YP FWHM / Pr FWHM lies in (a) 1 to 1.25 and (b) 1 to 1.5. Pr areas are located in their own images.
s2_cy3 = areas[(areas.channel == "Cy3") & (areas.rate == "0.2 MHz") & (areas.protocol == "CANCAN") & areas.usable]
# the YP image of the same plate pair: same subjects on the same sides and the same image width, imaged on May 24
def yp_partner(r):
f = files.set_index("file")
subj = [a["subject"] for a in f.loc[r.file, "areas"]]
cand = files[(files.channel == "FITC") & (files.start.str.startswith("2024-05-24"))]
for c in cand.itertuples():
if [a["subject"] for a in c.areas] == subj and c.cols == f.loc[r.file, "cols"]:
return c.file
s2 = s2_cy3.assign(yp_file=[yp_partner(r) for r in s2_cy3.itertuples()])
print(s2[["file", "side", "label", "located_by", "match", "flagged", "yp_file"]].to_string(index=False))
s2_yp = pd.concat([areas[(areas.file == r.yp_file) & (areas.side == r.side) & areas.usable] for r in s2.itertuples()])
print("YP partners found:", len(s2_yp), "of", len(s2))
s2_yp = s2_yp.assign(pr_flagged=s2.flagged.values)
rows = []
s2fits = {}
for gname, lab in {"x1": "x1", "x3": "x3", "x5": "x5", "pooled": None}.items():
for drop in (False, True):
keep = (s2.label == lab if lab else s2.label.notna()).values & ~(s2.flagged.values & drop)
py, pr = s2_yp[keep], s2[keep]
if len(pr) == 0:
rows.append({"group": gname, "Pr centres": "flagged dropped" if drop else "all", "n_YP": 0, "n_Pr": 0})
continue
for o1, o2 in READINGS:
pos, my, sy, _ = group_profile(py, "diag", o1, o2)
pos2, mp, sp, _ = group_profile(pr, "diag", o1, o2)
fy = cc.fit(cc.lorentz, pos, my, 5)
fp = cc.fit(cc.gauss, pos2, mp, 5)
s2fits[(gname, drop, o1, o2)] = (pos, my, pos2, mp)
ry = fy["w"] if fy else np.nan
rp = 2.4 * fp["w"] if fp else np.nan
rows.append({"group": gname, "Pr centres": "flagged dropped" if drop else "all", "O1": o1, "O2": o2, "n_YP": len(py),
"n_Pr": len(pr), "YP_FWHM": ry, "YP_at_bound": at_bound(fy), "Pr_FWHM_2.4s": rp, "Pr_at_bound": fp is None or fp["w"] > 99,
"ratio": ry / rp, "ratio_2.355s": ry / (2.355 * fp["w"]) if fp else np.nan,
"in_1_1.25": bool(1 <= ry / rp <= 1.25), "in_1_1.5": bool(1 <= ry / rp <= 1.5)})
t5 = pd.DataFrame(rows)
print(t5.round(3).to_string(index=False))
print("\nwithin 1 to 1.5 (the wider reading) under any O1/O2 reading, by group:", t5.groupby(["group", "Pr centres"])["in_1_1.5"].any().to_dict())
file side label located_by match flagged yp_file
sub-011CC-x1_obj-1drpw33 L x1 footprints 0.036289 True sub-011CC-x1_obj-1jakd50
sub-011CC-x3_obj-8i09ng L x3 footprints 0.039350 False sub-011CC-x3_obj-1us8p8
sub-011Control-x5_obj-kvqdb2 R x5 centroid 0.005598 True sub-011Control-x5_obj-mzbjav
sub-011Control-x5_obj-kydq4g R x5 footprints 0.081933 False sub-011Control-x5_obj-ljdh5h
sub-013CC-x2_obj-113jhpp R x3 centroid 0.030468 False sub-013CC-x2_obj-ig09wu
YP partners found: 5 of 5
group Pr centres O1 O2 n_YP n_Pr YP_FWHM YP_at_bound Pr_FWHM_2.4s Pr_at_bound ratio ratio_2.355s in_1_1.25 in_1_1.5
x1 all a a 1 1 100.000 True 36.170 False 2.765 2.818 False False
x1 all a b 1 1 100.000 True 36.170 False 2.765 2.818 False False
x1 all b a 1 1 100.000 True 240.000 True 0.417 0.425 False False
x1 all b b 1 1 100.000 True 240.000 True 0.417 0.425 False False
x1 flagged dropped NaN NaN 0 0 NaN NaN NaN NaN NaN NaN NaN NaN
x3 all a a 2 2 5.354 False 7.784 False 0.688 0.701 False False
x3 all a b 2 2 5.355 False 7.784 False 0.688 0.701 False False
x3 all b a 2 2 4.802 False 7.119 False 0.675 0.687 False False
x3 all b b 2 2 4.802 False 7.119 False 0.675 0.687 False False
x3 flagged dropped a a 2 2 5.354 False 7.784 False 0.688 0.701 False False
x3 flagged dropped a b 2 2 5.355 False 7.784 False 0.688 0.701 False False
x3 flagged dropped b a 2 2 4.802 False 7.119 False 0.675 0.687 False False
x3 flagged dropped b b 2 2 4.802 False 7.119 False 0.675 0.687 False False
x5 all a a 2 2 8.333 False 12.579 False 0.662 0.675 False False
x5 all a b 2 2 8.333 False 12.579 False 0.662 0.675 False False
x5 all b a 2 2 7.151 False 13.706 False 0.522 0.532 False False
x5 all b b 2 2 7.151 False 13.706 False 0.522 0.532 False False
x5 flagged dropped a a 1 1 8.208 False 13.884 False 0.591 0.602 False False
x5 flagged dropped a b 1 1 8.208 False 13.884 False 0.591 0.602 False False
x5 flagged dropped b a 1 1 6.939 False 13.958 False 0.497 0.507 False False
x5 flagged dropped b b 1 1 6.939 False 13.958 False 0.497 0.507 False False
pooled all a a 5 5 7.988 False 10.482 False 0.762 0.777 False False
pooled all a b 5 5 7.989 False 10.482 False 0.762 0.777 False False
pooled all b a 5 5 6.806 False 10.416 False 0.653 0.666 False False
pooled all b b 5 5 6.806 False 10.416 False 0.653 0.666 False False
pooled flagged dropped a a 3 3 7.736 False 10.669 False 0.725 0.739 False False
pooled flagged dropped a b 3 3 7.736 False 10.669 False 0.725 0.739 False False
pooled flagged dropped b a 3 3 6.527 False 10.134 False 0.644 0.656 False False
pooled flagged dropped b b 3 3 6.527 False 10.134 False 0.644 0.656 False False
within 1 to 1.5 (the wider reading) under any O1/O2 reading, by group: {('pooled', 'all'): False, ('pooled', 'flagged dropped'): False, ('x1', 'all'): False, ('x1', 'flagged dropped'): False, ('x3', 'all'): False, ('x3', 'flagged dropped'): False, ('x5', 'all'): False, ('x5', 'flagged dropped'): False}
Result: not reproduced. At x1 (2 bursts, adopted) there is one usable well, and its YP profile has no spot (the Lorentzian width runs to the 100-mm bound under every reading); its Pr area is one of the flagged ones, with a misplaced centre, and without it no x1 well is left. So at the S2 condition there is no YP spot to compare. At every other label the YP spot is narrower than the Pr spot under every reading: x3 0.68 to 0.69, x5 0.52 to 0.66 (0.50 to 0.59 without the flagged area), pooled 0.65 to 0.76 (0.644 to 0.725 without the flagged areas), all outside 1 to 1.5. The FWHM values (YP 4.8 to 8.3 mm, Pr 7.1 to 14.0 mm at x3 and x5) are far wider than the paper's (1.1 to 4 mm). This rests on the assumption that these wells had the electrodes touching the monolayer.
fig5b-gaussian-pr-lorentzian-yp¶
Pr intensity declined from the peak as a Gaussian and YP as a Lorentzian; for the 0-mm data the R2 was 0.92 (Lorentzian) against 0.96 (Gaussian) for Pr, and 0.98 against 0.95 for YP, fitted from -5 to 5 mm with p < 0.0001.
Same wells, groups and assumption as the previous claim; x1 decides. R2 = 1 - SSres / SStot over the fitted points, on the mean profiles in percent of their 0-mm value (the fits are unchanged by scale). Reproduced when every R2 is within tolerance (0.92 +/- 0.092, 0.96 +/- 0.096, 0.98 +/- 0.098, 0.95 +/- 0.095) and both orderings hold under every reading.
rows = []
for (gname, drop, o1, o2), (pos, my, pos2, mp) in s2fits.items():
my_pc, mp_pc = 100 * my / at(pos, my, 0), 100 * mp / at(pos2, mp, 0)
r = {"group": gname, "Pr centres": "flagged dropped" if drop else "all", "O1": o1, "O2": o2, "YP_0mm": at(pos, my, 0)}
for nm, x, y in (("Pr", pos2, mp_pc), ("YP", pos, my_pc)):
for fn, fun in (("L", cc.lorentz), ("G", cc.gauss)):
f = cc.fit(fun, x, y, 5)
r[f"{nm}_{fn}_R2"] = f["R2"] if f else np.nan
r[f"{nm}_{fn}_p"] = f["p"] if f else np.nan
r["within_tol"] = bool(abs(r["Pr_L_R2"] - 0.92) <= 0.092 and abs(r["Pr_G_R2"] - 0.96) <= 0.096 and
abs(r["YP_L_R2"] - 0.98) <= 0.098 and abs(r["YP_G_R2"] - 0.95) <= 0.095)
r["order_Pr_G_gt_L"] = bool(r["Pr_G_R2"] > r["Pr_L_R2"])
r["order_YP_L_gt_G"] = bool(r["YP_L_R2"] > r["YP_G_R2"])
rows.append(r)
t5b = pd.DataFrame(rows)
print(t5b.round(4).to_string(index=False))
print("\nevery O1/O2 reading, by group: within tolerance", t5b.groupby(["group", "Pr centres"]).within_tol.all().to_dict())
print("Pr ordering", t5b.groupby(["group", "Pr centres"]).order_Pr_G_gt_L.all().to_dict())
print("YP ordering", t5b.groupby(["group", "Pr centres"]).order_YP_L_gt_G.all().to_dict())
fig, axs = plt.subplots(2, 2, figsize=(13, 8))
xx = np.linspace(-7, 7, 200)
for row, gname in zip(axs, ("x1", "pooled")):
pos, my, pos2, mp = s2fits[(gname, False, "a", "a")]
for ax, (nm, x, y) in zip(row, (("YP", pos, 100 * my / at(pos, my, 0)), ("Pr", pos2, 100 * mp / at(pos2, mp, 0)))):
ax.plot(x, y, "ko", label=nm)
for fn, fun, col in (("Lorentzian", cc.lorentz, "g"), ("Gaussian", cc.gauss, "r")):
f = cc.fit(fun, x, y, 5)
ax.plot(xx, fun(xx, f["a"], f["w"]), col, label=f"{fn} R2={f['R2']:.3f}")
ax.set_title(f"{nm}, 0.2 MHz CANCAN wells with Pr, {gname} (O1 a, O2 a)")
ax.set_xlabel("distance (mm)")
ax.set_ylabel("% of 0-mm value")
ax.legend(fontsize=8)
print(gname, nm, "plotted range:", np.round(np.nanmin(y), 1), "to", np.round(np.nanmax(y), 1))
plt.tight_layout()
plt.show()
group Pr centres O1 O2 YP_0mm Pr_L_R2 Pr_L_p Pr_G_R2 Pr_G_p YP_L_R2 YP_L_p YP_G_R2 YP_G_p within_tol order_Pr_G_gt_L order_YP_L_gt_G
x1 all a a 3.3868 0.0005 0.9455 0.0006 0.9448 -0.0019 0.4694 -0.0002 0.4714 False True False
x1 all a b 3.3868 0.0094 0.7771 0.0096 0.7742 -0.0064 0.1664 -0.0008 0.1686 False True False
x1 all b a 3.3868 -0.0015 0.3358 -0.0002 0.3354 -0.0021 0.1184 -0.0003 0.1194 False True False
x1 all b b 3.3868 -0.0107 0.0049 -0.0013 0.0049 -0.0078 0.0011 -0.0010 0.0012 False True False
x3 all a a 352.6762 0.7662 0.0003 0.6659 0.0020 0.5026 0.0102 0.3672 0.0443 False False True
x3 all a b 352.6762 0.8126 0.0001 0.7062 0.0011 0.6893 0.0007 0.5037 0.0126 False False True
x3 all b a 352.6762 0.8002 0.0000 0.7298 0.0000 0.5918 0.0000 0.4850 0.0001 False False True
x3 all b b 352.6762 0.8861 0.0000 0.8081 0.0000 0.8514 0.0000 0.6978 0.0000 False False True
x3 flagged dropped a a 352.6762 0.7662 0.0003 0.6659 0.0020 0.5026 0.0102 0.3672 0.0443 False False True
x3 flagged dropped a b 352.6762 0.8126 0.0001 0.7062 0.0011 0.6893 0.0007 0.5037 0.0126 False False True
x3 flagged dropped b a 352.6762 0.8002 0.0000 0.7298 0.0000 0.5918 0.0000 0.4850 0.0001 False False True
x3 flagged dropped b b 352.6762 0.8861 0.0000 0.8081 0.0000 0.8514 0.0000 0.6978 0.0000 False False True
x5 all a a 1342.6460 0.5537 0.0086 0.5583 0.0082 0.7155 0.0010 0.6658 0.0022 False True True
x5 all a b 1342.6460 0.9854 0.0000 0.9937 0.0000 0.9276 0.0000 0.8631 0.0000 True True True
x5 all b a 1342.6460 0.5132 0.0001 0.5170 0.0001 0.7407 0.0000 0.6665 0.0000 False True True
x5 all b b 1342.6460 0.8458 0.0000 0.8521 0.0000 0.8291 0.0000 0.7461 0.0000 False True True
x5 flagged dropped a a 2523.6931 0.3528 0.0540 0.3469 0.0566 0.6814 0.0017 0.6361 0.0032 False False True
x5 flagged dropped a b 2523.6931 0.9039 0.0000 0.8888 0.0000 0.9274 0.0000 0.8657 0.0000 True False True
x5 flagged dropped b a 2523.6931 0.3705 0.0012 0.3616 0.0015 0.7329 0.0000 0.6606 0.0000 False False True
x5 flagged dropped b b 2523.6931 0.7196 0.0000 0.7024 0.0000 0.8295 0.0000 0.7477 0.0000 False False True
pooled all a a 678.8063 0.7930 0.0002 0.7607 0.0005 0.6845 0.0015 0.6190 0.0040 False False True
pooled all a b 678.8063 0.9258 0.0000 0.8880 0.0000 0.8815 0.0000 0.7972 0.0002 False False True
pooled all b a 678.8063 0.7709 0.0000 0.7427 0.0000 0.7467 0.0000 0.6618 0.0000 False False True
pooled all b b 678.8063 0.9118 0.0000 0.8783 0.0000 0.8384 0.0000 0.7431 0.0000 False False True
pooled flagged dropped a a 1076.3485 0.6944 0.0014 0.6491 0.0027 0.6534 0.0024 0.5898 0.0056 False False True
pooled flagged dropped a b 1076.3485 0.8681 0.0000 0.8115 0.0002 0.8822 0.0000 0.7964 0.0002 False False True
pooled flagged dropped b a 1076.3485 0.7104 0.0000 0.6668 0.0000 0.7406 0.0000 0.6560 0.0000 False False True
pooled flagged dropped b b 1076.3485 0.8625 0.0000 0.8095 0.0000 0.8440 0.0000 0.7476 0.0000 False False True
every O1/O2 reading, by group: within tolerance {('pooled', 'all'): False, ('pooled', 'flagged dropped'): False, ('x1', 'all'): False, ('x3', 'all'): False, ('x3', 'flagged dropped'): False, ('x5', 'all'): False, ('x5', 'flagged dropped'): False}
Pr ordering {('pooled', 'all'): False, ('pooled', 'flagged dropped'): False, ('x1', 'all'): True, ('x3', 'all'): False, ('x3', 'flagged dropped'): False, ('x5', 'all'): True, ('x5', 'flagged dropped'): False}
YP ordering {('pooled', 'all'): True, ('pooled', 'flagged dropped'): True, ('x1', 'all'): False, ('x3', 'all'): True, ('x3', 'flagged dropped'): True, ('x5', 'all'): True, ('x5', 'flagged dropped'): True}
x1 YP plotted range: -85.4 to 1154.1
x1 Pr plotted range: -41.7 to 303.4
pooled YP plotted range: 6.4 to 102.3
pooled Pr plotted range: 7.1 to 100.0
Result: not reproduced. At x1 (2 bursts, adopted, one well) all four R2 values are between -0.011 and 0.010 under every reading: neither profile has a shape a Lorentzian or a Gaussian fits (the x1 plots run from -85 to 1154% of the near-zero 0-mm value). At x3 no reading has all four within tolerance and the Pr ordering fails under every reading; at x5 all four are within tolerance with both orderings only under O1 a, O2 b, and without the flagged Pr area the Pr ordering fails under every reading. Pooled, no reading is within tolerance and the Pr ordering fails. This rests on the assumption that these wells had the electrodes touching the monolayer.
fig7be-peak-grows-with-bursts (blocked): the CT26 half¶
The Figure 7 claims need CT26 and CHO burst series. The CHO files carry one burst label, so the CHO half is missing and the claims are blocked. The CT26 half is tested here on the 0.2 MHz CANCAN wells (labels x1, x3, x5) under both label readings, and decides no outcome. Criterion (CT26 part): central YP per well against burst number, linear least squares, the 6-burst point left out of the fit (Figure 7B legend), slope tested two-sided, (O4 a) per well or (b) group means; saturation when the 6-burst mean lies below the 95% band of the fit extrapolated to 6 bursts. Under the total reading there is no 6-burst point.
def linfit(x, y):
"""Least-squares line with the two-sided t test of its slope, and the 95% band of the fitted mean at x0."""
x, y = np.asarray(x, float), np.asarray(y, float)
lr = stats.linregress(x, y)
n = len(x)
band = None
if n > 2:
s = np.sqrt(((y - lr.intercept - lr.slope * x) ** 2).sum() / (n - 2))
sxx = ((x - x.mean()) ** 2).sum()
band = lambda x0: stats.t.ppf(0.975, n - 2) * s * np.sqrt(1 / n + (x0 - x.mean()) ** 2 / sxx)
return lr, band
c7 = {lab: sel("0.2 MHz", "CANCAN", lab) for lab in ("x1", "x3", "x5")}
print("CT26 0.2 MHz CANCAN wells by label:", {k: len(v) for k, v in c7.items()})
rows = []
for reading, bmap in cc.BURSTS.items():
for o2 in "ab":
v = {lab: per_area(c7[lab], "diag", "a", o2, xs=[0]) for lab in c7}
six = [lab for lab in c7 if bmap[lab] == 6]
fit_labs = [lab for lab in c7 if bmap[lab] != 6] # Figure 7B legend: the 6-burst point is left out of the fit
for o4 in ("per-experiment", "means"):
if o4 == "per-experiment":
x = np.concatenate([[bmap[l]] * len(v[l]) for l in fit_labs]); y = np.concatenate([v[l] for l in fit_labs])
else:
x = np.array([bmap[l] for l in fit_labs]); y = np.array([v[l].mean() for l in fit_labs])
lr, band = linfit(x, y)
r = {"labels": reading, "O2": o2, "O4": o4, "bursts fitted": sorted(set(x.astype(int).tolist())), "points": len(x),
"slope": lr.slope, "p": lr.pvalue if len(x) > 2 else np.nan}
if six:
m6, pred = v[six[0]].mean(), lr.intercept + lr.slope * 6
r.update({"6-burst mean": m6, "fit at 6": pred, "band half-width": band(6) if band else np.nan,
"saturates": bool(band and m6 < pred - band(6))})
rows.append(r)
print(f"{reading}, O2 {o2}: centre YP (a.u.) by bursts:",
{bmap[l]: f"{v[l].mean():.1f} +/- {v[l].std(ddof=1) / np.sqrt(len(v[l])):.1f} (n={len(v[l])})" for l in c7})
t7b = pd.DataFrame(rows)
print(t7b.round(4).to_string(index=False))
CT26 0.2 MHz CANCAN wells by label: {'x1': 4, 'x3': 2, 'x5': 6}
after_first, O2 a: centre YP (a.u.) by bursts: {2: '-1.4 +/- 6.3 (n=4)', 4: '352.7 +/- 253.6 (n=2)', 6: '2275.3 +/- 921.8 (n=6)'}
after_first, O2 b: centre YP (a.u.) by bursts: {2: '-1.4 +/- 6.3 (n=4)', 4: '352.7 +/- 253.6 (n=2)', 6: '2275.3 +/- 921.8 (n=6)'}
total, O2 a: centre YP (a.u.) by bursts: {1: '-1.4 +/- 6.3 (n=4)', 3: '352.7 +/- 253.6 (n=2)', 5: '2275.3 +/- 921.8 (n=6)'}
total, O2 b: centre YP (a.u.) by bursts: {1: '-1.4 +/- 6.3 (n=4)', 3: '352.7 +/- 253.6 (n=2)', 5: '2275.3 +/- 921.8 (n=6)'}
labels O2 O4 bursts fitted points slope p 6-burst mean fit at 6 band half-width saturates
after_first a per-experiment [2, 4] 6 177.0427 0.0851 2275.3182 706.7615 748.0714 False
after_first a means [2, 4] 2 177.0427 NaN 2275.3182 706.7615 NaN False
after_first b per-experiment [2, 4] 6 177.0427 0.0851 2275.3182 706.7615 748.0714 False
after_first b means [2, 4] 2 177.0427 NaN 2275.3182 706.7615 NaN False
total a per-experiment [1, 3, 5] 12 582.7039 0.0507 NaN NaN NaN NaN
total a means [1, 3, 5] 3 569.1818 0.2410 NaN NaN NaN NaN
total b per-experiment [1, 3, 5] 12 582.7039 0.0507 NaN NaN NaN NaN
total b means [1, 3, 5] 3 569.1818 0.2410 NaN NaN NaN NaN
Result (CT26 half, decides no outcome). Central YP rises with the label (-1.4, 353 and 2275 a.u. at x1, x3, x5), but the slope over the fitted points is not significant: 177 a.u. per burst over 2 and 4 bursts, p = 0.085 (n = 6 wells) under the adopted reading, and 583 a.u. per burst over 1, 3 and 5 bursts, p = 0.051 (n = 12), under the total reading. Under the adopted reading the 6-burst mean (2275 a.u.) is far above the fit extrapolated to 6 bursts (707 +/- 748), the reverse of saturation. The CHO half is missing, so the claim stays blocked.
fig7cf-fwhm-bursts (blocked): the CT26 half¶
Per-well Lorentzian FWHM against burst number (fit limits 4, 5 and 6 mm, O3), linear least squares, slope tested two-sided; the 6-burst point included or left out, fits at the width bound kept or dropped, per well or group means (O4). Decides no outcome; the CHO half is missing.
rows, fwrows = [], []
for o2 in "ab":
for lim in (4, 5, 6):
for lab, s in c7.items():
for r in s.itertuples():
pos, y = area_profile((r.file, r.side), "diag", "a", o2)
f = cc.fit(cc.lorentz, pos, y, lim)
fwrows.append({"O2": o2, "fit_limit_mm": lim, "label": lab, "file": r.file, "side": r.side,
"FWHM_mm": f["w"] if f else np.nan, "at_bound": at_bound(f)})
fw7 = pd.DataFrame(fwrows)
print("per-area Lorentzian FWHM (mm), O1 a, by O2 reading, fit limit (O3) and label: mean, n, fits at the width bound")
print(fw7.groupby(["O2", "fit_limit_mm", "label"]).agg(mean=("FWHM_mm", "mean"), n=("FWHM_mm", "size"), at_bound=("at_bound", "sum")).round(2).to_string())
for reading, bmap in cc.BURSTS.items():
for (o2, lim), g in fw7.groupby(["O2", "fit_limit_mm"]):
for six in ("included", "excluded"):
if six == "excluded" and 6 not in bmap.values():
continue
for drop_bound in (False, True):
gg = g[~g.at_bound] if drop_bound else g
gg = gg[gg.label.map(bmap) != 6] if six == "excluded" else gg
for o4 in ("per-experiment", "means"):
if o4 == "per-experiment":
x, y = gg.label.map(bmap).values, gg.FWHM_mm.values
else:
mm = gg.groupby("label").FWHM_mm.mean()
x, y = mm.index.map(bmap).values, mm.values
if len(set(x)) < 2:
continue
lr, _ = linfit(x, y)
rows.append({"labels": reading, "O2": o2, "O3 limit": lim, "6 bursts": six, "fits at bound": "dropped" if drop_bound else "kept",
"O4": o4, "points": len(x), "slope_mm_per_burst": lr.slope, "p": lr.pvalue if len(x) > 2 else np.nan})
t7c = pd.DataFrame(rows)
print(t7c.round(4).to_string(index=False))
print("\nslope positive with p < 0.001:", int(((t7c.slope_mm_per_burst > 0) & (t7c.p < 0.001)).sum()), "of", len(t7c),
"; positive with p < 0.05:", int(((t7c.slope_mm_per_burst > 0) & (t7c.p < 0.05)).sum()))
per-area Lorentzian FWHM (mm), O1 a, by O2 reading, fit limit (O3) and label: mean, n, fits at the width bound
mean n at_bound
O2 fit_limit_mm label
a 4 x1 55.70 4 2
x3 51.38 2 1
x5 24.16 6 1
5 x1 54.74 4 2
x3 51.45 2 1
x5 31.12 6 1
6 x1 68.04 4 2
x3 19.76 2 0
x5 29.82 6 1
b 4 x1 55.70 4 2
x3 51.38 2 1
x5 24.16 6 1
5 x1 54.74 4 2
x3 51.45 2 1
x5 31.12 6 1
6 x1 68.05 4 2
x3 19.75 2 0
x5 29.83 6 1
labels O2 O3 limit 6 bursts fits at bound O4 points slope_mm_per_burst p
after_first a 4 included kept per-experiment 12 -8.0825 0.2863
after_first a 4 included kept means 3 -7.8851 0.2527
after_first a 4 included dropped per-experiment 8 -0.3651 0.7981
after_first a 4 included dropped means 3 -0.6024 0.8258
after_first a 4 excluded kept per-experiment 6 -2.1597 0.9339
after_first a 4 excluded kept means 2 -2.1597 NaN
after_first a 4 excluded dropped per-experiment 3 -4.3194 0.6879
after_first a 4 excluded dropped means 2 -4.3194 NaN
after_first a 5 included kept per-experiment 12 -6.0520 0.4211
after_first a 5 included kept means 3 -5.9052 0.2511
after_first a 5 included dropped per-experiment 8 2.3013 0.5057
after_first a 5 included dropped means 3 1.9656 0.6343
after_first a 5 excluded kept per-experiment 6 -1.6473 0.9500
after_first a 5 excluded kept means 2 -1.6473 NaN
after_first a 5 excluded dropped per-experiment 3 -3.2947 0.7007
after_first a 5 excluded dropped means 2 -3.2947 NaN
after_first a 6 included kept per-experiment 12 -9.0519 0.1742
after_first a 6 included kept means 3 -9.5550 0.4599
after_first a 6 included dropped per-experiment 9 -4.7315 0.3318
after_first a 6 included dropped means 3 -5.0748 0.2152
after_first a 6 excluded kept per-experiment 6 -24.1427 0.2525
after_first a 6 excluded kept means 2 -24.1427 NaN
after_first a 6 excluded dropped per-experiment 4 -8.1648 0.7089
after_first a 6 excluded dropped means 2 -8.1648 NaN
after_first b 4 included kept per-experiment 12 -8.0825 0.2863
after_first b 4 included kept means 3 -7.8851 0.2527
after_first b 4 included dropped per-experiment 8 -0.3651 0.7981
after_first b 4 included dropped means 3 -0.6024 0.8258
after_first b 4 excluded kept per-experiment 6 -2.1597 0.9339
after_first b 4 excluded kept means 2 -2.1597 NaN
after_first b 4 excluded dropped per-experiment 3 -4.3195 0.6879
after_first b 4 excluded dropped means 2 -4.3195 NaN
after_first b 5 included kept per-experiment 12 -6.0520 0.4211
after_first b 5 included kept means 3 -5.9052 0.2511
after_first b 5 included dropped per-experiment 8 2.3013 0.5057
after_first b 5 included dropped means 3 1.9655 0.6343
after_first b 5 excluded kept per-experiment 6 -1.6474 0.9500
after_first b 5 excluded kept means 2 -1.6474 NaN
after_first b 5 excluded dropped per-experiment 3 -3.2948 0.7007
after_first b 5 excluded dropped means 2 -3.2948 NaN
after_first b 6 included kept per-experiment 12 -9.0517 0.1742
after_first b 6 included kept means 3 -9.5549 0.4600
after_first b 6 included dropped per-experiment 9 -4.7312 0.3319
after_first b 6 included dropped means 3 -5.0749 0.2154
after_first b 6 excluded kept per-experiment 6 -24.1458 0.2524
after_first b 6 excluded kept means 2 -24.1458 NaN
after_first b 6 excluded dropped per-experiment 4 -8.1685 0.7088
after_first b 6 excluded dropped means 2 -8.1685 NaN
total a 4 included kept per-experiment 12 -8.0825 0.2863
total a 4 included kept means 3 -7.8851 0.2527
total a 4 included dropped per-experiment 8 -0.3651 0.7981
total a 4 included dropped means 3 -0.6024 0.8258
total a 5 included kept per-experiment 12 -6.0520 0.4211
total a 5 included kept means 3 -5.9052 0.2511
total a 5 included dropped per-experiment 8 2.3013 0.5057
total a 5 included dropped means 3 1.9656 0.6343
total a 6 included kept per-experiment 12 -9.0519 0.1742
total a 6 included kept means 3 -9.5550 0.4599
total a 6 included dropped per-experiment 9 -4.7315 0.3318
total a 6 included dropped means 3 -5.0748 0.2152
total b 4 included kept per-experiment 12 -8.0825 0.2863
total b 4 included kept means 3 -7.8851 0.2527
total b 4 included dropped per-experiment 8 -0.3651 0.7981
total b 4 included dropped means 3 -0.6024 0.8258
total b 5 included kept per-experiment 12 -6.0520 0.4211
total b 5 included kept means 3 -5.9052 0.2511
total b 5 included dropped per-experiment 8 2.3013 0.5057
total b 5 included dropped means 3 1.9655 0.6343
total b 6 included kept per-experiment 12 -9.0517 0.1742
total b 6 included kept means 3 -9.5549 0.4600
total b 6 included dropped per-experiment 9 -4.7312 0.3319
total b 6 included dropped means 3 -5.0749 0.2154
slope positive with p < 0.001: 0 of 72 ; positive with p < 0.05: 0
Result (CT26 half, decides no outcome). No combination of readings gives a positive slope with p < 0.05 (0 of 72); most slopes are negative. Many per-well fits have no peak (2 of 4 at x1, up to 1 of 2 at x3 and 1 of 6 at x5 at the width bound), and mean FWHM is about 20 to 68 mm, against the paper's 1.1 mm and up. The CHO half is missing, so the claim stays blocked.
fig7-sharpest-one-burst (blocked)¶
One burst (x4) in CT26 cells gave the sharpest peak of the study, FWHM 1.1 +/- 0.1 mm, 7.3% of the inter-electrode gap and 2.3% of the array area, with YP near the electrodes 40 to 50 times smaller than at the centre.
The claim is CT26 only, and needs 1 burst (x4). Under the adopted reading of the labels there is no 1-burst well (x1 is 2 bursts), so the claim is blocked for the labels. Under the total reading x1 is 1 burst; the result there is shown. FWHM per well with every fit limit of O3; the ratio from the fig4b-40-fold-ratio table (O5 6 or 7 mm).
one = fw7[fw7.label == "x1"]
print("0.2 MHz CANCAN x1 (1 burst (x4) under the total reading; 2 bursts under the adopted reading), per-area FWHM by O2 and O3:")
s = one.groupby(["O2", "fit_limit_mm"]).agg(mean=("FWHM_mm", "mean"), se=("FWHM_mm", lambda v: v.std(ddof=1) / np.sqrt(len(v))),
n=("FWHM_mm", "size"), at_bound=("at_bound", "sum"))
s["percent_of_15.3mm"] = 100 * s["mean"] / cc.DIAG_GAP_MM
print(s.round(2).to_string())
print("FWHM within 1.1 +/- 0.1 mm under any reading:", bool(((s["mean"] - 1.1).abs() <= 0.1).any()))
r1 = t40[t40.label == "x1"]
print("centre to near-electrode ratio, x1, every O1/O2/O5 reading:", r1.ratio.round(2).tolist(), "; y0:", r1.y0.round(1).unique().tolist())
print("ratio within 36 to 55 under any reading:", bool(((r1.ratio >= 36) & (r1.ratio <= 55)).any()))
print(f"\nnotebook ran in {(time.time() - started) / 60:.1f} min")
0.2 MHz CANCAN x1 (1 burst (x4) under the total reading; 2 bursts under the adopted reading), per-area FWHM by O2 and O3:
mean se n at_bound percent_of_15.3mm
O2 fit_limit_mm
a 4 55.70 25.86 4 2 364.06
5 54.74 26.31 4 2 357.79
6 68.04 23.10 4 2 444.73
b 4 55.70 25.86 4 2 364.06
5 54.74 26.31 4 2 357.79
6 68.05 23.10 4 2 444.74
FWHM within 1.1 +/- 0.1 mm under any reading: False
centre to near-electrode ratio, x1, every O1/O2/O5 reading: [-0.15, -0.29, -0.15, -0.29, -0.16, -0.28, -0.16, -0.28] ; y0: [-1.4]
ratio within 36 to 55 under any reading: False
notebook ran in 5.8 min
Result: blocked (labels). Under the adopted reading there is no 1-burst well. Under the total reading, x1 would be 1 burst, and it is far from the claim: mean FWHM 55 to 68 mm (2 of 4 per-well fits at the width bound under each reading), 358 to 445% of 15.3 mm, against 1.1 +/- 0.1 mm and 7.3%, and the centre to near-electrode ratio is -0.29 to -0.15 against 36 to 55.
Summary¶
| Claim | Outcome |
|---|---|
| fig2-heating | Blocked: data missing (no film images) |
| fig3a-centre-enhanced | Not reproduced |
| fig3bc-rate-suppresses-off-target | Not reproduced (assumes x1 is 2 bursts) |
| fig3-lorentzian-profile | Not reproduced |
| fig3-tail-deviation-falls-with-rate | Not reproduced (assumes x1 is 2 bursts) |
| fig4b-centre-no-rate-effect | Partially reproduced (assumes x1 is 2 bursts) |
| fig4b-near-electrode-decline | Not reproduced (assumes x1 is 2 bursts and the longer-arm reading of F8) |
| fig4b-40-fold-ratio | Not reproduced |
| fig4c-fwhm-falls-with-rate | Blocked: data missing (no 1 kHz or 10 kHz) |
| fig4c-log-trend | Blocked: data missing (no 1 kHz or 10 kHz) |
| fig5-raised-electrodes-weaken-centre | Blocked: metadata insufficient (no elevation given) |
| fig5ab-yp-spot-slightly-larger-than-pr | Not reproduced (assumes electrodes touching) |
| fig5b-gaussian-pr-lorentzian-yp | Not reproduced (assumes electrodes touching) |
| fig5c-nuclear-yp-centre | Blocked: data missing (no full-resolution images) |
| fig6b, fig6c, fig6d | Blocked: data missing (600-ns pulses only) |
| fig7be, fig7cf | Blocked: data missing (no CHO burst series) |
| fig7-sharpest-one-burst | Blocked: metadata insufficient (burst labels) |
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.
How the analysis differs from the paper's. The array centre is found from the electrode footprints in the YP image, or, in faint images, from the centroid of the stain; the paper placed ROIs by hand in MetaMorph. Six areas have visibly misplaced centres and are flagged; the claims they enter are also shown without them, with the same outcomes. Pixel sizes for the CT26 images are inferred from the array geometry. The burst number is read from the labels as repeats after the first burst, so x1 is the paper's 2 bursts (x4), an assumption; the other reading (the label is the total) is shown and leaves no 0.2 MHz group at 2 bursts. The paper's error-bar rule (F8) is read as a gap beyond the longer arm, with the other readings shown. Every rate claim can use only 1 Hz and 0.2 MHz.
What was found about the dataset. The protocol text in the dandiset and every file (3.8-mm electrode spacing, 3.2-kV first pulse, 75/50/25% steps) does not describe the paper's array or packets, and the CT26 images show the paper's 15.3-mm array; the voltage cannot be checked from the images. The burst number is given only as a subject label, all but one file say "Unknown repetitions", and the CT26 files state no pixel size or electrode elevation. The dataset holds no images at 1 kHz or 10 kHz, no other pulse durations, no film images and no CHO burst series, and it includes a 1 Hz CHO set the paper does not report. At x1, the 0.2 MHz CANCAN wells are unstained everywhere (group-mean profile within 32 a.u. of background), where the paper reports a stained centre at 2 bursts and a sharp spot even at 1 burst, so the images in the dandiset may not be the experiments of the paper's figures.