Reproducing Semenov et al. (2026) from dandiset 001521¶

Paper: Semenov I, Silkuniene G, Silkunas M, Pakhomova ON, Bixler A, Ibey BL, Kochunov P, Pakhomov AG (2026). Sub-microsecond optical measurements of cell membrane charging and lesioning by pulsed electric fields. Bioelectrochemistry 171, 109283. doi:10.1016/j.bioelechem.2026.109283. The claims file was written from the version of record (main text with Materials and methods, figure legends and Data availability; the paper has no supplement and links no code).

Dandiset: DANDI:001521, version 0.261006.1845. The paper cites no version of the dandiset (its Data availability says data are available on request). This is the only published version, so it is pinned; the draft lists the same 73 assets with the same total size.

What the dandiset holds: 73 widefield Fluo-4 time-lapses of CHO cells (one NWB file each, no ROIs or traces), in two sessions. The 2025-02-03 session (31 files) is the single 1-us pulse experiment of Fig. 6A, and the 2025-05-02 session (42 files) is the 60-pulse 6 Hz train experiment of Fig. 6B. The pulsed-laser potentiometric imaging behind Figs. 1 to 5 and 7 is not in it. Six other dandisets of the same lab name the same paper (001515, 001516, 001517, 001518, 001525 and 001526); by their titles they hold the potentiometric experiments, and they are targets of their own.

Each file gives the pulse as a generator voltage, while the paper gives only field strengths. Two readings of the conversion are run throughout, and the one that fits the fields and group sizes of Fig. 6B is adopted (see "Field strengths"). The cell regions are found here by a standard segmentation, as the paper drew them by hand in MetaMorph (see fluo4.py).

No random numbers are drawn anywhere in this notebook.

Claim Status
fig6a-ca-entry-1000-1200 tested
fig6a-ca-sustained tested
fig6a-780-not-significant tested
fig6b-train-threshold tested
fig6b-650-weak-but-significant tested
fig5b-conductivity-calibration tested (a computation from Eq. 2, no recordings)
fig1-pole-leakage-pattern, fig2a-fluovolt-saturation, fig2a-fluovolt-stable-within-set, fig2b-di8-decline-within-set, fig2cd-stable-then-drop, fig2c-1000-within-set-trend, fig3-dye-calibration, fig3-fluovolt-nonlinear, fig4a-fluovolt-maximum, fig4b-di8-maximum, fig4b-first-10-pulses, fig4b-cathodic-asymmetry, fig4c-equator-cosine, fig4d-short-pulses-same-limit, fig5a-gd-extends-anodic, fig5a-gd-cathodic-not-higher, fig5c-gd-reduces-conductivity, fig7a-test-pulse-cosine, fig7b-conditioning-sublinear, fig7b-test-pulse-recovery, fig7c-fast-resealing blocked: data missing (potentiometric imaging not in this dandiset)
In [1]:
import time

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy import stats

import fluo4

pd.set_option("display.width", 200)
pd.set_option("display.max_rows", 200)
print("dandiset", fluo4.DANDISET_ID, "version", fluo4.DANDISET_VERSION)
dandiset 001521 version 0.261006.1845
In [2]:
t0 = time.time()
files = fluo4.list_files()
res = fluo4.process_all(files.url.tolist())
print(f"{len(res)} files streamed and reduced to cell traces in {time.time() - t0:.0f} s")
73 files streamed and reduced to cell traces in 273 s

Data check¶

Every file is streamed once and reduced, as it loads, to one background-corrected trace per cell (about four minutes). Each check of the metadata against the values is stated under its output.

In [3]:
info = pd.DataFrame([{
    "path": p, "set": r["set"], "session_id": r["session_id"], "subject_id": r["subject_id"],
    "session_start": r["session_start"], "volts": r["volts"], "n_pulses": r["n_pulses"],
    "gap_mm": r["gap_mm"], "rate_hz": r["rate_hz"], "frames": r["shape"][0], "height": r["shape"][1],
    "width": r["shape"][2], "duration_s": r["shape"][0] / r["rate_hz"], "pulse_frame": r["pulse_frame"],
    "unit": r["unit"], "dtype": r["dtype"], "raw_min": r["raw_min"], "raw_max": r["raw_max"],
    "cells": r["dF"].shape[0]} for p, r in zip(files.path, res)])
info["fov"] = info.subject_id.str.replace("CHO_", "")
print("Files by set (session) and voltage:")
print(pd.crosstab(info.set, info.volts, margins=True).to_string())
print("\nFiles by set and pulses per file (0 = sham):")
print(pd.crosstab(info.set, info.n_pulses, margins=True).to_string())
print("\nSubject age and species as the files give them:",
      sorted({(r["subject_age"], r["subject_species"]) for r in res}))
print("\nPer set: frames, rate, duration, pulse frame, image size, electrode gap, unit, dtype, raw range")
print(info.groupby("set").agg(files=("path", "size"), subjects=("subject_id", "nunique"),
                              frames=("frames", "unique"), rate_hz=("rate_hz", "unique"),
                              duration_s=("duration_s", "unique"), pulse_frame=("pulse_frame", "unique"),
                              height=("height", "unique"), width=("width", "unique"),
                              gap_mm=("gap_mm", "unique"), unit=("unit", "unique"), dtype=("dtype", "unique"),
                              raw_min=("raw_min", "min"), raw_max=("raw_max", "max")).T.to_string())
print("\nGroups and objects held by the files (union over files):")
for key in ["groups", "acquisition", "processing", "stimulus", "analysis"]:
    print(f"  {key}:", sorted({x for r in res for x in r[key]}))
print("\nImage series descriptions:")
for s in sorted({r["series_description"] for r in res}):
    print("  -", s)
print("\nExample session_description (first file of each set):")
for s in ["single", "train"]:
    print(f"  [{s}]", next(r["description"] for r in res if r["set"] == s)[:400], "...")
Files by set (session) and voltage:
volts   0.0  70.0  100.0  150.0  170.0  180.0  190.0  200.0  210.0  220.0  230.0  250.0  300.0  350.0  400.0  All
set                                                                                                              
single    6     0      0      4      0      0      0      5      0      0      0      4      5      5      2   31
train     9     3      1      4      4      1      1      7      1      1      5      4      1      0      0   42
All      15     3      1      8      4      1      1     12      1      1      5      8      6      5      2   73

Files by set and pulses per file (0 = sham):
n_pulses   0   1  60  All
set                      
single     6  25   0   31
train      9   0  33   42
All       15  25  33   73

Subject age and species as the files give them: [('P1D', 'http://purl.obolibrary.org/obo/NCBITaxon_10029')]

Per set: frames, rate, duration, pulse frame, image size, electrode gap, unit, dtype, raw range
set            single     train
files              31        42
subjects            6         9
frames          [300]      [90]
rate_hz         [1.0]     [0.5]
duration_s    [300.0]   [180.0]
pulse_frame      [60]      [15]
height          [512]     [512]
width           [512]     [512]
gap_mm          [1.0]     [1.0]
unit           [n.a.]    [n.a.]
dtype        [uint16]  [uint16]
raw_min           119       124
raw_max         52927      6741

Groups and objects held by the files (union over files):
  groups: ['acquisition', 'analysis', 'file_create_date', 'general', 'identifier', 'processing', 'session_description', 'session_start_time', 'specifications', 'stimulus', 'timestamps_reference_time']
  acquisition: ['TimeLapseImaging']
  processing: []
  stimulus: []
  analysis: []

Image series descriptions:
  - Time-lapse recordings of Ca2+ uptake were performed at 0.5 Hz rate for 3 minutes. Recording started 30 s before pulses were applied.Cells were pre-loaded with a calcium-sensitive dye (2.5uM Fluo-4Ca2+) for 30 min.
  - Time-lapse recordings of Ca2+ uptake were performed at 1 Hz rate for 5 minutes. Recording started 1 min before pulse was applied.Cells were pre-loaded with a calcium-sensitive dye (2.5uM Fluo-4Ca2+) for 30 min.

Example session_description (first file of each set):
  [single] Subject ID: CHO_20250203_c1. Stimulation was applied with a sham recording where no pulse was applied (0 V). Distance between electrodes 1.00 mm, electrode diameter 0.5 mm. Ca2+ uptake in CHO cells was detected using Fluo-4 Ca2+ sensitive dye (cells were loaded with 2.5µM Fluo-4 for 30 min). Recordings were done using FITC filter cube and CoolLED illumination system. ####General experiment descrip ...
  [train] Subject ID: CHO_20250502_c1. Stimulation was applied with a sham recording where no pulse was applied (0 V). Distance between electrodes 1.00 mm, electrode diameter 0.5 mm. Ca2+ uptake in CHO cells was detected using Fluo-4 Ca2+ sensitive dye (cells were loaded with 2.5µM Fluo-4 for 30 min). Recordings were done using FITC filter cube and CoolLED illumination system. ####General experiment descrip ...

The files hold one image series each and nothing else (no ROIs, traces, stimulus series or tables). The image unit is "n.a."; the values are raw camera counts (minimum 119, above the camera offset). The rates, frame counts and durations match the series descriptions and Methods 2.4: single pulses at 1 frame/s for 300 s with the pulse at 60 s; trains at 0.5 frame/s for 180 s with the train from 30 s (Methods: "a burst of 60 pulses at 6 Hz began at 30 s ... and recording continued for 150 s", 30 + 150 = 180 s). Every file states an electrode gap of 1.00 mm; the paper's Methods 2.2 give a 1.6 mm spacing. The dandiset's title and description mention only single pulses (next cell), but 33 of the 73 files are trains of 60 pulses; the other 9 files of the train session are shams. Every subject has an age of P1D, though subjects are fields of view of a cell line, and the species NCBITaxon 10029, which the archive's summary names "Cricetulus griseus - Cricetulus aureus", two species for one taxon (next cell).

The archive's metadata, for this dandiset and for the six other dandisets of the lab that name the same paper (each lists the paper's DOI as IsPublishedIn). By their titles the six hold the membrane polarization and recovery experiments; they are targets of their own and are not used here.

In [4]:
# Archive metadata (DANDI API) at each dandiset's most recent published version.
from dandi.dandiapi import DandiAPIClient
client = DandiAPIClient()
meta = client.get_dandiset(fluo4.DANDISET_ID, fluo4.DANDISET_VERSION).get_raw_metadata()
print("001521 description:", meta["description"][:420], "...")
print("001521: species", [(s["name"], s["identifier"]) for s in meta["assetsSummary"].get("species", [])])
for d in ["001521", "001515", "001516", "001517", "001518", "001525", "001526"]:
    ds = client.get_dandiset(d)
    m = ds.get_raw_metadata()
    print(f"{d} {ds.version_id}: {m['name']}")
    print("    related:", [(r.get("relation"), r.get("identifier")) for r in m.get("relatedResource", [])])
001521 description: This experiment aimed to determine the threshold for calcium uptake in CHO cells subjected to 1-µs electric pulses. Cells were loaded with 2.5 µM Fluo-4, a Ca2+-sensitive dye, for 30 minutes in the dark. Fluo-4 fluorescence was excited using a FITC cube and a CoolLED illumination system. Time-lapse imaging was conducted at 1 frame per second for 5 minutes, with a single 1-µs pulse of varying amplitude applied 1 minut ...
001521: species [('Cricetulus griseus - Cricetulus aureus', 'http://purl.obolibrary.org/obo/NCBITaxon_10029')]
001521 0.261006.1845: Threshold Characterization of Ca2+ Uptake in CHO Cells Following 1-µs Electroporation Pulses
    related: [('dcite:IsPublishedIn', '10.1016/j.bioelechem.2026.109283')]
001515 0.261006.1841: Plasma Membrane Polarization Dynamics of CHO Cells During 1 us Pulses: Effects of Cell Size and Electric Field Strength
    related: [('dcite:IsPublishedIn', '10.1016/j.bioelechem.2026.109283')]
001516 0.261006.1833: Plasma Membrane Recovery of Cells Following 1 us Electroporation Pulse
    related: [('dcite:IsPublishedIn', '10.1016/j.bioelechem.2026.109283')]
001517 0.261006.1856: Evaluation of Di-8-ANEPPS Compatibility with Strobe Fluorescence Microscopy for Membrane Voltage Imaging
    related: [('dcite:IsPublishedIn', '10.1016/j.bioelechem.2026.109283')]
001518 0.261006.1957: Comparison of FluoVolt and Di-8-ANEPPS Responses to Plasma Membrane Polarization at Varying Electric Field Strengths
    related: [('dcite:IsPublishedIn', '10.1016/j.bioelechem.2026.109283')]
001525 0.261006.1856: Characterization of Plasma Membrane Recovery in Cells After 1-µs Electroporation Using Strobe Fluorescence Microscopy
    related: [('dcite:IsPublishedIn', '10.1016/j.bioelechem.2026.109283')]
001526 0.261006.1851: Assessment of Post-Electroporation Suitability of CHO Cells for Polarization Studies Using Strobe Fluorescence Microscopy
    related: [('dcite:IsPublishedIn', '10.1016/j.bioelechem.2026.109283')]
In [5]:
# Is a "subject" one cell, one dish or one field of view? Correlate the pre-pulse mean images.
Z = np.array([(r["pre_small"] - r["pre_small"].mean()).ravel() for r in res])
Z /= np.linalg.norm(Z, axis=1, keepdims=True)
C = Z @ Z.T
subj = info.subject_id.to_numpy(dtype=str)
same = subj[:, None] == subj[None, :]
off = ~np.eye(len(res), dtype=bool)
print(f"Correlation of pre-pulse images, files of the same subject: min {C[same & off].min():.3f}, "
      f"median {np.median(C[same & off]):.3f} ({(same & off).sum() // 2} pairs)")
print(f"Correlation of pre-pulse images, files of different subjects: max {C[~same].max():.3f}, "
      f"median {np.median(C[~same]):.3f}")
dups = [(info.path[i], info.path[j]) for i in range(len(res)) for j in range(i + 1, len(res))
        if np.array_equal(res[i]["pre_small"], res[j]["pre_small"])]
print("Pairs of files with identical pre-pulse images:", dups if dups else "none")
order = info.sort_values("session_start")
print("\nRecording order within each subject (voltages in order of session start):")
for s, g in order.groupby("subject_id", sort=False):
    print(f"  {s}: " + ", ".join(f"{v:.0f} V" for v in g.volts) + f"   cells per file {g.cells.tolist()}")
Correlation of pre-pulse images, files of the same subject: min 0.859, median 0.982 (169 pairs)
Correlation of pre-pulse images, files of different subjects: max 0.426, median 0.154
Pairs of files with identical pre-pulse images: none

Recording order within each subject (voltages in order of session start):
  CHO_20250203_c1: 0 V, 200 V, 300 V   cells per file [5, 5, 5]
  CHO_20250203_c2: 0 V, 150 V, 200 V, 250 V, 300 V, 350 V   cells per file [4, 4, 4, 4, 4, 4]
  CHO_20250203_c3: 0 V, 150 V, 200 V, 250 V, 300 V, 350 V   cells per file [4, 4, 4, 3, 3, 3]
  CHO_20250203_c4: 0 V, 150 V, 200 V, 250 V, 300 V, 350 V   cells per file [7, 7, 7, 7, 7, 7]
  CHO_20250203_c5: 0 V, 150 V, 200 V, 250 V, 300 V, 350 V, 400 V   cells per file [4, 4, 4, 4, 4, 4, 4]
  CHO_20250203_c6: 0 V, 350 V, 400 V   cells per file [3, 3, 3]
  CHO_20250502_c1: 0 V, 300 V   cells per file [6, 6]
  CHO_20250502_c2: 0 V, 70 V, 150 V, 200 V, 250 V   cells per file [12, 12, 13, 12, 12]
  CHO_20250502_c3: 0 V, 70 V, 150 V, 200 V, 250 V   cells per file [8, 8, 8, 8, 8]
  CHO_20250502_c4: 0 V, 250 V   cells per file [10, 11]
  CHO_20250502_c5: 0 V, 70 V, 100 V, 150 V, 170 V, 200 V, 230 V, 250 V   cells per file [8, 8, 8, 8, 8, 8, 8, 8]
  CHO_20250502_c6: 0 V, 150 V, 170 V, 200 V, 230 V   cells per file [9, 9, 9, 10, 9]
  CHO_20250502_c7: 0 V, 170 V, 200 V, 230 V   cells per file [9, 9, 9, 9]
  CHO_20250502_c8: 0 V, 170 V, 180 V, 190 V, 200 V, 210 V, 220 V, 230 V   cells per file [9, 10, 10, 10, 10, 11, 10, 10]
  CHO_20250502_c9: 0 V, 200 V, 230 V   cells per file [11, 11, 11]

A subject is a field of view: the files of one subject show the same image (correlation 0.86 or more) and different subjects differ (0.43 at most). No file is a copy of another. Each field of view was recorded first without a pulse and then at escalating voltages, so the same cells were exposed up to seven times. For the trains this disagrees with Section 3.6 ("Each cell was tested only once"); for single pulses it agrees with the Results' "escalating field strengths". Cells are treated as independent across files of one field of view, as each recording has its own F0; results are broken down by field of view.

In [6]:
# Cells detected, and what was dropped.
seg = pd.concat([r["cells"].assign(file=i, set=r["set"]) for i, r in enumerate(res)])
print("Objects found by the segmentation:", len(seg))
print("  dropped as smaller than", fluo4.MIN_AREA, "px:", int((seg.area < fluo4.MIN_AREA).sum()))
print("  dropped as touching the border:", int((seg.border & (seg.area >= fluo4.MIN_AREA)).sum()))
print("  kept:", int(seg.kept.sum()))
k = seg[seg.kept]
print("Kept cells, equivalent radius (px): median %.1f, range %.1f to %.1f" % (
    k.radius_px.median(), k.radius_px.min(), k.radius_px.max()))
print("Fraction of each image used as cell-free background: %.2f to %.2f" % (
    min(r["bg_fraction"] for r in res), max(r["bg_fraction"] for r in res)))
print("\nCell exposures by set and voltage (one per cell per file):")
print(info.pivot_table(index="set", columns="volts", values="cells", aggfunc="sum", margins=True).to_string())
first = order.groupby("subject_id").first()
print("\nCells per field of view, counted in its first (sham) recording:")
print(first.groupby("set").cells.agg(["sum", "min", "max", "size"]).to_string())
Objects found by the segmentation: 608
  dropped as smaller than 400 px: 69
  dropped as touching the border: 7
  kept: 532
Kept cells, equivalent radius (px): median 29.7, range 12.2 to 46.4
Fraction of each image used as cell-free background: 0.74 to 0.94

Cell exposures by set and voltage (one per cell per file):
volts     0.0  70.0  100.0  150.0  170.0  180.0  190.0  200.0  210.0  220.0  230.0  250.0  300.0  350.0  400.0  All
set                                                                                                                
single   27.0   NaN    NaN   19.0    NaN    NaN    NaN   24.0    NaN    NaN    NaN   18.0   23.0   21.0    7.0  139
train    82.0  28.0    8.0   38.0   36.0   10.0   10.0   68.0   11.0   10.0   47.0   39.0    6.0    NaN    NaN  393
All     109.0  28.0    8.0   57.0   36.0   10.0   10.0   92.0   11.0   10.0   47.0   57.0   29.0   21.0    7.0  532

Cells per field of view, counted in its first (sham) recording:
        sum  min  max  size
set                        
single   27    3    7     6
train    82    6   12     9

Of 608 objects, 76 were dropped (69 too small, 7 on the border). The single-pulse session holds 27 distinct cells in 6 fields of view (139 cell recordings); the paper averaged "across 14 cells" (Fig. 6A legend), which does not match either count, and the dataset does not say which 14. The train session holds 82 distinct cells in 9 fields of view (311 cell exposures to trains); the paper reports 224 cells, each tested once.

In [7]:
cells = pd.concat([pd.DataFrame({
    "file": i, "set": r["set"], "fov": info.fov[i], "volts": r["volts"], "cell": np.arange(len(r["F0"])),
    "F0": r["F0"], "end": fluo4.end_value(r["dF"]), "peak": fluo4.peak_value(r["dF"], r["pulse_frame"])})
    for i, r in enumerate(res)], ignore_index=True)
print("F0 (background-corrected camera counts) by set: median %s, range %s" % (
    cells.groupby("set").F0.median().round(1).to_dict(),
    cells.groupby("set").F0.agg(["min", "max"]).round(1).to_dict("index")))
print("Background (cell-free median, counts), range over files and frames:",
      {s: (round(min(r["bg"].min() for r in res if r["set"] == s), 1),
           round(max(r["bg"].max() for r in res if r["set"] == s), 1)) for s in ["single", "train"]})
print("dF range over all cells and frames (%):", round(min(r["dF"].min() for r in res), 1),
      "to", round(max(r["dF"].max() for r in res), 1))
for s in ["single", "train"]:
    sham = np.concatenate([r["dF"] for r in res if r["set"] == s and r["volts"] == 0])
    pf = next(r["pulse_frame"] for r in res if r["set"] == s)
    print(f"Sham ({s}), {len(sham)} cells: mean dF at frame 0 {sham[:, 0].mean():.1f}%, "
          f"at the pulse frame {sham[:, pf].mean():.1f}%, over the last {fluo4.END_FRAMES} frames "
          f"{sham[:, -fluo4.END_FRAMES:].mean():.1f}%")
F0 (background-corrected camera counts) by set: median {'single': 504.70001220703125, 'train': 128.10000610351562}, range {'single': {'min': 178.1999969482422, 'max': 1165.5}, 'train': {'min': 39.70000076293945, 'max': 623.2000122070312}}
Background (cell-free median, counts), range over files and frames: {'single': (np.float32(242.0), np.float32(265.0)), 'train': (np.float32(226.0), np.float32(260.0))}
dF range over all cells and frames (%): -39.1 to 872.1
Sham (single), 27 cells: mean dF at frame 0 3.5%, at the pulse frame -1.6%, over the last 10 frames -12.0%
Sham (train), 82 cells: mean dF at frame 0 3.0%, at the pulse frame -2.9%, over the last 10 frames -15.7%

Without a pulse, dF falls steadily through the recording, to -12% (single-pulse session) and -16% (train session) over the last 10 frames, with F0 taken before the pulse as Methods 2.4 say. The next cell shows where the decline comes from.

In [8]:
# Where does the decline of unpulsed cells come from? Per set and voltage: the end value (last 10
# frames) relative to the pre-pulse mean of the background-corrected cell signal (dF), of the raw
# cell signal without background subtraction, of the cell-free background, and of the whole frame.
def rel_end(x, pf):
    x = np.asarray(x, float)
    return 100 * (x[..., -fluo4.END_FRAMES:].mean(-1) / x[..., :pf].mean(-1) - 1)

rows = []
for s in ["single", "train"]:
    for v in sorted(info.volts[info.set == s].unique()):
        rs = [r for r in res if r["set"] == s and r["volts"] == v]
        pf = rs[0]["pulse_frame"]
        Fc = [r["F0"][:, None] * (1 + r["dF"] / 100) for r in rs]          # background-corrected F
        raw = [F + r["bg"][None] for F, r in zip(Fc, rs)]                   # F before background subtraction
        rows.append({"set": s, "volts": v, "files": len(rs), "cells": sum(len(F) for F in Fc),
                     "dF_end": np.concatenate([rel_end(F, pf) for F in Fc]).mean(),
                     "raw_cell_end": np.concatenate([rel_end(F, pf) for F in raw]).mean(),
                     "background_end": np.mean([rel_end(r["bg"], pf) for r in rs]),
                     "whole_frame_end": np.mean([rel_end(r["frame_mean"], pf) for r in rs]),
                     "background_over_F0": np.mean([r["bg"][:pf].mean() / r["F0"].mean() for r in rs])})
print("End value relative to the pre-pulse mean (%), by set and voltage:")
print(pd.DataFrame(rows).round(2).to_string(index=False))

# Decline before the pulse (no pulse has yet been given in that recording), by recording order
# within the field of view: slope of each cell's dF over the pre-pulse frames, in % per minute.
rec = order.assign(order_in_fov=order.groupby("subject_id").cumcount() + 1)
pre = []
for i, r in enumerate(res):
    pf, t_min = r["pulse_frame"], np.arange(r["pulse_frame"]) / r["rate_hz"] / 60
    for c in range(len(r["dF"])):
        pre.append({"set": r["set"], "volts": r["volts"], "order_in_fov": int(rec.order_in_fov[i]),
                    "slope_pct_per_min": np.polyfit(t_min, r["dF"][c, :pf], 1)[0]})
pre = pd.DataFrame(pre)
print("\nPre-pulse slope of dF (% per min), by recording order within the field of view (1 = the sham):")
print(pre.groupby(["set", "order_in_fov"]).slope_pct_per_min.agg(["size", "mean", "sem"]).round(2).unstack(0).to_string())

fig, axes = plt.subplots(1, 2, figsize=(10, 3))
for ax, s in zip(axes, ["single", "train"]):
    rs = [r for r in res if r["set"] == s and r["volts"] == 0]
    pf, t = rs[0]["pulse_frame"], np.arange(rs[0]["shape"][0]) / rs[0]["rate_hz"]
    Fc = np.concatenate([r["F0"][:, None] * (1 + r["dF"] / 100) for r in rs])
    raw = np.concatenate([r["F0"][:, None] * (1 + r["dF"] / 100) + r["bg"][None] for r in rs])
    bg = np.array([r["bg"] for r in rs])
    for y, lab in [(Fc, "dF, background-corrected"), (raw, "raw cell signal"), (bg, "background")]:
        ax.plot(t, 100 * (y.mean(0) / y[:, :pf].mean() - 1), label=lab)
    ax.axhline(0, color="k", lw=0.5, ls=":")
    ax.set_title(f"sham, {s} session ({len(Fc)} cells)", fontsize=9)
    ax.set_xlabel("time (s)")
axes[0].set_ylabel("change from pre-pulse mean (%)")
axes[0].legend(fontsize=7)
plt.tight_layout()
plt.show()
print("Plotted range (%):", {s: (round(float(d.min()), 1), round(float(d.max()), 1))
                             for s, d in [("single", np.concatenate([r["dF"] for r in res if r["set"] == "single" and r["volts"] == 0]).mean(0)),
                                          ("train", np.concatenate([r["dF"] for r in res if r["set"] == "train" and r["volts"] == 0]).mean(0))]})
End value relative to the pre-pulse mean (%), by set and voltage:
   set  volts  files  cells  dF_end  raw_cell_end  background_end  whole_frame_end  background_over_F0
single    0.0      6     27  -12.02         -8.70           -0.98            -1.98                0.45
single  150.0      4     19  -10.26         -7.26           -0.46            -1.26                0.41
single  200.0      5     24   -7.42         -5.05           -0.18            -0.84                0.47
single  250.0      4     18   -5.44         -3.76           -0.38            -0.78                0.47
single  300.0      5     23   -3.83         -2.51           -0.20            -0.39                0.50
single  350.0      5     21    1.24          0.76           -0.04             0.02                0.56
single  400.0      2      7    1.47          1.05           -0.02             0.26                0.67
 train    0.0      9     82  -15.68         -7.47           -0.55            -2.05                1.26
 train   70.0      3     28  -10.15         -4.25           -0.25            -1.02                1.44
 train  100.0      1      8   -9.03         -2.95           -0.17            -0.64                2.19
 train  150.0      4     38   -8.11         -2.83           -0.01            -0.49                1.93
 train  170.0      4     36   -9.17         -3.37           -0.12            -0.67                2.14
 train  180.0      1     10   -6.43         -2.17            0.04            -0.35                2.00
 train  190.0      1     10    0.98         -0.32            0.09             0.06                2.22
 train  200.0      7     68   -1.98         -0.64           -0.05            -0.11                2.03
 train  210.0      1     11    6.42          1.48            0.13             0.39                2.46
 train  220.0      1     10    9.63          2.96            0.03             0.41                2.39
 train  230.0      5     47   46.19         15.87            0.76             3.91                2.26
 train  250.0      4     39   80.99         36.06            1.78             9.31                1.59
 train  300.0      1      6  423.10        206.09            7.81            39.11                1.06

Pre-pulse slope of dF (% per min), by recording order within the field of view (1 = the sham):
               size         mean           sem      
set          single train single  train single train
order_in_fov                                        
1              27.0  82.0  -3.97 -11.39   0.55  0.81
2              27.0  84.0  -2.61  -5.12   0.23  0.54
3              27.0  68.0  -1.97  -4.48   0.37  0.70
4              18.0  57.0  -1.78  -2.04   0.31  0.74
5              18.0  47.0  -1.46  -2.98   0.27  1.19
6              18.0  19.0  -0.41  -2.75   0.60  1.36
7               4.0  18.0  -0.95  -2.04   0.51  1.79
8               NaN  18.0    NaN  -5.40    NaN  2.26
No description has been provided for this image
Plotted range (%): {'single': (-12.5, 3.5), 'train': (-16.3, 3.0)}

The decline is in the cells' own fluorescence, not in the background handling. In the sham recordings the raw cell signal, before any background is subtracted, falls by 8.7% (single) and 7.5% (train), while the cell-free background falls by 1.0% and 0.6%. Subtracting the background enlarges the relative decline (to -12.0% and -15.7%), because the background is 0.45 (single) to 1.26 (train) times the background-corrected cell signal; no choice of background region removes it, and dropping the subtraction, which Methods 2.4 prescribe, still leaves -8.7%. Fig. 6A of the paper shows the 0 V/cm mean trace rising to about +1% by 300 s, and the Fig. 6B traces are flat near zero, so the recordings or the processing behind the figure differ from these files in a way the paper does not describe (a question for the authors, see the summary).

The decline is also steepest in the first recording of a field of view and smaller in later ones: before the pulse, dF falls by 3.97% per minute in the first (sham) recording of the single-pulse session and by 1.46 to 0.41% per minute in the fifth to seventh; in the train session by 11.4% per minute in the first and 2.0 to 5.4% per minute later, without a steady trend after the first. As each field of view was recorded at escalating voltages, voltage and recording order go together, and any end value compared with the sham, which is always first, carries part of this difference. The step of dF at the pulse (shown for Fig. 6A below) does not.

In [9]:
# Segmentation check: the regions on the pre-pulse mean image of each field of view's first recording
# (images shown at the 4-fold reduced size kept while loading).
firsts = order.groupby("subject_id", sort=False).head(1)
fig, axes = plt.subplots(3, 5, figsize=(15, 9))
for ax, (i, row) in zip(axes.ravel(), firsts.iterrows()):
    r = res[i]
    ax.imshow(r["pre_small"], cmap="gray")
    for c in r["cells"].itertuples():
        col = "lime" if c.kept else "red"
        ax.add_patch(plt.Circle((c.x / 4, c.y / 4), c.radius_px / 4, fill=False, color=col, lw=0.6))
        if c.kept:
            ax.add_patch(plt.Circle((c.x / 4, c.y / 4), fluo4.ROI_FRACTION * c.radius_px / 4, fill=False,
                                    color="yellow", lw=0.6))
    ax.set_title(f"{row.subject_id} ({row.set}): {int(r['cells'].kept.sum())} kept", fontsize=8)
    ax.axis("off")
for ax in axes.ravel()[len(firsts):]:
    ax.axis("off")
plt.tight_layout()
plt.show()
print("Green: object outline as a circle of its equivalent radius (kept); red: dropped; yellow: the region used.")
No description has been provided for this image
Green: object outline as a circle of its equivalent radius (kept); red: dropped; yellow: the region used.

The regions sit on the cells in every field of view; the dropped objects are small specks and objects cut by the image border.

In [10]:
# Does the response start at the stated pulse time? Step = mean dF over W frames after a putative
# pulse frame minus the W frames before it, strongly pulsed files minus sham files, with the
# putative pulse frame shifted.
def step(r, at, w):
    m = r["dF"].mean(0)
    return m[at:at + w].mean() - m[at - w:at].mean()

for s, strong, w in [("single", 350, 10), ("train", 230, 5)]:
    rs = [r for r in res if r["set"] == s]
    pf, n = rs[0]["pulse_frame"], rs[0]["shape"][0]
    shifts = np.arange(-pf + w, n - pf - w)
    contrast = [np.mean([step(r, pf + d, w) for r in rs if r["volts"] >= strong])
                - np.mean([step(r, pf + d, w) for r in rs if r["volts"] == 0]) for d in shifts]
    best = shifts[int(np.argmax(contrast))]
    rise = [int(np.argmax(np.diff(r["dF"].mean(0)))) + 1 for r in rs if r["volts"] >= strong]
    print(f"{s}: stated pulse at frame {pf} ({fluo4.PULSE_S[s]:.0f} s). Contrast (>= {strong} V minus sham) "
          f"at the stated frame {contrast[list(shifts).index(0)]:.2f}%, largest at shift {best:+d} frames "
          f"({max(contrast):.2f}%); at shifts -{w}, +{w}: "
          f"{contrast[list(shifts).index(-w)]:.2f}%, {contrast[list(shifts).index(w)]:.2f}%")
    print(f"   frame of steepest rise of the mean trace in the {len(rise)} files at >= {strong} V: "
          f"{sorted(rise)}")
single: stated pulse at frame 60 (60 s). Contrast (>= 350 V minus sham) at the stated frame 6.49%, largest at shift +1 frames (7.02%); at shifts -10, +10: 0.17%, 1.85%
   frame of steepest rise of the mean trace in the 7 files at >= 350 V: [40, 61, 61, 61, 67, 83, 249]
train: stated pulse at frame 15 (30 s). Contrast (>= 230 V minus sham) at the stated frame 79.10%, largest at shift +3 frames (146.56%); at shifts -5, +5: 1.57%, 116.57%
   frame of steepest rise of the mean trace in the 10 files at >= 230 V: [17, 18, 18, 18, 18, 19, 20, 20, 20, 33]

The recordings share the clock the descriptions give: the difference between strongly pulsed and sham files in the step of dF across a putative pulse frame is largest within one frame of the stated pulse for single pulses (frame 60 against 61), and three frames after the train onset for trains (the train lasts 10 s, five frames). With the pulse frame shifted 10 frames earlier (single) or 5 earlier (train), the contrast is near zero. This is a check of the stated timing against the responses; it is consistent with the data in both sessions.

Field strengths¶

The paper gives field strengths (Sim4Life values 5 um above the glass, Methods 2.2); the files give only the generator voltage, and neither states the conversion. Two readings are run (author, step 1.6):

  • M1, 3.9 V/cm per V, adopted as the main reading (an assumption, see below). The single-pulse voltages 150, 200, 250, 300 and 350 V then give 585, 780, 975, 1170 and 1365 V/cm, the field series the paper uses throughout (570, 780, 1000, 1200, 1400 V/cm), and the train voltages 70, 150, 170, 200, 230 and 250 V fall nearest the six fields labelled in Fig. 6B (230, 570, 650, 780, 900, 1000 V/cm).
  • M2, 3.3 V/cm per V, a variant. The train voltages 70, 200 and 230 V then give 231, 660 and 759 V/cm, the 230, 650 and 780 V/cm named for the trains in Section 3.6.

A field the paper names is assigned to the voltage whose field under the reading is nearest. Neither factor follows from the electrode gap (V/d is 10 V/cm per V for the 1.00 mm the files state and 6.25 V/cm per V for the 1.6 mm of Methods 2.2), as the paper's fields are simulated at the cells.

In [11]:
print("Field strength (V/cm) of each voltage under the two readings of the conversion:")
volts = sorted(info.volts.unique())
print(pd.DataFrame({m: fluo4.field(volts, m).round() for m in fluo4.FIELD_PER_VOLT}, index=volts).T.to_string())
avail = {s: sorted(info.volts[info.set == s].unique()) for s in ["single", "train"]}
named = {"single": [780, 1000, 1200], "train": [230, 570, 650, 780, 900, 1000]}
for s in named:
    for m in fluo4.FIELD_PER_VOLT:
        print(f"  {s}, {m}: " + ", ".join(
            f"{f} V/cm -> {fluo4.voltage_for(f, m, avail[s]):.0f} V "
            f"({fluo4.field(fluo4.voltage_for(f, m, avail[s]), m):.0f} V/cm)" for f in named[s]))


def group_table(d, value):
    """Per voltage: n, mean, s.e., one-sample t-test against 0 and Student's t-test against sham."""
    sham = d[d.volts == 0][value]
    rows = []
    for v, g in d.groupby("volts"):
        x = g[value]
        one = stats.ttest_1samp(x, 0)
        two = stats.ttest_ind(x, sham) if v != 0 else None
        rows.append({"volts": v, "n": len(x), "mean": x.mean(), "se": x.sem(),
                     "t_vs_0": one.statistic, "p_vs_0": one.pvalue,
                     "diff_vs_sham": x.mean() - sham.mean() if v != 0 else np.nan,
                     "p_vs_sham": two.pvalue if two else np.nan})
    return pd.DataFrame(rows).set_index("volts")


# Fig. 6B: the fields labelled on its panels and the n printed below each (the paper's figure, a
# reference for the comparison, not a result).
FIG6B_N = {230: 28, 570: 37, 650: 34, 780: 54, 900: 35, 1000: 36}
n_by_volts = info[info.set == "train"].groupby("volts").cells.sum()
print("\nFig. 6B groups against the dataset's train groups under each reading (nearest voltage):")
rows = []
for m in fluo4.FIELD_PER_VOLT:
    for f, n_paper in FIG6B_N.items():
        v = fluo4.voltage_for(f, m, avail["train"])
        rows.append({"mapping": m, "field_fig6b": f, "n_fig6b": n_paper, "volts": v,
                     "field_of_volts": round(fluo4.field(v, m)), "n_dataset": int(n_by_volts[v])})
g6b = pd.DataFrame(rows)
print(g6b.to_string(index=False))
for m, g in g6b.groupby("mapping"):
    print(f"  {m}: distinct voltages used {g.volts.nunique()} of 6; sum of |n_dataset - n_fig6b| = "
          f"{int((g.n_dataset - g.n_fig6b).abs().sum())}; total n {int(g.drop_duplicates('volts').n_dataset.sum())} "
          f"(Fig. 6B {sum(FIG6B_N.values())})")
print("Train voltages in the dataset that no Fig. 6B field takes under M1:",
      [v for v in avail["train"] if v > 0 and v not in set(g6b[g6b.mapping == "M1"].volts)])
Field strength (V/cm) of each voltage under the two readings of the conversion:
    0.0    70.0   100.0  150.0  170.0  180.0  190.0  200.0  210.0  220.0  230.0  250.0   300.0   350.0   400.0
M1    0.0  273.0  390.0  585.0  663.0  702.0  741.0  780.0  819.0  858.0  897.0  975.0  1170.0  1365.0  1560.0
M2    0.0  231.0  330.0  495.0  561.0  594.0  627.0  660.0  693.0  726.0  759.0  825.0   990.0  1155.0  1320.0
  single, M1: 780 V/cm -> 200 V (780 V/cm), 1000 V/cm -> 250 V (975 V/cm), 1200 V/cm -> 300 V (1170 V/cm)
  single, M2: 780 V/cm -> 250 V (825 V/cm), 1000 V/cm -> 300 V (990 V/cm), 1200 V/cm -> 350 V (1155 V/cm)
  train, M1: 230 V/cm -> 70 V (273 V/cm), 570 V/cm -> 150 V (585 V/cm), 650 V/cm -> 170 V (663 V/cm), 780 V/cm -> 200 V (780 V/cm), 900 V/cm -> 230 V (897 V/cm), 1000 V/cm -> 250 V (975 V/cm)
  train, M2: 230 V/cm -> 70 V (231 V/cm), 570 V/cm -> 170 V (561 V/cm), 650 V/cm -> 200 V (660 V/cm), 780 V/cm -> 230 V (759 V/cm), 900 V/cm -> 250 V (825 V/cm), 1000 V/cm -> 300 V (990 V/cm)

Fig. 6B groups against the dataset's train groups under each reading (nearest voltage):
mapping  field_fig6b  n_fig6b  volts  field_of_volts  n_dataset
     M1          230       28   70.0             273         28
     M1          570       37  150.0             585         38
     M1          650       34  170.0             663         36
     M1          780       54  200.0             780         68
     M1          900       35  230.0             897         47
     M1         1000       36  250.0             975         39
     M2          230       28   70.0             231         28
     M2          570       37  170.0             561         36
     M2          650       34  200.0             660         68
     M2          780       54  230.0             759         47
     M2          900       35  250.0             825         39
     M2         1000       36  300.0             990          6
  M1: distinct voltages used 6 of 6; sum of |n_dataset - n_fig6b| = 32; total n 256 (Fig. 6B 224)
  M2: distinct voltages used 6 of 6; sum of |n_dataset - n_fig6b| = 76; total n 224 (Fig. 6B 224)
Train voltages in the dataset that no Fig. 6B field takes under M1: [np.float64(100.0), np.float64(180.0), np.float64(190.0), np.float64(210.0), np.float64(220.0), np.float64(300.0)]

Under M1 the six Fig. 6B fields take six different train voltages, and the dataset's group sizes are close to the figure's n (equal at 230 V/cm, and within 3 at 570, 650 and 1000 V/cm; larger at 780 and 900 V/cm). Under M2, 900 and 1000 V/cm fall on 250 and 300 V, and the 300 V group has 6 cells where the figure has 36; the six M2 groups happen to sum to 224, the paper's total, while the M1 groups sum to 256. The same electrodes and generator served both sessions (Methods 2.2), so M1 is adopted for both sessions, as an assumption; M2 is still shown for every claim. The train voltages 100, 180, 190, 210, 220 and 300 V are in no Fig. 6B group under M1.

fig6a-ca-entry-1000-1200¶

A single 1-us pulse causes clear Ca2+ entry into CHO cells at 1000 and 1200 V/cm, among field strengths escalated from 0 to 1200 V/cm.

Criterion (claims file): per cell dF as in Methods 2.4; reproduced when, under at least one reading, the post-pulse dF at both 1000 and 1200 V/cm is significantly above zero (one-sample t-test) or above the 0 V/cm group (two-tailed Student's t-test) at p < 0.05, and the group means at 1000 and 1200 V/cm exceed those of every field of 650 V/cm or less. The post-pulse value is read as (a) the mean of the last 10 frames or (b) each cell's peak after the pulse. Both conversions are run.

In [12]:
A = cells[cells.set == "single"]
tabs_A = {v: group_table(A, v) for v in ["end", "peak"]}
for v, t in tabs_A.items():
    print(f"Single pulses, per-cell {v} dF (%), by voltage:")
    print(t.round(4).to_string(), "\n")
rows = []
for m in fluo4.FIELD_PER_VOLT:
    v1000, v1200 = (fluo4.voltage_for(f, m, avail["single"]) for f in (1000, 1200))
    low = [v for v in avail["single"] if fluo4.field(v, m) <= 650]
    for value, t in tabs_A.items():
        exceed = all(t.loc[v, "mean"] > t.loc[low, "mean"].max() for v in (v1000, v1200))
        for test in ["one-sample vs 0", "Student vs 0 V/cm"]:
            if test.startswith("one"):
                sig = [t.loc[v, "p_vs_0"] < 0.05 and t.loc[v, "mean"] > 0 for v in (v1000, v1200)]
                ps = [t.loc[v, "p_vs_0"] for v in (v1000, v1200)]
            else:
                sig = [t.loc[v, "p_vs_sham"] < 0.05 and t.loc[v, "diff_vs_sham"] > 0 for v in (v1000, v1200)]
                ps = [t.loc[v, "p_vs_sham"] for v in (v1000, v1200)]
            rows.append({"mapping": m, "value": value, "test": test, "V_1000": v1000, "V_1200": v1200,
                         "low_fields_V": low, "mean_1000": t.loc[v1000, "mean"], "mean_1200": t.loc[v1200, "mean"],
                         "max_low_mean": t.loc[low, "mean"].max(), "p_1000": ps[0], "p_1200": ps[1],
                         "sig_1000": sig[0], "sig_1200": sig[1], "exceed_low": exceed,
                         "holds_both": sig[0] and sig[1] and exceed,
                         "holds_one": (sig[0] or sig[1]) and exceed})
readings_6a = pd.DataFrame(rows)
print(readings_6a.round(6).to_string())
print("\np-values of the table above, in scientific notation:")
print(readings_6a[["mapping", "value", "test", "p_1000", "p_1200"]].to_string(float_format="%.2e"))
for m in fluo4.FIELD_PER_VOLT:
    r = readings_6a[readings_6a.mapping == m]
    print(f"{m}: readings holding at both fields: {int(r.holds_both.sum())} of {len(r)}; at one or both: "
          f"{int(r.holds_one.sum())}")
Single pulses, per-cell end dF (%), by voltage:
        n     mean      se     t_vs_0  p_vs_0  diff_vs_sham  p_vs_sham
volts                                                                 
0.0    27 -12.0196  0.6371 -18.865101  0.0000           NaN        NaN
150.0  19 -10.2619  0.8786 -11.679300  0.0000        1.7576     0.1037
200.0  24  -7.4192  0.5193 -14.288100  0.0000        4.6003     0.0000
250.0  18  -5.4359  0.5534  -9.823200  0.0000        6.5837     0.0000
300.0  23  -3.8324  0.7148  -5.361300  0.0000        8.1872     0.0000
350.0  21   1.2426  1.9514   0.636800  0.5315       13.2621     0.0000
400.0   7   1.4665  1.8216   0.805100  0.4515       13.4861     0.0000 

Single pulses, per-cell peak dF (%), by voltage:
        n     mean      se  t_vs_0  p_vs_0  diff_vs_sham  p_vs_sham
volts                                                              
0.0    27   0.2256  0.2686  0.8400  0.4086           NaN        NaN
150.0  19   0.5479  0.3876  1.4137  0.1745        0.3223     0.4834
200.0  24   1.7551  0.6106  2.8744  0.0086        1.5295     0.0211
250.0  18   2.3045  0.2856  8.0683  0.0000        2.0789     0.0000
300.0  23   5.3462  0.7635  7.0027  0.0000        5.1206     0.0000
350.0  21  13.0542  3.1662  4.1230  0.0005       12.8286     0.0000
400.0   7  17.4242  7.8549  2.2183  0.0684       17.1986     0.0001 

  mapping value               test  V_1000  V_1200  low_fields_V  mean_1000  mean_1200  max_low_mean    p_1000    p_1200  sig_1000  sig_1200  exceed_low  holds_both  holds_one
0      M1   end    one-sample vs 0   250.0   300.0  [0.0, 150.0]  -5.435900  -3.832385    -10.261928  0.000000  0.000022     False     False        True       False      False
1      M1   end  Student vs 0 V/cm   250.0   300.0  [0.0, 150.0]  -5.435900  -3.832385    -10.261928  0.000000  0.000000      True      True        True        True       True
2      M1  peak    one-sample vs 0   250.0   300.0  [0.0, 150.0]   2.304490   5.346184      0.547896  0.000000  0.000000      True      True        True        True       True
3      M1  peak  Student vs 0 V/cm   250.0   300.0  [0.0, 150.0]   2.304490   5.346184      0.547896  0.000006  0.000000      True      True        True        True       True
4      M2   end    one-sample vs 0   300.0   350.0  [0.0, 150.0]  -3.832385   1.242578    -10.261928  0.000022  0.531498     False     False        True       False      False
5      M2   end  Student vs 0 V/cm   300.0   350.0  [0.0, 150.0]  -3.832385   1.242578    -10.261928  0.000000  0.000000      True      True        True        True       True
6      M2  peak    one-sample vs 0   300.0   350.0  [0.0, 150.0]   5.346184  13.054199      0.547896  0.000000  0.000528      True      True        True        True       True
7      M2  peak  Student vs 0 V/cm   300.0   350.0  [0.0, 150.0]   5.346184  13.054199      0.547896  0.000000  0.000035      True      True        True        True       True

p-values of the table above, in scientific notation:
  mapping value               test   p_1000   p_1200
0      M1   end    one-sample vs 0 2.01e-08 2.21e-05
1      M1   end  Student vs 0 V/cm 4.89e-09 3.02e-11
2      M1  peak    one-sample vs 0 3.25e-07 5.00e-07
3      M1  peak  Student vs 0 V/cm 6.14e-06 1.94e-08
4      M2   end    one-sample vs 0 2.21e-05 5.31e-01
5      M2   end  Student vs 0 V/cm 3.02e-11 5.98e-09
6      M2  peak    one-sample vs 0 5.00e-07 5.28e-04
7      M2  peak  Student vs 0 V/cm 1.94e-08 3.53e-05
M1: readings holding at both fields: 3 of 4; at one or both: 3
M2: readings holding at both fields: 3 of 4; at one or both: 3
In [13]:
print("Per field of view: mean end and peak dF (%) by voltage, single pulses")
print(A.pivot_table(index="fov", columns="volts", values="end", aggfunc="mean").round(2).to_string())
print(A.pivot_table(index="fov", columns="volts", values="peak", aggfunc="mean").round(2).to_string())
print(A.pivot_table(index="fov", columns="volts", values="end", aggfunc="size").to_string())

fig, axes = plt.subplots(1, len(avail["single"]), figsize=(18, 3.2), sharey=True)
lims = (-25, 40)
t_a = np.arange(300)
outside = 0
for ax, v in zip(axes, avail["single"]):
    tr = np.concatenate([r["dF"] for r in res if r["set"] == "single" and r["volts"] == v])
    outside += int(((tr < lims[0]) | (tr > lims[1])).any(1).sum())
    ax.plot(t_a, tr.T, color="tab:blue", lw=0.4, alpha=0.5)
    mu, se = tr.mean(0), tr.std(0, ddof=1) / np.sqrt(len(tr))
    ax.fill_between(t_a, mu - se, mu + se, color="0.6")
    ax.plot(t_a, mu, color="tab:orange", lw=2)
    ax.axvline(60, color="k", lw=0.8)
    ax.axhline(0, color="k", lw=0.5, ls=":")
    ax.set_title(f"{v:.0f} V\nM1 {fluo4.field(v, 'M1'):.0f}, M2 {fluo4.field(v, 'M2'):.0f} V/cm", fontsize=9)
    ax.set_xlabel("time (s)")
axes[0].set_ylim(*lims)
axes[0].set_ylabel("Fluo-4 dF (%)")
plt.tight_layout()
plt.show()
print(f"Axis {lims}; cell traces with points outside it: {outside}")
Per field of view: mean end and peak dF (%) by voltage, single pulses
volts        0.0    150.0  200.0  250.0  300.0  350.0  400.0
fov                                                         
20250203_c1 -13.33    NaN  -8.03    NaN  -4.97    NaN    NaN
20250203_c2 -11.54  -8.12  -5.87  -4.10  -3.73   1.38    NaN
20250203_c3 -14.09 -12.99  -9.27  -7.08  -4.47  13.20    NaN
20250203_c4 -11.16 -10.01  -7.02  -5.12  -2.57  -1.09    NaN
20250203_c5 -11.77 -10.12  -7.05  -6.10  -4.25  -0.90   2.88
20250203_c6 -10.05    NaN    NaN    NaN    NaN  -2.60  -0.42
volts        0.0    150.0  200.0  250.0  300.0      350.0      400.0
fov                                                                 
20250203_c1   1.86    NaN   1.57    NaN   8.02        NaN        NaN
20250203_c2   0.07   2.14   2.26   2.13   4.28  13.750000        NaN
20250203_c3  -0.58  -1.14   3.25   1.80   4.89  23.370001        NaN
20250203_c4   0.11   0.40   1.17   2.80   5.31  15.190000        NaN
20250203_c5  -0.56   0.90   1.02   2.00   3.48   5.620000  21.950001
20250203_c6   0.10    NaN    NaN    NaN    NaN   6.740000  11.390000
volts        0.0    150.0  200.0  250.0  300.0  350.0  400.0
fov                                                         
20250203_c1    5.0    NaN    5.0    NaN    5.0    NaN    NaN
20250203_c2    4.0    4.0    4.0    4.0    4.0    4.0    NaN
20250203_c3    4.0    4.0    4.0    3.0    3.0    3.0    NaN
20250203_c4    7.0    7.0    7.0    7.0    7.0    7.0    NaN
20250203_c5    4.0    4.0    4.0    4.0    4.0    4.0    4.0
20250203_c6    3.0    NaN    NaN    NaN    NaN    3.0    3.0
No description has been provided for this image
Axis (-25, 40); cell traces with points outside it: 2
In [14]:
# Variants (not the criterion's readings), against two confounds of the end and peak values: the
# decline of the signal, and the recording order (the sham is always recorded first, and the decline
# is steepest in the first recording, see the data check).
# (1) Step at the pulse: each cell's mean dF over the 5 frames after the pulse less the 5 before it.
A = A.assign(step=[s for r in res if r["set"] == "single" for s in
                   (r["dF"][:, r["pulse_frame"]:r["pulse_frame"] + 5].mean(1)
                    - r["dF"][:, r["pulse_frame"] - 5:r["pulse_frame"]].mean(1))])
print("Step at the pulse (%), single pulses, by voltage:")
print(group_table(A, "step").round(4).to_string())
print("p-values:", group_table(A, "step")[["p_vs_0", "p_vs_sham"]].to_string(float_format="%.2e"))
# (2) Fields of view as the unit: per field of view, mean at the voltage less mean at its own sham,
# one-sample t-test over fields of view.
rows = []
for value in ["end", "peak", "step"]:
    fm = A.pivot_table(index="fov", columns="volts", values=value, aggfunc="mean")
    for v in avail["single"][1:]:
        d = (fm[v] - fm[0]).dropna()
        rows.append({"value": value, "volts": v, "n_fov": len(d), "mean_diff_vs_own_sham": d.mean(),
                     "min_diff": d.min(), "p_one_sample": stats.ttest_1samp(d, 0).pvalue if len(d) > 1 else np.nan})
print("\nPer field of view, value at the voltage less its own sham (t-test over fields of view):")
print(pd.DataFrame(rows).round(4).to_string(index=False))
# (3) Fig. 6A averages 14 cells: cells per field of view and voltage, and the voltage groups' sizes.
n_fv = A.pivot_table(index="fov", columns="volts", values="end", aggfunc="size")
print("\nCells per voltage group:", A.groupby("volts").size().to_dict(),
      "; cells per field of view:", n_fv[0].astype(int).to_dict())
from itertools import combinations
sizes = n_fv[0].astype(int)
print("Sets of fields of view whose cells sum to 14:",
      [c for k in range(1, len(sizes) + 1) for c in combinations(sizes.index, k) if sizes[list(c)].sum() == 14])
Step at the pulse (%), single pulses, by voltage:
        n    mean      se  t_vs_0  p_vs_0  diff_vs_sham  p_vs_sham
volts                                                             
0.0    27 -0.2595  0.1801 -1.4414  0.1614           NaN        NaN
150.0  19 -0.6440  0.1492 -4.3163  0.0004       -0.3845     0.1294
200.0  24  0.1467  0.1595  0.9201  0.3671        0.4063     0.1011
250.0  18  0.3498  0.1721  2.0329  0.0580        0.6093     0.0247
300.0  23  1.7769  0.3063  5.8022  0.0000        2.0365     0.0000
350.0  21  3.9366  0.7091  5.5517  0.0000        4.1961     0.0000
400.0   7  5.7111  1.7651  3.2355  0.0178        5.9706     0.0000
p-values:         p_vs_0  p_vs_sham
volts                    
0.0   1.61e-01        NaN
150.0 4.16e-04   1.29e-01
200.0 3.67e-01   1.01e-01
250.0 5.80e-02   2.47e-02
300.0 7.74e-06   3.16e-07
350.0 1.96e-05   7.38e-08
400.0 1.78e-02   3.19e-07

Per field of view, value at the voltage less its own sham (t-test over fields of view):
value  volts  n_fov  mean_diff_vs_own_sham  min_diff  p_one_sample
  end  150.0      4               1.830300    1.0953        0.0437
  end  200.0      5               4.929500    4.1435        0.0000
  end  250.0      4               6.540700    5.6680        0.0005
  end  300.0      5               8.381000    7.5183        0.0000
  end  350.0      5              13.720200    7.4522        0.0173
  end  400.0      2              12.139800    9.6324        0.1297
 peak  150.0      4               0.815600   -0.5574        0.2598
 peak  200.0      5               1.672600   -0.2919        0.0688
 peak  250.0      4               2.420900    2.0598        0.0004
 peak  300.0      5               5.015700    4.0346        0.0002
 peak  350.0      5              13.105100    6.1786        0.0157
 peak  400.0      2              16.897301   11.2854        0.2041
 step  150.0      4              -0.390900   -0.9283        0.2121
 step  200.0      5               0.452200    0.0001        0.0616
 step  250.0      4               0.672600    0.2968        0.0165
 step  300.0      5               2.169700    1.5319        0.0010
 step  350.0      5               4.279800    2.2491        0.0046
 step  400.0      2               6.008900    5.8127        0.0208

Cells per voltage group: {0.0: 27, 150.0: 19, 200.0: 24, 250.0: 18, 300.0: 23, 350.0: 21, 400.0: 7} ; cells per field of view: {'20250203_c1': 5, '20250203_c2': 4, '20250203_c3': 4, '20250203_c4': 7, '20250203_c5': 4, '20250203_c6': 3}
Sets of fields of view whose cells sum to 14: [('20250203_c2', '20250203_c4', '20250203_c6'), ('20250203_c3', '20250203_c4', '20250203_c6'), ('20250203_c4', '20250203_c5', '20250203_c6')]

Result: reproduced, under both conversions. Under M1 (250 and 300 V), three of the four readings hold at both fields: the end value against the 0 V/cm group (p = 4.9e-09 and 3.0e-11), and the peak against zero or against the 0 V/cm group (means 2.30 and 5.35% against at most 0.55% at 650 V/cm or less). The end value against zero fails: it is below zero at both fields (-5.44 and -3.83%), as is the sham (-12.0%). Under M2 (300 and 350 V) the same three readings hold. In every field of view the peak at 250 V is above the peak at 0 V, and at 300 V above the peaks at 0 and 150 V. With fields of view as the unit, the peak less the same field of view's sham is 2.42% at 250 V (4 fields of view, p = 0.0004) and 5.02% at 300 V (5, p = 0.0002).

The end and peak readings compare later recordings with the sham, recorded first, while the decline is steepest in the first recording (data check). The step at the pulse, which that does not affect, is above the sham's at 250 V (0.35 +- 0.17%, against the sham p = 0.025) and 300 V (1.78 +- 0.31%, p = 3.2e-07), and not at 150 or 200 V (p = 0.13 and 0.10); at 250 V it is small and not above zero on its own (p = 0.058). The entry at 1000 V/cm under M1 is therefore a small one, a few tenths of a percent at the pulse, and clearer at 1200 V/cm (1.78%) and above (3.94% at 350 V). The single-pulse session holds 27 cells in 6 fields of view, and three sets of fields of view give 14 cells, so which 14 cells Fig. 6A averages cannot be told from the data.

fig6a-ca-sustained¶

After a single 1-us pulse at 1000 or 1200 V/cm the Ca2+ signal stays elevated through the 5-min observation period.

Criterion (claims file): at both 1000 and 1200 V/cm, the mean dF over the last 10 frames is above zero by a one-sample t-test at p < 0.05; partially reproduced when this holds at one of the two. The ratio of the final to the peak group mean is reported.

In [15]:
rows = []
for m in fluo4.FIELD_PER_VOLT:
    for f in (1000, 1200):
        v = fluo4.voltage_for(f, m, avail["single"])
        t = tabs_A["end"]
        tr = np.concatenate([r["dF"] for r in res if r["set"] == "single" and r["volts"] == v]).mean(0)
        one_sided = stats.ttest_1samp(A[A.volts == v].end, 0, alternative="greater").pvalue
        rows.append({"mapping": m, "field": f, "volts": v, "n": t.loc[v, "n"], "end_mean": t.loc[v, "mean"],
                     "end_se": t.loc[v, "se"], "p_vs_0_two_sided": t.loc[v, "p_vs_0"],
                     "p_above_0_one_sided": one_sided, "above_zero": t.loc[v, "mean"] > 0 and t.loc[v, "p_vs_0"] < 0.05,
                     "peak_of_mean_trace": tr[60:].max(), "final_over_peak": tr[-fluo4.END_FRAMES:].mean() / tr[60:].max(),
                     "end_minus_sham": t.loc[v, "diff_vs_sham"], "p_vs_sham": t.loc[v, "p_vs_sham"]})
print(pd.DataFrame(rows).round(6).to_string())
print("p-values:")
print(pd.DataFrame(rows)[["mapping", "field", "volts", "p_vs_0_two_sided", "p_above_0_one_sided", "p_vs_sham"]]
      .to_string(float_format="%.2e"))
  mapping  field  volts   n  end_mean    end_se  p_vs_0_two_sided  p_above_0_one_sided  above_zero  peak_of_mean_trace  final_over_peak  end_minus_sham  p_vs_sham
0      M1   1000  250.0  18 -5.435900  0.553374          0.000000             1.000000       False            0.085884       -63.293648        6.583669        0.0
1      M1   1200  300.0  23 -3.832385  0.714819          0.000022             0.999989       False            1.581884        -2.422671        8.187184        0.0
2      M2   1000  300.0  23 -3.832385  0.714819          0.000022             0.999989       False            1.581884        -2.422671        8.187184        0.0
3      M2   1200  350.0  21  1.242578  1.951384          0.531498             0.265749       False            6.779093         0.183296       13.262148        0.0
p-values:
  mapping  field    volts  p_vs_0_two_sided  p_above_0_one_sided  p_vs_sham
0      M1   1000 2.50e+02          2.01e-08             1.00e+00   4.89e-09
1      M1   1200 3.00e+02          2.21e-05             1.00e+00   3.02e-11
2      M2   1000 3.00e+02          2.21e-05             1.00e+00   3.02e-11
3      M2   1200 3.50e+02          5.31e-01             2.66e-01   5.98e-09
In [16]:
# Variant (not the claim's criterion): traces less the mean sham trace of the same field of view.
sham_by_fov = {info.fov[i]: r["dF"].mean(0) for i, r in enumerate(res) if r["set"] == "single" and r["volts"] == 0}
rows = []
for v in avail["single"][1:]:
    corr = np.concatenate([r["dF"] - sham_by_fov[info.fov[i]] for i, r in enumerate(res)
                           if r["set"] == "single" and r["volts"] == v])
    e = corr[:, -fluo4.END_FRAMES:].mean(1)
    rows.append({"volts": v, "n": len(e), "end_minus_own_sham": e.mean(), "se": e.std(ddof=1) / np.sqrt(len(e)),
                 "p_vs_0": stats.ttest_1samp(e, 0).pvalue})
print("End dF less the same field of view's sham trace (variant):")
print(pd.DataFrame(rows).set_index("volts").to_string(float_format="%.3g"))
End dF less the same field of view's sham trace (variant):
        n  end_minus_own_sham    se   p_vs_0
volts                                       
150.0  19                1.72 0.821   0.0502
200.0  24                4.85 0.484 7.45e-10
250.0  18                6.43 0.525 7.33e-10
300.0  23                8.35   0.7 4.46e-11
350.0  21                12.9   2.1 5.57e-06
400.0   7                12.5  1.98 0.000728

Result: not reproduced, under both conversions. The end-of-recording dF is below zero at 1000 and 1200 V/cm under M1 (-5.44 and -3.83%) and at 1000 V/cm under M2 (-3.83%); at 1200 V/cm under M2 (350 V) it is +1.24% with p = 0.53. The final over peak ratio of the group mean trace is -63 and -2.4 under M1. The decline is the one the sham shows, and it is in the cells' raw fluorescence (-8.7% in the sham), not in the background (-1.0%), so it does not come from how the background was taken (data check). Against the 0 V/cm group the end value is higher by 6.6 and 8.2% (p = 4.9e-09 and 3.0e-11), and less the same field of view's sham trace it is above zero at every voltage of 200 V or more (4.8 to 12.9%, p below 0.001), but these comparisons include the change of the decline with recording order. So the signal stays above that of unpulsed cells to the end, but not above its own pre-pulse level, which is what the criterion tests. The paper's Fig. 6A shows no such decline in its 0 V/cm trace, so the outcome rests on a difference between these recordings and the figure that the paper does not explain.

fig6a-780-not-significant¶

After a single 1-us pulse at 780 V/cm the Ca2+ signal rises only subtly, at the detection limit, and not significantly.

Criterion (claims file): a claim of absence, judged on a one-sample t-test of each cell's end-of-recording dF (last 10 frames) against zero at p < 0.05: reproduced when it does not find an increase. The t-test against the 0 V/cm group and each cell's peak are variants.

In [17]:
rows = []
for m in fluo4.FIELD_PER_VOLT:
    v = fluo4.voltage_for(780, m, avail["single"])
    for value in ["end", "peak", "step"]:
        x, sham = A[A.volts == v][value], A[A.volts == 0][value]
        rows.append({"mapping": m, "volts": v, "field": fluo4.field(v, m), "value": value, "n": len(x),
                     "mean": x.mean(), "se": x.sem(),
                     "p_increase_one_sample": stats.ttest_1samp(x, 0, alternative="greater").pvalue,
                     "p_two_sided_one_sample": stats.ttest_1samp(x, 0).pvalue,
                     "diff_vs_sham": x.mean() - sham.mean(), "p_vs_sham_two_tailed": stats.ttest_ind(x, sham).pvalue})
t780 = pd.DataFrame(rows)
print(t780.round(6).to_string())
main = t780[(t780.value == "end")]
print("\nMain reading (one-sample t-test of the end value against zero, p < 0.05 for an increase):")
for _, r in main.iterrows():
    print(f"  {r.mapping}: {r.volts:.0f} V, mean {r['mean']:.2f} +- {r.se:.2f}%, one-sided p = {r.p_increase_one_sample:.3g}"
          f" -> increase found: {r['mean'] > 0 and r.p_two_sided_one_sample < 0.05}")
x200, x250 = A[A.volts == 200].step, A[A.volts == 250].step
print(f"\nStep at 250 V against the step at 200 V (Student's t-test, two-tailed): p = {stats.ttest_ind(x250, x200).pvalue:.3g}")
print("\nVariant reading that detects 1000 and 1200 V/cm under M1: step at the pulse against the 0 V/cm group "
      "(Student's t-test, two-tailed):")
for v in (200, 250, 300):
    x, sham = A[A.volts == v].step, A[A.volts == 0].step
    print(f"  {v} V (M1 {fluo4.field(v, 'M1'):.0f} V/cm): {x.mean():.2f} +- {x.sem():.2f}% against {sham.mean():.2f}%, "
          f"p = {stats.ttest_ind(x, sham).pvalue:.3g}")
  mapping  volts  field value   n      mean        se  p_increase_one_sample  p_two_sided_one_sample  diff_vs_sham  p_vs_sham_two_tailed
0      M1  200.0  780.0   end  24 -7.419224  0.519258               1.000000                0.000000      4.600346              0.000001
1      M1  200.0  780.0  peak  24  1.755120  0.610610               0.004283                0.008566      1.529517              0.021086
2      M1  200.0  780.0  step  24  0.146748  0.159498               0.183546                0.367091      0.406282              0.101080
3      M2  250.0  825.0   end  18 -5.435900  0.553374               1.000000                0.000000      6.583669              0.000000
4      M2  250.0  825.0  peak  18  2.304490  0.285624               0.000000                0.000000      2.078886              0.000006
5      M2  250.0  825.0  step  18  0.349760  0.172050               0.028992                0.057984      0.609295              0.024695

Main reading (one-sample t-test of the end value against zero, p < 0.05 for an increase):
  M1: 200 V, mean -7.42 +- 0.52%, one-sided p = 1 -> increase found: False
  M2: 250 V, mean -5.44 +- 0.55%, one-sided p = 1 -> increase found: False

Step at 250 V against the step at 200 V (Student's t-test, two-tailed): p = 0.397

Variant reading that detects 1000 and 1200 V/cm under M1: step at the pulse against the 0 V/cm group (Student's t-test, two-tailed):
  200 V (M1 780 V/cm): 0.15 +- 0.16% against -0.26%, p = 0.101
  250 V (M1 975 V/cm): 0.35 +- 0.17% against -0.26%, p = 0.0247
  300 V (M1 1170 V/cm): 1.78 +- 0.31% against -0.26%, p = 3.16e-07

Result: reproduced, under both conversions. The one-sample test finds no increase at 780 V/cm: the end value is -7.42 +- 0.52% under M1 (200 V, n = 24) and -5.44 +- 0.55% under M2 (250 V, n = 18), one-sided p = 1 for an increase in both. This test cannot detect an increase here: it also finds none at 1000 and 1200 V/cm under M1, where the end values are -5.44 and -3.83%. Of the variants, the end value against the 0 V/cm group (higher by 4.60%, p = 1.3e-06) and the peak (1.76 +- 0.61%, against zero p = 0.0086, against the sham peak p = 0.021) find an effect at 780 V/cm under M1, and both also carry the order effect of the data check. The step at the pulse, which separates 1000 and 1200 V/cm from the sham (p = 0.025 and 3.2e-07), finds none at 780 V/cm under M1 (0.15 +- 0.16%, p = 0.10); under M2, where 780 V/cm is 250 V, it finds one (0.35 +- 0.17%, p = 0.025). The step is the only variant not confounded by recording order, but it is weak evidence here: it separates 1000 V/cm from the sham only barely (p = 0.025, and p = 0.058 against zero), and the steps at 780 and 1000 V/cm under M1 (0.15 +- 0.16 and 0.35 +- 0.17%) do not differ (p = 0.40). The end and peak readings against the sham, which also separate 1000 and 1200 V/cm from it, find an effect at 780 V/cm. So the outcome follows the claims file's test, which says little here. A question on the criterion's test is asked of a reader (questions/001521.yaml).

fig6b-train-threshold¶

With trains of sixty 1-us pulses at 6 Hz (each cell tested once, 224 cells), a strong Ca2+ response occurs in 28% of cells at 780 V/cm and in nearly all cells at higher fields.

Criterion (claims file): a strong response is (a) an end-of-recording dF above the largest among the 0 V/cm (or lowest field) cells, or (b) above 3 s.d. of that group, read here as above its mean plus 3 s.d.; reproduced when, under one reading, the fraction at 780 V/cm is within 28 +- 2.8% and at least 90% respond at every field above 780 V/cm. Expected: 224 cells tested with trains.

In [18]:
B = cells[cells.set == "train"]
sham_B, low_B = B[B.volts == 0].end, B[B.volts == min(v for v in avail["train"] if v > 0)].end
rules = {"a: > max of 0 V/cm": sham_B.max(), "a: > max of lowest field": low_B.max(),
         "b: > mean + 3 s.d. of 0 V/cm": sham_B.mean() + 3 * sham_B.std(),
         "b: > mean + 3 s.d. of lowest field": low_B.mean() + 3 * low_B.std(),
         "b literal: > 3 s.d. of 0 V/cm": 3 * sham_B.std(),
         "b literal: > 3 s.d. of lowest field": 3 * low_B.std()}
print("Thresholds on the end-of-recording dF (%):", {k: round(v, 2) for k, v in rules.items()})
frac = pd.DataFrame({k: B.groupby("volts").end.apply(lambda x, t=t: 100 * (x > t).mean()) for k, t in rules.items()})
frac.insert(0, "n", B.groupby("volts").size())
for m in fluo4.FIELD_PER_VOLT:
    frac[f"field_{m}"] = fluo4.field(frac.index, m).round()
print(frac.round(1).to_string())
rows = []
for m in fluo4.FIELD_PER_VOLT:
    v780 = fluo4.voltage_for(780, m, avail["train"])
    # the fields above 780 V/cm in Fig. 6B are 900 and 1000 V/cm
    higher = sorted({fluo4.voltage_for(f, m, avail["train"]) for f in (900, 1000)})
    for k in rules:
        rows.append({"mapping": m, "rule": k, "V_780": v780, "pct_780": frac.loc[v780, k],
                     "in_tolerance": abs(frac.loc[v780, k] - 28) <= 2.8, "higher_V": higher,
                     "min_pct_higher": frac.loc[higher, k].min(), "all_higher_ge_90": (frac.loc[higher, k] >= 90).all()})
readings_6b = pd.DataFrame(rows)
print(readings_6b.round(1).to_string())
n_exp = int((B.volts > 0).sum())
print(f"\nCell exposures to trains (non-sham files): {n_exp}; with sham files: {len(B)}; "
      f"distinct cells (cells in each field of view's sham recording): {int(first[first.set == 'train'].cells.sum())}")
print("Cells per voltage group (paper: 28 to 54 per field strength):", B[B.volts > 0].groupby("volts").size().to_dict())
for m in fluo4.FIELD_PER_VOLT:
    vs = sorted(set(g6b[g6b.mapping == m].volts))
    print(f"{m}: cells in the voltage groups of the six Fig. 6B fields {vs}: {int(B.volts.isin(vs).sum())} (paper: 224)")
Thresholds on the end-of-recording dF (%): {'a: > max of 0 V/cm': np.float32(12.02), 'a: > max of lowest field': np.float32(-1.45), 'b: > mean + 3 s.d. of 0 V/cm': np.float32(5.42), 'b: > mean + 3 s.d. of lowest field': np.float32(2.29), 'b literal: > 3 s.d. of 0 V/cm': np.float32(21.1), 'b literal: > 3 s.d. of lowest field': np.float32(12.44)}
        n  a: > max of 0 V/cm  a: > max of lowest field  b: > mean + 3 s.d. of 0 V/cm  b: > mean + 3 s.d. of lowest field  b literal: > 3 s.d. of 0 V/cm  b literal: > 3 s.d. of lowest field  field_M1  field_M2
volts                                                                                                                                                                                                            
0.0    82                 0.0                       1.2                           1.2                                 1.2                            0.0                                  0.0       0.0       0.0
70.0   28                 0.0                       0.0                           0.0                                 0.0                            0.0                                  0.0     273.0     231.0
100.0   8                 0.0                       0.0                           0.0                                 0.0                            0.0                                  0.0     390.0     330.0
150.0  38                 0.0                       2.6                           0.0                                 0.0                            0.0                                  0.0     585.0     495.0
170.0  36                 0.0                       5.6                           0.0                                 0.0                            0.0                                  0.0     663.0     561.0
180.0  10                 0.0                      10.0                           0.0                                 0.0                            0.0                                  0.0     702.0     594.0
190.0  10                10.0                      30.0                          20.0                                20.0                           10.0                                 10.0     741.0     627.0
200.0  68                 4.4                      22.1                           8.8                                 8.8                            2.9                                  4.4     780.0     660.0
210.0  11                18.2                      54.5                          27.3                                27.3                            9.1                                 18.2     819.0     693.0
220.0  10                20.0                      90.0                          40.0                                50.0                           20.0                                 20.0     858.0     726.0
230.0  47                51.1                      78.7                          63.8                                68.1                           44.7                                 51.1     897.0     759.0
250.0  39                61.5                      79.5                          64.1                                69.2                           61.5                                 61.5     975.0     825.0
300.0   6               100.0                     100.0                         100.0                               100.0                          100.0                                100.0    1170.0     990.0
   mapping                                 rule  V_780  pct_780  in_tolerance        higher_V  min_pct_higher  all_higher_ge_90
0       M1                   a: > max of 0 V/cm  200.0      4.4         False  [230.0, 250.0]            51.1             False
1       M1             a: > max of lowest field  200.0     22.1         False  [230.0, 250.0]            78.7             False
2       M1         b: > mean + 3 s.d. of 0 V/cm  200.0      8.8         False  [230.0, 250.0]            63.8             False
3       M1   b: > mean + 3 s.d. of lowest field  200.0      8.8         False  [230.0, 250.0]            68.1             False
4       M1        b literal: > 3 s.d. of 0 V/cm  200.0      2.9         False  [230.0, 250.0]            44.7             False
5       M1  b literal: > 3 s.d. of lowest field  200.0      4.4         False  [230.0, 250.0]            51.1             False
6       M2                   a: > max of 0 V/cm  230.0     51.1         False  [250.0, 300.0]            61.5             False
7       M2             a: > max of lowest field  230.0     78.7         False  [250.0, 300.0]            79.5             False
8       M2         b: > mean + 3 s.d. of 0 V/cm  230.0     63.8         False  [250.0, 300.0]            64.1             False
9       M2   b: > mean + 3 s.d. of lowest field  230.0     68.1         False  [250.0, 300.0]            69.2             False
10      M2        b literal: > 3 s.d. of 0 V/cm  230.0     44.7         False  [250.0, 300.0]            61.5             False
11      M2  b literal: > 3 s.d. of lowest field  230.0     51.1         False  [250.0, 300.0]            61.5             False

Cell exposures to trains (non-sham files): 311; with sham files: 393; distinct cells (cells in each field of view's sham recording): 82
Cells per voltage group (paper: 28 to 54 per field strength): {70.0: 28, 100.0: 8, 150.0: 38, 170.0: 36, 180.0: 10, 190.0: 10, 200.0: 68, 210.0: 11, 220.0: 10, 230.0: 47, 250.0: 39, 300.0: 6}
M1: cells in the voltage groups of the six Fig. 6B fields [70.0, 150.0, 170.0, 200.0, 230.0, 250.0]: 256 (paper: 224)
M2: cells in the voltage groups of the six Fig. 6B fields [70.0, 170.0, 200.0, 230.0, 250.0, 300.0]: 224 (paper: 224)
In [19]:
# Variant: each field of view's first exposure only, as if each cell had been tested once.
firstexp = (order[(order.set == "train") & (order.volts > 0)].groupby("subject_id").head(1))
fx = B[B.file.isin(firstexp.index)]
print("First train exposure of each field of view:",
      firstexp[["subject_id", "volts", "cells"]].to_string(index=False))
print(pd.DataFrame({k: fx.groupby("volts").end.apply(lambda x, t=t: 100 * (x > t).mean()) for k, t in rules.items()})
      .assign(n=fx.groupby("volts").size()).round(1).to_string())

fig, axes = plt.subplots(1, len(avail["train"]), figsize=(22, 3.2), sharey=True)
lims = (-40, 200)
t_b = np.arange(90) * 2.0
outside = 0
for ax, v in zip(axes, avail["train"]):
    tr = np.concatenate([r["dF"] for r in res if r["set"] == "train" and r["volts"] == v])
    outside += int(((tr < lims[0]) | (tr > lims[1])).any(1).sum())
    ax.plot(t_b, tr.T, color="tab:blue", lw=0.4, alpha=0.5)
    mu, se = tr.mean(0), tr.std(0, ddof=1) / np.sqrt(len(tr))
    ax.fill_between(t_b, mu - se, mu + se, color="0.6")
    ax.plot(t_b, mu, color="tab:orange", lw=2)
    ax.axvspan(30, 40, color="k", alpha=0.15)
    ax.set_title(f"{v:.0f} V, n={len(tr)}\nM1 {fluo4.field(v, 'M1'):.0f}, M2 {fluo4.field(v, 'M2'):.0f}", fontsize=8)
    ax.set_xlabel("time (s)")
axes[0].set_ylim(*lims)
axes[0].set_ylabel("Fluo-4 dF (%)")
plt.tight_layout()
plt.show()
print(f"Axis {lims}; cell traces with points outside it: {outside}")
First train exposure of each field of view:      subject_id  volts  cells
CHO_20250502_c1  300.0      6
CHO_20250502_c2   70.0     12
CHO_20250502_c3   70.0      8
CHO_20250502_c4  250.0     11
CHO_20250502_c5   70.0      8
CHO_20250502_c6  150.0      9
CHO_20250502_c7  170.0      9
CHO_20250502_c8  170.0     10
CHO_20250502_c9  200.0     11
       a: > max of 0 V/cm  a: > max of lowest field  b: > mean + 3 s.d. of 0 V/cm  b: > mean + 3 s.d. of lowest field  b literal: > 3 s.d. of 0 V/cm  b literal: > 3 s.d. of lowest field   n
volts                                                                                                                                                                                        
70.0                  0.0                       0.0                           0.0                                 0.0                            0.0                                  0.0  28
150.0                 0.0                       0.0                           0.0                                 0.0                            0.0                                  0.0   9
170.0                 0.0                       5.3                           0.0                                 0.0                            0.0                                  0.0  19
200.0                 9.1                      18.2                          18.2                                18.2                            9.1                                  9.1  11
250.0                90.9                     100.0                          90.9                                90.9                           90.9                                 90.9  11
300.0               100.0                     100.0                         100.0                               100.0                          100.0                                100.0   6
No description has been provided for this image
Axis (-40, 200); cell traces with points outside it: 43

Result: not reproduced. The fields above 780 V/cm are taken as those of Fig. 6B, 900 and 1000 V/cm (230 and 250 V under M1, 250 and 300 V under M2). With the criterion's end window of 10 frames, under no reading is the fraction at 780 V/cm within 25.2 to 30.8%: it is 2.9 to 22.1% under M1 (200 V, 68 cells) and 44.7 to 78.7% under M2 (230 V, 47 cells), over the six rules, including "above 3 s.d." read literally (a threshold of 3 s.d. of the reference group). Nor do 90% respond at both higher fields: the lower of the two fractions is 44.7 to 78.7% under M1 and 61.5 to 79.5% under M2. The six voltage groups that correspond to the Fig. 6B fields hold 256 cells under M1 and 224 under M2, against the paper's 224; in all, the dataset holds 311 cell exposures to trains, of 82 distinct cells, not 224 cells each tested once. Taking only the first train of each field of view, 9.1 to 18.2% respond at 200 V and 90.9 to 100% at 250 V (11 cells each). The region-size check below gives the same outcome in all 36 combinations, and so does a background less affected by dim cells. With an end window of 5 frames and a region of 0.5 of the radius, the 780 V/cm fraction under M1 and the rule "above the largest of the lowest field" is 27.9%, within tolerance, but the fields above it then reach 76.6% (230 V) and 82.1% (250 V), not 90%.

fig6b-650-weak-but-significant¶

With 60-pulse trains at 650 V/cm only 2 cells (5.9%) respond strongly, but the mean Ca2+ signal at the end of recording is significantly above zero and marginally above that at 230 V/cm.

Expected: 2 strongly responding cells, 5.9 +- 0.59%. The claim has no criterion of its own; a strong response is as in fig6b-train-threshold. The statement's tests (one-sample t-test against zero, p < 0.001; single-tailed t-test against 230 V/cm, p < 0.05) are run, with the direction of each effect.

In [20]:
print("Trains, per-cell end dF (%) by voltage: n, mean, s.e., one-sample t-test against 0, and against 70 V "
      "(two-tailed and single-tailed for higher):")
lowest = B[B.volts == 70].end
tb = group_table(B, "end").drop(columns=["diff_vs_sham", "p_vs_sham"])
tb["p_vs_70V_two_tailed"] = [stats.ttest_ind(B[B.volts == v].end, lowest).pvalue if v != 70 else np.nan for v in tb.index]
tb["p_above_70V_one_tailed"] = [stats.ttest_ind(B[B.volts == v].end, lowest, alternative="greater").pvalue
                                if v != 70 else np.nan for v in tb.index]
tb["M1_field"], tb["M2_field"] = fluo4.field(tb.index, "M1").round(), fluo4.field(tb.index, "M2").round()
print(tb.round(5).to_string(), "\n")
rows = []
for m in fluo4.FIELD_PER_VOLT:
    v650, v230 = (fluo4.voltage_for(f, m, avail["train"]) for f in (650, 230))
    x, y = B[B.volts == v650].end, B[B.volts == v230].end
    for k, t in rules.items():
        rows.append({"mapping": m, "V_650": v650, "V_230": v230, "rule": k, "n": len(x),
                     "strong": int((x > t).sum()), "pct_strong": 100 * (x > t).mean()})
    one = stats.ttest_1samp(x, 0)
    print(f"{m}: {v650:.0f} V ({fluo4.field(v650, m):.0f} V/cm), end dF {x.mean():.2f} +- {x.sem():.2f}% (n={len(x)}), "
          f"{'above' if x.mean() > 0 else 'below'} zero; one-sample t-test: t = {one.statistic:.2f}, two-sided p = {one.pvalue:.3g}, "
          f"one-sided p for above zero = {stats.ttest_1samp(x, 0, alternative='greater').pvalue:.3g}; "
          f"against {v230:.0f} V ({fluo4.field(v230, m):.0f} V/cm, {y.mean():.2f}%, n={len(y)}), single-tailed "
          f"Student's t-test for higher p = {stats.ttest_ind(x, y, alternative='greater').pvalue:.3g}")
print(pd.DataFrame(rows).round(1).to_string())
Trains, per-cell end dF (%) by voltage: n, mean, s.e., one-sample t-test against 0, and against 70 V (two-tailed and single-tailed for higher):
        n        mean         se     t_vs_0   p_vs_0  p_vs_70V_two_tailed  p_above_70V_one_tailed  M1_field  M2_field
volts                                                                                                                
0.0    82  -15.677970   0.776710 -20.185169  0.00000              0.00015                 0.99992       0.0       0.0
70.0   28  -10.152490   0.783680 -12.954860  0.00000                  NaN                     NaN     273.0     231.0
100.0   8   -9.028960   1.006140  -8.973890  0.00004              0.47891                 0.23946     390.0     330.0
150.0  38   -8.112160   0.569370 -14.247550  0.00000              0.03448                 0.01724     585.0     495.0
170.0  36   -9.171470   0.832860 -11.012060  0.00000              0.40526                 0.20263     663.0     561.0
180.0  10   -6.430020   1.308880  -4.912600  0.00083              0.01984                 0.00992     702.0     594.0
190.0  10    0.981610   6.716470   0.146150  0.88702              0.01060                 0.00530     741.0     627.0
200.0  68   -1.980660   1.772840  -1.117230  0.26789              0.00462                 0.00231     780.0     660.0
210.0  11    6.416230   6.043690   1.061640  0.31336              0.00015                 0.00007     819.0     693.0
220.0  10    9.634290   7.095770   1.357750  0.20761              0.00006                 0.00003     858.0     726.0
230.0  47   46.191139   9.954540   4.640200  0.00003              0.00004                 0.00002     897.0     759.0
250.0  39   80.989037  15.994040   5.063700  0.00001              0.00001                 0.00000     975.0     825.0
300.0   6  423.102509  18.264259  23.165600  0.00000              0.00000                 0.00000    1170.0     990.0 

M1: 170 V (663 V/cm), end dF -9.17 +- 0.83% (n=36), below zero; one-sample t-test: t = -11.01, two-sided p = 6.41e-13, one-sided p for above zero = 1; against 70 V (273 V/cm, -10.15%, n=28), single-tailed Student's t-test for higher p = 0.203
M2: 200 V (660 V/cm), end dF -1.98 +- 1.77% (n=68), below zero; one-sample t-test: t = -1.12, two-sided p = 0.268, one-sided p for above zero = 0.866; against 70 V (231 V/cm, -10.15%, n=28), single-tailed Student's t-test for higher p = 0.00231
   mapping  V_650  V_230                                 rule   n  strong  pct_strong
0       M1  170.0   70.0                   a: > max of 0 V/cm  36       0         0.0
1       M1  170.0   70.0             a: > max of lowest field  36       2         5.6
2       M1  170.0   70.0         b: > mean + 3 s.d. of 0 V/cm  36       0         0.0
3       M1  170.0   70.0   b: > mean + 3 s.d. of lowest field  36       0         0.0
4       M1  170.0   70.0        b literal: > 3 s.d. of 0 V/cm  36       0         0.0
5       M1  170.0   70.0  b literal: > 3 s.d. of lowest field  36       0         0.0
6       M2  200.0   70.0                   a: > max of 0 V/cm  68       3         4.4
7       M2  200.0   70.0             a: > max of lowest field  68      15        22.1
8       M2  200.0   70.0         b: > mean + 3 s.d. of 0 V/cm  68       6         8.8
9       M2  200.0   70.0   b: > mean + 3 s.d. of lowest field  68       6         8.8
10      M2  200.0   70.0        b literal: > 3 s.d. of 0 V/cm  68       2         2.9
11      M2  200.0   70.0  b literal: > 3 s.d. of lowest field  68       3         4.4

Result: not reproduced. The statement's two tests fail under the adopted conversion and one fails under the other. Under M1 the mean end value at 170 V (650 V/cm) is -9.17 +- 0.83%, significantly below zero, not above it (one-sided p = 1 for above zero), and not above the 70 V group (single-tailed p = 0.20). Under M2 the 200 V mean is -1.98 +- 1.77%, not different from zero (p = 0.27), though above the 70 V group (single-tailed p = 0.0023). The expected count is met under one combination only: M1 (170 V, 36 cells), the rule "above the largest of the lowest field", and the region of 0.7 of the cell radius (2 cells, 5.6%); the other five rules give 0 cells under M1, regions of 0.5 and 1.0 of the radius give 1 cell (2.8%) under the same rule, and M2 (200 V, 68 cells) gives 2 to 15 cells (2.9 to 22.1%). As the tests fail under both conversions, the outcome does not depend on which conversion or rule is taken. The end values carry the decline of the data check.

Region size and end window¶

The region over each cell is 0.7 of the cell's equivalent radius (a standard choice), and the end value is the mean of the last 10 frames (the claims' criterion). The key numbers under regions of 0.5 and 1.0 of the radius and end windows of 5 and 20 frames, then every reading for the trains.

In [21]:
# Region size and end window are standard choices (claims file, opening comment): the key numbers under others.
def cells_for(roi, frames):
    out = []
    for i, r in enumerate(res):
        d = r["dF"] if roi == fluo4.ROI_FRACTION else r["dF_roi"][roi]
        out.append(pd.DataFrame({"set": r["set"], "volts": r["volts"], "end": fluo4.end_value(d, frames),
                                 "peak": fluo4.peak_value(d, r["pulse_frame"])}))
    return pd.concat(out, ignore_index=True)

rows = []
for roi in (0.5, fluo4.ROI_FRACTION, 1.0):
    for frames in (5, fluo4.END_FRAMES, 20):
        c = cells_for(roi, frames)
        a, b = c[c.set == "single"], c[c.set == "train"]
        sham_a = a[a.volts == 0].end
        sb, lb = b[b.volts == 0].end, b[b.volts == 70].end
        row = {"roi_fraction": roi, "end_frames": frames}
        for v in (0, 150, 200, 250, 300, 350):
            row[f"A_end_{v}V"] = a[a.volts == v].end.mean()
        for v in (250, 300):
            row[f"A_p_vs_sham_{v}V"] = stats.ttest_ind(a[a.volts == v].end, sham_a).pvalue
        for v in (300, 350):
            row[f"A_p_vs_0_{v}V"] = stats.ttest_1samp(a[a.volts == v].end, 0).pvalue
        for v in (170, 200, 230, 250, 300):
            x = b[b.volts == v].end
            row[f"B_pct_{v}V_a0"] = 100 * (x > sb.max()).mean()
            row[f"B_pct_{v}V_alow"] = 100 * (x > lb.max()).mean()
        row["B_n_strong_170V_alow"] = int((b[b.volts == 170].end > lb.max()).sum())
        rows.append(row)
robust = pd.DataFrame(rows)
print(robust.round(3).T.to_string())
                            0        1        2        3        4        5        6        7        8
roi_fraction            0.500    0.500    0.500    0.700    0.700    0.700    1.000    1.000    1.000
end_frames              5.000   10.000   20.000    5.000   10.000   20.000    5.000   10.000   20.000
A_end_0V              -11.760  -11.651  -11.381  -12.122  -12.020  -11.720  -12.207  -12.041  -11.758
A_end_150V            -10.687  -10.362  -10.079  -10.532  -10.262  -10.018  -10.144   -9.958   -9.687
A_end_200V             -7.323   -7.285   -6.966   -7.565   -7.419   -7.130   -7.753   -7.611   -7.377
A_end_250V             -5.671   -5.500   -5.313   -5.539   -5.436   -5.290   -5.629   -5.529   -5.428
A_end_300V             -4.110   -4.073   -3.940   -3.873   -3.832   -3.746   -3.591   -3.552   -3.467
A_end_350V              1.496    1.633    1.816    1.140    1.243    1.454    0.834    0.922    1.149
A_p_vs_sham_250V        0.000    0.000    0.000    0.000    0.000    0.000    0.000    0.000    0.000
A_p_vs_sham_300V        0.000    0.000    0.000    0.000    0.000    0.000    0.000    0.000    0.000
A_p_vs_0_300V           0.000    0.000    0.000    0.000    0.000    0.000    0.000    0.000    0.000
A_p_vs_0_350V           0.444    0.409    0.357    0.559    0.531    0.466    0.669    0.644    0.567
B_pct_170V_a0           0.000    0.000    0.000    0.000    0.000    0.000    0.000    0.000    0.000
B_pct_170V_alow         8.333    2.778    0.000    8.333    5.556    0.000    5.556    2.778    0.000
B_pct_200V_a0           4.412    4.412    5.882    4.412    4.412    5.882    4.412    4.412    5.882
B_pct_200V_alow        27.941   25.000   14.706   25.000   22.059   11.765   30.882   25.000   14.706
B_pct_230V_a0          51.064   53.191   61.702   48.936   51.064   57.447   48.936   53.191   57.447
B_pct_230V_alow        76.596   72.340   68.085   80.851   78.723   72.340   87.234   85.106   80.851
B_pct_250V_a0          61.538   61.538   61.538   61.538   61.538   61.538   61.538   61.538   61.538
B_pct_250V_alow        82.051   79.487   76.923   82.051   79.487   76.923   79.487   79.487   76.923
B_pct_300V_a0         100.000  100.000  100.000  100.000  100.000  100.000  100.000  100.000  100.000
B_pct_300V_alow       100.000  100.000  100.000  100.000  100.000  100.000  100.000  100.000  100.000
B_n_strong_170V_alow    3.000    1.000    0.000    3.000    2.000    0.000    2.000    1.000    0.000

For single pulses the group end values move by less than 1% across these choices, and every comparison above keeps its direction and significance. For the trains, the fractions at 200 V under the rule "above the largest of the lowest field" range from 11.8 to 30.9%, and the count at 170 V from 0 to 3. One of these, the 5-frame window with a region of 0.5 of the radius, gives 27.9% at 200 V, within 28 +- 2.8%, but 76.6% at 230 V and 82.1% at 250 V, short of the 90% the criterion needs at the higher fields.

In [22]:
# Trains: every reading of the conversion, the response rule and the region size, with the end
# window of the criterion (last 10 frames).
rows = []
for roi in (0.5, fluo4.ROI_FRACTION, 1.0):
    c = cells_for(roi, fluo4.END_FRAMES)
    b = c[c.set == "train"]
    sb, lb = b[b.volts == 0].end, b[b.volts == 70].end
    thr = {"a: > max of 0 V/cm": sb.max(), "a: > max of lowest field": lb.max(),
           "b: > mean + 3 s.d. of 0 V/cm": sb.mean() + 3 * sb.std(),
           "b: > mean + 3 s.d. of lowest field": lb.mean() + 3 * lb.std(),
           "b literal: > 3 s.d. of 0 V/cm": 3 * sb.std(), "b literal: > 3 s.d. of lowest field": 3 * lb.std()}
    for m in fluo4.FIELD_PER_VOLT:
        v780, v650 = (fluo4.voltage_for(f, m, avail["train"]) for f in (780, 650))
        higher = sorted({fluo4.voltage_for(f, m, avail["train"]) for f in (900, 1000)})
        for k, t in thr.items():
            x650 = b[b.volts == v650].end
            rows.append({"roi": roi, "mapping": m, "rule": k,
                         "pct_780": 100 * (b[b.volts == v780].end > t).mean(),
                         "min_pct_higher": min(100 * (b[b.volts == v].end > t).mean() for v in higher),
                         "n_650": len(x650), "strong_650": int((x650 > t).sum()),
                         "pct_650": 100 * (x650 > t).mean()})
grid = pd.DataFrame(rows)
grid["fig6b_threshold_holds"] = (abs(grid.pct_780 - 28) <= 2.8) & (grid.min_pct_higher >= 90)
grid["fig6b_650_matches"] = (grid.strong_650 == 2) & (abs(grid.pct_650 - 5.9) <= 0.59)
print(grid.round(1).to_string())
print(f"\nfig6b-train-threshold criterion met in {int(grid.fig6b_threshold_holds.sum())} of {len(grid)} combinations; "
      f"780 V/cm fraction within 28 +- 2.8% in {int((abs(grid.pct_780 - 28) <= 2.8).sum())}")
print(f"fig6b-650 expected values (2 cells, 5.9 +- 0.59%) met in {int(grid.fig6b_650_matches.sum())} of {len(grid)}: "
      + "; ".join(f"{r.mapping}, {r.rule}, region {r.roi}" for r in grid[grid.fig6b_650_matches].itertuples()))
    roi mapping                                 rule  pct_780  min_pct_higher  n_650  strong_650  pct_650  fig6b_threshold_holds  fig6b_650_matches
0   0.5      M1                   a: > max of 0 V/cm      4.4            53.2     36           0      0.0                  False              False
1   0.5      M1             a: > max of lowest field     25.0            72.3     36           1      2.8                  False              False
2   0.5      M1         b: > mean + 3 s.d. of 0 V/cm      8.8            61.7     36           0      0.0                  False              False
3   0.5      M1   b: > mean + 3 s.d. of lowest field      8.8            61.7     36           0      0.0                  False              False
4   0.5      M1        b literal: > 3 s.d. of 0 V/cm      2.9            44.7     36           0      0.0                  False              False
5   0.5      M1  b literal: > 3 s.d. of lowest field      4.4            53.2     36           0      0.0                  False              False
6   0.5      M2                   a: > max of 0 V/cm     53.2            61.5     68           3      4.4                  False              False
7   0.5      M2             a: > max of lowest field     72.3            79.5     68          17     25.0                  False              False
8   0.5      M2         b: > mean + 3 s.d. of 0 V/cm     61.7            64.1     68           6      8.8                  False              False
9   0.5      M2   b: > mean + 3 s.d. of lowest field     61.7            66.7     68           6      8.8                  False              False
10  0.5      M2        b literal: > 3 s.d. of 0 V/cm     44.7            61.5     68           2      2.9                  False              False
11  0.5      M2  b literal: > 3 s.d. of lowest field     53.2            61.5     68           3      4.4                  False              False
12  0.7      M1                   a: > max of 0 V/cm      4.4            51.1     36           0      0.0                  False              False
13  0.7      M1             a: > max of lowest field     22.1            78.7     36           2      5.6                  False               True
14  0.7      M1         b: > mean + 3 s.d. of 0 V/cm      8.8            63.8     36           0      0.0                  False              False
15  0.7      M1   b: > mean + 3 s.d. of lowest field      8.8            68.1     36           0      0.0                  False              False
16  0.7      M1        b literal: > 3 s.d. of 0 V/cm      2.9            44.7     36           0      0.0                  False              False
17  0.7      M1  b literal: > 3 s.d. of lowest field      4.4            51.1     36           0      0.0                  False              False
18  0.7      M2                   a: > max of 0 V/cm     51.1            61.5     68           3      4.4                  False              False
19  0.7      M2             a: > max of lowest field     78.7            79.5     68          15     22.1                  False              False
20  0.7      M2         b: > mean + 3 s.d. of 0 V/cm     63.8            64.1     68           6      8.8                  False              False
21  0.7      M2   b: > mean + 3 s.d. of lowest field     68.1            69.2     68           6      8.8                  False              False
22  0.7      M2        b literal: > 3 s.d. of 0 V/cm     44.7            61.5     68           2      2.9                  False              False
23  0.7      M2  b literal: > 3 s.d. of lowest field     51.1            61.5     68           3      4.4                  False              False
24  1.0      M1                   a: > max of 0 V/cm      4.4            53.2     36           0      0.0                  False              False
25  1.0      M1             a: > max of lowest field     25.0            79.5     36           1      2.8                  False              False
26  1.0      M1         b: > mean + 3 s.d. of 0 V/cm      8.8            63.8     36           0      0.0                  False              False
27  1.0      M1   b: > mean + 3 s.d. of lowest field     13.2            70.2     36           0      0.0                  False              False
28  1.0      M1        b literal: > 3 s.d. of 0 V/cm      2.9            44.7     36           0      0.0                  False              False
29  1.0      M1  b literal: > 3 s.d. of lowest field      4.4            53.2     36           0      0.0                  False              False
30  1.0      M2                   a: > max of 0 V/cm     53.2            61.5     68           3      4.4                  False              False
31  1.0      M2             a: > max of lowest field     85.1            79.5     68          17     25.0                  False              False
32  1.0      M2         b: > mean + 3 s.d. of 0 V/cm     63.8            66.7     68           6      8.8                  False              False
33  1.0      M2   b: > mean + 3 s.d. of lowest field     70.2            74.4     68           9     13.2                  False              False
34  1.0      M2        b literal: > 3 s.d. of 0 V/cm     44.7            61.5     68           2      2.9                  False              False
35  1.0      M2  b literal: > 3 s.d. of lowest field     53.2            61.5     68           3      4.4                  False              False

fig6b-train-threshold criterion met in 0 of 36 combinations; 780 V/cm fraction within 28 +- 2.8% in 0
fig6b-650 expected values (2 cells, 5.9 +- 0.59%) met in 1 of 36: M1, a: > max of lowest field, region 0.7

The fig6b-train-threshold criterion is met in none of the 36 combinations of region size, conversion and rule. The fig6b-650 expected values are met in one.

A variant background: the 10th percentile of the cell-free pixels in place of their median, which lowers the effect of dim cells the threshold misses inside the cell-free region (seen in several train fields of view). The sham decline and the train readings under it:

In [23]:
# Variant background (fluo4.BG_LOW_PERCENTILE percentile of the cell-free pixels), main region size.
cl = pd.concat([pd.DataFrame({"set": r["set"], "volts": r["volts"], "end": fluo4.end_value(r["dF_bg_low"]),
                              "end_median_bg": fluo4.end_value(r["dF"])}) for r in res], ignore_index=True)
print("Background over F0 (pre-pulse means), median against variant:",
      {s: (round(float(np.mean([r["bg"][:r["pulse_frame"]].mean() / r["F0"].mean() for r in res if r["set"] == s])), 2),
           round(float(np.mean([r["bg_low"][:r["pulse_frame"]].mean() /
                          (r["F0"] + r["bg"][:r["pulse_frame"]].mean() - r["bg_low"][:r["pulse_frame"]].mean()).mean()
                          for r in res if r["set"] == s])), 2)) for s in ["single", "train"]})
print("Mean end dF (%) by set and voltage, median background against variant:")
print(cl.groupby(["set", "volts"])[["end_median_bg", "end"]].mean().unstack(0).to_string(float_format="%.2f"))
b = cl[cl.set == "train"]
sb, lb = b[b.volts == 0].end, b[b.volts == 70].end
thr = {"a: > max of 0 V/cm": sb.max(), "a: > max of lowest field": lb.max(),
       "b: > mean + 3 s.d. of 0 V/cm": sb.mean() + 3 * sb.std(),
       "b: > mean + 3 s.d. of lowest field": lb.mean() + 3 * lb.std(),
       "b literal: > 3 s.d. of 0 V/cm": 3 * sb.std(), "b literal: > 3 s.d. of lowest field": 3 * lb.std()}
rows = []
for m in fluo4.FIELD_PER_VOLT:
    v780, v650 = (fluo4.voltage_for(f, m, avail["train"]) for f in (780, 650))
    higher = sorted({fluo4.voltage_for(f, m, avail["train"]) for f in (900, 1000)})
    for k, t in thr.items():
        x650 = b[b.volts == v650].end
        rows.append({"mapping": m, "rule": k, "pct_780": 100 * (b[b.volts == v780].end > t).mean(),
                     "min_pct_higher": min(100 * (b[b.volts == v].end > t).mean() for v in higher),
                     "strong_650": int((x650 > t).sum()), "pct_650": 100 * (x650 > t).mean(),
                     "mean_650": x650.mean(), "p_650_above_0": stats.ttest_1samp(x650, 0, alternative="greater").pvalue})
gbg = pd.DataFrame(rows)
gbg["fig6b_threshold_holds"] = (abs(gbg.pct_780 - 28) <= 2.8) & (gbg.min_pct_higher >= 90)
gbg["fig6b_650_count_matches"] = (gbg.strong_650 == 2) & (abs(gbg.pct_650 - 5.9) <= 0.59)
print(gbg.round(3).to_string())
print(f"\nfig6b-train-threshold criterion met in {int(gbg.fig6b_threshold_holds.sum())} of {len(gbg)}; "
      f"fig6b-650 count met in {int(gbg.fig6b_650_count_matches.sum())} of {len(gbg)}")
Background over F0 (pre-pulse means), median against variant: {'single': (0.49, 0.37), 'train': (1.81, 1.21)}
Mean end dF (%) by set and voltage, median background against variant:
      end_median_bg           end       
set          single  train single  train
volts                                   
0.0          -12.02 -15.68 -11.33 -13.50
70.0            NaN -10.15    NaN  -8.49
100.0           NaN  -9.03    NaN  -7.05
150.0        -10.26  -8.11  -9.62  -6.45
170.0           NaN  -9.17    NaN  -7.37
180.0           NaN  -6.43    NaN  -4.97
190.0           NaN   0.98    NaN   0.29
200.0         -7.42  -1.98  -6.85  -1.54
210.0           NaN   6.42    NaN   4.54
220.0           NaN   9.63    NaN   7.24
230.0           NaN  46.19    NaN  36.07
250.0         -5.44  80.99  -5.06  69.04
300.0         -3.83 423.10  -3.51 369.99
350.0          1.24    NaN   1.10    NaN
400.0          1.47    NaN   1.32    NaN
   mapping                                 rule  pct_780  min_pct_higher  strong_650  pct_650  mean_650  p_650_above_0  fig6b_threshold_holds  fig6b_650_count_matches
0       M1                   a: > max of 0 V/cm    4.412          55.319           0    0.000    -7.369          1.000                  False                    False
1       M1             a: > max of lowest field   23.529          87.179           3    8.333    -7.369          1.000                  False                    False
2       M1         b: > mean + 3 s.d. of 0 V/cm    5.882          63.830           0    0.000    -7.369          1.000                  False                    False
3       M1   b: > mean + 3 s.d. of lowest field    8.824          70.213           0    0.000    -7.369          1.000                  False                    False
4       M1        b literal: > 3 s.d. of 0 V/cm    2.941          42.553           0    0.000    -7.369          1.000                  False                    False
5       M1  b literal: > 3 s.d. of lowest field    4.412          51.064           0    0.000    -7.369          1.000                  False                    False
6       M2                   a: > max of 0 V/cm   55.319          61.538           3    4.412    -1.539          0.871                  False                    False
7       M2             a: > max of lowest field   91.489          87.179          16   23.529    -1.539          0.871                  False                    False
8       M2         b: > mean + 3 s.d. of 0 V/cm   63.830          64.103           4    5.882    -1.539          0.871                  False                    False
9       M2   b: > mean + 3 s.d. of lowest field   70.213          84.615           6    8.824    -1.539          0.871                  False                    False
10      M2        b literal: > 3 s.d. of 0 V/cm   42.553          61.538           2    2.941    -1.539          0.871                  False                    False
11      M2  b literal: > 3 s.d. of lowest field   51.064          61.538           3    4.412    -1.539          0.871                  False                    False

fig6b-train-threshold criterion met in 0 of 12; fig6b-650 count met in 0 of 12

With the variant background, the background falls from 1.81 to 1.21 times the corrected cell signal in the train session (pre-pulse means over all train files), but the sham still declines, to -13.5% (train) and -11.3% (single), and the outcomes do not change: the fig6b-train-threshold criterion is met in none of the 12 combinations of conversion and rule (780 V/cm fraction 2.9 to 23.5% under M1 and 42.6 to 91.5% under M2, with the lower of the higher-field fractions at most 87.2%), the 650 V/cm mean stays below zero (-7.37% under M1, -1.54% under M2, one-sided p = 1.0 and 0.87 for above zero), and the count of 2 cells (5.9%) is met in none. So the dim cells inside the cell-free region do not decide these outcomes.

fig5b-conductivity-calibration¶

By Eq. 2, a 10-fold increase of effective membrane conductivity from 0.0003 mS/m reduces the pole IMP by only about 2%, and a 100-fold increase by 12%.

Criterion (claims file): a model computation with the paper's values (cytoplasm 0.4 S/m, medium 1.64 S/m, intact membrane 3e-7 S/m, R 8 um, d 5 nm). The reduction is 1 - f/1.5 (% of IMPC, Fig. 5B legend); f relative to f at the intact conductivity is a variant. Expected: 2 +- 0.2% and 12 +- 1.2%. This uses no recordings.

In [24]:
def f_eq2(lam_m, R=8e-6, d=5e-9, lam_i=0.4, lam_o=1.64):
    """Eq. 2 of the paper, with its constants (Methods 2.3)."""
    num = 3 * lam_o * (3 * R**2 * d * lam_i + (3 * d**2 * R - d**3) * (lam_m - lam_i))
    den = 2 * R**3 * (lam_m + 2 * lam_o) * (lam_m + 0.5 * lam_i) - 2 * (R - d)**3 * (lam_o - lam_m) * (lam_i - lam_m)
    return num / den

lam0 = 3e-7
rows = [{"fold": k, "lambda_m (S/m)": lam0 * k, "f": f_eq2(lam0 * k),
         "reduction_vs_IMPC_%": 100 * (1 - f_eq2(lam0 * k) / 1.5),
         "reduction_vs_intact_f_%": 100 * (1 - f_eq2(lam0 * k) / f_eq2(lam0))} for k in (1, 10, 100)]
print(pd.DataFrame(rows).to_string(index=False))
 fold  lambda_m (S/m)        f  reduction_vs_IMPC_%  reduction_vs_intact_f_%
    1    3.000000e-07 1.497983             0.134444                 0.000000
   10    3.000000e-06 1.480074             1.328367                 1.195530
  100    3.000000e-05 1.322023            11.865152                11.746501

Result: partially reproduced. Eq. 2 with the paper's constants gives a reduction of 1.33% for a 10-fold increase, outside 2 +- 0.2%, and 11.87% for a 100-fold increase, within 12 +- 1.2%. The variant relative to the intact f gives 1.20 and 11.75%. This is a computation from the paper's equation and does not test the dataset.

Summary¶

Claim Outcome
fig6a-ca-entry-1000-1200 reproduced
fig6a-ca-sustained not reproduced
fig6a-780-not-significant reproduced
fig6b-train-threshold not reproduced
fig6b-650-weak-but-significant not reproduced
fig5b-conductivity-calibration partially reproduced (no recordings used)
21 claims on potentiometric imaging (Figs. 1 to 5 and 7) blocked: data missing

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 cell regions are found by a segmentation, not drawn by hand; the region size and the cell-free background region are standard choices; the voltages of the files are converted to field strengths at 3.9 V/cm per V (M1), an assumption that fits the fields and group sizes of Fig. 6B, with 3.3 V/cm per V (M2) shown throughout; all cells in every file are used, where the paper used 14 cells for Fig. 6A and 224 cells, each tested once, for Fig. 6B; the tests the paper does not name are the claims file's readings. No outcome changes between M1 and M2.

What was found about the dataset: it holds the Fluo-4 imaging of Fig. 6 and none of the potentiometric imaging; it gives voltages, not field strengths, and an electrode gap of 1.00 mm where the paper gives 1.6 mm; the train session reuses the same fields of view at up to seven voltages (82 distinct cells, 311 exposures), where the paper says each cell was tested once (224 cells); the single-pulse session holds 27 cells where the paper averaged 14; the description mentions only single pulses though 33 of 73 files are 60-pulse trains; there are no cell regions, so the paper's cell selection cannot be repeated. The Fluo-4 fluorescence of unpulsed cells declines by 12 to 16% over a recording (8.7 and 7.5% before background subtraction, with the background nearly flat), where the 0 V/cm trace of Fig. 6A rises by about 1% and the Fig. 6B traces are flat; the decline decides fig6a-ca-sustained. It is steepest in the first recording of a field of view: in the single-pulse session it slows with each recording, and in the train session it is 11.4% per minute in the first and 2.0 to 5.4% per minute later, without a steady trend. Field strength and recording order therefore go together in both sessions. A background taken as a low percentile of the cell-free pixels, less affected by dim cells there, leaves the decline and every outcome unchanged.