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

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

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

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

In [5]:
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)
In [6]:
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")
No description has been provided for this image
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.

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

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

In [9]:
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
No description has been provided for this image
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.

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

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

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

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

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

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

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

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

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

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

In [20]:
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
No description has been provided for this image

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.

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

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

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