Reproducing Pakhomov et al. (2023) from dandiset 001025¶
Paper. Pakhomov, Gudvangen, Mangalanathan, Kondratiev, Redondo and Semenov (2023), "Next generation CANCAN focusing for remote stimulation by nanosecond electric pulses", Bioelectrochemistry 152, 108437, doi:10.1016/j.bioelechem.2023.108437.
Dataset. DANDI:001025, "The impact of voltage on CANCAN focusing of electroporation using four-electrode arrays of varying sizes", version 0.240605.1826 (doi:10.48324/dandi.001025/0.240605.1826). The paper cites no version of the dataset. This is the only published version, and its 21 assets are the same as the draft's (same count and size), so it is the most recent published version and the one pinned.
Text read. The claims file was written from the accepted author manuscript in PubMed Central (PMC10247516), with no supplement and no linked code. This attempt read the same text.
What the dataset holds. One stitched YO-PRO-1 fluorescence image per dish, taken after the
nsEP exposure, with the first-pulse voltage and the number of packets per electrode in the
session description. There are no ROI measurements, electrode positions or pixel size, so the
notebook finds the electrodes in each image and places the paper's ROIs itself (helper
cancan.py). The images are the experiments of Fig. 7: Packet 2 rotated once (1Px4) at 2.4 to
5.6 kV, and rotated ten times (10Px4) at 4.8 and 6.4 kV.
| Claim | Tested here |
|---|---|
| fig4-unipolar-strongest | blocked, data_missing (single-cell time-lapse data not in the dandiset) |
| fig4-bipolar-cancellation | blocked, data_missing (as above) |
| fig5a-center-enhanced-without-rotation | blocked, other (no 40-packet or 40-train dishes here; the Fig. 5 images appear to be in dandiset 000686) |
| fig5bc-rotation-focuses-center | blocked, other (no 1Px40 or 1Tx40 dishes here; dandiset 000686) |
| fig6a-more-packets-more-uptake | blocked, other (no 5Px4 dishes; 1Px4 and 10Px4 are on different arrays here; dandiset 000875) |
| fig6b-normalized-center-by-packets | blocked, other (as above) |
| fig7a-amplitude-scales-uptake | tested |
| fig7b-targeting-independent-of-amplitude | tested |
| fig7cd-holds-for-large-array | tested (two of the paper's voltages, see below) |
| discussion-independent-of-array-size | tested (follows from the two above) |
| fig8a-packets-differ-near-electrodes | blocked, other (no Packet 1 or Packet 3 dishes here; dandiset 001027) |
| fig8b-packet2-best-targeting | blocked, other (as above) |
| fig9-gap-boosts-targeting | blocked, other (no dishes with the array above the monolayer here; dandiset 001028) |
| fig9c-restricted-to-center | blocked, other (as above) |
The images of Figs. 5, 6, 8 and 9 appear, from their titles and descriptions (printed below), to be
in four other dandisets that cite the paper (000686, 000875,
001027 and 001028); the claims file gives every claim as data from this dandiset, so those claims
are blocked here with other. The 4.0 kV 1Px4 and 6.4 kV 10Px4 dishes here match the Packet 2
conditions of Figs. 8 and 9 (for Fig. 8, if it used the small array, which its legend does not
say); their values are set against the paper's in a section after the
tested claims, without deciding those claims.
Random numbers are drawn only in the fig7b section, for a simulation of the chance of a false positive among its tests, from its own generator with a fixed seed (2023).
import itertools
import time
import h5py
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import remfile
from scipy import stats
import cancan as cc
pd.set_option("display.width", 250)
pd.set_option("display.max_rows", 300)
pd.set_option("display.max_columns", 40)
print("dandiset", cc.DANDISET, "version", cc.VERSION)
dandiset 001025 version 0.240605.1826
Loading¶
Each image is streamed from the archive: the compressed HDF5 chunks of the YP channel (channel 0, "FITC") are fetched with range requests and reduced, as they arrive, to sums and counts of non-zero pixels in 4 x 4 blocks; nothing is written to disk. For each dish the electrodes are located on a 16 x 16 block image, and pixel sums are binned along the two diagonals of the array, from which every ROI layout below is computed, and the radius of each needle footprint is measured. This cell takes about 12 minutes.
assets = cc.list_assets()
res = {}
t0 = time.time()
for a in assets:
url = cc.s3_url(a["asset_id"])
f = h5py.File(remfile.File(url), "r")
ims = f["acquisition/ScanningImaging"]
info = dict(
path=a["path"],
subject=f["general/subject/subject_id"][()].decode(),
start=f["session_start_time"][()].decode()[:19],
description=f["session_description"][()].decode(),
imaging=ims.attrs["description"],
resolution_attr=float(ims["data"].attrs["resolution"]),
unit=ims["data"].attrs["unit"],
n_channels=ims["data"].shape[0],
)
f.close()
info["kV"], info["reps"] = cc.parse_description(info["description"])
s, n, st = cc.read_image(url, channel=0)
img, zero = cc.coarse(s, n)
size, pts, scores = cc.find_electrodes(img, zero)
# strips wide and long enough for every ROI layout and pixel-size reading used below
span = cc.SPAN_MM[size] * 1000 / cc.CAMERA_UM_PER_PX / cc.F
fp = cc.footprint_radius(s, n, pts) # dark footprint of each needle, to check the pixel size
hists, bin_px = cc.strip_histograms(s, n, pts, 1.25 * span, 1.25 * span / (cc.N_ROI[size] / 2) + 2)
res[a["path"]] = dict(info, size=size, pts=pts, scores=scores, hists=hists, fp=fp, img=img, zero=zero, stats=st,
nonzero_mean=s.sum() / max(n.sum(), 1))
del s, n
print(f"{len(res)} images streamed and reduced in {(time.time() - t0) / 60:.1f} min")
21 images streamed and reduced in 11.5 min
Data check¶
rows = []
for p, r in res.items():
d1 = np.hypot(*(r["pts"][0] - r["pts"][2])) * cc.FD
d2 = np.hypot(*(r["pts"][1] - r["pts"][3])) * cc.FD
rows.append(dict(
file=p.split("/")[1].replace("_image.nwb", ""), subject=r["subject"], start=r["start"],
kV=r["kV"], packets_per_electrode=r["reps"], channels=r["n_channels"],
shape=r["stats"]["shape"], zero_frac=round(r["stats"]["zero_px"] / np.prod(r["stats"]["shape"][1:]), 3),
max=r["stats"]["max"], mean_nonzero=round(r["nonzero_mean"], 1),
array=r["size"], fit_small=round(r["scores"]["small"], 2), fit_large=round(r["scores"]["large"], 2),
diag1_px=round(d1), diag2_px=round(d2),
))
dishes = pd.DataFrame(rows).sort_values(["packets_per_electrode", "kV", "start"]).reset_index(drop=True)
print(dishes.to_string())
print("\nDishes per condition (array found from the electrode spacing):")
print(dishes.groupby(["packets_per_electrode", "array", "kV"]).size().rename("dishes").to_string())
print("\nFiles per subject id:", dishes.subject.value_counts().to_dict())
print("Distinct subject ids:", dishes.subject.nunique(), "for", len(dishes), "files")
print("\nImaging descriptions:")
for k, v in pd.Series([r["imaging"].split("Additional metadata: ")[-1] for r in res.values()]).value_counts().items():
print(f" {v} files: {k}")
print("ImageSeries unit:", {r["unit"] for r in res.values()}, "; 'resolution' attribute:", {r["resolution_attr"] for r in res.values()})
# the array size is not in the metadata; check the geometric classification against the protocol
ok = ((dishes.packets_per_electrode == 1) & (dishes.array == "small")) | ((dishes.packets_per_electrode == 10) & (dishes.array == "large"))
print("\nEvery 1Px4 dish is on the small array and every 10Px4 dish on the large one (as Fig. 7 legend):", bool(ok.all()))
# pixel size: two readings
large = dishes[dishes.array == "large"]
small = dishes[dishes.array == "small"]
large_diag_px = large[["diag1_px", "diag2_px"]].values.mean()
small_diag_px = small[["diag1_px", "diag2_px"]].values.mean()
um = {"camera": cc.CAMERA_UM_PER_PX, "diag8.4": cc.LARGE_DIAGONAL_MM * 1000 / large_diag_px}
print(f"\nMean centre-to-centre diagonal: large {large_diag_px:.0f} px (sd {large[['diag1_px','diag2_px']].values.std():.0f}), "
f"small {small_diag_px:.0f} px (sd {small[['diag1_px','diag2_px']].values.std():.0f}); ratio {large_diag_px / small_diag_px:.2f} "
"(paper: 'approximately a 2-fold size difference')")
for k, v in um.items():
print(f" pixel size {k}: {v:.3f} um -> large diagonal {large_diag_px * v / 1000:.2f} mm, small {small_diag_px * v / 1000:.2f} mm, "
f"image width {20480 * v / 1000:.1f} mm")
print("\nWhere the paper's ROI span ends against the needles (radius 0.75 mm, Section 2.3) and the paper's exclusion zone"
" (5% of the diagonal, Section 2.4):")
for k, v in um.items():
for size, dpx in (("small", small_diag_px), ("large", large_diag_px)):
diag_mm = dpx * v / 1000
edge = diag_mm / 2 - cc.NEEDLE_RADIUS_MM
print(f" {k}, {size} array: needle edge {edge:.2f} mm from the centre; span ends at {cc.SPAN_MM[size]} mm, "
f"{edge - cc.SPAN_MM[size]:+.2f} mm from the needle edge (negative: on the needle); "
f"exclusion zone {cc.EXCLUDE_FRAC * diag_mm:.2f} mm")
# needle footprints: radius of the dark disk around each electrode centre, against the 0.75 mm needle radius
fpr = pd.DataFrame([dict(file=p.split("/")[1].replace("_image.nwb", ""), array=r["size"], kV=r["kV"], electrode=k,
r_mm_camera=x["r_px"] * cc.F * cc.CAMERA_UM_PER_PX / 1000, inside=x["inside"], outside=x["outside"])
for p, r in res.items() for k, x in enumerate(r["fp"])])
print("\nNeedle footprint radius at the camera scale (mm), per dish (4 electrodes), with the median a.u. inside and around:")
print(fpr.groupby(["array", "kV", "file"]).agg(r_min=("r_mm_camera", "min"), r_median=("r_mm_camera", "median"),
r_max=("r_mm_camera", "max"), inside=("inside", "median"),
outside=("outside", "median")).round(3).to_string())
print("Not found:", int(fpr.r_mm_camera.isna().sum()), "of", len(fpr))
for size, g in fpr.groupby("array"):
med = g.r_mm_camera.median()
print(f" {size} array: median radius {med:.3f} mm (IQR {g.r_mm_camera.quantile(0.25):.3f} to {g.r_mm_camera.quantile(0.75):.3f}), "
f"so a 0.75 mm needle radius implies {cc.CAMERA_UM_PER_PX * cc.NEEDLE_RADIUS_MM / med:.3f} um per pixel "
f"(camera reading 0.650, 8.4 mm reading {um['diag8.4']:.3f})")
# per-array scale implied by the footprints, used only as a variant below
um["footprint"] = {size: cc.CAMERA_UM_PER_PX * cc.NEEDLE_RADIUS_MM / g.r_mm_camera.median() for size, g in fpr.groupby("array")}
for size, dpx in (("small", small_diag_px), ("large", large_diag_px)):
v = um["footprint"][size]
diag_mm = dpx * v / 1000
edge = diag_mm / 2 - cc.NEEDLE_RADIUS_MM
print(f" footprint scale {v:.3f} um, {size} array: diagonal {diag_mm:.2f} mm (paper, large array: 8.4 mm); needle edge {edge:.2f} mm "
f"from the centre, span ends {edge - cc.SPAN_MM[size]:+.2f} mm from it; exclusion zone {cc.EXCLUDE_FRAC * diag_mm:.2f} mm")
# duplicates: correlation of the 16x16 block images over the area imaged in both
names = list(res)
cmax = []
for i, j in itertools.combinations(range(len(names)), 2):
a, b = res[names[i]], res[names[j]]
m = (a["zero"] < 0.01) & (b["zero"] < 0.01)
cmax.append((np.corrcoef(a["img"][m], b["img"][m])[0, 1], names[i].split("/")[1], names[j].split("/")[1]))
cmax.sort(reverse=True)
print("\nHighest correlations between two dishes' images (1 would be a duplicate):")
for c in cmax[:3]:
print(f" {c[0]:.3f} {c[1]} {c[2]}")
print("Identical mean intensities:", dishes.mean_nonzero.duplicated().sum())
# dark floor by imaging session (0.1st percentile of imaged pixels, 16x16 blocks)
dishes["floor_p0.1"] = [round(float(np.percentile(res[p]["img"][res[p]["zero"] < 0.01], 0.1)), 1) for p in
[next(q for q in res if q.split("/")[1].replace("_image.nwb", "") == f) for f in dishes.file]]
dishes["date"] = dishes.start.str[:10]
print("\nDark floor of the images (0.1st percentile, a.u.) by imaging date and condition:")
print(dishes.groupby(["date", "packets_per_electrode", "kV"])["floor_p0.1"].agg(["count", "min", "max"]).to_string())
print("\nNo data stream has a clock: each file is one image at one time point, so there is no timing to check.")
file subject start kV packets_per_electrode channels shape zero_frac max mean_nonzero array fit_small fit_large diag1_px diag2_px
0 sub-15_obj-t7o4u3 15 2021-04-02T14:07:47 2.4 1 1 (1, 20480, 20480) 0.259 65438 167.6 small -0.25 -0.21 7904 7306
1 sub-16 16 2021-04-02T14:09:23 2.4 1 1 (1, 20480, 20480) 0.259 63143 176.0 small -0.18 -0.14 7431 7457
2 sub-17 17 2021-04-02T14:10:48 2.4 1 1 (1, 20480, 20480) 0.259 65435 185.7 small -0.21 -0.12 7976 7538
3 sub-21 21 2021-04-02T14:12:17 2.4 1 1 (1, 20480, 20480) 0.259 65441 154.3 small -0.26 -0.17 7740 7333
4 sub-23 23 2021-02-16T17:03:39 3.2 1 1 (1, 20480, 20480) 0.259 65443 317.5 small -0.38 -0.20 7850 7423
5 sub-24 24 2021-02-16T17:09:42 3.2 1 1 (1, 20480, 20480) 0.259 65442 334.3 small -0.43 -0.23 7831 7707
6 sub-25_obj-1of0oop 25 2021-02-16T17:15:04 3.2 1 1 (1, 20480, 20480) 0.259 65438 263.7 small -0.38 -0.23 7920 8070
7 sub-03 03 2021-02-16T13:25:04 4.0 1 1 (1, 20480, 20480) 0.259 65440 275.6 small -0.50 -0.28 7804 7579
8 sub-08 08 2021-02-16T14:04:37 4.0 1 1 (1, 20480, 20480) 0.259 65447 346.4 small -0.47 -0.35 7835 7501
9 sub-14 14 2021-02-16T15:06:41 4.0 1 1 (1, 20480, 20480) 0.259 65444 371.3 small -0.42 -0.34 7902 7781
10 sub-05 05 2021-02-17T14:44:18 4.8 1 1 (1, 20480, 20480) 0.259 65442 350.0 small -0.55 -0.24 7852 7692
11 sub-12 12 2021-04-02T14:02:07 4.8 1 1 (1, 20480, 20480) 0.259 65443 431.3 small -0.58 -0.28 7750 7920
12 sub-13 13 2021-04-02T14:03:26 4.8 1 1 (1, 20480, 20480) 0.259 65443 434.3 small -0.60 -0.33 7899 7989
13 sub-25_obj-17bqgcb 25 2021-03-11T16:29:39 5.6 1 1 (1, 20480, 20480) 0.259 65444 508.7 small -0.60 -0.34 7785 7844
14 sub-26 26 2021-03-11T16:34:15 5.6 1 1 (1, 20480, 20480) 0.259 65443 505.4 small -0.52 -0.24 7879 7830
15 sub-27 27 2021-03-11T16:40:11 5.6 1 1 (1, 20480, 20480) 0.258 65444 452.4 small -0.63 -0.43 7852 8236
16 sub-02 02 2021-05-20T12:49:16 4.8 10 2 (2, 20480, 20480) 0.259 65445 605.5 large -0.19 -0.29 14296 14578
17 sub-15_obj-znj27w 15 2021-05-20T15:49:35 4.8 10 2 (2, 20480, 20480) 0.259 65438 763.0 large -0.30 -0.67 14240 14069
18 sub-04 04 2021-05-20T13:17:02 6.4 10 2 (2, 20480, 20480) 0.258 65445 1320.8 large -0.37 -0.81 14211 14011
19 sub-07 07 2021-05-20T14:40:14 6.4 10 2 (2, 20480, 20480) 0.257 65446 1488.2 large -0.37 -0.83 14244 14035
20 sub-11 11 2021-05-20T15:15:58 6.4 10 2 (2, 20480, 20480) 0.258 65438 1347.3 large -0.35 -0.80 14260 14099
Dishes per condition (array found from the electrode spacing):
packets_per_electrode array kV
1 small 2.4 4
3.2 3
4.0 3
4.8 3
5.6 3
10 large 4.8 2
6.4 3
Files per subject id: {'15': 2, '25': 2, '16': 1, '17': 1, '21': 1, '23': 1, '24': 1, '03': 1, '08': 1, '14': 1, '05': 1, '12': 1, '13': 1, '26': 1, '27': 1, '02': 1, '04': 1, '07': 1, '11': 1}
Distinct subject ids: 19 for 21 files
Imaging descriptions:
9 files: Microscope: Unknown microscope, Detector: Hamamatsu Hamamatsu ORCA-Flash4.0, Objective: Olympus IX3 Nosepiece NA=0.40000000000000002 Mag=10, Channels: FITC
7 files: Microscope: Unknown microscope, Detector: Unknown detector, Objective: Unknown objective, Channels: Unknown channels
5 files: Microscope: Unknown microscope, Detector: Hamamatsu Hamamatsu ORCA-Flash4.0, Objective: Olympus IX3 Nosepiece NA=0.40000000000000002 Mag=10, Channels: FITC, DAPI
ImageSeries unit: {'n.a.'} ; 'resolution' attribute: {96.0, 1.0}
Every 1Px4 dish is on the small array and every 10Px4 dish on the large one (as Fig. 7 legend): True
Mean centre-to-centre diagonal: large 14204 px (sd 158), small 7763 px (sd 213); ratio 1.83 (paper: 'approximately a 2-fold size difference')
pixel size camera: 0.650 um -> large diagonal 9.23 mm, small 5.05 mm, image width 13.3 mm
pixel size diag8.4: 0.591 um -> large diagonal 8.40 mm, small 4.59 mm, image width 12.1 mm
Where the paper's ROI span ends against the needles (radius 0.75 mm, Section 2.3) and the paper's exclusion zone (5% of the diagonal, Section 2.4):
camera, small array: needle edge 1.77 mm from the centre; span ends at 1.65 mm, +0.12 mm from the needle edge (negative: on the needle); exclusion zone 0.25 mm
camera, large array: needle edge 3.87 mm from the centre; span ends at 3.65 mm, +0.22 mm from the needle edge (negative: on the needle); exclusion zone 0.46 mm
diag8.4, small array: needle edge 1.55 mm from the centre; span ends at 1.65 mm, -0.10 mm from the needle edge (negative: on the needle); exclusion zone 0.23 mm
diag8.4, large array: needle edge 3.45 mm from the centre; span ends at 3.65 mm, -0.20 mm from the needle edge (negative: on the needle); exclusion zone 0.42 mm
Needle footprint radius at the camera scale (mm), per dish (4 electrodes), with the median a.u. inside and around:
r_min r_median r_max inside outside
array kV file
large 4.8 sub-02 0.707 0.853 0.926 306.609 466.719
sub-15_obj-znj27w 0.614 0.629 0.707 371.711 667.906
6.4 sub-04 0.634 0.650 0.728 387.078 1111.406
sub-07 0.468 0.614 0.645 406.023 1343.766
sub-11 0.634 0.660 0.666 307.688 1364.719
small 2.4 sub-15_obj-t7o4u3 0.634 0.676 0.863 148.547 176.594
sub-16 0.291 0.676 0.874 148.820 171.703
sub-17 0.468 0.603 0.718 165.648 196.812
sub-21 0.707 0.749 0.894 137.836 160.125
3.2 sub-23 0.697 0.712 0.728 294.102 409.578
sub-24 0.655 0.707 0.738 261.531 421.906
sub-25_obj-1of0oop 0.686 0.738 0.780 238.000 340.469
4.0 sub-03 0.676 0.733 0.759 216.547 388.453
sub-08 0.645 0.728 0.780 275.750 466.578
sub-14 0.707 0.728 0.759 295.664 456.594
4.8 sub-05 0.666 0.744 0.770 246.234 518.969
sub-12 0.603 0.702 0.738 316.070 530.500
sub-13 0.697 0.712 0.738 308.031 596.953
5.6 sub-25_obj-17bqgcb 0.686 0.759 0.822 382.328 709.594
sub-26 0.676 0.707 0.718 422.938 704.156
sub-27 0.655 0.712 0.780 313.148 518.719
Not found: 0 of 84
large array: median radius 0.650 mm (IQR 0.632 to 0.707), so a 0.75 mm needle radius implies 0.750 um per pixel (camera reading 0.650, 8.4 mm reading 0.591)
small array: median radius 0.718 mm (IQR 0.686 to 0.741), so a 0.75 mm needle radius implies 0.679 um per pixel (camera reading 0.650, 8.4 mm reading 0.591)
footprint scale 0.679 um, small array: diagonal 5.27 mm (paper, large array: 8.4 mm); needle edge 1.89 mm from the centre, span ends +0.24 mm from it; exclusion zone 0.26 mm
footprint scale 0.750 um, large array: diagonal 10.65 mm (paper, large array: 8.4 mm); needle edge 4.58 mm from the centre, span ends +0.93 mm from it; exclusion zone 0.53 mm
Highest correlations between two dishes' images (1 would be a duplicate):
0.387 sub-07_image.nwb sub-11_image.nwb
0.225 sub-11_image.nwb sub-15_obj-znj27w_image.nwb
0.219 sub-02_image.nwb sub-26_image.nwb
Identical mean intensities: 0
Dark floor of the images (0.1st percentile, a.u.) by imaging date and condition:
count min max
date packets_per_electrode kV
2021-02-16 1 3.2 3 60.5 60.7
4.0 3 61.2 62.3
2021-02-17 1 4.8 1 62.1 62.1
2021-03-11 1 5.6 3 53.5 53.8
2021-04-02 1 2.4 4 33.4 46.8
4.8 2 61.7 61.9
2021-05-20 10 4.8 2 77.7 79.3
6.4 3 79.3 81.0
No data stream has a clock: each file is one image at one time point, so there is no timing to check.
The metadata against the data:
- The session descriptions give the first-pulse voltage and the number of packets per electrode, but not the array. The electrode spacing separates the images into two groups whose diagonals differ by the factor printed above, and the group with the wider spacing holds exactly the 10Px4 dishes, as the Fig. 7 legend has it (1Px4 on the small array, 10Px4 on the large one).
- No pixel size is given; the ImageSeries
resolutionattribute is 96 or 1, which is not a pixel size, and 7 files name no detector, objective or channel. With the 0.65 um pixel that the named camera and objective give, the large-array diagonal is the value printed above, against the paper's 8.4 mm. The needles give a check: the dark footprint around each needle has the radius printed above, against the 0.75 mm of the 1.5 mm needles. The footprint edge is soft, so this check is approximate. It rules out the 8.4 mm scale, at which the needle edge would lie inside the paper's ROI span on both arrays (printed above), so that scale is not used as a reading. It does not confirm the camera scale either: the footprints imply about 0.68 um per pixel on the small array and 0.75 um on the large one (printed above), and at 0.75 um the large diagonal would be still further from the paper's 8.4 mm. The camera scale is kept for the main reading, since it rests on the hardware the files name and puts the large diagonal closer to 8.4 mm than the footprint scale does; the footprint scale is shown as a variant. At the camera scale the span ends within the paper's 5% exclusion zone; a variant below drops the end ROIs that lie in it. - Subject ids are reused: two subject ids each hold two dishes of different days and protocols, so a subject id does not identify a dish ("Each dish was used in one experiment", Section 2.1).
- No two images are duplicates (the highest correlation between any two is printed above).
- The dark floor of the images is not a fixed camera offset: the 2.4 kV dishes have a floor of 33 to 47 a.u. against 62 for the 4.8 kV dishes imaged on the same day, and the large-array session 78 to 81. The paper does not say whether a background was removed (see fig7a).
- Fig. 7 states n = 3 for every voltage; the dandiset has 4 dishes at 2.4 kV, 3 at the other small array voltages, 2 at 4.8 kV and 3 at 6.4 kV on the large array.
Other dandisets that cite the paper¶
The claims of Figs. 5, 6, 8 and 9 need dishes this dandiset does not hold. Four other dandisets cite the paper; their titles and descriptions are read here from the archive (their files are not opened), to say where those images appear to be.
PAPER_DOI = "10.1016/j.bioelechem.2023.108437"
for d in ("000686", "000875", "001027", "001028"):
info = cc.requests.get(f"{cc.API}/dandisets/{d}/", timeout=60).json()
v = (info.get("most_recent_published_version") or info["draft_version"])["version"]
meta = cc.requests.get(f"{cc.API}/dandisets/{d}/versions/{v}/", timeout=60).json()
cites = any(PAPER_DOI in (r.get("identifier", "") + r.get("url", "")).lower() for r in meta.get("relatedResource", []))
n = cc.requests.get(f"{cc.API}/dandisets/{d}/versions/{v}/assets/?page_size=1", timeout=60).json()["count"]
print(f"{d} version {v}, {n} assets, cites the paper: {cites}\n title: {meta['name']}\n description: {meta.get('description', '')[:300]}\n")
000686 version 0.240515.1909, 18 assets, cites the paper: True title: Focusing electroporation to the center of a quadrupole electrode array by interference of unipolar pulses (NextGeneration-CANCAN) description: These experimental trials demonstrate the feasibility of achieving remote targeting in electroporation by utilizing sequences of unipolar nanosecond electric pulses (nsEP), with the rotation of these packets further enhancing the process. Electroporation of the cells was evaluated using 1 uM YoPro-1
000875 version 0.240515.1925, 9 assets, cites the paper: True title: Influence of unipolar pulse packet quantity on centering the electroporation effect within a quadrupole electrode array. description: This data provides images of Yo-Pro uptake when different numbers of nanosecond electric pulse packets were applied in a four-electrode array. Increasing the total number of applied packets from 4 to 20 and 40 significantly enhanced the electroporative effect across the electrode array. The peak of
001027 version 0.240605.1859, 10 assets, cites the paper: True title: CANCAN focusing of electroporation in a quadrupole array by different pulsing protocols description: Protocols were applied once, starting from each of the electrodes, to a total of four protocols delivered. The 1st pulse amplitude in all protocols was 4 kV. Electroporation of the cells was evaluated using 1 uM YoPro-1 cell impermeable fluorescent dye. The study was supported in part by R21EY034258
001028 version 0.240605.1954, 12 assets, cites the paper: True title: CANCAN focusing of electroporation in 3D description: Tips of electrodes in a quadrupole array were either in contact with cell monolayer (0 mm distance) or were placed 1, 2, or 3 mm above it. Packet 2 was applied 10 times, starting from each electrode, to a total of 40 packets delivered. The amplitude of the first pulse in the packet was 6.4 kV. Elect
Electrode positions and ROI lines¶
The fitted electrode footprints (red circles) and the ROI span along each diagonal under the main pixel-size reading (diagonal 1 red, diagonal 2 cyan), on the 16 x 16 block images. Brightness is clipped at the 1st and 99th percentiles of each image.
order = [next(q for q in res if q.split("/")[1].replace("_image.nwb", "") == f) for f in dishes.file]
fig, axs = plt.subplots(4, 6, figsize=(24, 16))
for ax, p in zip(axs.ravel(), order):
r = res[p]
v = r["img"][r["zero"] < 0.5]
ax.imshow(r["img"], cmap="gray", vmin=np.percentile(v, 1), vmax=np.percentile(v, 99))
pts = r["pts"]
ctr = pts.mean(0)
for q in pts:
ax.add_patch(plt.Circle(q, cc.DISK_R[r["size"]], fill=False, color="r"))
span = cc.SPAN_MM[r["size"]] * 1000 / um["camera"] / cc.FD
for (i, j), col in (((0, 2), "r"), ((1, 3), "c")):
e = (pts[j] - pts[i]) / np.hypot(*(pts[j] - pts[i]))
ax.plot([ctr[0] - span * e[0], ctr[0] + span * e[0]], [ctr[1] - span * e[1], ctr[1] + span * e[1]], col, lw=1)
m = np.ptp(pts, 0).max() * 0.75
ax.set_xlim(ctr[0] - m, ctr[0] + m)
ax.set_ylim(ctr[1] + m, ctr[1] - m)
ax.set_title(f"{p.split('/')[1].replace('_image.nwb', '')} {r['kV']} kV {r['reps']}Px4", fontsize=10)
ax.axis("off")
for ax in axs.ravel()[len(order):]:
ax.axis("off")
plt.tight_layout()
plt.show()
Readings of the ROI layout¶
The claims file's opening comment names the readings the paper allows; one more is added here, the pixel size, which the dataset does not give (helper docstring). Each combination is run:
- pixel size:
camera(0.65 um, from the camera and objective the files name). The scale at which the large array's mean diagonal is 8.4 mm was a reading in round 1 and is dropped: it puts the end ROIs on the needles (data check). The scale implied by the needle footprints (about 0.68 um on the small array, 0.75 um on the large) is shown as a variant of the main reading; - ROI count:
both(92 or 224 ROIs over both diagonals, so 46 or 112 per diagonal) orper(92 or 224 per diagonal); - diagonals:
bothaveraged, ord1ord2alone (the files do not say which electrode is e1); - centre ROI:
aone ROI centred on the array centre,bthe mean of the two grid ROIs beside it; - source:
rawROI means or aloessfit (span 0.75) along each diagonal (for LOESS the two centre readings coincide, so onlyais listed); - normalisation to the most peripheral ROI:
athe mean of the end ROIs,beach half-diagonal to its own end ROI, then averaged.
This gives 18 layouts, and 36 readings with the two normalisations. The main reading is camera, both, both, a, raw, a. Its reasons: the camera and objective are named in the files, and it puts the large diagonal closer to the paper's 8.4 mm than the footprint scale does; the Fig. 5B legend averages both diagonals for rotated packets, and all Fig. 7 treatments are rotated; the Methods give one count for the measurement; the paper's numbers are measurements, not fits; and "in percent to the most peripheral ROI" names one reference value. It is an assumption wherever the readings disagree.
Three variants are shown beside the readings, none part of the criteria: the footprint
background subtracted from every ROI, the end ROIs that fall inside the paper's 5% exclusion
zone dropped (cc.inner_values, main reading otherwise), and the pixel size implied by the
needle footprints (main reading otherwise, FP).
def measures(scale, count):
out = {}
for p, r in res.items():
size = r["size"]
u = um[scale][size] if isinstance(um[scale], dict) else um[scale]
span = cc.SPAN_MM[size] * 1000 / u / cc.F
n_diag = cc.N_ROI[size] // 2 if count == "both" else cc.N_ROI[size]
out[p] = [cc.diagonal_measures(h, 1.0, span, n_diag) for h in r["hists"]]
return out
M = {(s, c): measures(s, c) for s in cc.SCALES for c in cc.COUNTS}
READINGS = [r for r in itertools.product(cc.SCALES, cc.COUNTS, cc.DIAGONALS, cc.CENTERS, cc.SOURCES)
if not (r[4] == "loess" and r[3] == "b")]
MAIN = ("camera", "both", "both", "a", "raw")
# variant: main reading at the per-array scale implied by the needle footprints (data check)
M[("footprint", "both")] = measures("footprint", "both")
FP = ("footprint", "both", "both", "a", "raw")
RCOLS = ["scale", "count", "diagonals", "centre", "source"]
def table(reps, reading, background=None):
# per-dish values under one reading; background: dict path -> a.u. to subtract (variant only)
scale, count, diag, center, source = reading
rows = []
for p, dm in M[(scale, count)].items():
r = res[p]
if r["reps"] != reps:
continue
if background is not None:
b = background[p]
dm = [{**d, source: {k: v - b for k, v in d[source].items()}} for d in dm]
rows.append(dict(file=p.split("/")[1].replace("_image.nwb", ""), kV=r["kV"], **cc.summarize(dm, diag, center, source)))
return pd.DataFrame(rows).sort_values(["kV", "file"]).reset_index(drop=True)
# background variant: median of the 16x16 blocks inside the electrode footprints (half radius),
# where the needles stood on the glass; a choice made here, used only as a variant
background = {}
for p, r in res.items():
yy, xx = np.mgrid[: r["img"].shape[0], : r["img"].shape[1]]
inside = np.zeros(r["img"].shape, bool)
for q in r["pts"]:
inside |= np.hypot(xx - q[0], yy - q[1]) < 0.5 * cc.DISK_R[r["size"]]
background[p] = float(np.median(r["img"][inside & (r["zero"] < 0.01)]))
# exclusion-zone variant: per dish, values with the end ROIs inside the 5% zone dropped (main reading otherwise)
INNER, INNER_FP = {}, {}
for p, r in res.items():
hd = [np.hypot(*(r["pts"][j] - r["pts"][i])) * cc.FD / cc.F / 2 for i, j in ((0, 2), (1, 3))]
INNER[p] = cc.inner_values(r["hists"], M[MAIN[:2]][p], hd, um["camera"])
INNER_FP[p] = cc.inner_values(r["hists"], M[FP[:2]][p], hd, um["footprint"][r["size"]])
print("Exclusion-zone variant, ROIs dropped at each end (both diagonals) by array:",
pd.DataFrame([dict(array=res[p]["size"], dropped=str(v["dropped"])) for p, v in INNER.items()])
.groupby("array").dropped.agg(lambda s: s.value_counts().to_dict()).to_dict())
print("At the footprint scale:",
pd.DataFrame([dict(array=res[p]["size"], dropped=str(v["dropped"])) for p, v in INNER_FP.items()])
.groupby("array").dropped.agg(lambda s: s.value_counts().to_dict()).to_dict())
def inner_table(reps, background=None, inner=None):
rows = []
for p, v in (INNER if inner is None else inner).items():
r = res[p]
if r["reps"] != reps:
continue
b = 0.0 if background is None else background[p]
c, h, e = v["center"] - b, v["half"] - b, v["periph"] - b
rows.append(dict(file=p.split("/")[1].replace("_image.nwb", ""), kV=r["kV"], periph=e, half=h, center=c,
nc_a=100 * c / e, nh_a=100 * h / e))
return pd.DataFrame(rows).sort_values(["kV", "file"]).reset_index(drop=True)
print("Footprint background (a.u.) by condition:")
print(pd.DataFrame([dict(reps=r["reps"], kV=r["kV"], bg=background[p]) for p, r in res.items()])
.groupby(["reps", "kV"]).bg.agg(["count", "mean", "min", "max"]).round(1).to_string())
Exclusion-zone variant, ROIs dropped at each end (both diagonals) by array: {'large': {'[4, 5]': 4, '[4, 3]': 1}, 'small': {'[2, 3]': 4, '[2, 2]': 4, '[2, 4]': 3, '[2, 1]': 2, '[4, 4]': 1, '[1, 3]': 1, '[2, 0]': 1}}
At the footprint scale: {'large': {'[0, 0]': 5}, 'small': {'[1, 2]': 3, '[0, 1]': 3, '[0, 0]': 3, '[1, 1]': 2, '[1, 0]': 1, '[0, 3]': 1, '[2, 2]': 1, '[0, 2]': 1, '[1, 3]': 1}}
Footprint background (a.u.) by condition:
count mean min max
reps kV
1 2.4 4 150.7 140.3 159.4
3.2 3 260.1 232.0 294.2
4.0 3 261.1 218.0 288.4
4.8 3 286.6 239.2 314.0
5.6 3 375.9 314.6 424.6
10 4.8 2 332.8 293.7 372.0
6.4 3 355.7 308.0 380.4
Profiles¶
Mean normalised profiles (main reading, both diagonals averaged and folded about the centre so that 0 is the centre and 1 the most peripheral ROI), mean +/- s.e. over dishes, for each voltage.
def folded_profiles(reps, scale="camera", count="both"):
# per dish: mean over the 4 half-diagonals of the raw ROI profile, from centre to periphery
out = {}
for p, dm in M[(scale, count)].items():
r = res[p]
if r["reps"] != reps:
continue
halves = []
for d in dm:
prof = d["raw"]["profile"]
h = len(prof) // 2
halves += [prof[:h][::-1], prof[h:]]
out[p] = np.nanmean(halves, axis=0)
return out
fig, axs = plt.subplots(2, 2, figsize=(13, 9))
for col, reps in enumerate((1, 10)):
prof = folded_profiles(reps)
for kv in sorted({res[p]["kV"] for p in prof}):
ps = [p for p in prof if res[p]["kV"] == kv]
P = np.array([prof[p] for p in ps])
x = np.linspace(0, 1, P.shape[1])
Pn = 100 * P / P[:, -1:]
for row, Q in enumerate((P, Pn)):
m, se = Q.mean(0), Q.std(0, ddof=1) / np.sqrt(len(Q))
axs[row, col].errorbar(x, m, se, label=f"{kv} kV (n={len(ps)})", lw=1, capsize=0)
print(f"{reps}Px4 {kv} kV {'a.u.' if row == 0 else '% periphery'}: range of means {m.min():.1f} to {m.max():.1f}")
axs[0, col].set_title(f"{'small' if reps == 1 else 'large'} array, {reps}Px4, a.u.")
axs[1, col].set_title(f"{'small' if reps == 1 else 'large'} array, {reps}Px4, % of most peripheral ROI")
for row in (0, 1):
axs[row, col].set_xlabel("distance from centre, fraction of ROI span")
axs[row, col].legend(fontsize=8)
plt.tight_layout()
plt.show()
1Px4 2.4 kV a.u.: range of means 192.6 to 249.9 1Px4 2.4 kV % periphery: range of means 87.3 to 113.1 1Px4 3.2 kV a.u.: range of means 481.7 to 708.8 1Px4 3.2 kV % periphery: range of means 90.4 to 132.4 1Px4 4.0 kV a.u.: range of means 513.5 to 862.9 1Px4 4.0 kV % periphery: range of means 84.8 to 143.3 1Px4 4.8 kV a.u.: range of means 751.9 to 1438.7 1Px4 4.8 kV % periphery: range of means 82.0 to 155.9 1Px4 5.6 kV a.u.: range of means 1000.3 to 1779.8 1Px4 5.6 kV % periphery: range of means 75.4 to 134.5 10Px4 4.8 kV a.u.: range of means 723.3 to 3186.9 10Px4 4.8 kV % periphery: range of means 75.4 to 372.5 10Px4 6.4 kV a.u.: range of means 1498.8 to 5727.9 10Px4 6.4 kV % periphery: range of means 40.9 to 156.8
fig7a-amplitude-scales-uptake¶
On the small array with 1Px4, raising the first pulse amplitude from 2.4 to 5.6 kV increases YP fluorescence 15.5 times at the most peripheral point, 16.2 times half-way to the center and 16.8 times in the center.
Criterion: ratios of the 5.6 kV mean to the 2.4 kV mean over dishes, in a.u., with no background subtracted; each within 10% (15.5 +/- 1.55, 16.2 +/- 1.62, 16.8 +/- 1.68) under a reading of the opening comment; partially reproduced when one or two are.
TOL = {"periph": (15.5, 1.55), "half": (16.2, 1.62), "center": (16.8, 1.68)}
def ratios(reading, background=None, drop=None):
df = table(1, reading, background)
if drop is not None:
df = df[df.file != drop]
g = df.groupby("kV")[["periph", "half", "center"]].mean()
return g.loc[5.6] / g.loc[2.4], df
rows = []
for rd in READINGS:
rat, _ = ratios(rd)
rows.append(dict(zip(RCOLS, rd), **{f"ratio_{k}": round(rat[k], 2) for k in TOL},
n_within=int(sum(abs(rat[k] - v) <= t for k, (v, t) in TOL.items()))))
r7a = pd.DataFrame(rows)
print(r7a.to_string())
print("\nReadings with at least one ratio within tolerance:", int((r7a.n_within > 0).sum()), "of", len(r7a))
print("Range over readings: periphery", r7a.ratio_periph.min(), "to", r7a.ratio_periph.max(),
"; half-way", r7a.ratio_half.min(), "to", r7a.ratio_half.max(),
"; centre", r7a.ratio_center.min(), "to", r7a.ratio_center.max())
scale count diagonals centre source ratio_periph ratio_half ratio_center n_within 0 camera both both a raw 5.99 5.65 7.25 0 1 camera both both a loess 5.57 5.90 6.09 0 2 camera both both b raw 5.99 5.65 7.31 0 3 camera both d1 a raw 5.62 5.69 7.25 0 4 camera both d1 a loess 5.11 5.85 6.25 0 5 camera both d1 b raw 5.62 5.69 7.16 0 6 camera both d2 a raw 6.35 5.62 7.25 0 7 camera both d2 a loess 6.03 5.94 5.93 0 8 camera both d2 b raw 6.35 5.62 7.46 0 9 camera per both a raw 5.48 5.34 7.51 0 10 camera per both a loess 5.53 5.90 6.10 0 11 camera per both b raw 5.48 5.34 7.37 0 12 camera per d1 a raw 5.21 5.38 7.43 0 13 camera per d1 a loess 5.11 5.78 6.12 0 14 camera per d1 b raw 5.21 5.38 7.19 0 15 camera per d2 a raw 5.74 5.31 7.59 0 16 camera per d2 a loess 5.95 6.01 6.09 0 17 camera per d2 b raw 5.74 5.31 7.55 0 Readings with at least one ratio within tolerance: 0 of 18 Range over readings: periphery 5.11 to 6.35 ; half-way 5.31 to 6.01 ; centre 5.93 to 7.59
rat, df = ratios(MAIN)
print("Main reading, per dish (a.u.):")
print(df[df.kV.isin([2.4, 5.6])][["file", "kV", "periph", "half", "center"]].round(1).to_string())
g = df.groupby("kV")[["periph", "half", "center"]].agg(["mean", "sem", "count"]).round(1)
print("\nMean, s.e. and n by voltage (all five voltages):")
print(g.to_string())
print("\nMain reading ratios 5.6/2.4 kV:", rat.round(2).to_dict())
print("\n2.4 kV has 4 dishes where the paper states n = 3; ratios with each one left out:")
for f in df[df.kV == 2.4].file:
r_, _ = ratios(MAIN, drop=f)
print(f" without {f}: {r_.round(2).to_dict()}")
print("\nVariant, footprint background subtracted from every ROI (not the criterion):")
rows = []
for rd in READINGS:
r_, _ = ratios(rd, background)
rows.append(dict(zip(RCOLS, rd), **{f"ratio_{k}": round(r_[k], 2) for k in TOL},
n_within=int(sum(abs(r_[k] - v) <= t for k, (v, t) in TOL.items()))))
r7a_bg = pd.DataFrame(rows)
print(r7a_bg[r7a_bg[RCOLS].apply(tuple, axis=1) == MAIN].to_string())
print("Readings with all three within tolerance:", int((r7a_bg.n_within == 3).sum()), "of", len(r7a_bg),
"; with at least one:", int((r7a_bg.n_within > 0).sum()))
print("Range over readings: periphery", r7a_bg.ratio_periph.min(), "to", r7a_bg.ratio_periph.max(),
"; half-way", r7a_bg.ratio_half.min(), "to", r7a_bg.ratio_half.max(),
"; centre", r7a_bg.ratio_center.min(), "to", r7a_bg.ratio_center.max())
print("\nVariant, end ROIs inside the 5% exclusion zone dropped (main reading otherwise):")
for bgname, bg in (("no background", None), ("footprint background subtracted", background)):
d = inner_table(1, bg)
gi = d.groupby("kV")[["periph", "half", "center"]].mean()
ri = gi.loc[5.6] / gi.loc[2.4]
print(f" {bgname}: ratios {ri.round(2).to_dict()}, within tolerance: "
f"{int(sum(abs(ri[k] - v) <= t for k, (v, t) in TOL.items()))} of 3")
print("\nVariant, pixel size from the needle footprints (main reading otherwise; not the criterion):")
for bgname, bg in (("no background", None), ("footprint background subtracted", background)):
r_, _ = ratios(FP, bg)
d = inner_table(1, bg, INNER_FP)
gi = d.groupby("kV")[["periph", "half", "center"]].mean()
ri = gi.loc[5.6] / gi.loc[2.4]
print(f" {bgname}: ratios {r_.round(2).to_dict()}, within tolerance {int(sum(abs(r_[k] - v) <= t for k, (v, t) in TOL.items()))} of 3; "
f"with the exclusion-zone ROIs dropped {ri.round(2).to_dict()}, within tolerance {int(sum(abs(ri[k] - v) <= t for k, (v, t) in TOL.items()))} of 3")
Main reading, per dish (a.u.):
file kV periph half center
0 sub-15_obj-t7o4u3 2.4 210.8 205.3 232.7
1 sub-16 2.4 218.6 200.1 209.8
2 sub-17 2.4 261.1 198.5 295.5
3 sub-21 2.4 198.4 185.6 227.5
13 sub-25_obj-17bqgcb 5.6 1437.8 1103.3 1603.5
14 sub-26 5.6 1255.9 1206.7 1866.2
15 sub-27 5.6 1301.2 1037.5 1780.4
Mean, s.e. and n by voltage (all five voltages):
periph half center
mean sem count mean sem count mean sem count
kV
2.4 222.2 13.6 4 197.4 4.2 4 241.4 18.7 4
3.2 533.2 24.0 3 520.5 54.9 3 661.1 87.5 3
4.0 614.9 94.8 3 540.4 70.3 3 718.8 70.8 3
4.8 922.7 64.1 3 772.2 81.4 3 1266.2 109.7 3
5.6 1331.6 54.7 3 1115.8 49.3 3 1750.0 77.3 3
Main reading ratios 5.6/2.4 kV: {'periph': 5.99, 'half': 5.65, 'center': 7.25}
2.4 kV has 4 dishes where the paper states n = 3; ratios with each one left out:
without sub-15_obj-t7o4u3: {'periph': 5.89, 'half': 5.73, 'center': 7.16}
without sub-16: {'periph': 5.96, 'half': 5.68, 'center': 6.95}
without sub-17: {'periph': 6.36, 'half': 5.66, 'center': 7.84}
without sub-21: {'periph': 5.79, 'half': 5.54, 'center': 7.11}
Variant, footprint background subtracted from every ROI (not the criterion):
scale count diagonals centre source ratio_periph ratio_half ratio_center n_within
0 camera both both a raw 13.37 15.87 15.16 2
Readings with all three within tolerance: 2 of 18 ; with at least one: 17
Range over readings: periphery 10.58 to 17.77 ; half-way 12.51 to 16.1 ; centre 12.97 to 16.22
Variant, end ROIs inside the 5% exclusion zone dropped (main reading otherwise):
no background: ratios {'periph': 5.77, 'half': 5.84, 'center': 7.25}, within tolerance: 0 of 3
footprint background subtracted: ratios {'periph': 14.88, 'half': 17.1, 'center': 15.16}, within tolerance: 3 of 3
Variant, pixel size from the needle footprints (main reading otherwise; not the criterion):
no background: ratios {'periph': 5.71, 'half': 5.45, 'center': 7.32}, within tolerance 0 of 3; with the exclusion-zone ROIs dropped {'periph': 5.78, 'half': 5.45, 'center': 7.32}, within tolerance 0 of 3
footprint background subtracted: ratios {'periph': 14.17, 'half': 14.82, 'center': 15.39}, within tolerance 3 of 3; with the exclusion-zone ROIs dropped {'periph': 15.01, 'half': 15.3, 'center': 15.39}, within tolerance 3 of 3
Result: not reproduced. Without background subtraction, as the criterion fixes, the 5.6 kV over
2.4 kV ratios under the main reading are 5.99 at the most peripheral ROI, 5.65 half-way and 7.25 at
the centre, against 15.5, 16.2 and 16.8, and none of the 18 readings brings any ratio within
tolerance (the largest is 7.59). Leaving out any one of the four 2.4 kV dishes changes the ratios by
at most 0.59, and dropping the end ROIs that lie in the paper's exclusion zone gives 5.77, 5.84 and
7.25. Fluorescence rises with voltage at every position. With the median inside the needle
footprints subtracted as a background (150.7 a.u. on average at 2.4 kV), a variant the criterion
excludes, the main reading gives 13.37, 15.87 and 15.16, two of three within tolerance, 2 of 18
readings put all three within it, and with the exclusion-zone ROIs also dropped all three are within
it (14.88, 17.10 and 15.16). At the pixel size implied by the needle footprints the ratios are
5.71, 5.45 and 7.32 without background (none within tolerance) and 14.17, 14.82 and 15.39 with it
(all three within). The outcome turns on whether an offset was removed. The claims file
says none was, as in the Methods, but the Methods do not say so; this is asked of a reader in
questions/001025.yaml.
fig7b-targeting-independent-of-amplitude¶
Normalized to the most peripheral ROI, the profiles for different first pulse amplitudes fully overlap, so the spatial distribution of electroporation, and NG-CANCAN targeting, does not depend on amplitude within the studied range.
Criterion: a claim of absence. Small array, 1Px4, every voltage (2.4, 3.2, 4.0, 4.8 and 5.6 kV, from the dataset). The normalised values at the centre ROI and at the half-way ROI are compared between every pair of voltages by the two-sided Student's t-test, no correction. Reproduced when no pair differs at p < 0.05; not reproduced when any does.
def pair_tests(reps, reading, norm="a", background=None):
df = table(reps, reading, background)
kvs = sorted(df.kV.unique())
out = []
for pos, lab in (("nc", "centre"), ("nh", "half-way")):
col = f"{pos}_{norm}"
for a, b in itertools.combinations(kvs, 2):
x, y = df[df.kV == a][col], df[df.kV == b][col]
t, p = stats.ttest_ind(x, y)
out.append(dict(position=lab, kV_a=a, kV_b=b, n_a=len(x), n_b=len(y), mean_a=x.mean(), mean_b=y.mean(),
diff=y.mean() - x.mean(), t=t, p=p))
return pd.DataFrame(out)
def readings_summary(reps, background=None):
rows = []
for rd in READINGS:
for norm in "ab":
t = pair_tests(reps, rd, norm, background)
rows.append(dict(zip(RCOLS, rd), norm=norm, n_tests=len(t), n_p_below_05=int((t.p < 0.05).sum()),
min_p=t.p.min(), max_abs_diff=t["diff"].abs().max()))
return pd.DataFrame(rows)
t7b = pair_tests(1, MAIN, "a")
print("Main reading (normalisation a), every pair of voltages, % of the most peripheral ROI:")
print(t7b.round(4).to_string())
print(f"\nPairs with p < 0.05: {(t7b.p < 0.05).sum()} of {len(t7b)}")
df = table(1, MAIN)
print("\nPer dish, normalised centre and half-way (%):")
print(df[["file", "kV", "nc_a", "nh_a", "nc_b", "nh_b"]].round(1).to_string())
print("\n2.4 kV has 4 dishes where the paper states n = 3; each left out (main reading, normalisation a):")
for f in df[df.kV == 2.4].file:
d = df[df.file != f]
sig = []
for pos, lab in (("nc_a", "centre"), ("nh_a", "half-way")):
for a, b in itertools.combinations(sorted(d.kV.unique()), 2):
_, p = stats.ttest_ind(d[d.kV == a][pos], d[d.kV == b][pos])
if p < 0.05:
sig.append(f"{lab} {a} vs {b} kV p = {p:.4f}")
print(f" without {f}: {len(sig)} pairs at p < 0.05: {sig}")
Main reading (normalisation a), every pair of voltages, % of the most peripheral ROI:
position kV_a kV_b n_a n_b mean_a mean_b diff t p
0 centre 2.4 3.2 4 3 108.5578 122.9758 14.4180 -1.3374 0.2387
1 centre 2.4 4.0 4 3 108.5578 118.7606 10.2029 -1.4183 0.2153
2 centre 2.4 4.8 4 3 108.5578 136.8757 28.3179 -5.1192 0.0037
3 centre 2.4 5.6 4 3 108.5578 132.3157 23.7579 -2.2704 0.0724
4 centre 3.2 4.0 3 3 122.9758 118.7606 -4.2151 0.3268 0.7602
5 centre 3.2 4.8 3 3 122.9758 136.8757 13.8999 -1.1911 0.2995
6 centre 3.2 5.6 3 3 122.9758 132.3157 9.3399 -0.5920 0.5857
7 centre 4.0 4.8 3 3 118.7606 136.8757 18.1150 -2.7285 0.0525
8 centre 4.0 5.6 3 3 118.7606 132.3157 13.5551 -1.0825 0.3399
9 centre 4.8 5.6 3 3 136.8757 132.3157 -4.5600 0.4053 0.7060
10 half-way 2.4 3.2 4 3 89.6222 97.0735 7.4513 -0.9798 0.3722
11 half-way 2.4 4.0 4 3 89.6222 88.5111 -1.1110 0.1901 0.8567
12 half-way 2.4 4.8 4 3 89.6222 83.2555 -6.3666 1.0150 0.3567
13 half-way 2.4 5.6 4 3 89.6222 84.1843 -5.4379 0.7258 0.5005
14 half-way 3.2 4.0 3 3 97.0735 88.5111 -8.5623 1.3003 0.2634
15 half-way 3.2 4.8 3 3 97.0735 83.2555 -13.8180 1.9395 0.1244
16 half-way 3.2 5.6 3 3 97.0735 84.1843 -12.8892 1.4908 0.2103
17 half-way 4.0 4.8 3 3 88.5111 83.2555 -5.2556 1.2769 0.2707
18 half-way 4.0 5.6 3 3 88.5111 84.1843 -4.3268 0.6763 0.5359
19 half-way 4.8 5.6 3 3 83.2555 84.1843 0.9288 -0.1336 0.9002
Pairs with p < 0.05: 1 of 20
Per dish, normalised centre and half-way (%):
file kV nc_a nh_a nc_b nh_b
0 sub-15_obj-t7o4u3 2.4 110.4 97.4 110.8 97.5
1 sub-16 2.4 96.0 91.5 96.6 92.0
2 sub-17 2.4 113.2 76.0 116.2 77.8
3 sub-21 2.4 114.7 93.6 116.6 94.5
4 sub-23 3.2 136.3 105.8 136.6 106.2
5 sub-24 3.2 132.2 100.4 132.7 100.8
6 sub-25_obj-1of0oop 3.2 100.4 85.0 102.2 86.7
7 sub-03 4.0 121.6 88.4 128.3 93.1
8 sub-08 4.0 107.1 84.8 110.3 85.1
9 sub-14 4.0 127.6 92.3 130.1 93.7
10 sub-05 4.8 131.9 76.9 133.5 77.0
11 sub-12 4.8 140.9 84.0 145.5 87.5
12 sub-13 4.8 137.8 88.9 144.6 92.4
13 sub-25_obj-17bqgcb 5.6 111.5 76.7 117.4 80.3
14 sub-26 5.6 148.6 96.1 152.1 96.8
15 sub-27 5.6 136.8 79.7 141.8 80.2
2.4 kV has 4 dishes where the paper states n = 3; each left out (main reading, normalisation a):
without sub-15_obj-t7o4u3: 1 pairs at p < 0.05: ['centre 2.4 vs 4.8 kV p = 0.0116']
without sub-16: 1 pairs at p < 0.05: ['centre 2.4 vs 4.8 kV p = 0.0012'] without sub-17: 2 pairs at p < 0.05: ['centre 2.4 vs 4.8 kV p = 0.0087', 'half-way 2.4 vs 4.8 kV p = 0.0485'] without sub-21: 1 pairs at p < 0.05: ['centre 2.4 vs 4.8 kV p = 0.0070']
s7b = readings_summary(1)
print(f"Every reading ({len(s7b)} = {len(READINGS)} layouts x 2 normalisations):")
print(s7b.round(4).to_string())
print("\nReadings with no pair at p < 0.05:", int((s7b.n_p_below_05 == 0).sum()), "of", len(s7b))
print("Number of significant pairs over readings:", s7b.n_p_below_05.value_counts().sort_index().to_dict())
# chance of at least one p < 0.05 among the 20 tests when no effect exists. The 10 pairs share groups and
# the two positions are measured on the same dishes, so the tests are not independent: simulate the design
# (group sizes of the dataset, normal values) with the two positions independent, identical, and correlated
# as in the data (within-voltage correlation of the normalised centre and half-way values, main reading)
rng = np.random.default_rng(2023)
d0 = table(1, MAIN)
ns = d0.groupby("kV").size().to_dict()
resid = d0[["nc_a", "nh_a"]] - d0.groupby("kV")[["nc_a", "nh_a"]].transform("mean")
rho = float(np.corrcoef(resid.nc_a, resid.nh_a)[0, 1])
NSIM = 20000
def fwer(rho_):
z = {kv: rng.multivariate_normal([0, 0], [[1, rho_], [rho_, 1]], size=(NSIM, n)) for kv, n in ns.items()}
anyp = np.zeros(NSIM, bool)
for a, b in itertools.combinations(sorted(ns), 2):
for k in (0, 1):
anyp |= stats.ttest_ind(z[a][:, :, k], z[b][:, :, k], axis=1).pvalue < 0.05
return anyp.mean()
print(f"Chance of at least one p < 0.05 among the 20 tests with no effect (group sizes {ns}, {NSIM} simulations, seed 2023):")
print(f" 20 independent tests (formula): {1 - 0.95 ** 20:.2f}")
for lab, r_ in (("positions independent", 0.0), (f"positions correlated as in the data (r = {rho:.2f})", rho), ("positions identical (10 tests)", 1.0)):
print(f" simulated, {lab}: {fwer(min(r_, 0.999999)):.2f}")
print(f"Bonferroni threshold for 20 tests: {0.05 / 20:.4f} (the criterion applies no correction); smallest p, main reading: {t7b.p.min():.4f}")
# variant: test at every ROI position of the folded normalised profile
prof = folded_profiles(1)
kvs = sorted({res[p]["kV"] for p in prof})
P = {kv: np.array([100 * prof[p] / prof[p][-1] for p in prof if res[p]["kV"] == kv]) for kv in kvs}
npos = P[kvs[0]].shape[1]
sig = np.zeros(npos, int)
for a, b in itertools.combinations(kvs, 2):
_, p = stats.ttest_ind(P[a][:, :-1], P[b][:, :-1], axis=0)
sig[:-1] += p < 0.05
print(f"\nVariant over all ROIs (folded profile, {npos - 1} positions x 10 pairs = {(npos - 1) * 10} tests; the last position is the 100% reference): "
f"{sig.sum()} at p < 0.05; positions with any: {int((sig > 0).sum())}")
s7b_bg = readings_summary(1, background)
print("\nVariant with the footprint background subtracted, every reading:")
print(s7b_bg.round(4).to_string())
m = s7b_bg[(s7b_bg[RCOLS].apply(tuple, axis=1) == MAIN) & (s7b_bg.norm == "a")]
print("Main reading:", m[["n_p_below_05", "min_p"]].to_dict("records"),
"; readings with no pair at p < 0.05:", int((s7b_bg.n_p_below_05 == 0).sum()), "of", len(s7b_bg))
print("\nMain reading with background subtracted, every pair:")
print(pair_tests(1, MAIN, "a", background).round(4).to_string())
print("\nPer dish, normalised centre and half-way with background subtracted (%):")
print(table(1, MAIN, background)[["file", "kV", "nc_a", "nh_a", "nc_b", "nh_b"]].round(1).to_string())
def inner_pairs(reps, bg=None, inner=None):
d = inner_table(reps, bg, inner)
out = []
for pos, lab in (("nc_a", "centre"), ("nh_a", "half-way")):
for a, b in itertools.combinations(sorted(d.kV.unique()), 2):
x, y = d[d.kV == a][pos], d[d.kV == b][pos]
t, p = stats.ttest_ind(x, y)
out.append(dict(position=lab, kV_a=a, kV_b=b, mean_a=x.mean(), mean_b=y.mean(), p=p))
return pd.DataFrame(out)
for bgname, bg in (("no background", None), ("footprint background subtracted", background)):
t = pair_tests(1, FP, "a", bg)
print(f"\nVariant, pixel size from the needle footprints, {bgname}: {(t.p < 0.05).sum()} of {len(t)} pairs at p < 0.05; "
f"smallest p {t.p.min():.4f}")
print(t[t.p < 0.05].round(4).to_string())
t = inner_pairs(1, bg, INNER_FP)
print(f" and with the exclusion-zone ROIs dropped: {(t.p < 0.05).sum()} of {len(t)} pairs at p < 0.05; smallest p {t.p.min():.4f}")
for bgname, bg in (("no background", None), ("footprint background subtracted", background)):
t = inner_pairs(1, bg)
print(f"\nVariant, end ROIs inside the exclusion zone dropped, {bgname}: {(t.p < 0.05).sum()} of {len(t)} pairs at p < 0.05; "
f"smallest p {t.p.min():.4f}; normalised centre means by kV "
f"{inner_table(1, bg).groupby('kV').nc_a.mean().round(1).to_dict()}")
print(t[t.p < 0.05].round(4).to_string())
Every reading (36 = 18 layouts x 2 normalisations):
scale count diagonals centre source norm n_tests n_p_below_05 min_p max_abs_diff
0 camera both both a raw a 20 1 0.0037 28.3179
1 camera both both a raw b 20 2 0.0046 31.1388
2 camera both both a loess a 20 2 0.0145 27.3957
3 camera both both a loess b 20 4 0.0112 30.2369
4 camera both both b raw a 20 2 0.0014 30.6075
5 camera both both b raw b 20 2 0.0026 33.2919
6 camera both d1 a raw a 20 2 0.0263 32.8797
7 camera both d1 a raw b 20 2 0.0334 36.6575
8 camera both d1 a loess a 20 4 0.0035 37.3274
9 camera both d1 a loess b 20 4 0.0051 36.7919
10 camera both d1 b raw a 20 2 0.0062 31.4659
11 camera both d1 b raw b 20 2 0.0068 35.1352
12 camera both d2 a raw a 20 5 0.0050 29.4571
13 camera both d2 a raw b 20 5 0.0090 33.8909
14 camera both d2 a loess a 20 0 0.2140 20.3525
15 camera both d2 a loess b 20 0 0.1271 23.6819
16 camera both d2 b raw a 20 5 0.0068 29.9300
17 camera both d2 b raw b 20 5 0.0098 34.3671
18 camera per both a raw a 20 0 0.0609 43.6138
19 camera per both a raw b 20 1 0.0383 46.3235
20 camera per both a loess a 20 4 0.0149 28.2418
21 camera per both a loess b 20 5 0.0053 31.4019
22 camera per both b raw a 20 0 0.0640 40.9401
23 camera per both b raw b 20 1 0.0431 43.5546
24 camera per d1 a raw a 20 1 0.0271 48.5725
25 camera per d1 a raw b 20 1 0.0396 49.4609
26 camera per d1 a loess a 20 4 0.0059 41.1300
27 camera per d1 a loess b 20 4 0.0053 40.4837
28 camera per d1 b raw a 20 1 0.0353 42.8223
29 camera per d1 b raw b 20 1 0.0487 43.5423
30 camera per d2 a raw a 20 0 0.0688 38.6754
31 camera per d2 a raw b 20 0 0.0712 43.1860
32 camera per d2 a loess a 20 0 0.2912 16.6310
33 camera per d2 a loess b 20 0 0.1832 22.3201
34 camera per d2 b raw a 20 0 0.1064 38.8342
35 camera per d2 b raw b 20 0 0.1067 43.5669
Readings with no pair at p < 0.05: 10 of 36
Number of significant pairs over readings: {0: 10, 1: 7, 2: 8, 4: 6, 5: 5}
Chance of at least one p < 0.05 among the 20 tests with no effect (group sizes {2.4: 4, 3.2: 3, 4.0: 3, 4.8: 3, 5.6: 3}, 20000 simulations, seed 2023):
20 independent tests (formula): 0.64
simulated, positions independent: 0.50
simulated, positions correlated as in the data (r = 0.64): 0.47
simulated, positions identical (10 tests): 0.29 Bonferroni threshold for 20 tests: 0.0025 (the criterion applies no correction); smallest p, main reading: 0.0037 Variant over all ROIs (folded profile, 22 positions x 10 pairs = 220 tests; the last position is the 100% reference): 23 at p < 0.05; positions with any: 14
Variant with the footprint background subtracted, every reading:
scale count diagonals centre source norm n_tests n_p_below_05 min_p max_abs_diff
0 camera both both a raw a 20 0 0.1802 26.0230
1 camera both both a raw b 20 0 0.2134 22.4587
2 camera both both a loess a 20 0 0.2289 29.6831
3 camera both both a loess b 20 0 0.3377 48.6562
4 camera both both b raw a 20 0 0.0707 29.5791
5 camera both both b raw b 20 0 0.2134 22.4587
6 camera both d1 a raw a 20 2 0.0415 30.0640
7 camera both d1 a raw b 20 0 0.0695 37.5830
8 camera both d1 a loess a 20 0 0.3646 26.9790
9 camera both d1 a loess b 20 0 0.3055 44.3812
10 camera both d1 b raw a 20 2 0.0415 30.0640
11 camera both d1 b raw b 20 0 0.0695 32.5476
12 camera both d2 a raw a 20 2 0.0098 40.1558
13 camera both d2 a raw b 20 2 0.0150 43.5068
14 camera both d2 a loess a 20 0 0.1453 60.1223
15 camera both d2 a loess b 20 0 0.1589 52.9312
16 camera both d2 b raw a 20 2 0.0098 40.1558
17 camera both d2 b raw b 20 2 0.0150 40.9230
18 camera per both a raw a 20 0 0.0903 53.6972
19 camera per both a raw b 20 0 0.1864 50.5258
20 camera per both a loess a 20 0 0.3374 28.8101
21 camera per both a loess b 20 0 0.2025 43.4346
22 camera per both b raw a 20 0 0.0903 48.6549
23 camera per both b raw b 20 0 0.1864 36.3645
24 camera per d1 a raw a 20 0 0.0623 56.4497
25 camera per d1 a raw b 20 0 0.0765 41.5076
26 camera per d1 a loess a 20 0 0.3500 39.8150
27 camera per d1 a loess b 20 0 0.1708 61.7005
28 camera per d1 b raw a 20 0 0.0623 47.6119
29 camera per d1 b raw b 20 0 0.0765 48.5409
30 camera per d2 a raw a 20 0 0.2052 46.7107
31 camera per d2 a raw b 20 0 0.2062 66.7062
32 camera per d2 a loess a 20 0 0.3418 46.1127
33 camera per d2 a loess b 20 0 0.3111 40.5040
34 camera per d2 b raw a 20 0 0.2295 51.2980
35 camera per d2 b raw b 20 0 0.2286 66.8134
Main reading: [{'n_p_below_05': 0, 'min_p': 0.1802027903657868}] ; readings with no pair at p < 0.05: 30 of 36
Main reading with background subtracted, every pair:
position kV_a kV_b n_a n_b mean_a mean_b diff t p
0 centre 2.4 3.2 4 3 127.4764 145.5970 18.1206 -0.7180 0.5049
1 centre 2.4 4.0 4 3 127.4764 137.3300 9.8536 -0.4705 0.6578
2 centre 2.4 4.8 4 3 127.4764 153.4995 26.0230 -1.5570 0.1802
3 centre 2.4 5.6 4 3 127.4764 145.9270 18.4505 -0.8582 0.4300
4 centre 3.2 4.0 3 3 145.5970 137.3300 -8.2670 0.2973 0.7810
5 centre 3.2 4.8 3 3 145.5970 153.4995 7.9025 -0.3384 0.7521
6 centre 3.2 5.6 3 3 145.5970 145.9270 0.3300 -0.0116 0.9913
7 centre 4.0 4.8 3 3 137.3300 153.4995 16.1695 -0.9986 0.3745
8 centre 4.0 5.6 3 3 137.3300 145.9270 8.5970 -0.3758 0.7262
9 centre 4.8 5.6 3 3 153.4995 145.9270 -7.5725 0.4403 0.6825
10 half-way 2.4 3.2 4 3 69.9994 94.7540 24.7545 -1.4890 0.1967
11 half-way 2.4 4.0 4 3 69.9994 79.7692 9.7698 -0.7394 0.4929
12 half-way 2.4 4.8 4 3 69.9994 75.7843 5.7848 -0.4200 0.6919
13 half-way 2.4 5.6 4 3 69.9994 78.4921 8.4926 -0.5765 0.5892
14 half-way 3.2 4.0 3 3 94.7540 79.7692 -14.9847 1.2223 0.2887
15 half-way 3.2 4.8 3 3 94.7540 75.7843 -18.9697 1.4470 0.2214
16 half-way 3.2 5.6 3 3 94.7540 78.4921 -16.2619 1.1199 0.3255
17 half-way 4.0 4.8 3 3 79.7692 75.7843 -3.9849 0.7750 0.4816
18 half-way 4.0 5.6 3 3 79.7692 78.4921 -1.2772 0.1579 0.8822
19 half-way 4.8 5.6 3 3 75.7843 78.4921 2.7078 -0.2903 0.7860
Per dish, normalised centre and half-way with background subtracted (%):
file kV nc_a nh_a nc_b nh_b
0 sub-15_obj-t7o4u3 2.4 138.5 90.2 146.1 90.5
1 sub-16 2.4 87.3 73.3 94.5 77.3
2 sub-17 2.4 133.9 38.4 164.0 43.8
3 sub-21 2.4 150.2 78.0 188.7 88.6
4 sub-23 3.2 176.5 112.2 178.3 113.8
5 sub-24 3.2 159.6 100.7 161.7 102.1
6 sub-25_obj-1of0oop 3.2 100.7 71.4 108.0 76.6
7 sub-03 4.0 136.2 80.6 158.6 93.9
8 sub-08 4.0 110.8 76.7 119.2 77.2
9 sub-14 4.0 165.0 82.0 188.1 90.2
10 sub-05 4.8 145.7 66.9 149.3 66.9
11 sub-12 4.8 159.9 76.6 171.5 83.9
12 sub-13 4.8 154.9 83.8 174.1 91.9
13 sub-25_obj-17bqgcb 5.6 115.8 68.1 128.4 74.6
14 sub-26 5.6 173.4 94.1 182.4 95.6
15 sub-27 5.6 148.6 73.3 158.8 73.9
Variant, pixel size from the needle footprints, no background: 1 of 20 pairs at p < 0.05; smallest p 0.0279
position kV_a kV_b n_a n_b mean_a mean_b diff t p
2 centre 2.4 4.8 4 3 115.4842 157.5198 42.0357 -3.0661 0.0279
and with the exclusion-zone ROIs dropped: 4 of 20 pairs at p < 0.05; smallest p 0.0055
Variant, pixel size from the needle footprints, footprint background subtracted: 0 of 20 pairs at p < 0.05; smallest p 0.1445
Empty DataFrame
Columns: [position, kV_a, kV_b, n_a, n_b, mean_a, mean_b, diff, t, p]
Index: []
and with the exclusion-zone ROIs dropped: 1 of 20 pairs at p < 0.05; smallest p 0.0398
Variant, end ROIs inside the exclusion zone dropped, no background: 4 of 20 pairs at p < 0.05; smallest p 0.0075; normalised centre means by kV {2.4: 117.5, 3.2: 122.4, 4.0: 114.2, 4.8: 148.9, 5.6: 147.9}
position kV_a kV_b mean_a mean_b p
2 centre 2.4 4.8 117.4656 148.8657 0.0298
3 centre 2.4 5.6 117.4656 147.8602 0.0075
8 centre 4.0 5.6 114.2127 147.8602 0.0164
14 half-way 3.2 4.0 98.1740 86.1356 0.0459
Variant, end ROIs inside the exclusion zone dropped, footprint background subtracted: 2 of 20 pairs at p < 0.05; smallest p 0.0426; normalised centre means by kV {2.4: 166.8, 3.2: 142.8, 4.0: 126.9, 4.8: 175.1, 5.6: 170.0}
position kV_a kV_b mean_a mean_b p
8 centre 4.0 5.6 126.8782 170.0271 0.0426
14 half-way 3.2 4.0 96.4259 76.1155 0.0432
Result: not reproduced, resting on the main reading. Under the main reading 1 of 20 pairs differs at p < 0.05: the normalised centre, 108.6% at 2.4 kV against 136.9% at 4.8 kV (p = 0.0037). The half-way values do not differ (smallest p = 0.12). The pair stays significant whichever 2.4 kV dish is left out (p = 0.0012 to 0.0116). The readings disagree: 10 of 36 give no significant pair and 26 give one to five. With no effect, the chance of at least one p below 0.05 among the 20 uncorrected tests is 0.64 if the tests were independent; they share groups and dishes, and the simulation of this design gives 0.47 with the two positions correlated as in the data (0.50 if they were independent). So a single significant pair is common by chance, though less common than 0.64 suggests. The p = 0.0037 would not pass a Bonferroni threshold for 20 tests (0.0025), which the criterion does not apply. The normalised centre means lie between 108.6% and 136.9% across voltages. The result is fragile in the other direction too: with the footprint background subtracted (a variant the claims file excludes), the main reading finds no pair (smallest p = 0.18) and 30 of 36 readings find none. With the end ROIs inside the exclusion zone dropped, 4 of 20 pairs differ without background and 2 of 20 with it. At the pixel size implied by the needle footprints, 1 of 20 pairs differs without background (the same pair, p = 0.0279) and none with it (smallest p = 0.14).
fig7cd-holds-for-large-array¶
The independence of the normalized distribution from amplitude also holds when rotating multiple packets (10Px4) on the larger array.
Criterion: as fig7b, on the large array with 10Px4 (n = 3), with the same test, positions and rule; the rise of a.u. values with voltage at the three positions is reported. The dataset holds two voltages for this array, 4.8 kV (2 dishes) and 6.4 kV (3 dishes); the paper's other voltages for Fig. 7C and 7D are not in it, so one pair is tested.
t7cd = pair_tests(10, MAIN, "a")
print("Main reading (normalisation a):")
print(t7cd.round(4).to_string())
df = table(10, MAIN)
print("\nPer dish:")
print(df[["file", "kV", "periph", "half", "center", "nc_a", "nh_a", "nc_b", "nh_b"]].round(1).to_string())
g = df.groupby("kV")[["periph", "half", "center"]].mean()
print("\nRise in a.u. from 4.8 to 6.4 kV (ratio of means):", (g.loc[6.4] / g.loc[4.8]).round(2).to_dict())
s7cd = readings_summary(10)
print("\nEvery reading:")
print(s7cd.round(4).to_string())
print("\nReadings with no pair at p < 0.05:", int((s7cd.n_p_below_05 == 0).sum()), "of", len(s7cd))
s7cd_bg = readings_summary(10, background)
print("\nVariant with the footprint background subtracted, every reading:")
print(s7cd_bg.round(4).to_string())
print("Readings with no pair at p < 0.05:", int((s7cd_bg.n_p_below_05 == 0).sum()), "of", len(s7cd_bg))
print("\nMain reading with background subtracted:")
print(pair_tests(10, MAIN, "a", background).round(4).to_string())
print("Per dish, with background subtracted:")
print(table(10, MAIN, background)[["file", "kV", "periph", "half", "center", "nc_a", "nh_a", "nc_b", "nh_b"]].round(1).to_string())
for bgname, bg in (("no background", None), ("footprint background subtracted", background)):
t = inner_pairs(10, bg)
print(f"\nVariant, end ROIs inside the exclusion zone dropped, {bgname}:")
print(t.round(4).to_string())
for bgname, bg in (("no background", None), ("footprint background subtracted", background)):
t = pair_tests(10, FP, "a", bg)
ti = inner_pairs(10, bg, INNER_FP)
print(f"\nVariant, pixel size from the needle footprints, {bgname}: centre p = {t.p.iloc[0]:.4f} "
f"({t.mean_a.iloc[0]:.1f}% against {t.mean_b.iloc[0]:.1f}%), half-way p = {t.p.iloc[1]:.4f}; "
f"with the exclusion-zone ROIs dropped, centre p = {ti.p.iloc[0]:.4f}, half-way p = {ti.p.iloc[1]:.4f}")
Main reading (normalisation a):
position kV_a kV_b n_a n_b mean_a mean_b diff t p
0 centre 4.8 6.4 2 3 232.6258 125.9528 -106.6730 3.1995 0.0494
1 half-way 4.8 6.4 2 3 94.7837 46.7986 -47.9851 1.8625 0.1594
Per dish:
file kV periph half center nc_a nh_a nc_b nh_b
0 sub-02 4.8 684.5 853.8 1852.0 270.6 124.7 274.1 125.3
1 sub-15_obj-znj27w 4.8 1461.7 947.9 2845.9 194.7 64.8 208.2 66.9
2 sub-04 6.4 3229.8 2140.9 4059.6 125.7 66.3 139.7 69.0
3 sub-07 6.4 4575.0 1524.7 4683.3 102.4 33.3 141.5 43.7
4 sub-11 6.4 3566.1 1454.4 5342.0 149.8 40.8 206.2 56.8
Rise in a.u. from 4.8 to 6.4 kV (ratio of means): {'periph': 3.53, 'half': 1.89, 'center': 2.0}
Every reading:
scale count diagonals centre source norm n_tests n_p_below_05 min_p max_abs_diff
0 camera both both a raw a 2 1 0.0494 106.6730
1 camera both both a raw b 2 0 0.1262 78.6885
2 camera both both a loess a 2 0 0.2773 142.1865
3 camera both both a loess b 2 0 0.3881 117.7976
4 camera both both b raw a 2 0 0.1139 67.3669
5 camera both both b raw b 2 0 0.1938 39.5998
6 camera both d1 a raw a 2 1 0.0027 157.4427
7 camera both d1 a raw b 2 1 0.0067 127.5610
8 camera both d1 a loess a 2 0 0.2064 325.1204
9 camera both d1 a loess b 2 0 0.2615 254.4350
10 camera both d1 b raw a 2 1 0.0007 116.7293
11 camera both d1 b raw b 2 1 0.0053 82.1037
12 camera both d2 a raw a 2 0 0.2233 63.2350
13 camera both d2 a raw b 2 0 0.3298 30.8881
14 camera both d2 a loess a 2 0 0.9552 6.4182
15 camera both d2 a loess b 2 0 0.8603 18.8398
16 camera both d2 b raw a 2 0 0.2233 40.7155
17 camera both d2 b raw b 2 0 0.3298 30.8881
18 camera per both a raw a 2 0 0.1161 223.1895
19 camera per both a raw b 2 0 0.0984 221.6592
20 camera per both a loess a 2 0 0.2525 223.7742
21 camera per both a loess b 2 0 0.3129 271.9355
22 camera per both b raw a 2 0 0.0970 153.1493
23 camera per both b raw b 2 0 0.0869 150.5094
24 camera per d1 a raw a 2 1 0.0237 328.4575
25 camera per d1 a raw b 2 1 0.0196 336.5575
26 camera per d1 a loess a 2 0 0.2151 562.3975
27 camera per d1 a loess b 2 0 0.2318 548.0606
28 camera per d1 b raw a 2 1 0.0017 244.6615
29 camera per d1 b raw b 2 1 0.0049 249.6098
30 camera per d2 a raw a 2 0 0.2665 148.8533
31 camera per d2 a raw b 2 0 0.4275 106.7609
32 camera per d2 a loess a 2 0 0.9338 12.6254
33 camera per d2 a loess b 2 0 0.9781 4.1896
34 camera per d2 b raw a 2 0 0.2665 97.0395
35 camera per d2 b raw b 2 0 0.6325 51.4091
Readings with no pair at p < 0.05: 27 of 36
Variant with the footprint background subtracted, every reading:
scale count diagonals centre source norm n_tests n_p_below_05 min_p max_abs_diff
0 camera both both a raw a 2 0 0.0705 184.1337
1 camera both both a raw b 2 0 0.1004 160.1306
2 camera both both a loess a 2 0 0.2389 818.0569
3 camera both both a loess b 2 0 0.3375 5208.1020
4 camera both both b raw a 2 0 0.1077 130.2326
5 camera both both b raw b 2 0 0.1859 98.0844
6 camera both d1 a raw a 2 1 0.0145 284.1408
7 camera both d1 a raw b 2 1 0.0205 252.8736
8 camera both d1 a loess a 2 0 0.3032 1649.1133
9 camera both d1 a loess b 2 0 0.3405 10487.2466
10 camera both d1 b raw a 2 1 0.0065 222.1608
11 camera both d1 b raw b 2 1 0.0116 183.4442
12 camera both d2 a raw a 2 0 0.2328 112.6595
13 camera both d2 a raw b 2 0 0.3705 67.3875
14 camera both d2 a loess a 2 0 0.7319 92.0013
15 camera both d2 a loess b 2 0 0.7810 71.0426
16 camera both d2 b raw a 2 0 0.2606 64.7042
17 camera both d2 b raw b 2 0 0.3705 36.7621
18 camera per both a raw a 2 0 0.1423 377.6489
19 camera per both a raw b 2 0 0.0743 452.3725
20 camera per both a loess a 2 0 0.2686 11595.6466
21 camera per both a loess b 2 2 0.0367 1264.4575
22 camera per both b raw a 2 0 0.1148 259.4286
23 camera per both b raw b 2 1 0.0361 319.5774
24 camera per d1 a raw a 2 1 0.0411 612.6347
25 camera per d1 a raw b 2 1 0.0220 722.1836
26 camera per d1 a loess a 2 0 0.3795 646.8981
27 camera per d1 a loess b 2 2 0.0083 2701.5782
28 camera per d1 b raw a 2 1 0.0025 451.9588
29 camera per d1 b raw b 2 1 0.0008 545.2047
30 camera per d2 a raw a 2 0 0.2620 246.3245
31 camera per d2 a raw b 2 0 0.3835 182.5613
32 camera per d2 a loess a 2 0 0.6406 213.5707
33 camera per d2 a loess b 2 0 0.7008 172.6632
34 camera per d2 b raw a 2 0 0.3024 165.7902
35 camera per d2 b raw b 2 0 0.5678 93.9501
Readings with no pair at p < 0.05: 25 of 36
Main reading with background subtracted:
position kV_a kV_b n_a n_b mean_a mean_b diff t p
0 centre 4.8 6.4 2 3 312.8647 128.7311 -184.1337 2.7548 0.0705
1 half-way 4.8 6.4 2 3 98.0704 41.4244 -56.6460 1.5604 0.2166
Per dish, with background subtracted:
file kV periph half center nc_a nh_a nc_b nh_b
0 sub-02 4.8 390.9 560.1 1558.4 398.7 143.3 416.1 146.2
1 sub-15_obj-znj27w 4.8 1089.7 575.9 2473.9 227.0 52.8 258.2 54.8
2 sub-04 6.4 2851.1 1762.2 3680.9 129.1 61.8 148.1 64.9
3 sub-07 6.4 4194.6 1144.3 4302.9 102.6 27.3 152.7 37.5
4 sub-11 6.4 3258.1 1146.4 5034.0 154.5 35.2 230.3 53.4
Variant, end ROIs inside the exclusion zone dropped, no background:
position kV_a kV_b mean_a mean_b p
0 centre 4.8 6.4 283.7001 185.3961 0.0173
1 half-way 4.8 6.4 114.4439 66.6179 0.1248
Variant, end ROIs inside the exclusion zone dropped, footprint background subtracted: position kV_a kV_b mean_a mean_b p 0 centre 4.8 6.4 415.1495 199.1151 0.0089 1 half-way 4.8 6.4 128.5018 61.1629 0.1714 Variant, pixel size from the needle footprints, no background: centre p = 0.5736 (249.5% against 226.6%), half-way p = 0.9624; with the exclusion-zone ROIs dropped, centre p = 0.5736, half-way p = 0.9624 Variant, pixel size from the needle footprints, footprint background subtracted: centre p = 0.2531 (326.2% against 254.6%), half-way p = 0.9974; with the exclusion-zone ROIs dropped, centre p = 0.2531, half-way p = 0.9974
Result: not reproduced, resting on the main reading. Under the main reading the normalised centre is 232.6% at 4.8 kV (2 dishes) and 126.0% at 6.4 kV (3 dishes), p = 0.0494; the half-way values do not differ (p = 0.16). Both 4.8 kV dishes (270.6% and 194.7%) lie above all three 6.4 kV dishes (102.4% to 149.8%). By the criterion the absence is not shown. 27 of 36 readings find no difference, and with the background subtracted the main reading gives p = 0.0705 (25 of 36 readings find none), so the outcome rests on the main reading and on a two-dish group; with two dishes against three a non-significant reading could not show absence either. With the end ROIs inside the exclusion zone dropped the centre differs more clearly (p = 0.0173, and 0.0089 with background subtracted). At the pixel size implied by the needle footprints the difference is gone: 249.5% against 226.6%, p = 0.57 (p = 0.25 with background subtracted), so the outcome rests on the camera scale as well. The a.u. values rise from 4.8 to 6.4 kV at all three positions (ratios 3.53 at the periphery, 1.89 half-way and 2.00 at the centre).
discussion-independent-of-array-size¶
Remote targeting, within the studied limits, depends neither on pulse amplitude nor on array size, so findings from small arrays can be extrapolated to larger inter-electrode distances.
Criterion: reproduced when fig7b-targeting-independent-of-amplitude and fig7cd-holds-for-large-array are both reproduced. The normalised profiles of the two arrays are shown side by side, on distance as a fraction of the ROI span; the small array received 1Px4 and the large array 10Px4, so the comparison mixes array size with packet number.
fig, ax = plt.subplots(figsize=(7, 4.5))
for reps, ls in ((1, "-"), (10, "--")):
prof = folded_profiles(reps)
for kv in sorted({res[p]["kV"] for p in prof}):
Q = np.array([100 * prof[p] / prof[p][-1] for p in prof if res[p]["kV"] == kv])
x = np.linspace(0, 1, Q.shape[1])
ax.plot(x, Q.mean(0), ls, label=f"{'small' if reps == 1 else 'large'} {reps}Px4 {kv} kV")
print(f"{'small' if reps == 1 else 'large'} {kv} kV: centre {Q.mean(0)[0]:.1f}%, minimum {Q.mean(0).min():.1f}% at "
f"{x[np.argmin(Q.mean(0))]:.2f} of the span")
ax.set_xlabel("distance from centre, fraction of ROI span")
ax.set_ylabel("% of most peripheral ROI")
ax.legend(fontsize=7)
plt.show()
print("fig7b pairs at p < 0.05 (main):", int((t7b.p < 0.05).sum()), "; fig7cd pairs at p < 0.05 (main):", int((t7cd.p < 0.05).sum()))
small 2.4 kV: centre 109.5%, minimum 87.3% at 0.55 of the span small 3.2 kV: centre 129.9%, minimum 90.4% at 0.68 of the span small 4.0 kV: centre 123.8%, minimum 84.8% at 0.68 of the span small 4.8 kV: centre 140.1%, minimum 82.0% at 0.59 of the span small 5.6 kV: centre 134.5%, minimum 75.4% at 0.59 of the span large 4.8 kV: centre 212.3%, minimum 75.4% at 0.78 of the span large 6.4 kV: centre 145.0%, minimum 40.9% at 0.76 of the span
fig7b pairs at p < 0.05 (main): 1 ; fig7cd pairs at p < 0.05 (main): 1
Result: not reproduced, resting on the main reading. Both claims it depends on are not reproduced under the main reading, each by one comparison near or below p = 0.05, so by its criterion this claim is not reproduced. The side-by-side profiles cannot isolate array size, since the arrays received different packet numbers.
Packet 2 dishes against Figs. 8 and 9 (not a test of those claims)¶
The fig8 and fig9 claims are blocked here, because their criteria need Packets 1 and 3 and the dishes with gaps, which are in other dandisets. Two conditions of this dandiset match conditions of those figures: 4.0 kV 1Px4 is the Packet 2 group of Fig. 8 (if Fig. 8 used the small array, which its legend does not say), and 6.4 kV 10Px4 on the large array is the 0 mm group of Fig. 9. Their values are set against the paper's numbers for Packet 2 in Section 3.4 (about 120 a.u. near the electrodes, 215 to 235 a.u. in the centre, 183% normalised centre), as a check of whether the dataset's a.u. are on the paper's scale. The text gives no a.u. for the 0 mm group of Fig. 9, only ratios to the gap groups, so that profile is printed without a reference.
PAPER8 = dict(periph=120, center=(215, 235), nc=183)
for bgname, bg in (("no background", None), ("footprint background subtracted", background)):
d = table(1, MAIN, bg)
d = d[d.kV == 4.0]
print(f"4.0 kV 1Px4, main reading, {bgname}:")
print(d[["file", "periph", "half", "center", "nc_a", "nc_b"]].round(1).to_string())
print(f" mean periphery {d.periph.mean():.1f} a.u. (paper about {PAPER8['periph']}), centre {d.center.mean():.1f} a.u. "
f"(paper {PAPER8['center'][0]} to {PAPER8['center'][1]}), normalised centre {d.nc_a.mean():.1f}% (a) / "
f"{d.nc_b.mean():.1f}% (b), ratio of means {100 * d.center.mean() / d.periph.mean():.1f}% (paper {PAPER8['nc']}%)")
di = inner_table(1, bg)
di = di[di.kV == 4.0]
print(f" exclusion-zone variant: periphery {di.periph.mean():.1f}, centre {di.center.mean():.1f}, normalised centre {di.nc_a.mean():.1f}%")
for bgname, bg in (("no background", None), ("footprint background subtracted", background)):
d = table(10, MAIN, bg)
d = d[d.kV == 6.4]
print(f"\n6.4 kV 10Px4 (Fig. 9, 0 mm), main reading, {bgname}: periphery {d.periph.mean():.1f}, half-way {d.half.mean():.1f}, "
f"centre {d.center.mean():.1f} a.u.; normalised centre {d.nc_a.mean():.1f}% (per dish {d.nc_a.round(1).tolist()})")
prof = folded_profiles(10)
Q = np.array([prof[p] for p in prof if res[p]["kV"] == 6.4])
x = np.linspace(0, 1, Q.shape[1])
dbb = table(10, ("camera", "both", "both", "b", "raw"))
print("\n6.4 kV 10Px4, two definitions of the centre, % of the mean of the end ROIs, mean over dishes:\n"
f" centre reading a (one ROI centred on the array centre; the main reading): {table(10, MAIN).query('kV == 6.4').nc_a.mean():.1f}%\n"
f" centre reading b (mean of the two grid ROIs beside the centre, the first position of the folded profile): "
f"{dbb[dbb.kV == 6.4].nc_a.mean():.1f}%")
Pn = 100 * Q / Q[:, -1:]
print(f"Folded normalised profile (each dish to its own end ROIs, as in the discussion section): first position {Pn.mean(0)[0]:.1f}%, "
f"minimum {Pn.mean(0).min():.1f}% at {x[np.argmin(Pn.mean(0))]:.2f} of the span")
print("\n6.4 kV 10Px4 folded profile (no background), mean over 3 dishes at tenths of the span (0 centre, 1 periphery):")
print(pd.DataFrame(dict(position=x, au=Q.mean(0), pct=100 * Q.mean(0) / Q.mean(0)[-1])).iloc[np.searchsorted(x, np.linspace(0, 1, 11)).clip(0, len(x) - 1)]
.round(2).to_string(index=False))
4.0 kV 1Px4, main reading, no background:
file periph half center nc_a nc_b
7 sub-03 539.8 477.4 656.3 121.6 128.3
8 sub-08 803.2 680.8 860.1 107.1 110.3
9 sub-14 501.5 463.0 640.0 127.6 130.1
mean periphery 614.9 a.u. (paper about 120), centre 718.8 a.u. (paper 215 to 235), normalised centre 118.8% (a) / 122.9% (b), ratio of means 116.9% (paper 183%)
exclusion-zone variant: periphery 639.0, centre 718.8, normalised centre 114.2%
4.0 kV 1Px4, main reading, footprint background subtracted:
file periph half center nc_a nc_b
7 sub-03 321.8 259.4 438.3 136.2 158.6
8 sub-08 526.3 403.9 583.2 110.8 119.2
9 sub-14 213.2 174.7 351.7 165.0 188.1
mean periphery 353.8 a.u. (paper about 120), centre 457.7 a.u. (paper 215 to 235), normalised centre 137.3% (a) / 155.3% (b), ratio of means 129.4% (paper 183%)
exclusion-zone variant: periphery 377.9, centre 457.7, normalised centre 126.9%
6.4 kV 10Px4 (Fig. 9, 0 mm), main reading, no background: periphery 3790.3, half-way 1706.7, centre 4695.0 a.u.; normalised centre 126.0% (per dish [125.7, 102.4, 149.8])
6.4 kV 10Px4 (Fig. 9, 0 mm), main reading, footprint background subtracted: periphery 3434.6, half-way 1351.0, centre 4339.3 a.u.; normalised centre 128.7% (per dish [129.1, 102.6, 154.5])
6.4 kV 10Px4, two definitions of the centre, % of the mean of the end ROIs, mean over dishes:
centre reading a (one ROI centred on the array centre; the main reading): 126.0%
centre reading b (mean of the two grid ROIs beside the centre, the first position of the folded profile): 145.0%
Folded normalised profile (each dish to its own end ROIs, as in the discussion section): first position 145.0%, minimum 40.9% at 0.76 of the span
6.4 kV 10Px4 folded profile (no background), mean over 3 dishes at tenths of the span (0 centre, 1 periphery):
position au pct
0.00 5390.17 142.21
0.11 4427.53 116.81
0.22 2793.91 73.71
0.31 2089.82 55.14
0.42 2093.73 55.24
0.51 1814.66 47.88
0.62 1817.59 47.95
0.71 1686.21 44.49
0.82 2077.18 54.80
0.91 2389.79 63.05
1.00 3790.30 100.00
The 4.0 kV 1Px4 dishes are not on the paper's Fig. 8 scale: without background their periphery is 614.9 a.u. and their centre 718.8 a.u., against about 120 and 215 to 235 a.u., and their normalised centre is 118.8% (ratio of means 116.9%) against 183%. With the footprint background subtracted they are 353.8 and 457.7 a.u. and 137.3%, still far from the paper's values. Either Fig. 8's Packet 2 dishes are not these dishes, or the paper's a.u. were scaled or corrected in a way the text does not describe. For the 6.4 kV 10Px4 dishes the centre ROI of the main reading is 126.0% of the end ROIs; the first position of the folded profile, which is the mean of the two grid ROIs beside the centre (centre reading b) and is the value given in the discussion section, is 145.0%. The folded profile has its minimum, 40.9%, at 0.76 of the span. The text gives no a.u. for this group to compare with. These numbers decide none of the claims.
Summary¶
A failure to reproduce here may be a failure of this attempt, not of the paper: see the reasoning and the differences from the paper's method.
| Claim | Outcome |
|---|---|
| fig4-unipolar-strongest | blocked (data_missing) |
| fig4-bipolar-cancellation | blocked (data_missing) |
| fig5a-center-enhanced-without-rotation | blocked (other: data in dandiset 000686) |
| fig5bc-rotation-focuses-center | blocked (other: data in dandiset 000686) |
| fig6a-more-packets-more-uptake | blocked (other: data in dandiset 000875) |
| fig6b-normalized-center-by-packets | blocked (other: data in dandiset 000875) |
| fig7a-amplitude-scales-uptake | not reproduced |
| fig7b-targeting-independent-of-amplitude | not reproduced (assumed: main reading) |
| fig7cd-holds-for-large-array | not reproduced (assumed: main reading) |
| discussion-independent-of-array-size | not reproduced (assumed: main reading) |
| fig8a-packets-differ-near-electrodes | blocked (other: data in dandiset 001027) |
| fig8b-packet2-best-targeting | blocked (other: data in dandiset 001027) |
| fig9-gap-boosts-targeting | blocked (other: data in dandiset 001028) |
| fig9c-restricted-to-center | blocked (other: data in dandiset 001028) |
How the analysis differs from the paper's:
- The paper measured ROIs in MetaMorph on images whose electrode positions the experimenters knew; here the electrodes are found by fitting four dark disks, and the ROIs placed from them.
- The pixel size is not in the dataset; the camera scale (0.65 um) is used. The needle footprints rule out the 8.4 mm scale of round 1, which would put the end ROIs on the needles, but do not confirm the camera scale: they imply about 0.68 um (small array) and 0.75 um (large array). At that scale, a variant, fig7cd's centre difference disappears (p = 0.57).
- At the camera scale the paper's ROI spans end inside its own 5% exclusion zone; the spans are kept as stated, and a variant drops the end ROIs inside the zone.
- ROI width across the diagonal is not given; square ROIs are used.
- Whether a background was subtracted is not stated; the claims file fixes none, and the footprint background is shown only as a variant. It changes fig7a from about 6-fold to near the paper's 16-fold and removes the one significant pair of fig7b. A reader is asked whether the claims file reads the Methods correctly here.
- 2.4 kV has four dishes here, against n = 3 in the paper; all four are used, with leave-one-out shown for fig7a and fig7b.
- The large array has two voltages here (4.8 kV with 2 dishes and 6.4 kV with 3).
- The packet is not named in the files; Packet 2 is assumed, as in Fig. 7 and the dandiset's title.
What was found about the dataset:
- It holds only the images of Fig. 7: no single-cell time-lapse data (Fig. 4) and none of the experiments of Figs. 5, 6, 8 and 9 (other than, possibly, dishes shared with Fig. 7), which appear, from their titles and descriptions, to be in other dandisets.
- The array size, pixel size, packet, electrode positions and ROI measurements are not in the files.
- Subject ids are reused for different dishes, and the archive counts 19 subjects for 21 dishes.
- The dark floor of the images differs between dishes and sessions (33 to 81 a.u.), and the paper does not say whether a background was removed; the fold changes of fig7a depend on it.
- The 4.0 kV 1Px4 dishes, the Packet 2 condition of Fig. 8 if Fig. 8 used the small array (its legend does not say), read about 615 a.u. at the periphery and 719 a.u. in the centre, against about 120 and 215 to 235 a.u. in the paper.
- Of the 21 files, 7 name no detector, objective or channel.