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.

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

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

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

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

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

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

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

In [8]:
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)")
No description has been provided for this image
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)
In [9]:
# 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.
In [10]:
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)
In [11]:
# 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.

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

In [14]:
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'}
In [15]:
# 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
In [16]:
# (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.

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

In [19]:
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'}}
In [20]:
# 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.

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

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

In [24]:
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
In [25]:
# 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.
In [26]:
print(f"notebook ran in {(time.time() - started) / 60:.1f} min")
notebook ran in 4.8 min