Reproducing Kim et al. (2023) from dandiset 000631¶
Paper. Kim V, Semenov I, Kiester AS, Keppler MA, Ibey BL, Bixler JN, Colunga Biancatelli RML, Pakhomov AG (2023). Control of the Electroporation Efficiency of Nanosecond Pulses by Swinging the Electric Field Vector Direction. Int J Mol Sci 24, 10921. doi:10.3390/ijms241310921. The claims file was written from the version of record as served by Europe PMC (PMC10341945), with no supplement (the paper has none) and no figures; the paper links no code.
Dandiset. DANDI:000631, version 0.231012.1508. The paper cites no version of the dandiset (its data statement says the data are available on request), so the most recent published version is pinned; it is the only one. The draft holds the same 15 assets (same asset ids and sizes).
What the dandiset holds. Fifteen stitched images (MemBrite and YO-PRO-1) of BPAE monolayers
after paired-pulse and single-pulse trains in the triangular array at 1 Hz and 833 kHz: the
experiment of Figure 2, whose 833 kHz dishes are also those of Figure 4. One 1 Hz control image is
deposited twice. The square-array experiment of Figure 3 and the FluoVolt strobe imaging of
Figure 5 are not in it. The files hold no pixel size, electrode positions, ROIs, angles or
measured values: all of these are computed here from the images (see the helper swing.py).
Questions answered. A reader answered one question on the claims file
(questions/000631.yaml): reading (a) of the background, the ROI intensity with no correction
for spontaneous uptake, is not a reading the paper allows, as Methods 4.4 says the footprints were
measured "to account for" it. The claims file now decides every claim under reading (b), the ROI
intensity minus the footprint intensity of the same dish, and keeps (a) as a variant that decides
no outcome. The claims of Figures 2 and 4 are re-run under the amended criteria; (a) is still
printed beside (b).
Outside inputs. None.
| Claim | Figure | Tested here |
|---|---|---|
| fig2-180-cancellation | 2 | tested |
| fig2-crossover-angle | 2 | tested |
| fig2-facilitation-plateau | 2 | tested |
| fig2-frequency-dependence | 2 | tested |
| fig2-60-fold-co-counter | 2 | tested |
| fig3-1hz-co-equals-cross | 3 | blocked: square-array data not in the dandiset (data_missing) |
| fig3-770khz-co-enhanced | 3 | blocked: data_missing |
| fig3-770khz-cross-same-as-1hz | 3 | blocked: data_missing |
| fig3-no-area-dependence | 3 | blocked: depends on fig3-1hz-co-equals-cross (data_missing) |
| fig4-unipolar-pattern | 4 | tested |
| fig5-base-sawtooth | 5 | blocked: FluoVolt strobe data not in the dandiset (data_missing) |
| fig5-apex-larger-polarization | 5 | blocked: data_missing |
The notebook streams every file (about 18 GB) into memory, one at a time, and reduces each image to 10 x 10 pixel block means as it loads; it runs in about four minutes.
import time
import warnings
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy import stats
import swing
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)
started = time.time()
assets = swing.list_assets()
dishes = [swing.load(a) for a in assets]
print(f"loaded {len(dishes)} files of dandiset {swing.DANDISET_ID} version {swing.DANDISET_VERSION} "
f"in {time.time() - started:.0f} s; images reduced to {swing.BLOCK} x {swing.BLOCK} pixel block means")
loaded 15 files of dandiset 000631 version 0.231012.1508 in 256 s; images reduced to 10 x 10 pixel block means
Data check¶
What the files hold, checked against the descriptions. Each check is stated under its output.
info = pd.DataFrame([{**{k: d["meta"][k] for k in ("subject", "freq", "train", "date", "session_id",
"session_start_time", "shape", "dtype", "full_min", "full_max")},
"digest": d["meta"]["chunk_digest"][:10]} for d in dishes])
print(info.to_string(index=False))
print("\nFiles by frequency and train:")
print(pd.crosstab(info.freq, info.train).to_string())
m0 = dishes[0]["meta"]
print("\nContainers in every file (acquisition / processing / analysis / stimulus / intervals):",
sorted({(tuple(d["meta"]["acquisition"]), tuple(d["meta"]["processing"]), tuple(d["meta"]["analysis"]),
tuple(d["meta"]["stimulus"]), tuple(d["meta"]["intervals"])) for d in dishes}))
print("Data attributes:", m0["data_attrs"], "; chunks", m0["chunks"])
print("ImageSeries descriptions identical in all files:", len({d["meta"]["series_description"] for d in dishes}) == 1)
print("ImageSeries description:", m0["series_description"])
print("\nSession descriptions, by file group:")
for desc, grp in info.assign(desc=[d["meta"]["session_description"] for d in dishes]).groupby("desc"):
print(f"- {len(grp)} file(s): {', '.join(grp.subject)}\n {desc}\n")
subject freq train date session_id session_start_time shape dtype full_min full_max digest
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 833 kHz paired 8-24-21 BPAE_5xBipolar_0,83MHz 2021-08-24T19:09:14-04:00 (22323, 22323, 2) uint16 0 65441 4e47e3164e
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 833 kHz paired 8-24-21 600ns_4kV_0.8MHz paired pulse trains 2021-08-24T19:42:30-04:00 (22323, 22323, 2) uint16 0 65443 bccd717b6f
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 833 kHz paired 8-24-21 600ns_4kV_0.8MHz paired pulse trains 2021-08-24T20:56:26-04:00 (22323, 22323, 2) uint16 0 65442 582f19a4da
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 833 kHz single 10-5-21 600ns_4kV_0.8MHz single pulse trains 2021-10-05T15:56:34-04:00 (29696, 29696, 2) uint16 0 65464 b34380b13a
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 833 kHz single 10-5-21 600ns_4kV_0.8MHz single pulse trains 2021-10-05T16:14:24-04:00 (29696, 29696, 2) uint16 0 65464 03ece91bfa
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 833 kHz single 10-5-21 600ns_4kV_0.8MHz single pulse trains 2021-10-05T17:30:59-04:00 (29696, 29696, 2) uint16 0 65464 4989bd91d5
600ns-5kV-1HzBP-8-9-21-BPAE-13 1 Hz paired 8-9-21 600ns_5kV_1Hz paired pulse trains 2021-08-09T18:05:32-04:00 (22323, 22323, 2) uint16 0 65439 01efd894d0
600ns-5kV-1HzBP-8-9-21-BPAE-15 1 Hz paired 8-9-21 600ns_5kV_1Hz paired pulse trains 2021-08-09T18:33:59-04:00 (22323, 22323, 2) uint16 0 65445 125d252730
600ns-5kV-1HzBP-8-9-21-BPAE-8 1 Hz paired 8-9-21 600ns_5kV_1Hz paired pulse trains 2021-08-09T16:51:41-04:00 (22323, 22323, 2) uint16 0 65448 69a6a7e720
600ns-5kV-1HzUP-7-31-21-BPAE-03 1 Hz single 7-31-21 600ns_5kV_1Hz single pulse trains 2021-07-31T20:19:49-04:00 (22323, 22323, 2) uint16 0 65440 3eac71aad8
600ns-5kV-1HzUP-7-31-21-BPAE-10 1 Hz single 7-31-21 600ns_5kV_1Hz single pulse trains 2021-07-31T18:25:08-04:00 (22323, 22323, 2) uint16 0 65447 82c325767f
600ns-5kV-1HzUP-7-31-21-BPAE-18 1 Hz single 7-31-21 600ns_5kV_1Hz single pulse trains 2021-07-31T20:19:49-04:00 (22323, 22323, 2) uint16 0 65440 3eac71aad8
600ns-5kV-1HzUP-7-31-21-BPAE-5 1 Hz single 7-31-21 600ns_5kV_1Hz single pulse trains 2021-07-31T17:12:48-04:00 (22323, 22323, 2) uint16 0 65440 9921bf8306
600ns-5kV-1HzUP-8-9-21-BPAE-10 1 Hz single 8-9-21 600ns_5kV_1Hz single pulse trains 2021-08-09T17:21:31-04:00 (22323, 22323, 2) uint16 0 65445 15e5c3efa5
600ns-5kV-1HzUP-8-9-21-BPAE-14 1 Hz single 8-9-21 600ns_5kV_1Hz single pulse trains 2021-08-09T18:20:34-04:00 (22323, 22323, 2) uint16 0 65445 a0107d0511
Files by frequency and train:
train paired single
freq
1 Hz 3 6
833 kHz 3 3
Containers in every file (acquisition / processing / analysis / stimulus / intervals): [(('ScaningImaging',), (), (), (), ())]
Data attributes: {'conversion': np.float64(1.0), 'offset': np.float64(0.0), 'resolution': np.float64(-1.0), 'unit': 'n.a.'} ; chunks (175, 349, 1)
ImageSeries descriptions identical in all files: True
ImageSeries description: IX83 microscope used; MS-2000 scanning stage; X-Cite 110LED illuminator; ORCA-Flash4 sCMOS camera;CellSens software for automation; Channel 1 ex/em 350/460nm for MemBrite dye; ex/em 480/535nm for YOPRO-1; dark circles indicate electrode footprints; 144 images captured in a 12x12 grid from a six-well plate; 216 additional images with DAPI and FITC filter cubes; all images automatically stitched for a final composite; 10×, 0.38NA objective used for high-resolution imaging.
Session descriptions, by file group:
- 1 file(s): 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13
Cells were electroporated by a train of 5 600+600 ns bipolar pulses (amplitude of the second phase was 50% the first phase's amplitude) at 0,83 MHz.The electrodes were two parallel stainless-steel cylinders, 1.5 mm in diameter and 3 mm apart
- 3 file(s): 600ns-5kV-1HzBP-8-9-21-BPAE-13, 600ns-5kV-1HzBP-8-9-21-BPAE-15, 600ns-5kV-1HzBP-8-9-21-BPAE-8
Cells were electroporated by paired-pulse trains, consisting of 5 pulses of 600 + 600 ns, trains were applied at frequencies of 1 Hz, amplitude 5 kV. The stainless-steel electrodes were aranged in triangular array;two base electrodes connected to channels; 2nd channel energized makes 1st channel ground and vice versa; apex electrode constantly grounded; base electrodes energized alternately, creating a variable electric field; electric field vector direction changes with position in the array; 180° angle change at the base transitions to co-directional pulses near the apex; trains of paired pulses induce a swinging motion of the electric field vector direction.
- 2 file(s): 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15, 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20
Cells were electroporated by paired-pulse trains, consisting of 5 pulses of 600 + 600 ns, trains were applied at frequencies of 833 kHz, amplitude 4 kV. The stainless-steel electrodes were aranged in triangular array;two base electrodes connected to channels; 2nd channel energized makes 1st channel ground and vice versa; apex electrode constantly grounded; base electrodes energized alternately, creating a variable electric field; electric field vector direction changes with position in the array; 180° angle change at the base transitions to co-directional pulses near the apex; trains of paired pulses induce a swinging motion of the electric field vector direction.
- 6 file(s): 600ns-5kV-1HzUP-7-31-21-BPAE-03, 600ns-5kV-1HzUP-7-31-21-BPAE-10, 600ns-5kV-1HzUP-7-31-21-BPAE-18, 600ns-5kV-1HzUP-7-31-21-BPAE-5, 600ns-5kV-1HzUP-8-9-21-BPAE-10, 600ns-5kV-1HzUP-8-9-21-BPAE-14
Cells were electroporated by single-pulse trains, consisting of 5 pulses of 600ns, trains were applied at frequencies of 1 Hz, amplitude 5 kV. The stainless-steel electrodes were aranged in triangular array;two base electrodes connected to channels; 2nd channel energized makes 1st channel ground; apex electrode constantly grounded.
- 3 file(s): 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05, 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06, 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10
Cells were electroporated by single-pulse trains, consisting of 5 pulses of 600ns, trains were applied at frequencies of 833 kHz, amplitude 4 kV. The stainless-steel electrodes were aranged in triangular array;two base electrodes connected to channels; 2nd channel energized makes 1st channel ground; apex electrode constantly grounded.
Each file holds one stitched image of two channels (MemBrite and YO-PRO-1, in the order the
ImageSeries description gives them) and nothing else: no pixel size (the resolution is -1 and the
unit "n.a."), no electrode positions, no ROIs, no angles and no measured values. The conditions
are in the subject names: 1 Hz at 5 kV and 833 kHz ("0,8MHz") at 4 kV, paired ("BP") or single
("UP") trains. One 833 kHz paired file (BPAE-13 of 8-24-21) carries the description of another
experiment (a bipolar pulse between two parallel electrodes 3 mm apart, session id
"BPAE_5xBipolar_0,83MHz"); its image is checked below. The ImageSeries description says 144
images in a 12 x 12 grid "from a six-well plate" and also the 216 images of the paper; the
dishes are 35 mm dishes (Methods 4.1).
# Recordings deposited more than once: compare the compressed chunks and the decoded images.
groups = info.groupby("digest").subject.apply(list)
dups = [g for g in groups if len(g) > 1]
print("Files whose image chunks are all identical:", dups if dups else "none")
for g in dups:
a, b = [dishes[info.index[info.subject == s][0]]["img"] for s in g]
print(f" {g}: max abs difference of the block-mean images = {float(np.abs(a - b).max())}, "
f"session_start_time {set(info.session_start_time[info.subject.isin(g)])}")
# Any other pair with near-identical images?
near = []
for i in range(len(dishes)):
for j in range(i + 1, len(dishes)):
if dishes[i]["img"].shape == dishes[j]["img"].shape and info.digest[i] != info.digest[j]:
n0 = dishes[i]["img"].shape[1]
a_, b_ = (dishes[k]["img"][1][n0 // 4:3 * n0 // 4:5, n0 // 4:3 * n0 // 4:5].ravel() for k in (i, j))
r = np.corrcoef(a_, b_)[0, 1]
near.append((info.subject[i], info.subject[j], r))
near = pd.DataFrame(near, columns=["a", "b", "r_YP"]).sort_values("r_YP", ascending=False)
print("\nHighest YP correlations between distinct files of the same image size (central half of the image):")
print(near.head(4).round(3).to_string(index=False))
keep = [i for i in range(len(dishes)) if not any(info.subject[i] in g[1:] for g in dups)]
print("\nDishes used (each recording once):", len(keep))
print(pd.crosstab(info.freq[keep], info.train[keep]).to_string())
Files whose image chunks are all identical: [['600ns-5kV-1HzUP-7-31-21-BPAE-03', '600ns-5kV-1HzUP-7-31-21-BPAE-18']]
['600ns-5kV-1HzUP-7-31-21-BPAE-03', '600ns-5kV-1HzUP-7-31-21-BPAE-18']: max abs difference of the block-mean images = 0.0, session_start_time {'2021-07-31T20:19:49-04:00'}
Highest YP correlations between distinct files of the same image size (central half of the image):
a b r_YP
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 0.204
600ns-5kV-1HzBP-8-9-21-BPAE-13 600ns-5kV-1HzBP-8-9-21-BPAE-15 0.183
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 0.177
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 0.162
Dishes used (each recording once): 14
train paired single
freq
1 Hz 3 5
833 kHz 3 3
BPAE-03 and BPAE-18 of 7-31-21 (1 Hz, single pulses) are the same recording deposited twice: every compressed chunk is identical and they share a start time. BPAE-18 is left out, so each recording is counted once. No other pair of files is alike (the highest correlation of the central YP images of two files is printed above). That leaves 3 paired and 5 single dishes at 1 Hz, and 3 paired and 3 single at 833 kHz. The paper gives n = 3 for Figure 2; it does not say how many control dishes there were. The 1 Hz controls come from two days (7-31-21: 3 dishes; 8-9-21, the day of the paired dishes: 2); the 833 kHz controls (10-5-21) were imaged on a later day than the paired dishes (8-24-21), with a larger stitched image.
# Electrodes: the footprints are dark disks in the YP channel ("dark circles indicate electrode footprints").
free = [swing.find_electrodes_free(dishes[i]["img"][1]) for i in keep]
good = [g for g in free if min(g["scores"]) > 0.3]
template = swing.triangle_template(good)
geoms = {}
rows = []
mpb = swing.mm_per_block()
for i, g0 in zip(keep, free):
g = swing.fit_template(dishes[i]["img"][1], template)
geoms[i] = g
dev = max(np.linalg.norm(g[k] - g0[k]) for k in ("apex", "base1", "base2")) * mpb
side = lambda p, q: np.linalg.norm(g[p] - g[q]) * mpb
rows.append({"subject": info.subject[i], "base_mm": side("base1", "base2"), "apex_base1_mm": side("apex", "base1"),
"apex_base2_mm": side("apex", "base2"),
"midpoint_to_apex_mm": np.linalg.norm(g["apex"] - (g["base1"] + g["base2"]) / 2) * mpb,
"score_apex": g["scores"][0], "score_min": min(g["scores"]),
"free_min_score": min(g0["scores"]), "moved_from_free_mm": dev})
geo = pd.DataFrame(rows)
print(f"Template from the {len(good)} dishes whose three strongest footprints all score above 0.3 "
f"(base {np.linalg.norm(template[1] - template[2]) * mpb:.2f} mm, height {abs(template[0, 0] - template[1, 0]) * mpb:.2f} mm, at {swing.UM_PER_PX} um per pixel)")
print(geo.round(2).to_string(index=False))
print("\nSpread of the triangle over dishes (s.d., mm):", geo[["base_mm", "apex_base1_mm", "apex_base2_mm"]].std().round(3).to_dict())
Template from the 10 dishes whose three strongest footprints all score above 0.3 (base 4.63 mm, height 5.96 mm, at 0.65 um per pixel)
subject base_mm apex_base1_mm apex_base2_mm midpoint_to_apex_mm score_apex score_min free_min_score moved_from_free_mm
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 4.78 6.35 6.66 6.05 0.71 0.28 0.28 0.00
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 4.62 6.35 6.61 6.06 0.65 0.22 0.22 0.00
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 4.78 6.43 6.72 6.13 0.75 0.18 0.23 6.72
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 4.65 6.39 6.79 6.17 0.63 0.63 0.63 0.00
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 4.69 6.42 6.76 6.16 0.53 0.53 0.53 0.00
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 4.68 6.31 6.83 6.15 0.55 0.55 0.55 0.00
600ns-5kV-1HzBP-8-9-21-BPAE-13 4.56 6.36 6.64 6.09 0.64 0.29 0.29 0.00
600ns-5kV-1HzBP-8-9-21-BPAE-15 4.61 6.30 6.64 6.05 0.66 0.40 0.40 0.00
600ns-5kV-1HzBP-8-9-21-BPAE-8 4.60 6.46 6.64 6.13 0.52 0.37 0.37 0.00
600ns-5kV-1HzUP-7-31-21-BPAE-03 4.68 6.41 6.84 6.20 0.49 0.45 0.45 0.00
600ns-5kV-1HzUP-7-31-21-BPAE-10 4.60 6.38 6.70 6.13 0.48 0.36 0.36 0.00
600ns-5kV-1HzUP-7-31-21-BPAE-5 4.55 6.41 6.70 6.15 0.40 0.40 0.40 0.00
600ns-5kV-1HzUP-8-9-21-BPAE-10 4.65 6.40 6.68 6.11 0.39 0.32 0.32 0.00
600ns-5kV-1HzUP-8-9-21-BPAE-14 4.60 6.39 6.69 6.13 0.40 0.40 0.40 0.00
Spread of the triangle over dishes (s.d., mm): {'base_mm': 0.072, 'apex_base1_mm': 0.045, 'apex_base2_mm': 0.073}
The three electrodes are found in every dish as dark disks in the YP image: the free search (the three strongest disks) and the fit of a rigid triangle agree to the block in all dishes but one (BPAE-20 of 8-24-21), where the base electrodes have almost no uptake around them and the free search takes a wrong third point; the triangle fit finds them. The triangle has the same size in every dish, including the 833 kHz controls imaged on another day at a larger image size, so the same array and the same image scale were used throughout. The overview below shows the fit.
# Pixel size: the files give none. Checks of 0.65 um per pixel (6.5 um camera pixels, 10x objective).
ext = pd.DataFrame([{"subject": info.subject[i], "image_mm": dishes[i]["meta"]["shape"][0] * swing.UM_PER_PX / 1000} for i in keep])
print("Stitched image width at 0.65 um/pixel (mm):", sorted(set(ext.image_mm.round(1))))
edges = []
for i in keep:
g = geoms[i]
for k in ("apex", "base1", "base2"):
r, p = swing.radial_profile(dishes[i]["img"][1], g[k])
if p.max() > 1.5 * p[r < 0.3].mean(): # a clear rim of uptake around the footprint
edges.append(swing.edge_radius(r, p))
edges = np.array(edges)
print(f"Radius of the dark footprint where the uptake rim is clear: median {np.nanmedian(edges):.2f} mm "
f"(range {np.nanmin(edges):.2f} to {np.nanmax(edges):.2f}, {np.isfinite(edges).sum()} electrodes); needle outside radius 0.75 mm (Methods 4.2)")
print(f"Base midpoint to apex centre: {geo.midpoint_to_apex_mm.mean():.2f} mm; to the apex footprint edge: "
f"{geo.midpoint_to_apex_mm.mean() - 0.75:.2f} mm; 32 ROIs of 200 um span {32 * 0.2:.1f} mm (Methods 4.4)")
Stitched image width at 0.65 um/pixel (mm): [14.5, 19.3]
Radius of the dark footprint where the uptake rim is clear: median 0.72 mm (range 0.65 to 0.80, 39 electrodes); needle outside radius 0.75 mm (Methods 4.2) Base midpoint to apex centre: 6.12 mm; to the apex footprint edge: 5.37 mm; 32 ROIs of 200 um span 6.4 mm (Methods 4.4)
Three checks are consistent with 0.65 um per pixel, the size the camera and objective of Methods 4.4 give. At that size the stitched images are 14.5 mm and 19.3 mm wide, the sizes of the glass bottoms of 14 mm and 20 mm glass-bottom dishes; the dark footprints have the radius printed above, a little less than the 0.75 mm outside radius of the needles; and 32 contiguous ROIs of 200 um span about the distance from the base to the apex. At twice that pixel size none of these would hold. The field strength along the bisector, a fourth check, is printed with the field models below. The pixel size remains an assumption, as the files do not state it.
# Background inside the footprints. The needles are hollow (0.5 mm inside diameter, Methods 4.2),
# so a footprint is the lumen (radius 0.25 mm) inside a steel wall (to 0.75 mm). YP in rings of
# 0.05 mm from each electrode centre, mean over the three electrodes of a dish, and the largest
# single ring within the lumen (bright material in the lumen shows there).
rows = []
for i in keep:
g = geoms[i]
profs = [swing.radial_profile(dishes[i]["img"][1], g[k], r_max_mm=1.0, step_mm=0.05) for k in ("apex", "base1", "base2")]
r = profs[0][0]
mean = np.mean([p for _, p in profs], axis=0)
rows.append({"dish": info.subject[i][-17:], **{f"{x:.3f}": y for x, y in zip(r, mean)},
"largest lumen ring": max(p[r < 0.25].max() for _, p in profs)})
print("YP (counts) in rings around the electrode centres, ring mid radius in mm:")
print(pd.DataFrame(rows).round(0).to_string(index=False))
YP (counts) in rings around the electrode centres, ring mid radius in mm:
dish 0.025 0.075 0.125 0.175 0.225 0.275 0.325 0.375 0.425 0.475 0.525 0.575 0.625 0.675 0.725 0.775 0.825 0.875 0.925 0.975 largest lumen ring
P-8-24-21-BPAE-13 203.0 207.0 199.0 197.0 195.0 198.0 202.0 202.0 204.0 200.0 204.0 205.0 228.0 278.0 358.0 448.0 458.0 470.0 490.0 478.0 218.0
P-8-24-21-BPAE-15 211.0 214.0 217.0 217.0 225.0 221.0 223.0 221.0 227.0 223.0 223.0 223.0 257.0 278.0 319.0 347.0 385.0 423.0 466.0 451.0 250.0
P-8-24-21-BPAE-20 204.0 211.0 209.0 212.0 212.0 208.0 208.0 209.0 212.0 214.0 215.0 219.0 242.0 266.0 347.0 446.0 483.0 524.0 549.0 560.0 230.0
P-10-5-21-BPAE-05 303.0 290.0 218.0 213.0 210.0 207.0 206.0 205.0 206.0 211.0 216.0 254.0 382.0 463.0 572.0 691.0 845.0 803.0 782.0 822.0 493.0
P-10-5-21-BPAE-06 243.0 264.0 255.0 240.0 243.0 252.0 237.0 223.0 218.0 231.0 228.0 274.0 414.0 488.0 529.0 616.0 750.0 801.0 772.0 810.0 334.0
P-10-5-21-BPAE-10 344.0 387.0 276.0 320.0 248.0 240.0 223.0 224.0 227.0 230.0 251.0 271.0 348.0 395.0 519.0 603.0 652.0 743.0 777.0 768.0 556.0
BP-8-9-21-BPAE-13 250.0 257.0 263.0 259.0 256.0 251.0 249.0 250.0 249.0 252.0 253.0 261.0 293.0 347.0 393.0 457.0 546.0 535.0 500.0 509.0 288.0
BP-8-9-21-BPAE-15 263.0 264.0 262.0 266.0 261.0 265.0 261.0 264.0 267.0 266.0 270.0 281.0 315.0 383.0 456.0 515.0 585.0 575.0 616.0 655.0 279.0
zBP-8-9-21-BPAE-8 247.0 250.0 245.0 241.0 245.0 241.0 242.0 242.0 247.0 244.0 244.0 247.0 299.0 319.0 398.0 448.0 495.0 461.0 498.0 489.0 262.0
P-7-31-21-BPAE-03 268.0 271.0 274.0 279.0 271.0 271.0 273.0 268.0 269.0 268.0 271.0 275.0 305.0 371.0 478.0 506.0 546.0 612.0 689.0 620.0 315.0
P-7-31-21-BPAE-10 250.0 249.0 249.0 251.0 249.0 249.0 248.0 248.0 244.0 247.0 249.0 253.0 272.0 342.0 411.0 491.0 538.0 522.0 538.0 536.0 260.0
UP-7-31-21-BPAE-5 256.0 258.0 254.0 254.0 250.0 253.0 254.0 270.0 250.0 251.0 256.0 268.0 301.0 349.0 428.0 494.0 543.0 588.0 614.0 615.0 272.0
UP-8-9-21-BPAE-10 254.0 250.0 248.0 255.0 252.0 252.0 250.0 252.0 254.0 256.0 258.0 256.0 269.0 297.0 330.0 386.0 421.0 454.0 451.0 461.0 269.0
UP-8-9-21-BPAE-14 218.0 220.0 220.0 219.0 218.0 217.0 217.0 219.0 221.0 222.0 224.0 226.0 272.0 312.0 366.0 402.0 444.0 454.0 481.0 472.0 239.0
Inside the footprints the YP intensity is flat from the centre to about 0.55 mm and rises from about 0.6 mm, where the dark disk ends. The lumen of the needle (radius 0.25 mm) and the wall ring around it (0.25 to 0.5 mm) read the same to within 8 counts in 11 of the 14 dishes. In the three 833 kHz controls of 10-5-21 the lumen holds bright material (its brightest ring is 334 to 556 counts, against about 210 to 230 in the wall ring), which puts a lumen-only mean up to 66 counts above the wall ring there. Background (b) uses the mean over the 0.5 mm disk, the flat part of the footprint, which takes in the lumen and the inner wall, as the claims file's "mean YP intensity in the electrode footprints". The lumen alone, the wall ring alone and a 0.6 mm disk are run as sensitivity checks with each Figure 2 measure.
grids = {i: swing.make_grid(geoms[i], dishes[i]["img"].shape) for i in keep}
volts_kv = {"1 Hz": 5.0, "833 kHz": 4.0} # amplitudes in the session descriptions and file names
rows = []
for i in keep:
g = geoms[i]
for lay in ("L1", "L2"):
aB, fB = swing.bisector_angles(g, lay, "B", None, grids[i])
aC, fC = swing.bisector_angles(g, lay, "C", None)
kv = volts_kv[info.freq[i]]
rows.append({"subject": info.subject[i], "layout": lay, "angle_first_B": aB[0], "angle_last_B": aB[-1],
"angle_max_B": aB.max(), "angle_first_C": aC[0], "angle_last_C": aC[-1], "angle_max_C": aC.max(),
"E_kV_cm_min_B": (fB * kv * 10).min(), "E_kV_cm_max_B": (fB * kv * 10).max()})
ang = pd.DataFrame(rows)
print("Angle of field direction change and field strength (model B, one base electrode at the stated amplitude) along the ROIs:")
print(ang.round(1).to_string(index=False))
# Grid step check for model B on one dish
i0 = keep[0]
gfine = swing.GridModel(np.array([geoms[i0][k] for k in ("apex", "base1", "base2")]) * mpb,
np.array(dishes[i0]["img"].shape[-2:]) / 2 * mpb, h_mm=0.05)
a1, _ = swing.bisector_angles(geoms[i0], "L2", "B", None, grids[i0])
a2, _ = swing.bisector_angles(geoms[i0], "L2", "B", None, gfine)
# Where along the bisector is the angle largest? A scan from 2 mm beyond the base midpoint (away from
# the apex, negative) to 1 mm toward the apex, in 0.01 mm steps; and where each layout's first ROI is.
rows = []
for i in keep:
g = geoms[i]
a_, m_ = g["apex"], (g["base1"] + g["base2"]) / 2
u_ = (a_ - m_) / np.linalg.norm(a_ - m_)
s_mm = np.round(np.arange(-2.0, 1.0001, 0.01), 2)
pts = (m_ + np.outer(s_mm / mpb, u_)) * mpb
cen = np.array([g["apex"], g["base1"], g["base2"]]) * mpb
first = {lay: float(np.dot(swing.roi_centres(g, lay)[0] - m_, u_) * mpb) for lay in ("L1", "L2")}
for model in ("B", "C"):
if model == "C":
E1 = swing.field_line_sources(cen, np.array([0.0, 1.0, 0.0]), pts)
E2 = swing.field_line_sources(cen, np.array([0.0, 0.0, 1.0]), pts)
else:
E1 = grids[i].field([0.0, 1.0, 0.0], pts); E2 = grids[i].field([0.0, 0.0, 1.0], pts)
an = swing.angle_deg(E1, E2)
k = int(np.argmax(an))
rows.append({"subject": info.subject[i], "model": model, "angle_at_base_midpoint": an[s_mm == 0][0],
"largest_angle": an[k], "at_mm_from_base_line": s_mm[k],
"L1_first_roi_mm": first["L1"], "L2_first_roi_mm": first["L2"]})
p180 = pd.DataFrame(rows)
print("\nAngle along the bisector (position in mm from the base midpoint, negative = away from the apex):")
print(p180.round(2).to_string(index=False))
print(f"\nModel B, grid 0.1 against 0.05 mm on {info.subject[i0]}: largest angle difference {np.abs(a1 - a2).max():.2f} degrees")
Angle of field direction change and field strength (model B, one base electrode at the stated amplitude) along the ROIs:
subject layout angle_first_B angle_last_B angle_max_B angle_first_C angle_last_C angle_max_C E_kV_cm_min_B E_kV_cm_max_B
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 L1 169.1 9.1 169.1 167.7 22.7 167.7 4.6 9.4
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 L2 179.7 9.1 179.7 179.4 22.7 179.4 4.6 9.4
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 L1 169.9 7.3 169.9 168.4 22.6 168.4 4.6 9.9
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 L2 179.7 7.3 179.7 178.7 22.6 179.0 4.6 9.9
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 L1 169.4 7.6 169.4 167.9 22.3 167.9 4.6 9.4
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 L2 179.3 7.6 179.3 179.9 22.3 179.9 4.6 9.4
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 L1 170.2 6.9 170.2 168.6 22.1 168.6 4.6 9.8
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 L2 179.6 6.9 179.6 179.9 22.1 179.9 4.6 9.8
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 L1 170.0 8.6 170.0 168.4 22.1 168.4 4.6 9.7
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 L2 179.4 8.6 179.4 180.0 22.1 180.0 4.6 9.7
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 L1 169.8 7.0 169.8 168.4 22.2 168.4 4.7 9.7
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 L2 179.4 7.0 179.4 179.9 22.2 179.9 4.7 9.7
600ns-5kV-1HzBP-8-9-21-BPAE-13 L1 170.3 6.8 170.3 168.7 22.4 168.7 5.8 12.7
600ns-5kV-1HzBP-8-9-21-BPAE-13 L2 179.6 6.8 179.6 178.8 22.4 178.9 5.8 12.7
600ns-5kV-1HzBP-8-9-21-BPAE-15 L1 170.0 8.2 170.0 168.4 22.6 168.4 5.9 12.4
600ns-5kV-1HzBP-8-9-21-BPAE-15 L2 179.5 8.2 179.5 178.6 22.6 179.1 5.9 12.4
600ns-5kV-1HzBP-8-9-21-BPAE-8 L1 170.2 7.1 170.2 168.7 22.2 168.7 5.7 12.5
600ns-5kV-1HzBP-8-9-21-BPAE-8 L2 179.9 7.1 179.9 179.4 22.2 179.4 5.7 12.5
600ns-5kV-1HzUP-7-31-21-BPAE-03 L1 170.0 7.4 170.0 168.6 21.9 168.6 5.7 12.1
600ns-5kV-1HzUP-7-31-21-BPAE-03 L2 179.0 7.4 179.0 179.6 21.9 179.6 5.8 12.1
600ns-5kV-1HzUP-7-31-21-BPAE-10 L1 170.1 7.6 170.1 168.7 22.2 168.7 5.8 12.4
600ns-5kV-1HzUP-7-31-21-BPAE-10 L2 179.9 7.6 179.9 179.3 22.2 179.3 5.8 12.4
600ns-5kV-1HzUP-7-31-21-BPAE-5 L1 170.4 7.1 170.4 168.9 22.1 168.9 5.7 12.6
600ns-5kV-1HzUP-7-31-21-BPAE-5 L2 179.9 7.1 179.9 179.3 22.1 179.3 5.7 12.6
600ns-5kV-1HzUP-8-9-21-BPAE-10 L1 169.8 7.8 169.8 168.4 22.3 168.4 5.8 12.2
600ns-5kV-1HzUP-8-9-21-BPAE-10 L2 179.8 7.8 179.8 179.4 22.3 179.4 5.8 12.2
600ns-5kV-1HzUP-8-9-21-BPAE-14 L1 170.3 8.1 170.3 168.7 22.2 168.7 5.7 12.5
600ns-5kV-1HzUP-8-9-21-BPAE-14 L2 180.0 8.1 180.0 179.3 22.2 179.3 5.7 12.5
Angle along the bisector (position in mm from the base midpoint, negative = away from the apex):
subject model angle_at_base_midpoint largest_angle at_mm_from_base_line L1_first_roi_mm L2_first_roi_mm
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 B 169.13 180.00 -1.03 0.0 -1.00
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 C 167.73 179.96 -0.94 0.0 -1.00
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 B 169.87 179.98 -0.95 0.0 -0.99
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 C 168.39 179.99 -0.88 0.0 -0.99
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 B 169.43 179.98 -1.00 0.0 -0.92
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 C 167.94 179.96 -0.93 0.0 -0.92
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 B 170.21 179.96 -0.93 0.0 -0.88
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 C 168.60 179.96 -0.88 0.0 -0.88
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 B 169.98 179.98 -0.96 0.0 -0.89
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 C 168.41 179.97 -0.89 0.0 -0.89
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 B 169.81 179.98 -0.97 0.0 -0.90
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 C 168.44 179.98 -0.89 0.0 -0.90
600ns-5kV-1HzBP-8-9-21-BPAE-13 B 170.26 179.98 -0.91 0.0 -0.96
600ns-5kV-1HzBP-8-9-21-BPAE-13 C 168.74 179.98 -0.85 0.0 -0.96
600ns-5kV-1HzBP-8-9-21-BPAE-15 B 169.96 179.98 -0.94 0.0 -1.00
600ns-5kV-1HzBP-8-9-21-BPAE-15 C 168.41 179.99 -0.88 0.0 -1.00
600ns-5kV-1HzBP-8-9-21-BPAE-8 B 170.23 179.97 -0.93 0.0 -0.92
600ns-5kV-1HzBP-8-9-21-BPAE-8 C 168.67 179.96 -0.86 0.0 -0.92
600ns-5kV-1HzUP-7-31-21-BPAE-03 B 169.97 179.98 -0.96 0.0 -0.85
600ns-5kV-1HzUP-7-31-21-BPAE-03 C 168.58 179.96 -0.88 0.0 -0.85
600ns-5kV-1HzUP-7-31-21-BPAE-10 B 170.14 179.99 -0.93 0.0 -0.92
600ns-5kV-1HzUP-7-31-21-BPAE-10 C 168.69 179.98 -0.86 0.0 -0.92
600ns-5kV-1HzUP-7-31-21-BPAE-5 B 170.36 180.00 -0.91 0.0 -0.90
600ns-5kV-1HzUP-7-31-21-BPAE-5 C 168.94 179.99 -0.84 0.0 -0.90
600ns-5kV-1HzUP-8-9-21-BPAE-10 B 169.83 179.99 -0.96 0.0 -0.94
600ns-5kV-1HzUP-8-9-21-BPAE-10 C 168.40 179.96 -0.89 0.0 -0.94
600ns-5kV-1HzUP-8-9-21-BPAE-14 B 170.26 179.97 -0.92 0.0 -0.92
600ns-5kV-1HzUP-8-9-21-BPAE-14 C 168.68 179.98 -0.86 0.0 -0.92
Model B, grid 0.1 against 0.05 mm on 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13: largest angle difference 2.07 degrees
Along layout L2 the angle runs from 180 degrees beyond the base midpoint to near 0 next to the apex, as Figure 1 describes; along L1, which starts at the base midpoint, it starts below 180 degrees, because the grounded apex pulls the field at the midpoint toward it. The scan along the bisector puts the largest angle (180 degrees to within 0.05) 0.84 to 1.03 mm beyond the base midpoint, away from the apex, under both models; at the base midpoint the angle is 167.7 to 170.4 degrees. The first ROI of L2 is centred 0.85 to 1.00 mm beyond the midpoint, at that point; the first ROI of L1 is at the midpoint. The field strength along the ROIs is printed for each dish; Results 2.1 gives 5 to 10 kV/cm, which the values at 0.65 um per pixel are consistent with (at twice the pixel size the field would be about half).
fig, axes = plt.subplots(3, 5, figsize=(22, 13))
for ax, i in zip(axes.flat, keep):
yp = dishes[i]["img"][1]
n0 = yp.shape[0]
lo, hi = np.percentile(yp[n0 // 4:3 * n0 // 4, n0 // 4:3 * n0 // 4], [1, 99.5])
ax.imshow(yp, cmap="gray", vmin=lo, vmax=hi)
g = geoms[i]
for k, col in (("apex", "r"), ("base1", "c"), ("base2", "y")):
ax.add_patch(plt.Circle(g[k][::-1], 0.75 / mpb, fill=False, color=col))
for lay, col in (("L1", "m"), ("L2", "lime")):
r = swing.roi_centres(g, lay)
ax.plot(r[:, 1], r[:, 0], ".", color=col, ms=2)
ax.set_title(f"{info.subject[i][7:]}\nYP {lo:.0f} to {hi:.0f} counts", fontsize=8)
ax.set_xticks([]); ax.set_yticks([])
for ax in axes.flat[len(keep):]:
ax.axis("off")
plt.tight_layout(); plt.show()
print("Display range: 1st to 99.5th percentile of the central half of each YP image (in titles); brighter pixels are clipped.")
print("Red: apex; cyan and yellow: base electrodes (circles of 1.5 mm); magenta: ROI layout L1; green: L2.")
for i in keep:
print(f"{info.subject[i]}: YP block means {dishes[i]['img'][1].min():.0f} to {dishes[i]['img'][1].max():.0f}, "
f"MemBrite {dishes[i]['img'][0].min():.0f} to {dishes[i]['img'][0].max():.0f} counts (0 and ~65535 are padding outside the tiles)")
Display range: 1st to 99.5th percentile of the central half of each YP image (in titles); brighter pixels are clipped. Red: apex; cyan and yellow: base electrodes (circles of 1.5 mm); magenta: ROI layout L1; green: L2. 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13: YP block means 0 to 27207, MemBrite 0 to 65435 counts (0 and ~65535 are padding outside the tiles) 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15: YP block means 0 to 51383, MemBrite 0 to 65435 counts (0 and ~65535 are padding outside the tiles) 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20: YP block means 0 to 40020, MemBrite 0 to 65340 counts (0 and ~65535 are padding outside the tiles) 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05: YP block means 0 to 65436, MemBrite 0 to 65435 counts (0 and ~65535 are padding outside the tiles) 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06: YP block means 0 to 65436, MemBrite 0 to 65265 counts (0 and ~65535 are padding outside the tiles) 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10: YP block means 0 to 65436, MemBrite 0 to 65008 counts (0 and ~65535 are padding outside the tiles) 600ns-5kV-1HzBP-8-9-21-BPAE-13: YP block means 0 to 21571, MemBrite 0 to 47138 counts (0 and ~65535 are padding outside the tiles) 600ns-5kV-1HzBP-8-9-21-BPAE-15: YP block means 0 to 43274, MemBrite 0 to 65435 counts (0 and ~65535 are padding outside the tiles) 600ns-5kV-1HzBP-8-9-21-BPAE-8: YP block means 0 to 49629, MemBrite 0 to 65435 counts (0 and ~65535 are padding outside the tiles) 600ns-5kV-1HzUP-7-31-21-BPAE-03: YP block means 0 to 33584, MemBrite 0 to 65435 counts (0 and ~65535 are padding outside the tiles) 600ns-5kV-1HzUP-7-31-21-BPAE-10: YP block means 0 to 35488, MemBrite 0 to 65435 counts (0 and ~65535 are padding outside the tiles) 600ns-5kV-1HzUP-7-31-21-BPAE-5: YP block means 0 to 44452, MemBrite 0 to 50129 counts (0 and ~65535 are padding outside the tiles) 600ns-5kV-1HzUP-8-9-21-BPAE-10: YP block means 0 to 37131, MemBrite 0 to 65435 counts (0 and ~65535 are padding outside the tiles) 600ns-5kV-1HzUP-8-9-21-BPAE-14: YP block means 0 to 45629, MemBrite 0 to 65435 counts (0 and ~65535 are padding outside the tiles)
# Do the condition labels match the images? Uptake next to each electrode (ROI centred 0.95 mm from
# the centre toward the triangle's centroid), minus the footprint background, per dish.
rows = []
for i in keep:
g = geoms[i]; yp = dishes[i]["img"][1]
bg = swing.footprint_background(yp, g)
v = {k: swing.square_mean(yp, swing.adjacent_rois(g, k, 0.0, "centroid")[0], np.array([1.0, 0.0])) - bg
for k in ("apex", "base1", "base2")}
rows.append({"subject": info.subject[i], "freq": info.freq[i], "train": info.train[i], **v,
"apex_over_max_base": v["apex"] / max(v["base1"], v["base2"])})
lab = pd.DataFrame(rows)
print(lab.round(2).to_string(index=False))
b13 = lab[lab.subject.str.contains("0,8MHz-BP-8-24-21-BPAE-13")]
print("\nRatio of apex to the larger base uptake, by group (min to max):")
print(lab.groupby(["freq", "train"]).apex_over_max_base.agg(["min", "max"]).round(2).to_string())
subject freq train apex base1 base2 apex_over_max_base
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 833 kHz paired 958.08 106.15 213.29 4.49
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 833 kHz paired 708.19 88.86 201.12 3.52
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 833 kHz paired 1024.93 81.54 307.27 3.34
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 833 kHz single 508.38 933.09 1027.82 0.49
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 833 kHz single 253.98 1340.65 647.01 0.19
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 833 kHz single 220.40 933.45 692.68 0.24
600ns-5kV-1HzBP-8-9-21-BPAE-13 1 Hz paired 1622.36 183.57 514.18 3.16
600ns-5kV-1HzBP-8-9-21-BPAE-15 1 Hz paired 1487.01 415.31 542.85 2.74
600ns-5kV-1HzBP-8-9-21-BPAE-8 1 Hz paired 854.17 263.54 564.96 1.51
600ns-5kV-1HzUP-7-31-21-BPAE-03 1 Hz single 580.41 767.87 541.40 0.76
600ns-5kV-1HzUP-7-31-21-BPAE-10 1 Hz single 433.71 979.40 507.10 0.44
600ns-5kV-1HzUP-7-31-21-BPAE-5 1 Hz single 335.15 1031.54 363.96 0.32
600ns-5kV-1HzUP-8-9-21-BPAE-10 1 Hz single 311.64 525.61 266.31 0.59
600ns-5kV-1HzUP-8-9-21-BPAE-14 1 Hz single 460.36 603.88 288.76 0.76
Ratio of apex to the larger base uptake, by group (min to max):
min max
freq train
1 Hz paired 1.51 3.16
single 0.32 0.76
833 kHz paired 3.34 4.49
single 0.19 0.49
The images agree with their labels: in every paired dish the uptake next to the apex is larger than next to either base electrode, and in every single-pulse dish the uptake next to one base electrode is the largest. BPAE-13 of 8-24-21, whose session description is that of a two-electrode bipolar experiment, shows the three footprints of the triangular array at the same spacing and the paired-pulse pattern of the other two 833 kHz paired dishes; it is used as a paired dish, and its description is recorded as a metadata error. The files have no time series, so there is no clock to check against a trial table.
Measures along the bisector¶
For every dish, layout and background reading, the mean YP intensity in each of the 32 ROIs of 200 x 200 um, the angle at each ROI under models B and C, and, as a variant, YP over MemBrite in the same ROI.
- Background (claims file, as amended in reply to
questions/000631.yaml): (b), which decides, is the ROI mean minus the mean YP within 0.5 mm of the three electrode centres of the same dish (the lumen alone, the wall ring alone and a 0.6 mm disk are sensitivity checks) (Methods 4.4, "to account for 'spontaneous' YP uptake, measurements were also performed within the hollow electrodes' footprints"); (a), the ROI mean as measured, is a variant only. - Layout (left open by the paper; see the helper). The claims file's standard layout, 32 contiguous ROIs from the base midpoint toward the apex outside the footprints, does not fit here: 32 ROIs of 200 um span 6.4 mm, and the base midpoint is 5.37 mm from the apex footprint (printed above). This is a deviation from the claims file. Two substitutes are run: L1, 32 ROIs evenly spaced from the base midpoint to the apex footprint (they overlap by about 15%); L2, 32 contiguous ROIs ending at the apex footprint and running back past the base midpoint. The rule: L2 decides and L1 is a variant. L2 keeps two of the three properties of the claims file's layout (ROIs of 200 um side by side, outside the footprints), and its first ROI lies at the point where the angle is 180 degrees (0.84 to 1.03 mm beyond the base midpoint, printed above), so its angles run from 180 degrees at the base to near 0 at the apex, as Results 2.1 says. L1 keeps the start at the base midpoint, but its ROIs overlap and its largest angle is 168 to 170 degrees, so it never reaches the 180 degrees the paper places at the base.
- Angles: models B and C of the claims file, per paired dish, averaged over the paired dishes of a frequency. The controls use their own dish's ROIs at the same positions relative to the array.
- Controls: all unique single-pulse dishes of the same frequency; subsets shown as variants.
rows = []
for i in keep:
g = geoms[i]; img = dishes[i]["img"]
bg = swing.footprint_background(img[1], g)
bgalt = {"bg_lumen": swing.footprint_background(img[1], g, 0.25),
"bg_wall": swing.footprint_background(img[1], g, 0.5, 0.25),
"bg_06": swing.footprint_background(img[1], g, 0.6)}
for lay in ("L1", "L2"):
yp = swing.bisector_profile(img[1], g, lay)
mb = swing.bisector_profile(img[0], g, lay)
sat = swing.bisector_profile(dishes[i]["sat"], g, lay)
aB, _ = swing.bisector_angles(g, lay, "B", None, grids[i])
aC, _ = swing.bisector_angles(g, lay, "C", None)
for k in range(swing.N_ROI):
rows.append({"subject": info.subject[i], "freq": info.freq[i], "train": info.train[i], "date": info.date[i],
"layout": lay, "roi": k, "yp": yp[k], "membrite": mb[k], "bg": bg, **bgalt,
"sat": sat[k], "angle_B": aB[k], "angle_C": aC[k]})
prof = pd.DataFrame(rows)
print("Footprint background (counts) per dish:")
print(prof.groupby(["freq", "train", "subject"]).bg.first().round(1).to_string())
print("\nROI means (YP counts), range over all dishes and ROIs:", prof.yp.min().round(1), "to", prof.yp.max().round(1))
print("ROIs whose background-subtracted value is 0 or below:", int(((prof.yp - prof.bg) <= 0).sum()), "of", len(prof))
# Footprint regions for background (b): the 0.5 mm disk decides; the others test its sensitivity.
BG_REGIONS = {"disk 0.5 mm (decides)": "bg", "lumen 0.25 mm": "bg_lumen", "wall ring 0.25-0.5 mm": "bg_wall",
"disk 0.6 mm": "bg_06"}
def curves(freq, layout, model, bg, norm, measure="yp", controls=None, paired=None, bgcol="bg"):
"""Returns angle per ROI (mean over paired dishes), the normalised values (rows: per-pair
ratios for (i), one row for (ii)), and the paired and single matrices. bgcol names the
footprint region whose mean is subtracted under background (b)."""
v = prof[(prof.freq == freq) & (prof.layout == layout)].copy()
if measure == "yp":
v["val"] = v.yp - (v[bgcol] if bg == "b" else 0)
else: # YP over MemBrite, background subtracted from YP only
v["val"] = (v.yp - (v[bgcol] if bg == "b" else 0)) / v.membrite
P = v[v.train == "paired"].pivot(index="subject", columns="roi", values="val")
S = v[v.train == "single"].pivot(index="subject", columns="roi", values="val")
if controls is not None:
S = S.loc[[s for s in S.index if controls(s)]]
if paired is not None:
P = P.loc[[s for s in P.index if paired(s)]]
angles = v[(v.train == "paired") & v.subject.isin(P.index)].groupby("roi")[f"angle_{model}"].mean().values
return angles, swing.normalised(P.values, S.values, norm), P, S
READINGS = [(lay, model, bg, norm) for lay in ("L1", "L2") for model in ("B", "C") for bg in ("a", "b") for norm in ("i", "ii")]
print("\nReadings run for each Figure 2 measure:", len(READINGS), "(layout x angle model x background x normalisation)")
Footprint background (counts) per dish:
freq train subject
1 Hz paired 600ns-5kV-1HzBP-8-9-21-BPAE-13 252.2
600ns-5kV-1HzBP-8-9-21-BPAE-15 264.4
600ns-5kV-1HzBP-8-9-21-BPAE-8 243.8
single 600ns-5kV-1HzUP-7-31-21-BPAE-03 270.5
600ns-5kV-1HzUP-7-31-21-BPAE-10 247.7
600ns-5kV-1HzUP-7-31-21-BPAE-5 254.8
600ns-5kV-1HzUP-8-9-21-BPAE-10 252.7
600ns-5kV-1HzUP-8-9-21-BPAE-14 219.6
833 kHz paired 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 200.4
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 222.3
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 210.8
single 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 211.8
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 234.8
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 244.8
ROI means (YP counts), range over all dishes and ROIs: 225.5 to 1845.5
ROIs whose background-subtracted value is 0 or below: 0 of 896
Readings run for each Figure 2 measure: 16 (layout x angle model x background x normalisation)
# Saturation in the bisector ROIs: fraction of YP pixels at or above 60000, counted before block
# averaging (the padding outside the tiles is also near 65535, so it is checked here).
print("Fraction of saturated YP pixels per ROI, largest by group:")
print(prof.groupby(["freq", "train", "layout"]).sat.max().to_string())
print("ROIs with any saturated pixel:", int((prof.sat > 0).sum()), "of", len(prof))
# Can intensities be compared across days? The files give no exposure or gain. Per dish: YP in the
# footprints (lumen, wall ring, 0.5 mm disk), and YP and MemBrite in a ring 5 to 6 mm from the
# triangle's centroid, far from the electrodes; MemBrite stains every cell, so its level reflects
# staining and imaging, not the pulses.
rows = []
for i in keep:
g = geoms[i]; img = dishes[i]["img"]
cen = (g["apex"] + g["base1"] + g["base2"]) / 3
rows.append({"date": info.date[i], "freq": info.freq[i], "train": info.train[i], "dish": info.subject[i][-7:],
"YP lumen": swing.footprint_background(img[1], g, 0.25),
"YP wall ring": swing.footprint_background(img[1], g, 0.5, 0.25),
"YP disk 0.5": swing.footprint_background(img[1], g),
"YP 5-6 mm": swing.disk_mean(img[1], cen, 6.0, 5.0),
"MemBrite 5-6 mm": swing.disk_mean(img[0], cen, 6.0, 5.0),
"MemBrite disk 0.5": swing.footprint_background(img[0], g),
"saturated in 5-6 mm ring": swing.disk_mean(dishes[i]["sat"], cen, 6.0, 5.0)})
day = pd.DataFrame(rows)
print("\nPer dish (counts):")
print(day.round(1).to_string(index=False))
print("\nMean by day and condition:")
print(day.groupby(["date", "freq", "train"]).mean(numeric_only=True).round(1).to_string())
Fraction of saturated YP pixels per ROI, largest by group:
freq train layout
1 Hz paired L1 0.000306
L2 0.000306
single L1 0.000148
L2 0.000151
833 kHz paired L1 0.000000
L2 0.000000
single L1 0.000000
L2 0.000000
ROIs with any saturated pixel: 12 of 896
Per dish (counts):
date freq train dish YP lumen YP wall ring YP disk 0.5 YP 5-6 mm MemBrite 5-6 mm MemBrite disk 0.5 saturated in 5-6 mm ring
8-24-21 833 kHz paired BPAE-13 198.0 201.2 200.4 296.4 285.6 260.0 0.0
8-24-21 833 kHz paired BPAE-15 219.4 223.3 222.3 345.9 305.1 248.9 0.0
8-24-21 833 kHz paired BPAE-20 211.0 210.7 210.8 335.3 295.5 245.7 0.0
10-5-21 833 kHz single BPAE-05 225.8 207.1 211.8 240.8 430.8 439.2 0.0
10-5-21 833 kHz single BPAE-06 247.3 230.6 234.8 265.0 431.8 439.5 0.0
10-5-21 833 kHz single BPAE-10 294.1 228.4 244.8 259.3 429.7 421.9 0.0
8-9-21 1 Hz paired BPAE-13 258.2 250.3 252.2 285.7 241.7 220.1 0.0
8-9-21 1 Hz paired BPAE-15 263.3 264.8 264.4 302.2 256.8 224.8 0.0
8-9-21 1 Hz paired -BPAE-8 244.6 243.6 243.8 288.7 264.8 223.7 0.0
7-31-21 1 Hz single BPAE-03 273.7 269.4 270.5 396.7 437.6 219.9 0.0
7-31-21 1 Hz single BPAE-10 249.6 247.1 247.7 340.6 297.7 212.9 0.0
7-31-21 1 Hz single -BPAE-5 252.9 255.5 254.8 305.2 276.4 223.5 0.0
8-9-21 1 Hz single BPAE-10 252.1 252.9 252.7 331.1 252.8 216.0 0.0
8-9-21 1 Hz single BPAE-14 219.0 219.7 219.6 254.5 224.2 229.8 0.0
Mean by day and condition:
YP lumen YP wall ring YP disk 0.5 YP 5-6 mm MemBrite 5-6 mm MemBrite disk 0.5 saturated in 5-6 mm ring
date freq train
10-5-21 833 kHz single 255.8 222.1 230.5 255.0 430.8 433.6 0.0
7-31-21 1 Hz single 258.7 257.3 257.7 347.5 337.3 218.7 0.0
8-24-21 833 kHz paired 209.5 211.7 211.2 325.9 295.4 251.5 0.0
8-9-21 1 Hz paired 255.3 252.9 253.5 292.2 254.4 222.9 0.0
single 235.5 236.3 236.1 292.8 238.5 222.9 0.0
Saturation does not touch the measures: at most 0.03% of the YP pixels of any bisector ROI are at or above 60000, in 12 of 896 ROIs, and none at 833 kHz. The files give no exposure or gain. The YP level in the footprints, which holds the camera offset, is close on every day (211 to 258 counts by day and condition), but MemBrite is not: the 833 kHz controls of 10-5-21 read about 431 counts 5 to 6 mm from the array and 434 in the footprints, against 295 and 252 in the 833 kHz paired dishes of 8-24-21. Staining, or imaging in the MemBrite channel, differed between those two days; whether the YP channel was imaged alike cannot be checked from the files. Every 833 kHz value is a ratio of 8-24-21 dishes to 10-5-21 dishes and rests on that comparability. YP over MemBrite, a variant, gives smaller 833 kHz folds (7.70 to 13.06 at the 180 degree readings). The 1 Hz paired dishes and two of the five 1 Hz controls were imaged on one day (8-9-21), with similar MemBrite levels.
fig, axes = plt.subplots(2, 2, figsize=(13, 9), sharex=True)
for ax, (lay, bg) in zip(axes.flat, [(l, b) for b in ("a", "b") for l in ("L1", "L2")]):
for freq, col in (("1 Hz", "tab:blue"), ("833 kHz", "tab:red")):
a, r, P, S = curves(freq, lay, "B", bg, "ii")
# s.e. over paired dishes, each divided by the mean control
se = (100 * P.values / S.values.mean(0)).std(0, ddof=1) / np.sqrt(len(P))
ax.errorbar(a, r[0], yerr=se, fmt="o-", ms=3, color=col, label=f"{freq} (paired n={len(P)}, controls n={len(S)})")
print(f"{lay} background {bg} {freq}: plotted values {r[0].min():.1f} to {r[0].max():.1f} %, angles {a.min():.0f} to {a.max():.0f} deg")
ax.axhline(100, color="k", lw=0.8)
ax.set_title(f"layout {lay}, background {bg}, normalisation (ii), angles model B")
ax.set_xlabel("angle of field direction change (deg)"); ax.set_ylabel("paired YP, % of single")
ax.invert_xaxis(); ax.legend(fontsize=8)
plt.tight_layout(); plt.show()
print("All points are within the axes.")
for bg in ("a", "b"):
tab = {}
for freq in ("1 Hz", "833 kHz"):
a, r, P, S = curves(freq, "L2", "B", bg, "ii")
tab[f"angle {freq}"] = a; tab[f"% {freq}"] = r[0]
print(f"\nLayout L2, model B, background {bg}, normalisation (ii), ROI 0 (base end) to 31 (apex end):")
print(pd.DataFrame(tab).round(1).T.to_string())
L1 background a 1 Hz: plotted values 65.8 to 231.8 %, angles 7 to 170 deg L1 background a 833 kHz: plotted values 31.5 to 201.4 %, angles 8 to 169 deg L2 background a 1 Hz: plotted values 64.3 to 231.8 %, angles 7 to 180 deg L2 background a 833 kHz: plotted values 29.5 to 199.9 %, angles 8 to 180 deg L1 background b 1 Hz: plotted values 40.3 to 315.0 %, angles 7 to 170 deg L1 background b 833 kHz: plotted values 8.7 to 366.0 %, angles 8 to 169 deg L2 background b 1 Hz: plotted values 39.8 to 308.1 %, angles 7 to 180 deg L2 background b 833 kHz: plotted values 4.5 to 336.6 %, angles 8 to 180 deg
All points are within the axes.
Layout L2, model B, background a, normalisation (ii), ROI 0 (base end) to 31 (apex end):
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
angle 1 Hz 179.7 178.5 176.6 174.5 172.2 169.6 166.8 163.7 160.2 156.4 152.3 147.8 143.0 137.9 132.5 126.8 120.8 114.5 108.0 101.3 94.3 87.2 79.9 72.5 64.9 57.3 49.6 41.8 33.8 25.6 16.9 7.4
% 1 Hz 78.9 81.6 78.9 73.7 76.3 68.7 69.2 64.3 80.2 73.3 89.1 105.2 96.6 88.7 97.2 114.7 103.3 104.3 111.0 108.9 107.7 124.4 113.9 129.3 132.9 132.1 191.2 189.4 170.9 212.8 177.7 231.8
angle 833 kHz 179.6 178.0 176.1 173.9 171.6 169.1 166.3 163.1 159.7 156.0 151.9 147.5 142.7 137.7 132.3 126.7 120.8 114.6 108.1 101.4 94.5 87.5 80.2 72.8 65.3 57.7 50.0 42.2 34.2 26.0 17.4 8.0
% 833 kHz 36.2 33.7 29.5 33.0 34.1 35.6 32.3 34.2 41.6 46.9 53.6 59.3 79.2 70.8 86.8 88.0 122.5 134.3 137.7 127.2 136.4 134.3 169.9 173.1 158.2 178.3 160.3 170.5 199.9 199.5 186.3 195.9
Layout L2, model B, background b, normalisation (ii), ROI 0 (base end) to 31 (apex end):
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
angle 1 Hz 179.7 178.5 176.6 174.5 172.2 169.6 166.8 163.7 160.2 156.4 152.3 147.8 143.0 137.9 132.5 126.8 120.8 114.5 108.0 101.3 94.3 87.2 79.9 72.5 64.9 57.3 49.6 41.8 33.8 25.6 16.9 7.4
% 1 Hz 40.6 52.0 48.9 41.2 51.7 43.2 45.0 39.8 66.4 56.2 78.5 108.4 92.2 79.1 92.8 130.3 104.9 107.1 122.3 119.4 114.7 155.7 129.4 161.4 167.9 165.0 282.8 273.8 220.7 295.8 221.8 308.1
angle 833 kHz 179.6 178.0 176.1 173.9 171.6 169.1 166.3 163.1 159.7 156.0 151.9 147.5 142.7 137.7 132.3 126.7 120.8 114.6 108.1 101.4 94.5 87.5 80.2 72.8 65.3 57.7 50.0 42.2 34.2 26.0 17.4 8.0
% 833 kHz 7.9 6.2 4.5 5.2 7.1 8.9 9.0 8.5 11.8 18.3 27.9 34.4 64.5 49.9 79.8 82.6 185.7 219.1 207.6 209.1 258.5 226.8 323.5 319.3 293.0 336.6 246.0 247.2 305.0 292.5 246.9 268.7
def combine(outcomes, bg="b"):
"""Outcome over the angle models, per layout, under one background reading. Reading (b)
decides (claims file, opening comment, as amended in reply to questions/000631.yaml: the
paper's values are corrected for spontaneous uptake, and (a) is a variant only); called with
bg="a" it gives the variant. Blocked as methods_underspecified when angle models B and C give
different outcomes; a model under which the measure cannot be taken ("untestable") leaves
the other to decide. Layout L2 decides and L1 is a variant (the rule is stated below)."""
out = {}
for lay, name in (("L2", "L2 (decides)"), ("L1", "L1 (variant)")):
per_model = {model: outcomes[(lay, model, bg)] for model in ("B", "C")}
vals = {v for v in per_model.values() if v != "untestable"}
out[name] = vals.pop() if len(vals) == 1 else ("untestable" if not vals else "blocked (angle models differ)")
return out
fig2-180-cancellation¶
At a 180 degree change of field direction (near the base electrodes), paired-pulse trains give less YP uptake than single-pulse trains, 2-fold less at 1 Hz and 20-fold less at 833 kHz.
Criterion (claims file): per ROI, paired as % of single, under normalisations (i) and (ii), the fold being 100 / %. "At 180 degrees" is (1) the ROI of largest angle or (2) the ROI of lowest mean value; a value holds (2 +- 0.2 at 1 Hz, 20 +- 2 at 833 kHz) under any of these with either normalisation. Reproduced when both hold, under background reading (b); reading (a) is a variant only.
EXPECT_180 = {"1 Hz": (2.0, 0.2), "833 kHz": (20.0, 2.0)}
rows = []
for lay, model, bg, norm in READINGS:
for freq in ("1 Hz", "833 kHz"):
a, r, P, S = curves(freq, lay, model, bg, norm)
c = r.mean(0)
for rd in (1, 2):
val, k = swing.at_180(c, a, rd)
fold = 100 / val if val > 0 else np.inf
e, t = EXPECT_180[freq]
rows.append({"layout": lay, "model": model, "bg": bg, "norm": norm, "freq": freq, "reading": rd,
"roi": k, "angle": a[k], "pct": val, "fold": fold, "holds": abs(fold - e) <= t})
t180 = pd.DataFrame(rows)
print(t180.pivot_table(index=["layout", "model", "bg", "reading"], columns=["freq", "norm"], values="fold").round(2).to_string())
hold = t180.groupby(["layout", "model", "bg", "freq"]).holds.any().unstack("freq")
hold["both hold"] = hold.all(axis=1)
print("\nDoes each value hold under some normalisation and 180-degree reading?")
print(hold.to_string())
res180 = {k: ("criterion met" if v else "criterion not met") for k, v in hold["both hold"].items()}
out180 = combine(res180)
print("\nOutcome per layout, background (b), which decides:", out180)
print("Variant, background (a), deciding nothing:", combine(res180, "a"))
freq 1 Hz 833 kHz
norm i ii i ii
layout model bg reading
L1 B a 1 1.36 1.39 2.83 2.83
2 1.48 1.52 3.14 3.18
b 1 1.92 2.19 11.22 11.27
2 2.34 2.48 11.24 11.48
C a 1 1.36 1.39 2.83 2.83
2 1.48 1.52 3.14 3.18
b 1 1.92 2.19 11.22 11.27
2 2.34 2.48 11.24 11.48
L2 B a 1 1.26 1.27 2.66 2.76
2 1.49 1.55 3.05 3.38
b 1 2.42 2.46 11.77 12.67
2 2.42 2.51 18.52 21.98
C a 1 1.26 1.27 2.66 2.76
2 1.49 1.55 3.05 3.38
b 1 2.42 2.46 11.77 12.67
2 2.42 2.51 18.52 21.98
Does each value hold under some normalisation and 180-degree reading?
freq 1 Hz 833 kHz both hold
layout model bg
L1 B a False False False
b True False False
C a False False False
b True False False
L2 B a False False False
b False True False
C a False False False
b False True False
Outcome per layout, background (b), which decides: {'L2 (decides)': 'criterion not met', 'L1 (variant)': 'criterion not met'}
Variant, background (a), deciding nothing: {'L2 (decides)': 'criterion not met', 'L1 (variant)': 'criterion not met'}
# Variants: control subsets, the 833 kHz paired dish with the wrong description left out, YP over MemBrite.
variants = {
"all controls": dict(),
"1 Hz controls of 7-31-21 only": dict(controls=lambda s: "7-31-21" in s),
"1 Hz controls of 8-9-21 only (day of the paired dishes)": dict(controls=lambda s: "8-9-21" in s),
"833 kHz without BPAE-13 of 8-24-21": dict(paired=lambda s: not s.endswith("8-24-21-BPAE-13")),
"YP / MemBrite": dict(measure="ratio"),
}
rows = []
for name, kw in variants.items():
for bg in ("a", "b"):
for freq in ("1 Hz", "833 kHz"):
if ("1 Hz" in name and freq != "1 Hz") or ("833" in name and freq != "833 kHz"):
continue
for norm in ("i", "ii"):
a, r, P, S = curves(freq, "L2", "B", bg, norm, **kw)
c = r.mean(0)
v1, _ = swing.at_180(c, a, 1); v2, _ = swing.at_180(c, a, 2)
rows.append({"variant": name, "bg": bg, "freq": freq, "norm": norm, "n_paired": len(P), "n_single": len(S),
"fold_reading1": 100 / v1 if v1 > 0 else np.inf, "fold_reading2": 100 / v2 if v2 > 0 else np.inf})
print("Layout L2, model B:")
print(pd.DataFrame(rows).round(2).to_string(index=False))
# per paired dish, fold at the ROI of largest angle against the mean control (bg b, L2)
rows = []
for freq in ("1 Hz", "833 kHz"):
a, r, P, S = curves(freq, "L2", "B", "b", "ii")
k = int(np.argmax(a))
for s in P.index:
rows.append({"freq": freq, "paired dish": s, "pct_at_largest_angle": 100 * P.loc[s].values[k] / S.values.mean(0)[k]})
print("\nPer paired dish (L2, model B, background b), % of the mean control at the ROI of largest angle:")
print(pd.DataFrame(rows).round(1).to_string(index=False))
Layout L2, model B:
variant bg freq norm n_paired n_single fold_reading1 fold_reading2
all controls a 1 Hz i 3 5 1.26 1.49
all controls a 1 Hz ii 3 5 1.27 1.55
all controls a 833 kHz i 3 3 2.66 3.05
all controls a 833 kHz ii 3 3 2.76 3.38
all controls b 1 Hz i 3 5 2.42 2.42
all controls b 1 Hz ii 3 5 2.46 2.51
all controls b 833 kHz i 3 3 11.77 18.52
all controls b 833 kHz ii 3 3 12.67 21.98
1 Hz controls of 7-31-21 only a 1 Hz i 3 3 1.31 1.67
1 Hz controls of 7-31-21 only a 1 Hz ii 3 3 1.32 1.69
1 Hz controls of 7-31-21 only b 1 Hz i 3 3 2.54 2.73
1 Hz controls of 7-31-21 only b 1 Hz ii 3 3 2.59 2.82
1 Hz controls of 8-9-21 only (day of the paired dishes) a 1 Hz i 3 2 1.19 1.42
1 Hz controls of 8-9-21 only (day of the paired dishes) a 1 Hz ii 3 2 1.19 1.43
1 Hz controls of 8-9-21 only (day of the paired dishes) b 1 Hz i 3 2 2.26 2.26
1 Hz controls of 8-9-21 only (day of the paired dishes) b 1 Hz ii 3 2 2.27 2.27
833 kHz without BPAE-13 of 8-24-21 a 833 kHz i 2 3 2.63 2.96
833 kHz without BPAE-13 of 8-24-21 a 833 kHz ii 2 3 2.73 3.34
833 kHz without BPAE-13 of 8-24-21 b 833 kHz i 2 3 12.64 19.45
833 kHz without BPAE-13 of 8-24-21 b 833 kHz ii 2 3 13.61 24.36
YP / MemBrite a 1 Hz i 3 5 1.41 1.48
YP / MemBrite a 1 Hz ii 3 5 1.43 1.53
YP / MemBrite a 833 kHz i 3 3 1.73 2.10
YP / MemBrite a 833 kHz ii 3 3 1.79 2.12
YP / MemBrite b 1 Hz i 3 5 2.72 2.72
YP / MemBrite b 1 Hz ii 3 5 2.78 2.78
YP / MemBrite b 833 kHz i 3 3 7.70 11.78
YP / MemBrite b 833 kHz ii 3 3 8.23 13.06
Per paired dish (L2, model B, background b), % of the mean control at the ROI of largest angle:
freq paired dish pct_at_largest_angle
1 Hz 600ns-5kV-1HzBP-8-9-21-BPAE-13 38.9
1 Hz 600ns-5kV-1HzBP-8-9-21-BPAE-15 50.5
1 Hz 600ns-5kV-1HzBP-8-9-21-BPAE-8 32.5
833 kHz 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 9.0
833 kHz 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 8.9
833 kHz 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 5.8
# (1) Per dish, the ROIs nearest the base on layout L2 (background b, counts above the footprint
# mean), and each paired dish as % of the mean control: is the 833 kHz denominator one dish?
for freq in ("1 Hz", "833 kHz"):
a, r, P, S = curves(freq, "L2", "B", "b", "ii")
tab = pd.concat([P.iloc[:, :8].assign(train="paired"), S.iloc[:, :8].assign(train="single")])
tab.columns = [f"ROI {k} ({a[k]:.0f} deg)" if isinstance(k, (int, np.integer)) else k for k in tab.columns]
print(f"{freq}, YP above the footprint mean (counts), ROIs 0 to 7 of L2:")
print(tab.round(1).to_string())
pct = 100 * P.iloc[:, :8].values / S.iloc[:, :8].values.mean(0)
print("paired dish as % of the mean control:")
print(pd.DataFrame(pct, index=P.index, columns=[f"ROI {k}" for k in range(8)]).round(1).to_string(), "\n")
# (2) Reading (2) takes the lowest of 32 ROI values, each with sampling error, so it is biased
# toward a larger fold. Its size: the lowest ROI against the mean of it and its two neighbours, and
# against the mean of all ROIs above 160 degrees; and a bootstrap over dishes (paired and control
# dishes resampled with replacement, 2000 times) of how much lower the minimum of a resampled curve
# is than the minimum of the observed one.
rng = np.random.default_rng(0)
rows = []
for lay in ("L2", "L1"):
for freq in ("1 Hz", "833 kHz"):
for norm in ("i", "ii"):
a, r, P, S = curves(freq, lay, "B", "b", norm)
c = r.mean(0); k = int(np.argmin(c))
boot = []
for _ in range(2000):
Pb = P.values[rng.integers(0, len(P), len(P))]; Sb = S.values[rng.integers(0, len(S), len(S))]
boot.append(swing.normalised(Pb, Sb, norm).mean(0).min())
bias = np.mean(boot) - c.min()
rows.append({"layout": lay, "freq": freq, "norm": norm, "lowest ROI": k, "angle": a[k],
"fold at lowest ROI": 100 / c[k], "fold, lowest and 2 neighbours": 100 / c[max(k - 1, 0):k + 2].mean(),
"fold, mean of ROIs above 160 deg": 100 / c[a > 160].mean(),
"bootstrap bias of the minimum (% points)": bias,
"fold at lowest ROI, bias removed": 100 / (c[k] - bias) if c[k] - bias > 0 else np.inf})
print(pd.DataFrame(rows).round(2).to_string(index=False))
# (3) Sensitivity to the footprint region of background (b), model B.
rows = []
for region, col in BG_REGIONS.items():
for lay in ("L2", "L1"):
row = {"footprint region": region, "layout": lay}
for freq in ("1 Hz", "833 kHz"):
for norm in ("i", "ii"):
a, r, P, S = curves(freq, lay, "B", "b", norm, bgcol=col)
c = r.mean(0)
for rd in (1, 2):
row[f"{freq} {norm} r{rd}"] = 100 / swing.at_180(c, a, rd)[0]
e, t = EXPECT_180[freq]
row[f"{freq} holds"] = any(abs(row[f"{freq} {n} r{rd}"] - e) <= t for n in ("i", "ii") for rd in (1, 2))
row["both hold"] = row["1 Hz holds"] and row["833 kHz holds"]
rows.append(row)
print("\nFold by footprint region (r1: ROI of largest angle; r2: ROI of lowest value):")
print(pd.DataFrame(rows).round(2).to_string(index=False))
1 Hz, YP above the footprint mean (counts), ROIs 0 to 7 of L2:
ROI 0 (180 deg) ROI 1 (178 deg) ROI 2 (177 deg) ROI 3 (174 deg) ROI 4 (172 deg) ROI 5 (170 deg) ROI 6 (167 deg) ROI 7 (164 deg) train
subject
600ns-5kV-1HzBP-8-9-21-BPAE-13 57.8 45.8 76.9 77.8 114.3 124.5 113.7 144.6 paired
600ns-5kV-1HzBP-8-9-21-BPAE-15 75.1 115.4 119.8 108.4 164.7 186.6 250.7 190.2 paired
600ns-5kV-1HzBP-8-9-21-BPAE-8 48.3 103.5 81.5 80.0 119.7 109.2 87.4 118.9 paired
600ns-5kV-1HzUP-7-31-21-BPAE-03 142.9 164.5 194.6 91.7 147.2 336.7 333.5 242.0 single
600ns-5kV-1HzUP-7-31-21-BPAE-10 138.4 112.9 90.0 207.8 304.7 244.1 415.2 311.0 single
600ns-5kV-1HzUP-7-31-21-BPAE-5 187.4 247.5 379.5 408.0 370.7 535.1 526.1 664.6 single
600ns-5kV-1HzUP-8-9-21-BPAE-10 126.4 131.0 82.5 139.4 150.6 177.4 246.0 286.5 single
600ns-5kV-1HzUP-8-9-21-BPAE-14 148.4 192.8 201.3 229.5 312.1 326.8 153.0 397.0 single
paired dish as % of the mean control:
ROI 0 ROI 1 ROI 2 ROI 3 ROI 4 ROI 5 ROI 6 ROI 7
subject
600ns-5kV-1HzBP-8-9-21-BPAE-13 38.9 27.0 40.5 36.2 44.5 38.4 34.0 38.0
600ns-5kV-1HzBP-8-9-21-BPAE-15 50.5 68.0 63.2 50.4 64.1 57.6 74.9 50.0
600ns-5kV-1HzBP-8-9-21-BPAE-8 32.5 60.9 43.0 37.2 46.6 33.7 26.1 31.3
833 kHz, YP above the footprint mean (counts), ROIs 0 to 7 of L2:
ROI 0 (180 deg) ROI 1 (178 deg) ROI 2 (176 deg) ROI 3 (174 deg) ROI 4 (172 deg) ROI 5 (169 deg) ROI 6 (166 deg) ROI 7 (163 deg) train
subject
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 40.4 42.5 31.1 25.1 31.4 47.8 46.1 44.4 paired
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 40.1 27.5 24.4 16.8 34.1 40.6 58.4 39.8 paired
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 26.1 20.9 22.6 33.8 39.9 40.9 54.3 46.9 paired
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 355.7 388.1 363.0 590.2 450.3 583.9 571.6 437.6 single
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 637.0 745.7 997.7 366.8 513.2 491.8 717.1 646.4 single
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 357.7 321.9 356.1 499.4 512.1 378.5 471.4 459.8 single
paired dish as % of the mean control:
ROI 0 ROI 1 ROI 2 ROI 3 ROI 4 ROI 5 ROI 6 ROI 7
subject
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 9.0 8.7 5.4 5.2 6.4 9.9 7.9 8.6
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 8.9 5.7 4.3 3.5 6.9 8.4 10.0 7.7
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 5.8 4.3 3.9 7.0 8.1 8.4 9.3 9.1
layout freq norm lowest ROI angle fold at lowest ROI fold, lowest and 2 neighbours fold, mean of ROIs above 160 deg bootstrap bias of the minimum (% points) fold at lowest ROI, bias removed
L2 1 Hz i 0 179.66 2.42 2.05 1.74 -2.24 2.30
L2 1 Hz ii 7 163.65 2.51 1.98 2.10 -2.86 2.35
L2 833 kHz i 3 173.94 18.52 16.42 13.10 -0.76 16.22
L2 833 kHz ii 2 176.05 21.98 18.77 13.93 -0.42 20.14
L1 1 Hz i 2 165.30 2.34 1.91 1.91 -1.42 2.27
L1 1 Hz ii 2 165.30 2.48 2.14 2.16 -1.68 2.38
L1 833 kHz i 2 164.62 11.24 10.70 10.83 -0.53 10.60
L1 833 kHz ii 2 164.62 11.48 11.16 11.19 -0.52 10.83
Fold by footprint region (r1: ROI of largest angle; r2: ROI of lowest value):
footprint region layout 1 Hz i r1 1 Hz i r2 1 Hz ii r1 1 Hz ii r2 1 Hz holds 833 kHz i r1 833 kHz i r2 833 kHz ii r1 833 kHz ii r2 833 kHz holds both hold
disk 0.5 mm (decides) L2 2.42 2.42 2.46 2.51 False 11.77 18.52 12.67 21.98 True False
disk 0.5 mm (decides) L1 1.92 2.34 2.19 2.48 True 11.22 11.24 11.27 11.48 False False
lumen 0.25 mm L2 2.49 2.49 2.53 2.54 False 10.38 16.44 11.42 19.73 True False
lumen 0.25 mm L1 1.93 2.37 2.22 2.51 True 10.18 10.33 10.29 10.67 False False
wall ring 0.25-0.5 mm L2 2.40 2.40 2.44 2.50 False 12.25 19.27 13.12 22.80 True False
wall ring 0.25-0.5 mm L1 1.91 2.33 2.18 2.47 True 11.57 11.57 11.61 11.82 False False
disk 0.6 mm L2 2.47 2.47 2.52 2.54 False 11.99 19.19 12.94 22.79 True False
disk 0.6 mm L1 1.93 2.36 2.21 2.50 True 11.39 11.39 11.44 11.65 False False
Result: partially reproduced. Paired trains give less uptake than single trains at the largest angles at both frequencies under every reading, and less at 833 kHz than at 1 Hz, but the two fold values never both fall within tolerance. With the footprint background subtracted (reading b, 0.5 mm disk), layout L2 gives 2.42 to 2.51 fold at 1 Hz (expected 2 +- 0.2) and 11.77 to 21.98 fold at 833 kHz (the ROI of lowest value, 18.52 to 21.98, is within 20 +- 2); layout L1 gives 1.92 to 2.48 fold at 1 Hz (1.92 and 2.19 within tolerance) and 11.22 to 11.48 fold at 833 kHz. The same pattern holds under every footprint region (lumen, wall ring, 0.6 mm disk): on L2 the 1 Hz fold is 2.40 to 2.54 and the 833 kHz value holds only at the ROI of lowest value (16.44 to 22.80); on L1 the 1 Hz value holds and the 833 kHz one is 10.18 to 11.82. The 833 kHz match on L2 rests on reading (2), the lowest of 32 ROIs, which is biased toward a larger fold: the lowest ROI and its two neighbours give 16.42 and 18.77 fold, all ROIs above 160 degrees 13.10 and 13.93, and the lowest ROI with the bootstrap bias removed 16.22 and 20.14 (normalisations i and ii). No single paired dish drives the 833 kHz value: per dish, ROIs 0 to 7 are 3.5 to 10.0% of the mean control. Without subtraction (reading a) the values are 1.26 to 1.55 and 2.66 to 3.38 fold, neither within tolerance; this variant decides nothing under the amended claims file, and gives the same outcome. The two angle models give the same values. The cancellation is present, and its size matches at one frequency at a time. The outcome rests on the assumed pixel size and ROI layout.
fig2-crossover-angle¶
Cancellation weakens as the angle is reduced, and paired and single pulses become equally effective at about 160 degrees at 1 Hz and about 130 degrees at 833 kHz.
Criterion: the largest angle at which the mean paired value reaches 100%, by linear interpolation between neighbouring ROIs; both angles within tolerance (160 +- 16, 130 +- 13) under one normalisation at least, under background reading (b); reading (a) is a variant only.
EXPECT_X = {"1 Hz": (160.0, 16.0), "833 kHz": (130.0, 13.0)}
rows = []
for lay, model, bg, norm in READINGS:
for freq in ("1 Hz", "833 kHz"):
a, r, P, S = curves(freq, lay, model, bg, norm)
x = swing.crossover(r.mean(0), a)
e, t = EXPECT_X[freq]
rows.append({"layout": lay, "model": model, "bg": bg, "norm": norm, "freq": freq, "crossover": x,
"holds": bool(abs(x - e) <= t)})
tx = pd.DataFrame(rows)
print(tx.pivot_table(index=["layout", "model", "bg"], columns=["freq", "norm"], values="crossover").round(1).to_string())
both = tx.groupby(["layout", "model", "bg", "norm"]).holds.all()
dec = both.groupby(["layout", "model", "bg"]).any()
print("\nBoth angles within tolerance under one normalisation:")
print(dec.to_string())
resx = {k: ("criterion met" if v else "criterion not met") for k, v in dec.items()}
outx = combine(resx)
print("\nOutcome per layout, background (b), which decides:", outx)
print("Variant, background (a), deciding nothing:", combine(resx, "a"))
# Variants on L2, model B
rows = []
for name, kw in variants.items():
for bg in ("a", "b"):
for freq in ("1 Hz", "833 kHz"):
if ("1 Hz" in name and freq != "1 Hz") or ("833" in name and freq != "833 kHz"):
continue
for norm in ("i", "ii"):
a, r, P, S = curves(freq, "L2", "B", bg, norm, **kw)
rows.append({"variant": name, "bg": bg, "freq": freq, "norm": norm, "crossover": swing.crossover(r.mean(0), a)})
print("\nVariants, layout L2, model B:")
print(pd.DataFrame(rows).pivot_table(index=["variant", "freq"], columns=["bg", "norm"], values="crossover").round(1).to_string())
freq 1 Hz 833 kHz
norm i ii i ii
layout model bg
L1 B a 147.6 147.3 124.4 124.3
b 147.6 147.0 125.7 125.5
C a 145.9 145.6 124.2 124.1
b 145.9 145.3 125.4 125.3
L2 B a 149.5 149.3 124.8 124.6
b 149.6 149.1 125.9 125.7
C a 147.7 147.5 124.6 124.5
b 147.8 147.3 125.6 125.5
Both angles within tolerance under one normalisation:
layout model bg
L1 B a True
b True
C a True
b True
L2 B a True
b True
C a True
b True
Outcome per layout, background (b), which decides: {'L2 (decides)': 'criterion met', 'L1 (variant)': 'criterion met'}
Variant, background (a), deciding nothing: {'L2 (decides)': 'criterion met', 'L1 (variant)': 'criterion met'}
Variants, layout L2, model B:
bg a b
norm i ii i ii
variant freq
1 Hz controls of 7-31-21 only 1 Hz 149.4 149.3 149.8 149.6
1 Hz controls of 8-9-21 only (day of the paired dishes) 1 Hz 160.6 152.5 161.4 130.5
833 kHz without BPAE-13 of 8-24-21 833 kHz 125.0 124.9 125.9 125.7
YP / MemBrite 1 Hz 149.7 149.4 149.5 149.1
833 kHz 144.2 143.6 135.4 135.0
all controls 1 Hz 149.5 149.3 149.6 149.1
833 kHz 124.8 124.6 125.9 125.7
# How much the crossover rests on single ROIs (L2, model B, background b): the crossover with each
# ROI near it left out in turn, and on a centred running mean of three ROIs.
rows = []
for freq, (lo, hi) in (("1 Hz", (125, 165)), ("833 kHz", (110, 145))):
for norm in ("i", "ii"):
a, r, P, S = curves(freq, "L2", "B", "b", norm)
c = r.mean(0)
row = {"freq": freq, "norm": norm, "crossover": swing.crossover(c, a),
"running mean of 3": swing.crossover(pd.Series(c).rolling(3, center=True, min_periods=1).mean().values, a)}
for j in np.nonzero((a >= lo) & (a <= hi))[0]:
row[f"without ROI {j} ({a[j]:.1f})"] = swing.crossover(np.delete(c, j), np.delete(a, j))
rows.append(row)
print(pd.DataFrame(rows).round(1).T.to_string())
# Sensitivity to the footprint region of background (b)
rows = []
for region, col in BG_REGIONS.items():
for lay in ("L2", "L1"):
for norm in ("i", "ii"):
row = {"footprint region": region, "layout": lay, "norm": norm}
for freq in ("1 Hz", "833 kHz"):
a, r, P, S = curves(freq, lay, "B", "b", norm, bgcol=col)
row[f"crossover {freq}"] = swing.crossover(r.mean(0), a)
rows.append(row)
print("\nCrossover (model B) by footprint region:")
print(pd.DataFrame(rows).round(1).to_string(index=False))
0 1 2 3 freq 1 Hz 1 Hz 833 kHz 833 kHz norm i ii i ii crossover 149.6 149.1 125.9 125.7 running mean of 3 135.6 132.8 128.9 128.7 without ROI 7 (163.7) 149.6 149.1 NaN NaN without ROI 8 (160.2) 149.6 149.1 NaN NaN without ROI 9 (156.4) 149.6 149.1 NaN NaN without ROI 10 (152.3) 150.2 149.2 NaN NaN without ROI 11 (147.8) 143.3 131.4 NaN NaN without ROI 12 (143.0) 149.6 149.1 NaN NaN without ROI 13 (137.9) 149.6 149.1 NaN NaN without ROI 14 (132.5) 149.6 149.1 NaN NaN without ROI 15 (126.8) 149.6 149.1 NaN NaN without ROI 12 (142.7) NaN NaN 125.9 125.7 without ROI 13 (137.7) NaN NaN 125.9 125.7 without ROI 14 (132.3) NaN NaN 125.9 125.7 without ROI 15 (126.7) NaN NaN 130.1 130.1 without ROI 16 (120.8) NaN NaN 125.5 125.1 without ROI 17 (114.6) NaN NaN 125.9 125.7
Crossover (model B) by footprint region:
footprint region layout norm crossover 1 Hz crossover 833 kHz
disk 0.5 mm (decides) L2 i 149.6 125.9
disk 0.5 mm (decides) L2 ii 149.1 125.7
disk 0.5 mm (decides) L1 i 147.6 125.7
disk 0.5 mm (decides) L1 ii 147.0 125.5
lumen 0.25 mm L2 i 149.5 128.6
lumen 0.25 mm L2 ii 149.0 126.7
lumen 0.25 mm L1 i 147.6 126.8
lumen 0.25 mm L1 ii 146.9 126.8
wall ring 0.25-0.5 mm L2 i 149.6 125.5
wall ring 0.25-0.5 mm L2 ii 149.1 125.3
wall ring 0.25-0.5 mm L1 i 147.7 125.2
wall ring 0.25-0.5 mm L1 ii 147.0 125.0
disk 0.6 mm L2 i 149.5 126.1
disk 0.6 mm L2 ii 149.0 125.9
disk 0.6 mm L1 i 147.6 125.9
disk 0.6 mm L1 ii 147.0 125.8
Result: reproduced, with low confidence at 1 Hz. The mean paired value reaches 100% at 145.3 to 149.6 degrees at 1 Hz (expected 160 +- 16) and at 124.1 to 125.9 degrees at 833 kHz (expected 130 +- 13), under all 16 readings (layout, angle model, background, normalisation), so the 8 readings under background (b), which decide, and the variant (a) agree, and under every footprint region (1 Hz 146.9 to 149.6, 833 kHz 125.0 to 128.6). The 1 Hz angle is 1.3 to 5.6 degrees above the lower bound of its tolerance and rests on one ROI: on L2, ROI 11 (147.8 degrees) is above 100% while ROIs 12 to 14 (143 to 132 degrees) fall back to 79 to 93%. Without ROI 11 the 1 Hz crossover is 143.3 (normalisation i) or 131.4 degrees (ii), and on a running mean of three ROIs 135.6 or 132.8, all below 144; the 833 kHz crossover moves by at most 4.2 degrees under the same checks. With only the two 1 Hz controls of 8-9-21 it is 130.5 to 161.4 degrees, and with only those of 7-31-21, 149.3 to 149.8. The criterion, the largest angle at which the curve reaches 100% by interpolation between neighbouring ROIs, is met as written. In the curve tables above, every ROI above the crossover angle is below 100% and the ROIs next to the crossover are higher than those at the largest angles, so cancellation weakens as the angle falls. The outcome rests on the assumed pixel size.
fig2-facilitation-plateau¶
Below the crossover, paired pulses enhance electroporation up to about three-fold over single pulses at 90 to 100 degrees for both frequencies, followed by a plateau down to 0 degrees, so pulses at 90 degrees and at smaller angles are as efficient as co-directional pulses.
Criterion: level (1) the mean of the ROIs between 90 and 100 degrees, or (2) the mean of the ROIs from 100 degrees to the smallest angle, 3 +- 0.3 fold, either normalisation. Plateau readings over the ROIs from 0 to 90 degrees: (P1) a linear fit whose slope's 95% CI includes zero; (P2) the mean of the ROIs below 10 degrees within 10% of the mean of the ROIs between 90 and 100 degrees. The angle where the plateau is reached is the largest angle at which the value first comes within 10% of the level, and must lie within 81 to 110 degrees. All at both frequencies, under background reading (b); blocked when the plateau readings disagree. Reading (a) is a variant only.
def plateau_measures(c, a):
lv1 = c[(a >= 90) & (a <= 100)]
lv1 = lv1.mean() / 100 if len(lv1) else np.nan
lv2 = c[a <= 100].mean() / 100
sel = a <= 90
fit = stats.linregress(a[sel], c[sel])
tcrit = stats.t.ppf(0.975, sel.sum() - 2)
p1 = bool(abs(fit.slope) <= tcrit * fit.stderr)
low = c[a < 10]
m90 = c[(a >= 90) & (a <= 100)]
# P2 cannot be taken when no ROI lies below 10 degrees (or none between 90 and 100): untestable
p2 = bool(abs(low.mean() - m90.mean()) <= 0.1 * m90.mean()) if (len(low) and len(m90)) else None
o = np.argsort(a)[::-1]
def reach(level):
if not np.isfinite(level):
return np.nan
hit = [a[j] for j in o if abs(c[j] / 100 - level) <= 0.1 * level]
return hit[0] if hit else np.nan
return {"level1": lv1, "level2": lv2, "angle_at_level1": reach(lv1), "angle_at_level2": reach(lv2),
"slope_pct_per_deg": fit.slope, "slope_ci95": tcrit * fit.stderr, "n_le90": int(sel.sum()), "P1": p1,
"n_below10": int(len(low)), "n_90_100": int(len(m90)), "P2": p2}
rows = []
for lay, model, bg, norm in READINGS:
for freq in ("1 Hz", "833 kHz"):
a, r, P, S = curves(freq, lay, model, bg, norm)
rows.append({"layout": lay, "model": model, "bg": bg, "norm": norm, "freq": freq, **plateau_measures(r.mean(0), a)})
tp = pd.DataFrame(rows)
print(tp.round(2).to_string(index=False))
def ok(row, plateau):
good = []
for lv in ("1", "2"):
level, angle = row[f"level{lv}"], row[f"angle_at_level{lv}"]
good.append(bool(abs(level - 3) <= 0.3 and 81 <= angle <= 110 and row[plateau]))
return any(good)
rows = []
for (lay, model, bg), grp in tp.groupby(["layout", "model", "bg"]):
for plateau in ("P1", "P2"):
per_freq = {f: any(ok(r, plateau) for _, r in grp[grp.freq == f].iterrows()) for f in ("1 Hz", "833 kHz")}
testable = bool(grp[plateau].notna().all())
rows.append({"layout": lay, "model": model, "bg": bg, "plateau": plateau, **per_freq, "testable": testable,
"both hold": all(per_freq.values()) if testable else None})
dp = pd.DataFrame(rows)
print("\nLevel, angle and plateau all hold (some normalisation and level reading), per frequency:")
print(dp.to_string(index=False))
outp = {}
for plateau in ("P1", "P2"):
resp = {(r["layout"], r["model"], r["bg"]): ("untestable" if not r["testable"] else
"criterion met" if r["both hold"] else "criterion not met")
for _, r in dp[dp.plateau == plateau].iterrows()}
outp[plateau] = combine(resp)
print(f"Variant, background (a), plateau reading {plateau}, deciding nothing:", combine(resp, "a"))
print("\nOutcome per plateau reading and layout, background (b), which decides:", outp)
layout model bg norm freq level1 level2 angle_at_level1 angle_at_level2 slope_pct_per_deg slope_ci95 n_le90 P1 n_below10 n_90_100 P2
L1 B a i 1 Hz 1.12 1.64 145.17 36.86 -1.52 0.48 13 False 1 1 False
L1 B a i 833 kHz 1.32 1.74 116.71 82.25 -0.61 0.32 13 False 1 1 False
L1 B a ii 1 Hz 1.10 1.58 145.17 63.11 -1.35 0.48 13 False 1 1 False
L1 B a ii 833 kHz 1.32 1.71 116.71 82.25 -0.53 0.31 13 False 1 1 False
L1 B b i 1 Hz 1.28 2.31 122.09 36.86 -2.76 1.25 13 False 1 1 False
L1 B b i 833 kHz 2.57 2.96 111.36 76.06 0.32 1.11 13 True 1 1 False
L1 B b ii 1 Hz 1.19 2.07 127.13 63.11 -2.06 1.01 13 False 1 1 False
L1 B b ii 833 kHz 2.39 2.80 116.71 76.06 0.55 1.04 13 True 1 1 False
L1 C a i 1 Hz 1.16 1.64 143.62 43.59 -1.88 0.64 12 False 0 2 None
L1 C a i 833 kHz 1.33 1.74 117.05 84.50 -0.55 0.39 12 False 0 2 None
L1 C a ii 1 Hz 1.14 1.58 143.62 66.81 -1.67 0.66 12 False 0 2 None
L1 C a ii 833 kHz 1.32 1.71 117.05 84.50 -0.46 0.36 12 False 0 2 None
L1 C b i 1 Hz 1.36 2.31 126.85 43.59 -3.41 1.70 12 False 0 2 None
L1 C b i 833 kHz 2.48 2.96 117.05 78.66 0.97 1.27 12 True 0 2 None
L1 C b ii 1 Hz 1.30 2.07 126.85 66.81 -2.52 1.39 12 False 0 2 None
L1 C b ii 833 kHz 2.29 2.80 117.05 78.66 1.33 1.00 12 False 0 2 None
L2 B a i 1 Hz 1.10 1.66 147.81 33.78 -1.53 0.54 11 False 1 1 False
L2 B a i 833 kHz 1.37 1.74 114.58 80.21 -0.68 0.35 11 False 1 1 False
L2 B a ii 1 Hz 1.08 1.60 147.81 33.78 -1.35 0.52 11 False 1 1 False
L2 B a ii 833 kHz 1.36 1.72 114.58 80.21 -0.61 0.35 11 False 1 1 False
L2 B b i 1 Hz 1.24 2.36 147.81 33.78 -2.79 1.52 11 False 1 1 False
L2 B b i 833 kHz 2.78 2.96 94.55 94.55 0.04 1.11 11 True 1 1 False
L2 B b ii 1 Hz 1.15 2.08 147.81 33.78 -2.00 1.08 11 False 1 1 False
L2 B b ii 833 kHz 2.58 2.80 94.55 94.55 0.29 1.05 11 True 1 1 True
L2 C a i 1 Hz 1.10 1.66 146.09 41.08 -1.80 0.63 11 False 0 1 None
L2 C a i 833 kHz 1.37 1.74 115.05 82.58 -0.80 0.40 11 False 0 1 None
L2 C a ii 1 Hz 1.08 1.60 146.09 41.08 -1.58 0.61 11 False 0 1 None
L2 C a ii 833 kHz 1.36 1.72 115.05 82.58 -0.71 0.41 11 False 0 1 None
L2 C b i 1 Hz 1.24 2.36 146.09 41.08 -3.31 1.74 11 False 0 1 None
L2 C b i 833 kHz 2.78 2.96 96.14 96.14 0.05 1.29 11 True 0 1 None
L2 C b ii 1 Hz 1.15 2.08 146.09 41.08 -2.36 1.25 11 False 0 1 None
L2 C b ii 833 kHz 2.58 2.80 96.14 96.14 0.32 1.23 11 True 0 1 None
Level, angle and plateau all hold (some normalisation and level reading), per frequency:
layout model bg plateau 1 Hz 833 kHz testable both hold
L1 B a P1 False False True False
L1 B a P2 False False True False
L1 B b P1 False False True False
L1 B b P2 False False True False
L1 C a P1 False False True False
L1 C a P2 False False False None
L1 C b P1 False False True False
L1 C b P2 False False False None
L2 B a P1 False False True False
L2 B a P2 False False True False
L2 B b P1 False True True False
L2 B b P2 False True True False
L2 C a P1 False False True False
L2 C a P2 False False False None
L2 C b P1 False True True False
L2 C b P2 False False False None
Variant, background (a), plateau reading P1, deciding nothing: {'L2 (decides)': 'criterion not met', 'L1 (variant)': 'criterion not met'}
Variant, background (a), plateau reading P2, deciding nothing: {'L2 (decides)': 'criterion not met', 'L1 (variant)': 'criterion not met'}
Outcome per plateau reading and layout, background (b), which decides: {'P1': {'L2 (decides)': 'criterion not met', 'L1 (variant)': 'criterion not met'}, 'P2': {'L2 (decides)': 'criterion not met', 'L1 (variant)': 'criterion not met'}}
# Sensitivity of the plateau levels to the footprint region of background (b): layout L2, model B.
rows = []
for region, col in BG_REGIONS.items():
for freq in ("1 Hz", "833 kHz"):
for norm in ("i", "ii"):
a, r, P, S = curves(freq, "L2", "B", "b", norm, bgcol=col)
pm = plateau_measures(r.mean(0), a)
rows.append({"footprint region": region, "freq": freq, "norm": norm,
**{k: pm[k] for k in ("level1", "n_90_100", "level2", "angle_at_level1", "angle_at_level2", "P1")}})
print(pd.DataFrame(rows).round(2).to_string(index=False))
footprint region freq norm level1 n_90_100 level2 angle_at_level1 angle_at_level2 P1
disk 0.5 mm (decides) 1 Hz i 1.24 1 2.36 147.81 33.78 False
disk 0.5 mm (decides) 1 Hz ii 1.15 1 2.08 147.81 33.78 False
disk 0.5 mm (decides) 833 kHz i 2.78 1 2.96 94.55 94.55 True
disk 0.5 mm (decides) 833 kHz ii 2.58 1 2.80 94.55 94.55 True
lumen 0.25 mm 1 Hz i 1.23 1 2.35 147.81 33.78 False
lumen 0.25 mm 1 Hz ii 1.14 1 2.08 147.81 33.78 False
lumen 0.25 mm 833 kHz i 13.31 1 4.52 94.55 101.44 False
lumen 0.25 mm 833 kHz ii 3.71 1 3.39 94.55 94.55 True
wall ring 0.25-0.5 mm 1 Hz i 1.25 1 2.37 120.78 33.78 False
wall ring 0.25-0.5 mm 1 Hz ii 1.15 1 2.08 147.81 33.78 False
wall ring 0.25-0.5 mm 833 kHz i 2.41 1 2.79 94.55 80.21 True
wall ring 0.25-0.5 mm 833 kHz ii 2.34 1 2.66 94.55 65.31 True
disk 0.6 mm 1 Hz i 1.25 1 2.37 120.78 33.78 False
disk 0.6 mm 1 Hz ii 1.15 1 2.08 147.81 33.78 False
disk 0.6 mm 833 kHz i 3.00 1 3.09 94.55 94.55 True
disk 0.6 mm 833 kHz ii 2.76 1 2.91 94.55 94.55 True
Result: partially reproduced. Facilitation below the crossover is present at both frequencies, but the three-fold plateau from 90 to 100 degrees holds only at 833 kHz. At 833 kHz with background (b), the level at 90 to 100 degrees is 2.29 to 2.78 fold and the mean from 100 degrees down is 2.80 to 2.96 fold; on layout L2 the level is reached at 94.6 to 96.1 degrees and the slope over 0 to 90 degrees has a 95% CI that includes zero (P1). Level reading (1) rests on a single ROI on L2 (94.3 or 94.5 degrees; one or two on L1), as the ROIs are about 7 degrees apart there. At 1 Hz there is no plateau: the value keeps rising as the angle falls (slope -2.00 to -3.41 % per degree over 0 to 90 degrees under background b, CI excluding zero under every reading), the level at 90 to 100 degrees is 1.15 to 1.36 fold, and the mean from 100 degrees down 2.07 to 2.36 fold. The 1 Hz result does not depend on the footprint region (1.14 to 1.25 and 2.08 to 2.37 fold on L2). The 833 kHz half does: it holds with the 0.5 mm disk and the 0.6 mm disk (levels 2.58 to 3.09 fold, reached at 94.6 degrees), but not with the lumen alone (levels 3.39 to 13.31 fold, none within 2.7 to 3.3) or the wall ring alone (2.34 to 2.79 fold; the one level within tolerance, 2.79, is reached at 80.2 degrees, outside 81 to 110). So the half of the claim that holds rests on the choice of footprint region as well as on the layout. In the variant without background subtraction (a), which decides nothing, no level is within 2.7 to 3.3 (1.08 to 1.74 fold). Plateau reading P2 needs ROIs below 10 degrees; under angle model C there are none (the smallest angle is about 22 degrees), so P2 is untestable there and model B decides it; it is met only at 833 kHz on L2 with normalisation (ii). Under background (b) the criterion is not met under either plateau reading, so the claim is not blocked.
fig2-frequency-dependence¶
Pairing pulses enhances electroporation at small angles and inhibits it at large angles in a frequency-dependent manner, with stronger cancellation at 833 kHz than at 1 Hz.
Criterion: under one normalisation at least, the value at the 180 degree reading is lower at 833 kHz than at 1 Hz, the 833 kHz crossover angle is smaller than the 1 Hz one, and at both frequencies the value is above 100% at the smallest angle and below 100% at the largest; under background reading (b), with (a) a variant only.
rows = []
for lay, model, bg, norm in READINGS:
d = {}
for freq in ("1 Hz", "833 kHz"):
a, r, P, S = curves(freq, lay, model, bg, norm)
c = r.mean(0)
d[freq] = {"v180_1": swing.at_180(c, a, 1)[0], "v180_2": swing.at_180(c, a, 2)[0], "x": swing.crossover(c, a),
"small": c[np.argmin(a)], "large": c[np.argmax(a)]}
lower = any(d["833 kHz"][k] < d["1 Hz"][k] for k in ("v180_1", "v180_2"))
xs = d["833 kHz"]["x"] < d["1 Hz"]["x"]
ends = all(d[f]["small"] > 100 and d[f]["large"] < 100 for f in d)
rows.append({"layout": lay, "model": model, "bg": bg, "norm": norm,
"v180_1Hz": d["1 Hz"]["v180_1"], "v180_833": d["833 kHz"]["v180_1"],
"min_1Hz": d["1 Hz"]["v180_2"], "min_833": d["833 kHz"]["v180_2"],
"x_1Hz": d["1 Hz"]["x"], "x_833": d["833 kHz"]["x"],
"smallest_angle_1Hz": d["1 Hz"]["small"], "smallest_angle_833": d["833 kHz"]["small"],
"lower_at_833": lower, "crossover_smaller_at_833": xs, "ends_ok": ends, "all": lower and xs and ends})
tf = pd.DataFrame(rows)
print(tf.round(1).to_string(index=False))
decf = tf.groupby(["layout", "model", "bg"])["all"].any()
resf = {k: ("criterion met" if v else "criterion not met") for k, v in decf.items()}
outf = combine(resf)
print("\nOutcome per layout, background (b), which decides:", outf)
print("Variant, background (a), deciding nothing:", combine(resf, "a"))
layout model bg norm v180_1Hz v180_833 min_1Hz min_833 x_1Hz x_833 smallest_angle_1Hz smallest_angle_833 lower_at_833 crossover_smaller_at_833 ends_ok all
L1 B a i 73.8 35.4 67.4 31.9 147.6 124.4 236.5 203.8 True True True True
L1 B a ii 71.7 35.4 65.8 31.5 147.3 124.3 231.8 195.9 True True True True
L1 B b i 52.2 8.9 42.7 8.9 147.6 125.7 323.4 307.2 True True True True
L1 B b ii 45.7 8.9 40.3 8.7 147.0 125.5 308.1 268.7 True True True True
L1 C a i 73.8 35.4 67.4 31.9 145.9 124.2 236.5 203.8 True True True True
L1 C a ii 71.7 35.4 65.8 31.5 145.6 124.1 231.8 195.9 True True True True
L1 C b i 52.2 8.9 42.7 8.9 145.9 125.4 323.4 307.2 True True True True
L1 C b ii 45.7 8.9 40.3 8.7 145.3 125.3 308.1 268.7 True True True True
L2 B a i 79.3 37.6 67.2 32.8 149.5 124.8 236.5 203.8 True True True True
L2 B a ii 78.9 36.2 64.3 29.5 149.3 124.6 231.8 195.9 True True True True
L2 B b i 41.3 8.5 41.3 5.4 149.6 125.9 323.4 307.2 True True True True
L2 B b ii 40.6 7.9 39.8 4.5 149.1 125.7 308.1 268.7 True True True True
L2 C a i 79.3 37.6 67.2 32.8 147.7 124.6 236.5 203.8 True True True True
L2 C a ii 78.9 36.2 64.3 29.5 147.5 124.5 231.8 195.9 True True True True
L2 C b i 41.3 8.5 41.3 5.4 147.8 125.6 323.4 307.2 True True True True
L2 C b ii 40.6 7.9 39.8 4.5 147.3 125.5 308.1 268.7 True True True True
Outcome per layout, background (b), which decides: {'L2 (decides)': 'criterion met', 'L1 (variant)': 'criterion met'}
Variant, background (a), deciding nothing: {'L2 (decides)': 'criterion met', 'L1 (variant)': 'criterion met'}
Result: reproduced. Under all 16 readings, the value at the 180 degree reading is lower at 833 kHz than at 1 Hz (on L2 with background b, 8.5 against 41.3% at the ROI of largest angle; with background a, 37.6 against 79.3%), the crossover angle is smaller at 833 kHz (124.1 to 125.9 against 145.3 to 149.6 degrees), and at both frequencies the value is above 100% at the smallest angle (195.9 to 323.4%) and below 100% at the largest (7.9 to 79.3%). The outcome is decided by the 8 readings under background (b); the variant (a) agrees. The ordering of the crossovers holds under every footprint region and with the 1 Hz crossover taken from a running mean of three ROIs (about 133 to 136 against 129 degrees, fig2-crossover-angle).
fig2-60-fold-co-counter¶
At 833 kHz, co-directional and counter-directional pulse pairs differ about 60-fold in electroporation efficiency.
Criterion: at 833 kHz, (a) the ROI of smallest angle over the ROI of largest angle, or (b) the plateau level of fig2-facilitation-plateau over the 180 degree value of fig2-180-cancellation; 60 +- 6 under one of these and one normalisation, under background reading (b); reading (a) is a variant only.
rows = []
for lay, model, bg, norm in READINGS:
a, r, P, S = curves("833 kHz", lay, model, bg, norm)
c = r.mean(0)
pm = plateau_measures(c, a)
v1, v2 = swing.at_180(c, a, 1)[0], swing.at_180(c, a, 2)[0]
ratio = lambda x, y: x / y if y > 0 else np.inf
cand = {"a_ends": ratio(c[np.argmin(a)], c[np.argmax(a)]),
"b_level1_over_180r1": ratio(100 * pm["level1"], v1), "b_level1_over_180r2": ratio(100 * pm["level1"], v2),
"b_level2_over_180r1": ratio(100 * pm["level2"], v1), "b_level2_over_180r2": ratio(100 * pm["level2"], v2)}
# uptake in the paired dishes alone (not normalised), smallest over largest angle
praw = P.values.mean(0)
rows.append({"layout": lay, "model": model, "bg": bg, "norm": norm, **cand,
"paired_raw_ends": ratio(praw[np.argmin(a)], praw[np.argmax(a)]),
"holds": any(abs(v - 60) <= 6 for v in cand.values())})
t60 = pd.DataFrame(rows)
print(t60.round(1).to_string(index=False))
dec60 = t60.groupby(["layout", "model", "bg"]).holds.any()
res60 = {k: ("criterion met" if v else "criterion not met") for k, v in dec60.items()}
out60 = combine(res60)
print("\nOutcome per layout, background (b), which decides:", out60)
print("Variant, background (a), deciding nothing:", combine(res60, "a"))
cand_cols = ["a_ends", "b_level1_over_180r1", "b_level1_over_180r2", "b_level2_over_180r1", "b_level2_over_180r2"]
within = t60[t60.bg == "b"].set_index(["layout", "model", "norm"])[cand_cols].apply(lambda s: (s - 60).abs() <= 6)
print("\nBackground (b): which ratios are within 60 +- 6, per layout, model and normalisation:")
print(within.to_string())
layout model bg norm a_ends b_level1_over_180r1 b_level1_over_180r2 b_level2_over_180r1 b_level2_over_180r2 paired_raw_ends holds
L1 B a i 5.8 3.7 4.1 4.9 5.4 4.3 False
L1 B a ii 5.5 3.7 4.2 4.8 5.4 4.3 False
L1 B b i 34.5 28.8 28.9 33.1 33.2 20.4 False
L1 B b ii 30.3 27.0 27.5 31.5 32.1 20.4 False
L1 C a i 5.8 3.8 4.2 4.9 5.4 4.3 False
L1 C a ii 5.5 3.7 4.2 4.8 5.4 4.3 False
L1 C b i 34.5 27.8 27.9 33.1 33.2 20.4 False
L1 C b ii 30.3 25.8 26.3 31.5 32.1 20.4 False
L2 B a i 5.4 3.6 4.2 4.6 5.3 4.5 False
L2 B a ii 5.4 3.8 4.6 4.7 5.8 4.5 False
L2 B b i 36.2 32.7 51.5 34.9 54.8 25.0 True
L2 B b ii 34.1 32.8 56.8 35.5 61.6 25.0 True
L2 C a i 5.4 3.6 4.2 4.6 5.3 4.5 False
L2 C a ii 5.4 3.8 4.6 4.7 5.8 4.5 False
L2 C b i 36.2 32.7 51.5 34.9 54.8 25.0 True
L2 C b ii 34.1 32.8 56.8 35.5 61.6 25.0 True
Outcome per layout, background (b), which decides: {'L2 (decides)': 'criterion met', 'L1 (variant)': 'criterion not met'}
Variant, background (a), deciding nothing: {'L2 (decides)': 'criterion not met', 'L1 (variant)': 'criterion not met'}
Background (b): which ratios are within 60 +- 6, per layout, model and normalisation:
a_ends b_level1_over_180r1 b_level1_over_180r2 b_level2_over_180r1 b_level2_over_180r2
layout model norm
L1 B i False False False False False
ii False False False False False
C i False False False False False
ii False False False False False
L2 B i False False False False True
ii False False True False True
C i False False False False True
ii False False True False True
# Sensitivity of the ratios to the footprint region of background (b), model B (the ROI of
# largest angle and of lowest value do not depend on the angle model).
rows = []
for region, col in BG_REGIONS.items():
for lay in ("L2", "L1"):
for norm in ("i", "ii"):
a, r, P, S = curves("833 kHz", lay, "B", "b", norm, bgcol=col)
c = r.mean(0); pm = plateau_measures(c, a)
v1, v2 = swing.at_180(c, a, 1)[0], swing.at_180(c, a, 2)[0]
ratio = lambda x, y: x / y if y > 0 else np.inf
cand = {"a_ends": ratio(c[np.argmin(a)], c[np.argmax(a)]),
"b_level1_over_180r1": ratio(100 * pm["level1"], v1), "b_level1_over_180r2": ratio(100 * pm["level1"], v2),
"b_level2_over_180r1": ratio(100 * pm["level2"], v1), "b_level2_over_180r2": ratio(100 * pm["level2"], v2)}
rows.append({"footprint region": region, "layout": lay, "norm": norm, **cand,
"holds": any(abs(v - 60) <= 6 for v in cand.values())})
s60 = pd.DataFrame(rows)
print(s60.round(1).to_string(index=False))
print("\nCriterion met (some ratio and normalisation), per region and layout:")
print(s60.groupby(["footprint region", "layout"], sort=False).holds.any().unstack().to_string())
# Reading (2) of the 180 degree value is the lowest of 32 ROIs, biased low (see fig2-180-cancellation).
# The ratios of the plateau level over that value with the bias removed by the bootstrap over dishes:
rng = np.random.default_rng(0)
rows = []
for norm in ("i", "ii"):
a, r, P, S = curves("833 kHz", "L2", "B", "b", norm)
c = r.mean(0); pm = plateau_measures(c, a)
boot = [swing.normalised(P.values[rng.integers(0, len(P), len(P))], S.values[rng.integers(0, len(S), len(S))], norm).mean(0).min()
for _ in range(2000)]
low = c.min() - (np.mean(boot) - c.min())
rows.append({"layout": "L2", "norm": norm, "lowest ROI (%)": c.min(), "bias removed (%)": low,
"level1 / it": 100 * pm["level1"] / low, "level2 / it": 100 * pm["level2"] / low})
print("\nLayout L2, model B, background b, with the bias of the minimum removed:")
print(pd.DataFrame(rows).round(2).to_string(index=False))
# The bootstrap resamples dishes, so it misses most of the bias of a minimum over 32 ROIs. The
# plateau levels over less selected 180 degree values: the lowest ROI with its two neighbours,
# and the mean of all ROIs above 160 degrees.
rows = []
for norm in ("i", "ii"):
a, r, P, S = curves("833 kHz", "L2", "B", "b", norm)
c = r.mean(0); pm = plateau_measures(c, a); k = int(np.argmin(c))
for name, den in (("lowest ROI", c[k]), ("lowest ROI and 2 neighbours", c[max(k - 1, 0):k + 2].mean()),
("mean of ROIs above 160 deg", c[a > 160].mean())):
rows.append({"norm": norm, "180 degree value": name, "value (%)": den,
"level1 / it": 100 * pm["level1"] / den, "level2 / it": 100 * pm["level2"] / den,
"within 60 +- 6": any(abs(100 * pm[l] / den - 60) <= 6 for l in ("level1", "level2"))})
print("\nLayout L2, model B, background b, plateau level over less selected 180 degree values:")
print(pd.DataFrame(rows).round(2).to_string(index=False))
footprint region layout norm a_ends b_level1_over_180r1 b_level1_over_180r2 b_level2_over_180r1 b_level2_over_180r2 holds
disk 0.5 mm (decides) L2 i 36.2 32.7 51.5 34.9 54.8 True
disk 0.5 mm (decides) L2 ii 34.1 32.8 56.8 35.5 61.6 True
disk 0.5 mm (decides) L1 i 34.5 28.8 28.9 33.1 33.2 False
disk 0.5 mm (decides) L1 ii 30.3 27.0 27.5 31.5 32.1 False
lumen 0.25 mm L2 i 36.7 138.1 218.9 46.9 74.3 False
lumen 0.25 mm L2 ii 33.3 42.4 73.3 38.7 66.9 False
lumen 0.25 mm L1 i 36.0 101.2 102.8 43.0 43.6 False
lumen 0.25 mm L1 ii 30.0 35.1 36.4 34.9 36.2 False
wall ring 0.25-0.5 mm L2 i 36.2 29.6 46.5 34.2 53.8 False
wall ring 0.25-0.5 mm L2 ii 34.4 30.8 53.5 34.8 60.5 True
wall ring 0.25-0.5 mm L1 i 34.2 25.9 25.9 32.3 32.3 False
wall ring 0.25-0.5 mm L1 ii 30.4 25.2 25.7 30.8 31.3 False
disk 0.6 mm L2 i 37.5 35.9 57.5 37.0 59.2 True
disk 0.6 mm L2 ii 35.3 35.8 63.0 37.6 66.3 True
disk 0.6 mm L1 i 35.7 31.4 31.4 35.1 35.1 False
disk 0.6 mm L1 ii 31.3 29.2 29.7 33.2 33.8 False
Criterion met (some ratio and normalisation), per region and layout:
layout L2 L1
footprint region
disk 0.5 mm (decides) True False
lumen 0.25 mm False False
wall ring 0.25-0.5 mm True False
disk 0.6 mm True False
Layout L2, model B, background b, with the bias of the minimum removed:
layout norm lowest ROI (%) bias removed (%) level1 / it level2 / it
L2 i 5.40 6.14 45.30 48.22
L2 ii 4.55 4.93 52.42 56.85
Layout L2, model B, background b, plateau level over less selected 180 degree values:
norm 180 degree value value (%) level1 / it level2 / it within 60 +- 6
i lowest ROI 5.40 51.52 54.84 True
i lowest ROI and 2 neighbours 6.09 45.68 48.63 False
i mean of ROIs above 160 deg 7.63 36.44 38.79 False
ii lowest ROI 4.55 56.82 61.61 True
ii lowest ROI and 2 neighbours 5.33 48.51 52.61 False
ii mean of ROIs above 160 deg 7.18 36.01 39.05 False
Result: reproduced, on layout L2, which is adopted as an assumption, with low confidence. Under background (b) with the 0.5 mm footprint disk, which decides, layout L2 gives a plateau level over the ROI of lowest value of 51.5 to 61.6 fold under both angle models, three of its four values within 60 +- 6 (level reading 1 with normalisation ii, 56.8; level reading 2, 54.8 and 61.6); the other ratios are 32.7 to 36.2 fold. The criterion is met only through reading (2) of the 180 degree value, the ROI of lowest value. That minimum is biased low. The bootstrap over dishes puts the bias at 0.4 to 0.8 percentage points (with it removed, level 2 over the minimum is 56.9 under normalisation ii and 48.2 under i), but it resamples whole dishes, keeping each dish's ROI-to-ROI noise, so it underestimates the bias of a minimum over 32 ROIs and does not settle it. Over less selected 180 degree values the ratio is outside 60 +- 6: 45.7 to 52.6 fold over the lowest ROI and its two neighbours, and 36.0 to 39.1 fold over the mean of the ROIs above 160 degrees. The criterion is met only at the single lowest ROI. It is also sensitive to the footprint region of the background: met with the 0.5 mm disk, the 0.6 mm disk and the wall ring alone, not met with the lumen alone (33.3 to 218.9 fold, scattered, as the lumen of the three 833 kHz controls holds bright material). On layout L1 every ratio under (b) is 25.8 to 34.5 fold, so the criterion is not met there; L2 decides by the rule stated under "Measures along the bisector". The variant without background subtraction (a), which decides nothing under the amended claims file, gives 3.6 to 5.8 fold on both layouts. The claim was blocked before the reader's answer only because (a) and (b) disagreed. Paired uptake alone, not normalised to the controls, is 25.0 times higher at the apex end than at the base end on L2 with background (b), and 4.5 times with (a). The ratio rests on a denominator of about 5% of control, from 8-24-21 paired dishes against 10-5-21 controls whose MemBrite level differs (data check above).
fig4-unipolar-pattern¶
With single-pulse trains on one base electrode, YP uptake follows the electric field map; with paired pulses on both base electrodes at 833 kHz, uptake is strong near the grounded apex electrode and weak or absent near the base electrodes despite the strong field there.
Criterion: mean YP in 200 x 200 um ROIs adjacent to the apex and to each base electrode, under background reading (b), with (a) a variant only; in the paired sample the apex exceeds each base, and in the single sample the energised base exceeds the apex. "Near": ROIs touching the footprint edge (1.5 mm diameter) and ROIs 200 um farther out, both run. The match to the field map is the Spearman correlation of ROI uptake with field strength, with no threshold. The 833 kHz, 4 kV dishes are the dishes of Figure 4 (Results 2.3). Which side of the electrode the ROI is on is not given; the ROI on the line to the triangle's centroid and the mean of eight ROIs around the electrode are both run. The files do not say which base electrode was energised in the single-pulse dishes; the base with the larger adjacent uptake is taken as energised, and the other is shown.
rows = []
for i in keep:
if info.freq[i] != "833 kHz":
continue
g = geoms[i]; yp = dishes[i]["img"][1]
bgv = swing.footprint_background(yp, g)
for gap in (0.0, 0.2):
for direction in ("centroid", "ring"):
v = {k: np.mean([swing.square_mean(yp, c, np.array([1.0, 0.0])) for c in swing.adjacent_rois(g, k, gap, direction)])
for k in ("apex", "base1", "base2")}
for bg in ("a", "b"):
sub = bgv if bg == "b" else 0.0
rows.append({"subject": info.subject[i], "train": info.train[i], "gap_mm": gap, "side": direction, "bg": bg,
**{k: v[k] - sub for k in v}})
t4 = pd.DataFrame(rows)
t4["base_hi"] = t4[["base1", "base2"]].max(axis=1)
t4["base_lo"] = t4[["base1", "base2"]].min(axis=1)
print("Per dish:")
print(t4.round(1).to_string(index=False))
m4 = t4.groupby(["train", "gap_mm", "side", "bg"])[["apex", "base1", "base2", "base_hi", "base_lo"]].mean()
print("\nMean over dishes:")
print(m4.round(1).to_string())
rows = []
for (gap, side, bg), grp in t4.groupby(["gap_mm", "side", "bg"]):
pr = grp[grp.train == "paired"]; sg = grp[grp.train == "single"]
paired_ok = bool(pr.apex.mean() > pr.base1.mean() and pr.apex.mean() > pr.base2.mean())
# energised base per dish = the base with larger uptake; compare its mean with the apex mean
single_ok = bool(sg.base_hi.mean() > sg.apex.mean())
rows.append({"gap_mm": gap, "side": side, "bg": bg,
"paired apex/base (each dish, min)": (pr.apex / pr.base_hi).min(),
"paired dishes apex > both bases": int((pr.apex > pr.base_hi).sum()),
"single energised/apex (each dish, min)": (sg.base_hi / sg.apex).min(),
"single dishes energised > apex": int((sg.base_hi > sg.apex).sum()),
"single other base/apex (mean)": sg.base_lo.mean() / sg.apex.mean(),
"paired_ok": paired_ok, "single_ok": single_ok, "both": paired_ok and single_ok})
d4 = pd.DataFrame(rows)
print("\nPer reading (means over 3 paired and 3 single dishes):")
print(d4.round(2).to_string(index=False))
print("\nAll readings agree:", d4.both.nunique() == 1, "; outcome under every reading:", bool(d4.both.all()))
print("Background (b), which decides: criterion met under every reading:", bool(d4[d4.bg == "b"].both.all()),
"; variant, background (a):", bool(d4[d4.bg == "a"].both.all()))
Per dish:
subject train gap_mm side bg apex base1 base2 base_hi base_lo
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 paired 0.0 centroid a 1158.5 306.5 413.7 413.7 306.5
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 paired 0.0 centroid b 958.1 106.2 213.3 213.3 106.2
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 paired 0.0 ring a 745.0 266.9 348.0 348.0 266.9
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 paired 0.0 ring b 544.6 66.5 147.6 147.6 66.5
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 paired 0.2 centroid a 1121.7 306.8 526.8 526.8 306.8
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 paired 0.2 centroid b 921.3 106.4 326.5 326.5 106.4
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 paired 0.2 ring a 749.8 274.0 386.2 386.2 274.0
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 paired 0.2 ring b 549.5 73.7 185.8 185.8 73.7
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 paired 0.0 centroid a 930.5 311.2 423.4 423.4 311.2
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 paired 0.0 centroid b 708.2 88.9 201.1 201.1 88.9
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 paired 0.0 ring a 611.7 278.3 328.0 328.0 278.3
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 paired 0.0 ring b 389.4 56.0 105.7 105.7 56.0
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 paired 0.2 centroid a 845.9 305.9 396.2 396.2 305.9
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 paired 0.2 centroid b 623.6 83.7 173.9 173.9 83.7
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 paired 0.2 ring a 726.2 282.7 361.7 361.7 282.7
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 paired 0.2 ring b 503.9 60.4 139.4 139.4 60.4
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 paired 0.0 centroid a 1235.7 292.3 518.1 518.1 292.3
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 paired 0.0 centroid b 1024.9 81.5 307.3 307.3 81.5
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 paired 0.0 ring a 843.1 262.2 368.2 368.2 262.2
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 paired 0.0 ring b 632.3 51.3 157.4 157.4 51.3
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 paired 0.2 centroid a 1367.2 261.1 549.0 549.0 261.1
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 paired 0.2 centroid b 1156.4 50.3 338.2 338.2 50.3
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 paired 0.2 ring a 923.8 266.8 392.8 392.8 266.8
600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 paired 0.2 ring b 713.0 56.0 182.0 182.0 56.0
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 single 0.0 centroid a 720.2 1144.9 1239.6 1239.6 1144.9
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 single 0.0 centroid b 508.4 933.1 1027.8 1027.8 933.1
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 single 0.0 ring a 620.6 865.9 831.8 865.9 831.8
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 single 0.0 ring b 408.8 654.1 620.0 654.1 620.0
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 single 0.2 centroid a 712.2 1301.3 1040.5 1301.3 1040.5
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 single 0.2 centroid b 500.4 1089.5 828.7 1089.5 828.7
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 single 0.2 ring a 566.2 929.2 813.6 929.2 813.6
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 single 0.2 ring b 354.4 717.4 601.8 717.4 601.8
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 single 0.0 centroid a 488.8 1575.5 881.8 1575.5 881.8
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 single 0.0 centroid b 254.0 1340.6 647.0 1340.6 647.0
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 single 0.0 ring a 493.0 1138.0 633.3 1138.0 633.3
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 single 0.0 ring b 258.2 903.2 398.5 903.2 398.5
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 single 0.2 centroid a 430.7 1863.6 719.7 1863.6 719.7
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 single 0.2 centroid b 195.9 1628.8 484.9 1628.8 484.9
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 single 0.2 ring a 483.5 1336.1 577.1 1336.1 577.1
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 single 0.2 ring b 248.7 1101.3 342.2 1101.3 342.2
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 single 0.0 centroid a 465.2 1178.3 937.5 1178.3 937.5
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 single 0.0 centroid b 220.4 933.4 692.7 933.4 692.7
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 single 0.0 ring a 543.5 872.6 629.5 872.6 629.5
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 single 0.0 ring b 298.7 627.8 384.7 627.8 384.7
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 single 0.2 centroid a 629.1 1502.0 933.2 1502.0 933.2
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 single 0.2 centroid b 384.3 1257.1 688.3 1257.1 688.3
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 single 0.2 ring a 576.6 996.8 649.4 996.8 649.4
600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 single 0.2 ring b 331.8 752.0 404.5 752.0 404.5
Mean over dishes:
apex base1 base2 base_hi base_lo
train gap_mm side bg
paired 0.0 centroid a 1108.2 303.3 451.7 451.7 303.3
b 897.1 92.2 240.6 240.6 92.2
ring a 733.2 269.1 348.0 348.0 269.1
b 522.1 58.0 136.9 136.9 58.0
0.2 centroid a 1111.6 291.3 490.7 490.7 291.3
b 900.4 80.1 279.5 279.5 80.1
ring a 799.9 274.5 380.2 380.2 274.5
b 588.8 63.4 169.1 169.1 63.4
single 0.0 centroid a 558.1 1299.5 1019.7 1331.1 988.1
b 327.6 1069.1 789.2 1100.6 757.6
ring a 552.4 958.8 698.2 958.8 698.2
b 321.9 728.4 467.7 728.4 467.7
0.2 centroid a 590.7 1555.6 897.8 1555.6 897.8
b 360.2 1325.1 667.3 1325.1 667.3
ring a 542.1 1087.4 680.0 1087.4 680.0
b 311.6 856.9 449.5 856.9 449.5
Per reading (means over 3 paired and 3 single dishes):
gap_mm side bg paired apex/base (each dish, min) paired dishes apex > both bases single energised/apex (each dish, min) single dishes energised > apex single other base/apex (mean) paired_ok single_ok both
0.0 centroid a 2.20 3 1.72 3 1.77 True True True
0.0 centroid b 3.34 3 2.02 3 2.31 True True True
0.0 ring a 1.86 3 1.40 3 1.26 True True True
0.0 ring b 3.68 3 1.60 3 1.45 True True True
0.2 centroid a 2.13 3 1.83 3 1.52 True True True
0.2 centroid b 2.82 3 2.18 3 1.85 True True True
0.2 ring a 1.94 3 1.64 3 1.25 True True True
0.2 ring b 2.96 3 2.02 3 1.44 True True True
All readings agree: True ; outcome under every reading: True
Background (b), which decides: criterion met under every reading: True ; variant, background (a): True
# Match to the field map: Spearman correlation over a grid of 200 um ROIs within 4 mm of the
# triangle's centroid, outside the footprints, background b. Single dishes: field of the energised
# base (the base with larger adjacent uptake); paired dishes: the same field of one base, shown as a contrast.
rows = []
for i in keep:
if info.freq[i] != "833 kHz":
continue
g = geoms[i]; yp = dishes[i]["img"][1]
pts = swing.tile_rois(g)
up = np.array([swing.square_mean(yp, p, np.array([1.0, 0.0])) for p in pts]) - swing.footprint_background(yp, g)
sub = t4[(t4.subject == info.subject[i]) & (t4.gap_mm == 0.0) & (t4.side == "centroid") & (t4.bg == "b")].iloc[0]
energised = "base1" if sub.base1 >= sub.base2 else "base2"
for model in ("B", "C"):
E = swing.field_at(g, pts, energised, model, grids[i])
rho, p = stats.spearmanr(up, E)
rows.append({"subject": info.subject[i], "train": info.train[i], "energised (taken)": energised, "model": model,
"n_rois": len(pts), "spearman_rho": rho, "p": p})
print(pd.DataFrame(rows).round(4).to_string(index=False))
subject train energised (taken) model n_rois spearman_rho p 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 paired base2 B 1071 -0.1664 0.0000 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-13 paired base2 C 1071 -0.1669 0.0000 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 paired base2 B 1066 -0.2472 0.0000 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-15 paired base2 C 1066 -0.2588 0.0000 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 paired base2 B 1071 -0.1040 0.0007 600ns-4kV-0,8MHz-BP-8-24-21-BPAE-20 paired base2 C 1071 -0.1169 0.0001 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 single base2 B 1075 0.5008 0.0000 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-05 single base2 C 1075 0.5255 0.0000 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 single base1 B 1074 0.9606 0.0000 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-06 single base1 C 1074 0.9584 0.0000 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 single base1 B 1076 0.9315 0.0000 600ns-4kV-0,8MHz-UP-10-5-21-BPAE-10 single base1 C 1076 0.9321 0.0000
Result: reproduced. In each of the three 833 kHz paired dishes, uptake next to the apex exceeds uptake next to both base electrodes under all eight readings (ROI touching the footprint or 200 um out, on the line to the centroid or around the electrode, background b, which decides, or the variant a); the smallest per-dish ratio of apex to the larger base is 1.86 to 3.68 across readings. In each of the three single-pulse dishes, uptake next to the base taken as energised exceeds uptake next to the apex (smallest per-dish ratio 1.40 to 2.18); the other base also exceeds the apex on average (1.25 to 2.31). Over the dish, uptake in the single-pulse dishes follows the field strength of the energised base (Spearman rho 0.50 to 0.96, both field models), while in the paired dishes it does not (rho -0.10 to -0.26). Which base was energised is taken from the uptake, as the files do not say.
Summary¶
| Claim | Outcome | Rests on an assumption |
|---|---|---|
| fig2-180-cancellation | partially reproduced | pixel size, ROI layout |
| fig2-crossover-angle | reproduced | pixel size |
| fig2-facilitation-plateau | partially reproduced | pixel size, ROI layout |
| fig2-frequency-dependence | reproduced | pixel size |
| fig2-60-fold-co-counter | reproduced | pixel size, ROI layout |
| fig3-1hz-co-equals-cross | blocked (data_missing) | |
| fig3-770khz-co-enhanced | blocked (data_missing) | |
| fig3-770khz-cross-same-as-1hz | blocked (data_missing) | |
| fig3-no-area-dependence | blocked (data_missing) | |
| fig4-unipolar-pattern | reproduced | pixel size |
| fig5-base-sawtooth | blocked (data_missing) | |
| fig5-apex-larger-polarization | blocked (data_missing) |
Four Figure 2 outcomes rest on fragile values, shown in the checks under each claim: the 1 Hz crossover angle rests on one ROI; the 833 kHz 20-fold value and the 60-fold ratio rest on the lowest of 32 ROIs, a minimum biased toward a larger fold (over the lowest ROI and its two neighbours the ratio is 45.7 to 52.6 fold, outside 60 +- 6); the 60-fold ratio is not met when the background is taken from the needle lumen alone; and the 833 kHz half of fig2-facilitation-plateau holds only with the 0.5 mm and 0.6 mm footprint disks, not with the lumen or the wall ring alone. For fig2-crossover-angle and fig2-frequency-dependence the two ROI layouts give the same outcome, so those two do not rest on the layout.
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 paper measured 32 ROIs in MetaMorph; the files hold no ROIs, so they are placed here from the electrode footprints found in each image. Their layout along the dashed line of Figure 7B is not given, and the claims file's standard layout (32 contiguous ROIs from the base midpoint toward the apex, outside the footprints) does not fit, a deviation from the claims file. Two layouts were run. L2 (32 contiguous ROIs from the apex footprint back past the base midpoint) decides and L1 is a variant, as an assumption: L2's first ROI lies at the point where the angle is 180 degrees, so its angles run from 180 degrees at the base to near 0 at the apex, as Results 2.1 and Figure 1 describe, and it keeps the ROIs at 200 um side by side; L1 reaches only 168 to 170 degrees, and its 32 ROIs overlap. The layouts give different outcomes for fig2-60-fold-co-counter only.
- The angle of each ROI is computed from two field models (claims file readings B and C), as the paper gives neither the angles nor how they were obtained. Both give the same outcomes.
- The pixel size is taken as 0.65 um, from the camera and objective of Methods 4.4, and is consistent with the needle footprints, the base-to-apex distance and the field strength the paper gives; the files do not state it.
- Controls: all five unique 1 Hz single-pulse dishes (from two days) and the three 833 kHz ones; the paper does not say how many control dishes were used or how they were paired with the paired dishes. Subsets are shown as variants; the 1 Hz crossover moves with them.
- Spontaneous uptake is corrected by subtracting the mean YP in the electrode footprints of the same dish (background reading b). The paper says the footprints were measured "to account for" it (Methods 4.4) but not how. A reader, asked whether the uncorrected reading (a) is one the paper allows, answered that it is not, and the claims file now decides every claim under (b). Reading (a) is still shown as a variant: the ROI means include about 200 to 270 counts of camera offset and spontaneous uptake (the footprint values), so it cannot give a 20-fold cancellation. The answer changed one outcome, fig2-60-fold-co-counter, from blocked to reproduced. The footprint value is the mean over a disk of 0.5 mm radius at each electrode centre, the flat part of the dark footprint, covering the needle's lumen (0.25 mm radius) and the inner part of its wall. The lumen alone, the wall ring alone and a 0.6 mm disk give the same outcome labels except for fig2-60-fold-co-counter, which the lumen alone does not meet: the lumen of the three 833 kHz controls holds bright material. They do change the basis of one other label: the 833 kHz half of fig2-facilitation-plateau, the half that makes it partial, fails with the lumen alone and with the wall ring alone.
- The 833 kHz paired dishes (8-24-21) and controls (10-5-21) were imaged on different days with different MemBrite levels (about 295 against 431 counts away from the array); the files give no exposure or gain, so whether their YP values compare cannot be checked.
What was found about the dataset.
- BPAE-03 and BPAE-18 of 7-31-21 (1 Hz, single pulses) are the same recording deposited twice under two subjects (identical image chunks and start time).
- BPAE-13 of 8-24-21, filed as an 833 kHz paired-pulse dish, carries the session description and id of a different experiment (bipolar pulses between two parallel electrodes); its image shows the triangular array and the paired-pulse pattern.
- The files hold no pixel size (resolution -1, unit "n.a."), no exposure or gain, no electrode positions, no ROIs, no angles and no measured values; which base electrode was energised in the single-pulse dishes is not stated.
- The ImageSeries description ("144 images captured in a 12x12 grid from a six-well plate") does not fit the 35 mm dishes and 216 images of the paper.
- The data of Figures 3 and 5 are not in the dandiset.
print(f"notebook ran in {(time.time() - started) / 60:.1f} min")
notebook ran in 4.8 min