Reproducing the MICrONS Consortium (2025) from dandiset 000402¶

Paper. The MICrONS Consortium (2025). Functional connectomics spanning multiple areas of mouse visual cortex. Nature 640, 435-447. doi:10.1038/s41586-025-08790-w. The claims file was written from the open access version of record (PMC11981939): main text, figure legends, Methods and the captions of the Extended Data. The claims file is claims/000402/10.1038-s41586-025-08790-w.yaml.

Dandiset. DANDI:000402, "MICrONS Two Photon Functional Imaging", version 0.230307.2132 (doi:10.48324/dandi.000402/0.230307.2132). This is the version the paper cites (reference 73, "version 0.230307.2132") and the only published version. The draft lists the same 19 assets and size; it is not used.

What the dandiset holds. 19 NWB files, one per two-photon scan of mouse 17797, each with the raw imaging of every field, the segmentation masks (soma or artifact) and their fluorescence traces, the stimulus video and three stimulus tables (Clip, Monet2, Trippy), treadmill velocity, and pupil position and radii. For some fields the masks carry EM identifiers from CAVE. It does not hold the deconvolved traces, the structural stack, the stack coordinates or visual area of each mask, the oracle scores, the per-period Monet2 directions, the photodiode signal or any EM data.

Which 14 scans. The paper describes 14 of 19 completed scans and lists them in Extended Data Table 1. The text the claims file was read from holds only that table's caption; its cells were read from the table's image in PMC11981939 (41586_2025_8790_Tab1_ESM.jpg): sessions and scans 4-7, 5-6, 5-7, 6-2, 6-4, 6-6, 6-7, 7-3, 7-4, 7-5, 8-5, 9-3, 9-4 and 9-6. The analysis uses these 14 and shows the other 5 where they bear on a count (author step 1.6, a detail taken from the paper).

Outside inputs. Two claims whose data are elsewhere (data: elsewhere), nuclei-144120 and cell-classification, are tested on the MICrONS tables that the BossDB open data bucket serves without an account (s3://bossdb-open-data/iarpa_microns/minnie/minnie65/). The CAVE tables that the other EM and co-registration claims name need an account token, which this run does not have. The bucket also holds exports of the synapse table (versions 1078, 1181 and 1300, about 20 GB each, and a table of subvolume 35; counted, with the EM sections, in em_volume_counts.ipynb, which also prints the files cited here with their sizes) and a connection table with nucleus ids (version 1300), but not the co-registration table or the proofreading table of version 1181 that those claims use to choose their neurons; its proofreading release is from 2021, with 600 rows. It also holds a structural stack (functional_data/two_photon_structural_stacks/); the Fig. 1 legend lists the 2P structural images among the public data resources. The claims that need the stack are limited to the dandiset by the claims file, so they are not tested on it here, and a reader is asked about it (questions/000402.yaml).

Random numbers. None are used.

Claim Data Tested here
scans-14-p75-p81 dataset yes
scan-configurations dataset yes
masks-125413-somatic-115372 dataset yes
unique-neurons-75909 dataset blocked: no stack coordinates in the dataset
area-split-half-visp dataset blocked: no area label per mask
z-drift-under-10um dataset blocked: no structural stack
structural-stack dataset blocked: no structural stack
stimulus-composition dataset yes
stimulus-conserved-across-scans dataset yes
oracle-same-order dataset yes
spontaneous-periods dataset yes
monet2-directions dataset yes, directions measured from the stimulus video
fig3f-oracle-reliability dataset yes, on fluorescence
fig3g-direction-tuning dataset yes, on fluorescence
behaviour-recorded dataset yes
photodiode-89pct dataset blocked: no photodiode signal
manual-coreg-19181, ed4-matched-higher-signal-correlation, fig6k-target-responses with_inputs blocked: CAVE table needs a token; the dataset's own EM identifiers are counted, and the ed4 test is run on them, as variants
nuclei-144120, cell-classification elsewhere yes, BossDB tables
em-sections, synapse-detection elsewhere in em_volume_counts.ipynb
the other 13 EM and co-registration claims elsewhere blocked (see the attempt record)
In [1]:
import time
import warnings

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

import microns

pd.set_option("display.width", 220)
pd.set_option("display.max_columns", 40)
pd.set_option("display.max_rows", 200)
warnings.filterwarnings("ignore", category=RuntimeWarning)

# Extended Data Table 1 (read from its image in PMC11981939): the 14 scans the paper describes.
PAPER_SCANS = [(4, 7), (5, 6), (5, 7), (6, 2), (6, 4), (6, 6), (6, 7), (7, 3), (7, 4), (7, 5), (8, 5),
               (9, 3), (9, 4), (9, 6)]

started = time.time()
assets = microns.list_assets()
meta = microns.load_all_metadata(assets.asset_id, workers=19)
M = {(r.session, r.scan): m for (_, r), m in zip(assets.iterrows(), meta)}
AID = {(r.session, r.scan): r.asset_id for _, r in assets.iterrows()}
print(f"dandiset {microns.DANDISET_ID} version {microns.DANDISET_VERSION}: {len(assets)} assets, "
      f"{assets['size'].sum() / 1e12:.2f} TB; metadata read in {time.time() - started:.0f} s")
dandiset 000402 version 0.230307.2132: 19 assets, 1.35 TB; metadata read in 10 s

Data check¶

Each check of the description against the values is stated under its output.

In [2]:
rows = []
for k in sorted(M):
    m, F = M[k], M[k]["fields"]
    d = pd.Timestamp(m["session_start_time"]).date()
    rows.append({"session": k[0], "scan": k[1], "in_paper": k in PAPER_SCANS, "session_id": m["session_id"],
                 "date": str(d), "age_days": microns.age_days(d), "fields": len(F),
                 "depths_um": sorted(set(F.depth_um.round(1))), "rate_hz": round(F.rate_hz.mean(), 3),
                 "rate_in_description": F.description.iloc[0].split(" at ")[1].split(" Hz")[0],
                 "frames": int(F.n_frames.iloc[0]), "rois": int(F.n_rois.sum()), "soma": int(F.n_soma.sum()),
                 "artifact": int(F.n_artifact.sum()), "fields_with_em_ids": int(F.has_em_columns.sum()),
                 "rois_with_root_id": int(F.n_with_root_id.fillna(0).sum()) if "n_with_root_id" in F else 0})
scans = pd.DataFrame(rows)
print("Subjects:", sorted({m["subject_id"] for m in meta}), "sex", sorted({m["sex"] for m in meta}),
      "age field", sorted({m["age"] for m in meta}))
print("Related publications in the files:", sorted({p for m in meta for p in m["related_publications"]}))
print(f"Files: {len(scans)}; in Extended Data Table 1: {scans.in_paper.sum()}; "
      f"not in it: {[(r.session, r.scan) for r in scans[~scans.in_paper].itertuples()]}")
print(scans.to_string(index=False))
allF = pd.concat([M[k]["fields"].assign(session=k[0], scan=k[1]) for k in sorted(M)])
print("\nPer field, every file:")
print("  field of view (um):", sorted(set(zip(allF.fov_x_um.round(1), allF.fov_y_um.round(1)))),
      " pixels:", sorted(set(zip(allF.dim_x, allF.dim_y))), " grid spacing (um):",
      sorted(set(allF.grid_spacing_um.round(3))))
print("  imaging_rate attribute of the imaging planes:", sorted(set(allF.imaging_rate_attr.astype(str))))
print("  plane location:", sorted(set(allF.location)))
print("  mask types:", sorted({t for ts in allF.mask_types for t in ts}))
print("  trace timestamps equal the imaging timestamps in every field:", bool(allF.trace_same_ts.all()))
print("  ROI ids are 1..n and the traces' rois index them in order:", bool(allF.roi_ids_1_to_n.all() & allF.trace_rois_match.all()))
same_ts = all(all(np.array_equal(t, list(M[k]["frame_ts"].values())[0]) for t in M[k]["frame_ts"].values()) for k in M)
print("  all fields of a scan share one array of frame time stamps:", same_ts)
print("  field origins (x, y um) per scan:", {k: sorted(set(zip(M[k]['fields'].origin_x_um.round(0), M[k]['fields'].origin_y_um.round(0)))) for k in PAPER_SCANS[:3]}, "...")
print("  groups of file 4-7:")
for g, keys in M[(4, 7)]["other_groups"].items():
    print(f"    {g}: {keys}")
strip = lambda d: {g: sorted({x.rstrip("0123456789") for x in ks if not x.startswith("Video")}) for g, ks in d.items()}
print("  every file holds the same kinds of groups (numbered per field, the stimulus video's name aside):",
      all(strip(M[k]["other_groups"]) == strip(M[(4, 7)]["other_groups"]) for k in M))
em = allF[allF.has_em_columns]
print("  fields with EM columns are those with origin x:", sorted(set(em.origin_x_um.round(0))),
      "; without:", sorted(set(allF[~allF.has_em_columns].origin_x_um.round(0))))
Subjects: ['17797'] sex ['M'] age field ['P75D/P81D']
Related publications in the files: ['https://doi.org/10.1101/2021.07.28.454025']
Files: 19; in Extended Data Table 1: 14; not in it: [(4, 9), (4, 10), (5, 3), (8, 7), (8, 9)]
 session  scan  in_paper session_id       date  age_days  fields                    depths_um  rate_hz rate_in_description  frames  rois  soma  artifact  fields_with_em_ids  rois_with_root_id
       4     7      True   4-scan-7 2018-03-04        75       8  [80.0, 220.0, 360.0, 500.0]    6.298              6.3009   40000  8395  7493       902                   4                685
       4     9     False   4-scan-9 2018-03-04        75       8  [90.0, 230.0, 370.0, 510.0]    6.298              6.3009   35112  8548  7652       896                   0                  0
       4    10     False  4-scan-10 2018-03-04        75       8 [100.0, 240.0, 380.0, 520.0]    6.297              6.3009   40000  9758  8481      1277                   0                  0
       5     3     False   5-scan-3 2018-03-05        76       8 [180.0, 320.0, 460.0, 600.0]    6.298              6.3009   40000  8790  8220       570                   4                571
       5     6      True   5-scan-6 2018-03-05        76       8 [150.0, 290.0, 430.0, 570.0]    6.297              6.3009   40000  9254  8592       662                   4                836
       5     7      True   5-scan-7 2018-03-05        76       8 [140.0, 280.0, 420.0, 560.0]    6.297              6.3009   40000  8862  8138       724                   4                747
       6     2      True   6-scan-2 2018-03-06        77       8 [130.0, 270.0, 410.0, 550.0]    6.298              6.3009   40000  8756  8158       598                   4                786
       6     4      True   6-scan-4 2018-03-06        77       8 [115.0, 255.0, 395.0, 535.0]    6.298              6.3009   40000  8826  8221       605                   4                891
       6     6      True   6-scan-6 2018-03-06        77       8 [110.0, 250.0, 390.0, 530.0]    6.297              6.3009   40000  8551  7971       580                   4                702
       6     7      True   6-scan-7 2018-03-06        77       8 [100.0, 240.0, 380.0, 520.0]    6.297              6.3009   40000  8602  7887       715                   4                543
       7     3      True   7-scan-3 2018-03-07        78       8 [190.0, 330.0, 405.0, 470.0]    6.297              6.3009   40000  9296  8618       678                   4                490
       7     4      True   7-scan-4 2018-03-07        78       8 [130.0, 200.0, 340.0, 480.0]    6.297              6.3009   40000 12675 11201      1474                   4                872
       7     5      True   7-scan-5 2018-03-07        78       8 [175.0, 215.0, 350.0, 490.0]    6.297              6.3009   40000  9034  8194       840                   4                426
       8     5      True   8-scan-5 2018-03-08        79       8 [210.0, 220.0, 350.0, 490.0]    6.297              6.3009   40000 10579  9941       638                   4                646
       8     7     False   8-scan-7 2018-03-08        79       8 [170.0, 180.0, 460.0, 600.0]    6.297              6.3009   40000  7927  7530       397                   0                  0
       8     9     False   8-scan-9 2018-03-08        79       8 [310.0, 320.0, 450.0, 590.0]    6.297              6.3009   40000  8535  7898       637                   0                  0
       9     3      True   9-scan-3 2018-03-09        80       6        [295.0, 305.0, 315.0]    8.613             8.61754   50000  8661  7973       688                   3                584
       9     4      True   9-scan-4 2018-03-09        80       6        [150.0, 160.0, 170.0]    8.613             8.61754   50000  8415  7855       560                   3                640
       9     6      True   9-scan-6 2018-03-09        80       4               [155.0, 175.0]    9.616             9.62246   57000  5507  5130       377                   2                 15

Per field, every file:
  field of view (um): [(620.0, 1000.0), (620.0, 1100.0)]  pixels: [(440, 248), (600, 372)]  grid spacing (um): [1.667, 2.5]
  imaging_rate attribute of the imaging planes: [nan]
  plane location: ['VISp,VISrl,VISlm,VISal']
  mask types: [np.str_('artifact'), np.str_('soma')]
  trace timestamps equal the imaging timestamps in every field: True
  ROI ids are 1..n and the traces' rois index them in order: True
  all fields of a scan share one array of frame time stamps: True
  field origins (x, y um) per scan: {(4, 7): [(-485.0, -235.0), (95.0, -235.0)], (5, 6): [(-510.0, -290.0), (70.0, -290.0)], (5, 7): [(-510.0, -290.0), (70.0, -290.0)]} ...
  groups of file 4-7:
    file root: ['acquisition', 'analysis', 'file_create_date', 'general', 'identifier', 'intervals', 'processing', 'session_description', 'session_start_time', 'specifications', 'stimulus', 'timestamps_reference_time']
    acquisition: ['EyeTracking', 'PupilTracking', 'TwoPhotonSeries1', 'TwoPhotonSeries2', 'TwoPhotonSeries3', 'TwoPhotonSeries4', 'TwoPhotonSeries5', 'TwoPhotonSeries6', 'TwoPhotonSeries7', 'TwoPhotonSeries8', 'Video: stimulus_17797_4_7_v4', 'treadmill_velocity']
    stimulus/presentation: []
    stimulus/templates: []
    analysis: []
    processing: ['ophys']
    processing/ophys: ['Fluorescence', 'ImageSegmentation', 'SegmentationImages1', 'SegmentationImages2', 'SegmentationImages3', 'SegmentationImages4', 'SegmentationImages5', 'SegmentationImages6', 'SegmentationImages7', 'SegmentationImages8']
    general: ['devices', 'experiment_description', 'optophysiology', 'related_publications', 'session_id', 'subject']
  every file holds the same kinds of groups (numbered per field, the stimulus video's name aside): True
  fields with EM columns are those with origin x: [35.0, 55.0, 60.0, 70.0, 95.0, 100.0] ; without: [-545.0, -525.0, -520.0, -510.0, -485.0, -480.0, 55.0, 95.0]

The files hold one subject (17797, male), as the paper describes. The age field is the same range, P75D/P81D, in every file, so the day of each scan is taken from the session start date. Frame rates from the time stamps agree with the rates in the field descriptions to within 0.01 Hz; the imaging_rate attribute of every imaging plane is NaN. The field of view and pixel size agree with the Methods (620 x 1,100 um at 0.4 pixels per um, and 620 x 1,000 um at 0.6 pixels per um for scan 9-6). Every plane's location lists all four areas, so no field or mask carries an area. All fields of a scan share one array of time stamps, though the fields are imaged one after another within a frame. The EM columns (pt_root_id and others) exist only in the fields on one side of the field of view (the second field of each depth), and in 15 of the 19 files. The listing of file 4-7 shows every group the files hold, and all files hold the same kinds: under acquisition the two-photon series, the stimulus video, the treadmill and the eye and pupil tracking; under processing/ophys the fluorescence traces, the segmentation and the summary images; stimulus and analysis are empty. There is no structural stack, no deconvolved trace and no photodiode signal.

In [3]:
rows = []
for k in sorted(M):
    m = M[k]
    ts = m["frame_ts"][1]
    stim = pd.concat([m[t][["start_time", "stop_time"]] for t in ("clip", "monet2", "trippy")])
    rows.append({"session": k[0], "scan": k[1], "frames_s": f"{ts[0]:.2f}-{ts[-1]:.1f}",
                 "stimulus_s": f"{stim.start_time.min():.1f}-{stim.stop_time.max():.1f}",
                 "video_s": f"{m['video']['t0']:.2f}-{m['video']['t1']:.1f}",
                 "video_hz": round(m["video"]["rate_hz"], 2),
                 "treadmill_s": f"{m['treadmill']['t0']:.2f}-{m['treadmill']['t1']:.1f}",
                 "treadmill_hz": round(m["treadmill"]["rate_hz"], 1),
                 "treadmill_nan_t": m["treadmill"]["n_nan_t"],
                 "eye_s": f"{m['eye']['t0']:.2f}-{m['eye']['t1']:.1f}", "eye_hz": round(m["eye"]["rate_hz"], 2),
                 "eye_nan_t": m["eye"]["n_nan_t"], "pupil_fit": round(m["eye"]["pupil_fit_fraction"], 3)})
clocks = pd.DataFrame(rows)
print("Time ranges (s) of each stream, on the file's time base:")
print(clocks.to_string(index=False))
print("\nUnits: treadmill", "m/s (conversion 0.01 in the file)", "; eye position and pupil radii: px")
# Recordings deposited twice: compare frame time stamps, stimulus sequences and ROI counts.
keys = sorted(M)
dup = []
for i, a in enumerate(keys):
    for b in keys[i + 1:]:
        same_ts = len(M[a]["frame_ts"][1]) == len(M[b]["frame_ts"][1]) and np.allclose(M[a]["frame_ts"][1], M[b]["frame_ts"][1])
        same_clip = list(M[a]["clip"].condition_hash) == list(M[b]["clip"].condition_hash)
        same_rois = list(M[a]["fields"].n_rois) == list(M[b]["fields"].n_rois)
        if same_ts or same_clip or same_rois:
            dup.append((a, b, same_ts, same_clip, same_rois))
print("\nPairs of files with identical frame times, clip sequences or ROI counts per field:", dup or "none")
Time ranges (s) of each stream, on the file's time base:
 session  scan     frames_s   stimulus_s      video_s  video_hz treadmill_s  treadmill_hz  treadmill_nan_t        eye_s  eye_hz  eye_nan_t  pupil_fit
       4     7 10.15-6360.8  18.9-5089.2 10.14-6360.9      60.0 0.00-6362.4          76.1                0 10.29-5392.9   20.01          0      0.849
       4     9  9.92-5584.7  18.7-5089.4  9.92-5585.0      60.0 0.00-5584.9          72.0               57 11.69-5580.8   19.99         16      0.981
       4    10 10.03-6361.1 209.7-5280.1 10.02-6361.2      60.0 0.00-6362.6          82.9                0 10.19-3097.3   20.01          0      0.714
       5     3  9.95-6360.7 342.3-5412.5  9.94-6360.9      60.0 0.00-6362.7          66.9                0 10.09-1778.4   20.01          0      0.916
       5     6 10.69-6362.1 206.9-5277.4 10.69-6362.2      60.0 0.00-6364.8          63.7              140 10.81-6362.3   19.98         39      0.996
       5     7 10.10-6361.5 212.2-5282.5 10.09-6361.6      60.0 0.00-6364.2          65.5              190 10.21-6361.8   19.97         57      0.977
       6     2 10.35-6360.8 300.8-5373.0 10.34-6360.9      60.0 0.00-6361.8          57.0               25 10.47-6361.1   20.01          6      0.998
       6     4 11.39-6362.1 205.4-5276.1 11.39-6362.2      60.0 0.00-6362.7          58.9              101 11.51-6362.3   20.00         29      0.993
       6     6 11.61-6362.5  20.4-5091.0 11.60-6362.6      60.0 0.00-6364.3         100.1               37 11.69-6362.7   20.01          9      0.986
       6     7 10.45-6361.4 232.8-5303.4 10.44-6361.5      60.0 0.00-6363.3          99.9               58 10.59-6361.6   20.01         12      0.973
       7     3 10.94-6361.8  19.7-5090.6 10.93-6361.9      60.0 0.00-6364.4         100.0                0 11.00-6362.0   20.01          0      0.978
       7     4 10.13-6361.3 204.7-5275.8 10.13-6361.5      60.0 0.00-6362.9         100.0                0 10.30-6361.7   20.01          0      0.999
       7     5 11.60-6363.1 223.2-5294.2 11.59-6363.2      60.0 0.00-6365.6         100.2                0 11.69-6363.3   20.00          0      0.996
       8     5 10.12-6361.3 212.6-5285.2 10.12-6361.4      60.0 0.00-6361.7         100.1               13 10.18-6361.4   20.01          2      0.994
       8     7 10.91-6362.2  19.7-5091.8 10.90-6362.4      60.0 0.00-6364.4          99.7               13 11.03-2238.2   20.01          0      0.936
       8     9 11.15-6362.5 272.5-5343.5 11.14-6362.7      60.0 0.00-6364.4          99.8               12 11.25-6356.4   20.01          3      0.631
       9     3 10.42-5814.8 196.9-5266.9 10.41-5814.9      60.0 0.00-5817.5         100.2                0 10.65-5815.0   20.00          0      0.978
       9     4 10.90-5815.5 209.2-5280.0 10.89-5815.6      60.0 0.00-5817.8          98.8                7 10.95-5815.5   19.99          4      0.991
       9     6 10.65-5937.2 201.4-5272.0 10.64-5937.3      60.0 0.00-5939.3          99.7                0 10.67-5937.3   19.96          0      0.966

Units: treadmill m/s (conversion 0.01 in the file) ; eye position and pupil radii: px

Pairs of files with identical frame times, clip sequences or ROI counts per field: none

The stimulus tables, the stimulus video, the frames, the treadmill and the eye tracking all lie on one time base starting near 0 s: the first imaging frame is at about 10 s, the video spans the imaging period, and the stimulus tables fall inside it. This is consistent with one shared clock; that the traces and the stimulus tables share it is shown below, in the section that reads the traces, by shifting the Monet2 windows in time. Some files have missing (NaN) time stamps in the treadmill and eye series; rates are taken as the median interval, ignoring them. No two files share frame times, stimulus sequences or ROI counts, so no recording was deposited twice.

scans-14-p75-p81¶

The calcium imaging data comprise 14 scans of one mouse, collected between P75 and P81, spanning a volume of about 1,200 x 1,100 x 500 um.

Criterion: count the distinct scans and days; ages from the recording dates against the birth date of 19 December 2017; the claim holds when the dates fall within P75 to P81; 6 or fewer days.

In [4]:
p14 = scans[scans.in_paper]
print("Scans in the dataset:", len(scans), "; distinct (session, scan):", scans[["session", "scan"]].drop_duplicates().shape[0])
print("Scans of Extended Data Table 1 found in the dataset:", len(p14), "of", len(PAPER_SCANS))
print("Days of the 14 scans:", sorted(p14.date.unique()), "->", p14.date.nunique(), "days")
print("Ages (days after 19 December 2017):", sorted(p14.age_days.unique()), "; all within P75-P81:",
      bool(p14.age_days.between(75, 81).all()))
print("Days of all 19 files:", scans.date.nunique(), "; ages", sorted(scans.age_days.unique()))
F14 = allF[allF.set_index(["session", "scan"]).index.isin(PAPER_SCANS)]
print("Field width x height per scan (um), origins on each scan's own axes:")
for k in PAPER_SCANS:
    F = M[k]["fields"]
    print(f"  {k[0]}-{k[1]}: {(F.origin_x_um + F.fov_x_um).max() - F.origin_x_um.min():.0f} x "
          f"{(F.origin_y_um + F.fov_y_um).max() - F.origin_y_um.min():.0f}, depths {F.depth_um.min():.0f}-{F.depth_um.max():.0f}")
print(f"Field depths over the 14 scans: {F14.depth_um.min():.0f}-{F14.depth_um.max():.0f} um")
Scans in the dataset: 19 ; distinct (session, scan): 19
Scans of Extended Data Table 1 found in the dataset: 14 of 14
Days of the 14 scans: ['2018-03-04', '2018-03-05', '2018-03-06', '2018-03-07', '2018-03-08', '2018-03-09'] -> 6 days
Ages (days after 19 December 2017): [np.int64(75), np.int64(76), np.int64(77), np.int64(78), np.int64(79), np.int64(80)] ; all within P75-P81: True
Days of all 19 files: 6 ; ages [np.int64(75), np.int64(76), np.int64(77), np.int64(78), np.int64(79), np.int64(80)]
Field width x height per scan (um), origins on each scan's own axes:
  4-7: 1200 x 1100, depths 80-500
  5-6: 1200 x 1100, depths 150-570
  5-7: 1200 x 1100, depths 140-560
  6-2: 1200 x 1100, depths 130-550
  6-4: 1200 x 1100, depths 115-535
  6-6: 1200 x 1100, depths 110-530
  6-7: 1200 x 1100, depths 100-520
  7-3: 1200 x 1100, depths 190-470
  7-4: 1200 x 1100, depths 130-480
  7-5: 1200 x 1100, depths 175-490
  8-5: 1200 x 1100, depths 210-490
  9-3: 1200 x 1100, depths 295-315
  9-4: 1200 x 1100, depths 150-170
  9-6: 1200 x 1000, depths 155-175
Field depths over the 14 scans: 80-570 um

Result: reproduced. The 14 scans of Extended Data Table 1 are all in the dataset, recorded on 6 days from P75 to P80, within P75 to P81. The dataset holds 19 scans, the 19 completed scans of the Methods, and does not mark which 14 the paper describes; they are identified here only from the table's image. Each scan's fields tile 1,200 x 1,100 um (1,200 x 1,000 um for 9-6), at depths from 80 to 570 um; the dataset gives no common frame for the scans, so the extent of the whole volume (about 500 um deep) is not shown.

scan-configurations¶

Eleven scans were imaged at 6.3 Hz with four depths (eight fields), two at 8.6 Hz with three depths (six fields) and one at 9.6 Hz with two depths (four fields); in the 11 scans the four planes span roughly 300 to 400 um.

Criterion: per scan, the rate, number of fields and distinct depths; rate within 0.1 Hz; counts exact; depth span within 270 to 440 um under reading (a) every scan or (b) the median.

In [5]:
from collections import Counter

def group(r):
    for hz, nd in ((6.3, 4), (8.6, 3), (9.6, 2)):
        if abs(r.rate_hz - hz) <= 0.1 and len(r.depths_um) == nd:
            return f"{hz} Hz, {nd} depths"
    return "none"

sc = scans.copy()
sc["group"] = sc.apply(group, axis=1)
sc["depth_span_um"] = sc.depths_um.apply(lambda d: max(d) - min(d))
sc["fields_by_depth"] = [dict(Counter(M[(r.session, r.scan)]["fields"].depth_um.round(1))) for r in sc.itertuples()]
print(sc[["session", "scan", "in_paper", "rate_hz", "fields", "depths_um", "fields_by_depth", "group", "depth_span_um"]].to_string(index=False))
print("Scans with exactly two fields at every depth:", int(sc.fields_by_depth.apply(lambda d: set(d.values()) == {2}).sum()), "of", len(sc))
p = sc[sc.in_paper]
print("\nThe 14 scans by group:", p.group.value_counts().to_dict())
print("All 19 files by group:", sc.group.value_counts().to_dict())
four = p[p.group == "6.3 Hz, 4 depths"]
print("Depth span of the 6.3 Hz scans (um):", four.depth_span_um.tolist())
print(f"(a) every span in 270-440 um: {bool(four.depth_span_um.between(270, 440).all())}; "
      f"(b) median {four.depth_span_um.median():.0f} um")
print("Smallest gap between depths in the 6.3 Hz scans (um):", four.depths_um.apply(lambda d: min(np.diff(d))).tolist())
 session  scan  in_paper  rate_hz  fields                    depths_um                          fields_by_depth            group  depth_span_um
       4     7      True    6.298       8  [80.0, 220.0, 360.0, 500.0]  {80.0: 2, 220.0: 2, 360.0: 2, 500.0: 2} 6.3 Hz, 4 depths          420.0
       4     9     False    6.298       8  [90.0, 230.0, 370.0, 510.0]  {90.0: 2, 230.0: 2, 370.0: 2, 510.0: 2} 6.3 Hz, 4 depths          420.0
       4    10     False    6.297       8 [100.0, 240.0, 380.0, 520.0] {100.0: 2, 240.0: 2, 380.0: 2, 520.0: 2} 6.3 Hz, 4 depths          420.0
       5     3     False    6.298       8 [180.0, 320.0, 460.0, 600.0] {180.0: 2, 320.0: 2, 460.0: 2, 600.0: 2} 6.3 Hz, 4 depths          420.0
       5     6      True    6.297       8 [150.0, 290.0, 430.0, 570.0] {150.0: 2, 290.0: 2, 430.0: 2, 570.0: 2} 6.3 Hz, 4 depths          420.0
       5     7      True    6.297       8 [140.0, 280.0, 420.0, 560.0] {140.0: 2, 280.0: 2, 420.0: 2, 560.0: 2} 6.3 Hz, 4 depths          420.0
       6     2      True    6.298       8 [130.0, 270.0, 410.0, 550.0] {130.0: 2, 270.0: 2, 410.0: 2, 550.0: 2} 6.3 Hz, 4 depths          420.0
       6     4      True    6.298       8 [115.0, 255.0, 395.0, 535.0] {115.0: 2, 255.0: 2, 395.0: 2, 535.0: 2} 6.3 Hz, 4 depths          420.0
       6     6      True    6.297       8 [110.0, 250.0, 390.0, 530.0] {110.0: 2, 250.0: 2, 390.0: 2, 530.0: 2} 6.3 Hz, 4 depths          420.0
       6     7      True    6.297       8 [100.0, 240.0, 380.0, 520.0] {100.0: 2, 240.0: 2, 380.0: 2, 520.0: 2} 6.3 Hz, 4 depths          420.0
       7     3      True    6.297       8 [190.0, 330.0, 405.0, 470.0] {190.0: 2, 330.0: 2, 405.0: 2, 470.0: 2} 6.3 Hz, 4 depths          280.0
       7     4      True    6.297       8 [130.0, 200.0, 340.0, 480.0] {130.0: 2, 200.0: 2, 340.0: 2, 480.0: 2} 6.3 Hz, 4 depths          350.0
       7     5      True    6.297       8 [175.0, 215.0, 350.0, 490.0] {175.0: 2, 215.0: 2, 350.0: 2, 490.0: 2} 6.3 Hz, 4 depths          315.0
       8     5      True    6.297       8 [210.0, 220.0, 350.0, 490.0] {210.0: 2, 220.0: 2, 350.0: 2, 490.0: 2} 6.3 Hz, 4 depths          280.0
       8     7     False    6.297       8 [170.0, 180.0, 460.0, 600.0] {170.0: 2, 180.0: 2, 460.0: 2, 600.0: 2} 6.3 Hz, 4 depths          430.0
       8     9     False    6.297       8 [310.0, 320.0, 450.0, 590.0] {310.0: 2, 320.0: 2, 450.0: 2, 590.0: 2} 6.3 Hz, 4 depths          280.0
       9     3      True    8.613       6        [295.0, 305.0, 315.0]           {295.0: 2, 305.0: 2, 315.0: 2} 8.6 Hz, 3 depths           20.0
       9     4      True    8.613       6        [150.0, 160.0, 170.0]           {150.0: 2, 160.0: 2, 170.0: 2} 8.6 Hz, 3 depths           20.0
       9     6      True    9.616       4               [155.0, 175.0]                     {155.0: 2, 175.0: 2} 9.6 Hz, 2 depths           20.0
Scans with exactly two fields at every depth: 19 of 19

The 14 scans by group: {'6.3 Hz, 4 depths': 11, '8.6 Hz, 3 depths': 2, '9.6 Hz, 2 depths': 1}
All 19 files by group: {'6.3 Hz, 4 depths': 16, '8.6 Hz, 3 depths': 2, '9.6 Hz, 2 depths': 1}
Depth span of the 6.3 Hz scans (um): [420.0, 420.0, 420.0, 420.0, 420.0, 420.0, 420.0, 280.0, 350.0, 315.0, 280.0]
(a) every span in 270-440 um: True; (b) median 420 um
Smallest gap between depths in the 6.3 Hz scans (um): [140.0, 140.0, 140.0, 140.0, 140.0, 140.0, 140.0, 65.0, 70.0, 40.0, 10.0]

Result: reproduced. Of the 14 scans, 11 are at 6.3 Hz with 4 depths and 8 fields, 2 at 8.6 Hz with 3 depths and 6 fields, and 1 at 9.6 Hz with 2 depths and 4 fields, two fields per depth (the table of fields by depth shows exactly two at each depth in all 19 files). The depth spans of the 11 scans are 280 to 420 um, all inside 270 to 440 um (reading a), median 420 um (reading b). In scan 8-5 two depths are 10 um apart (210 and 220 um).

masks-125413-somatic-115372¶

In total 125,413 masks were segmented across the 14 scans, of which 115,372 were classified as somatic by a trained classifier (8.1% excluded).

Criterion: count all masks over the 14 scans and those marked somatic; reading (a) the dataset holds all masks with a label, (b) only the somatic ones.

In [6]:
tot, soma = int(p14.rois.sum()), int(p14.soma.sum())
print(f"14 scans: {tot} masks, {soma} soma ({100 * (1 - soma / tot):.2f}% artifact)")
print(f"All 19 files: {int(scans.rois.sum())} masks, {int(scans.soma.sum())} soma")
print("Per scan:")
print(p14[["session", "scan", "rois", "soma", "artifact"]].to_string(index=False))
14 scans: 125413 masks, 115372 soma (8.01% artifact)
All 19 files: 168971 masks, 155153 soma
Per scan:
 session  scan  rois  soma  artifact
       4     7  8395  7493       902
       5     6  9254  8592       662
       5     7  8862  8138       724
       6     2  8756  8158       598
       6     4  8826  8221       605
       6     6  8551  7971       580
       6     7  8602  7887       715
       7     3  9296  8618       678
       7     4 12675 11201      1474
       7     5  9034  8194       840
       8     5 10579  9941       638
       9     3  8661  7973       688
       9     4  8415  7855       560
       9     6  5507  5130       377

Result: reproduced. The dataset holds every mask with its label (reading a): 125,413 masks over the 14 scans, 115,372 of them somatic, 8.01% excluded, exactly the paper's counts. Over all 19 files the counts are 168,971 and 155,153.

stimulus-composition¶

Each scan's stimulus lasted about 84 min and was made of 6 oracle clips of 10 s repeated 10 times (10 min), 144 unique natural clips of 10 s (24 min), 90 natural clips of 10 s shown twice (30 min), and 20 Trippy and 20 Monet2 seeds of 15 s each shown twice (10 min each).

Criterion: per scan, count clips by class and their repeats from the stimulus tables; total duration as (a) the sum of trial start to end and (b) the nominal durations; all 14 scans must match. A clip is identified by its condition_hash; seeds by rng_seed.

In [7]:
rows = []
for k in PAPER_SCANS:
    m = M[k]
    c, mo, tr = m["clip"], m["monet2"], m["trippy"]
    vc = c.condition_hash.value_counts()
    cat = c.drop_duplicates("condition_hash").set_index("condition_hash").short_movie_name
    stim = pd.concat([c, mo, tr])
    rows.append({"scan": f"{k[0]}-{k[1]}", "clip_trials": len(c),
                 "shown_10x": int((vc == 10).sum()), "shown_2x": int((vc == 2).sum()), "shown_1x": int((vc == 1).sum()),
                 "other_counts": sorted(set(vc) - {1, 2, 10}),
                 "monet2_seeds": mo.rng_seed.nunique(), "monet2_repeats": sorted(set(mo.rng_seed.value_counts())),
                 "trippy_seeds": tr.rng_seed.nunique(), "trippy_repeats": sorted(set(tr.rng_seed.value_counts())),
                 "clip_s": sorted(set(c.duration)), "param_s": sorted(set(mo.duration) | set(tr.duration)),
                 "sum_trials_min": round((stim.stop_time - stim.start_time).sum() / 60, 2),
                 "nominal_min": round(stim.duration.sum() / 60, 2),
                 "oracle_by_category": dict(cat[vc[vc == 10].index].value_counts()),
                 "twice_by_category": dict(cat[vc[vc == 2].index].value_counts()),
                 "once_by_category": dict(cat[vc[vc == 1].index].value_counts())})
comp = pd.DataFrame(rows)
print(comp.drop(columns=["oracle_by_category", "twice_by_category", "once_by_category"]).to_string(index=False))
print("\nClips per category (oracle / twice / once), same in every scan:",
      all(r == comp.oracle_by_category[0] for r in comp.oracle_by_category),
      all(r == comp.twice_by_category[0] for r in comp.twice_by_category),
      all(r == comp.once_by_category[0] for r in comp.once_by_category))
print(comp.oracle_by_category[0], comp.twice_by_category[0], comp.once_by_category[0])
print("Trial durations from the table (start to stop, s):",
      np.round(np.percentile(np.concatenate([(M[k]['clip'].stop_time - M[k]['clip'].start_time).values for k in PAPER_SCANS]), [0, 50, 100]), 3))
ok = ((comp.shown_10x == 6) & (comp.shown_2x == 90) & (comp.shown_1x == 144) & (comp.monet2_seeds == 20)
      & (comp.trippy_seeds == 20) & comp.monet2_repeats.apply(lambda r: r == [2]) & comp.trippy_repeats.apply(lambda r: r == [2]))
print("Scans matching every count:", int(ok.sum()), "of", len(comp))
print(f"Total duration (a) {comp.sum_trials_min.min()}-{comp.sum_trials_min.max()} min; (b) {comp.nominal_min.unique()} min")
scan  clip_trials  shown_10x  shown_2x  shown_1x other_counts  monet2_seeds monet2_repeats  trippy_seeds trippy_repeats clip_s param_s  sum_trials_min  nominal_min
 4-7          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0
 5-6          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0
 5-7          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0
 6-2          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0
 6-4          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0
 6-6          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0
 6-7          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0
 7-3          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0
 7-4          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0
 7-5          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0
 8-5          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0
 9-3          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0
 9-4          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0
 9-6          384          6        90       144           []            20            [2]            20            [2] [10.0]  [15.0]           83.76         84.0

Clips per category (oracle / twice / once), same in every scan: True True True
{'Rendered': np.int64(2), 'Cinematic': np.int64(2), 'sports1m': np.int64(2)} {'Rendered': np.int64(30), 'Cinematic': np.int64(30), 'sports1m': np.int64(30)} {'sports1m': np.int64(48), 'Cinematic': np.int64(48), 'Rendered': np.int64(48)}
Trial durations from the table (start to stop, s): [9.9   9.967 9.967]
Scans matching every count: 14 of 14
Total duration (a) 83.76-83.76 min; (b) [84.] min

Result: reproduced. All 14 scans have 6 clips shown 10 times, 90 shown twice, 144 shown once, and 20 Monet2 and 20 Trippy seeds shown twice each, with 10 s clips and 15 s parametric trials. The total is 83.76 min from the trial times (reading a) and 84.0 min nominal (reading b). Each category (cinematic, Sports-1M, rendered) has 2 oracle, 30 repeated and 48 unique clips in every scan, as the Methods give.

stimulus-conserved-across-scans¶

The oracle clips, the 90 twice-repeated clips and 10 of the 20 seeds of each parametric stimulus were the same in every scan; the 144 unique clips and the other 10 seeds differed between scans.

Criterion: identify clips by condition_hash and seeds by rng_seed (and their hash); the 6 oracle and 90 repeated clips, and 10 Monet2 and 10 Trippy seeds, occur in all 14 scans; the rest each occur in one scan only.

In [8]:
from collections import Counter, defaultdict

def scans_per(ident):
    where = defaultdict(set)
    for k in PAPER_SCANS:
        for x in ident(M[k]):
            where[x].add(k)
    return where

clip_where = scans_per(lambda m: set(m["clip"].condition_hash))
rep_hash = set.intersection(*[set(M[k]["clip"].condition_hash.value_counts().loc[lambda v: v >= 2].index) for k in PAPER_SCANS])
once_hash = set().union(*[set(M[k]["clip"].condition_hash.value_counts().loc[lambda v: v == 1].index) for k in PAPER_SCANS])
print("Clips: number of scans each hash occurs in:", dict(Counter(len(v) for v in clip_where.values())))
print("  clips shown 2 or 10 times in a scan that occur in all 14 scans:", len(rep_hash))
print("  clips shown once in a scan:", len(once_hash), "; occurring in more than one scan:", sum(len(clip_where[h]) > 1 for h in once_hash))
for name in ("monet2", "trippy"):
    w = scans_per(lambda m: set(m[name].rng_seed))
    wh = scans_per(lambda m: set(m[name].condition_hash))
    counts = Counter(len(v) for v in w.values())
    print(f"{name}: seeds by number of scans {dict(counts)}; hashes by number of scans {dict(Counter(len(v) for v in wh.values()))}")
    for s, ks in w.items():
        if 1 < len(ks) < 14:
            hashes = {h for k in ks for h in M[k][name].loc[M[k][name].rng_seed == s, "condition_hash"]}
            print(f"   seed {s:.0f} occurs in scans {sorted(ks)}, condition hash {hashes}, "
                  f"times shown {[int((M[k][name].rng_seed == s).sum()) for k in sorted(ks)]}")
# The same comparison over all 19 files
all_where = defaultdict(set)
for k in M:
    for h in set(M[k]["clip"].condition_hash):
        all_where[h].add(k)
print("Over all 19 files, clip hashes by number of files:", dict(Counter(len(v) for v in all_where.values())))
Clips: number of scans each hash occurs in: {1: 2016, 14: 96}
  clips shown 2 or 10 times in a scan that occur in all 14 scans: 96
  clips shown once in a scan: 2016 ; occurring in more than one scan: 0
monet2: seeds by number of scans {14: 10, 1: 140}; hashes by number of scans {1: 140, 14: 10}
trippy: seeds by number of scans {1: 138, 14: 10, 2: 1}; hashes by number of scans {1: 138, 14: 10, 2: 1}
   seed 6831 occurs in scans [(6, 4), (7, 5)], condition hash {'Xe6/vzamVqYgecKk7y51'}, times shown [2, 2]
Over all 19 files, clip hashes by number of files: {1: 2736, 19: 96}

Result: partially reproduced. The 96 clips shown 2 or 10 times in a scan (6 oracle and 90 repeated) occur in all 14 scans, the 2,016 clips shown once each occur in one scan only, and 10 Monet2 and 10 Trippy seeds occur in all 14 scans. Of the other seeds, the 140 Monet2 seeds each occur in one scan, but one Trippy seed (6831, the same condition hash) is shown in both scans 6-4 and 7-5, so not every remaining seed is unique to its scan, as the criterion requires. Over all 19 files the 96 shared clips occur in every file.

oracle-same-order¶

The six oracle clips were repeated in the same order ten times per scan.

Criterion: per scan, the 6 clips shown 10 times; each repeat is the 6 trials from each onset of the first oracle clip, in trial order (the paper's code, as the claims file gives it); each of the 10 repeats shows the same 6 clips consecutively and in the same order.

In [9]:
rows = []
orders = {}
for k in PAPER_SCANS:
    c = M[k]["clip"].sort_values("start_time").reset_index(drop=True)
    allstim = pd.concat([M[k][t][["start_time"]] for t in ("clip", "monet2", "trippy")])
    oracle = set(microns.oracle_hashes(c))
    first, reps = microns.oracle_repeats(c)
    seqs = [tuple(r.condition_hash) for r in reps]
    orders[k] = seqs[0]
    rows.append({"scan": f"{k[0]}-{k[1]}", "oracle_clips": len(oracle), "repeats": len(reps),
                 "repeats_all_6_oracle": all(set(s) == oracle and len(s) == 6 for s in seqs),
                 "other_trials_inside_repeats": int(sum(((allstim.start_time > r.start_time.iloc[0]) & (allstim.start_time < r.stop_time.iloc[-1])).sum() - 5 for r in reps)),
                 "distinct_orders": len(set(seqs)),
                 "max_gap_within_repeat_s": round(max((r.start_time.values[1:] - r.stop_time.values[:-1]).max() for r in reps), 3),
                 "repeat_onsets_min": [round(r.start_time.iloc[0] / 60, 1) for r in reps]})
so = pd.DataFrame(rows)
print(so.to_string(index=False))
print("Same oracle clips in every scan:", len({frozenset(o) for o in orders.values()}) == 1,
      "; distinct orders across the 14 scans:", len(set(orders.values())))
scan  oracle_clips  repeats  repeats_all_6_oracle  other_trials_inside_repeats  distinct_orders  max_gap_within_repeat_s                                            repeat_onsets_min
 4-7             6       10                  True                            0                1                    0.117  [6.3, 13.4, 21.4, 28.5, 35.5, 48.6, 55.6, 63.7, 70.7, 77.8]
 5-6             6       10                  True                            0                1                    0.117  [9.5, 16.5, 24.6, 31.6, 38.7, 51.7, 58.8, 66.8, 73.9, 80.9]
 5-7             6       10                  True                            0                1                    0.117  [9.6, 16.6, 24.7, 31.7, 38.7, 51.8, 58.9, 66.9, 74.0, 81.0]
 6-2             6       10                  True                            0                1                    0.117 [11.1, 18.1, 26.1, 33.2, 40.2, 53.3, 60.4, 68.4, 75.5, 82.5]
 6-4             6       10                  True                            0                1                    0.100  [9.5, 16.5, 24.6, 31.6, 38.6, 51.7, 58.8, 66.8, 73.9, 80.9]
 6-6             6       10                  True                            0                1                    0.117  [6.4, 13.4, 21.5, 28.5, 35.6, 48.6, 55.7, 63.7, 70.8, 77.8]
 6-7             6       10                  True                            0                1                    0.117  [9.9, 17.0, 25.0, 32.0, 39.1, 52.2, 59.2, 67.3, 74.3, 81.3]
 7-3             6       10                  True                            0                1                    0.117  [6.4, 13.4, 21.5, 28.5, 35.5, 48.6, 55.7, 63.7, 70.8, 77.8]
 7-4             6       10                  True                            0                1                    0.133  [9.4, 16.5, 24.5, 31.6, 38.6, 51.7, 58.8, 66.8, 73.8, 80.9]
 7-5             6       10                  True                            0                1                    0.117  [9.8, 16.8, 24.9, 31.9, 38.9, 52.0, 59.1, 67.1, 74.2, 81.2]
 8-5             6       10                  True                            0                1                    0.633  [9.6, 16.6, 24.7, 31.7, 38.8, 51.8, 58.9, 66.9, 74.0, 81.0]
 9-3             6       10                  True                            0                1                    0.117  [9.3, 16.4, 24.4, 31.4, 38.5, 51.6, 58.6, 66.7, 73.7, 80.7]
 9-4             6       10                  True                            0                1                    0.100  [9.5, 16.6, 24.6, 31.7, 38.7, 51.8, 58.8, 66.9, 73.9, 81.0]
 9-6             6       10                  True                            0                1                    0.100  [9.4, 16.4, 24.5, 31.5, 38.6, 51.7, 58.7, 66.7, 73.8, 80.8]
Same oracle clips in every scan: True ; distinct orders across the 14 scans: 14

Result: reproduced. In every scan the 10 repeats each show the 6 oracle clips consecutively and in one order (one distinct order per scan), with gaps of at most 0.633 s between clips, and no other trial of any stimulus starts inside a repeat. The order differs between scans (14 distinct orders); the claim is about the order within a scan.

spontaneous-periods¶

Each scan was preceded by 0.15 to 5.5 min with the monitor on and followed by 8.3 to 21.2 min with the monitor off, to record spontaneous activity.

Criterion: per scan, the time from the first imaging frame to the first stimulus onset, and from the last stimulus offset to the last imaging frame; every scan within 0.135 to 6.05 min and 7.47 to 23.32 min.

In [10]:
rows = []
for k in sorted(M):
    m = M[k]
    ts = m["frame_ts"][1]
    stim = pd.concat([m[t][["start_time", "stop_time"]] for t in ("clip", "monet2", "trippy")])
    rows.append({"scan": f"{k[0]}-{k[1]}", "in_paper": k in PAPER_SCANS,
                 "before_min": (stim.start_time.min() - ts[0]) / 60, "after_min": (ts[-1] - stim.stop_time.max()) / 60})
sp = pd.DataFrame(rows)
print(sp.round(3).to_string(index=False))
q = sp[sp.in_paper]
print(f"14 scans: before {q.before_min.min():.3f}-{q.before_min.max():.3f} min, after {q.after_min.min():.2f}-{q.after_min.max():.2f} min")
print("All within the widened ranges:", bool(q.before_min.between(0.135, 6.05).all() and q.after_min.between(7.47, 23.32).all()))
# The four bounds against the claims file's expected values and tolerances
exp = [("before, lower", "before_min", min, 0.15, 0.015), ("before, upper", "before_min", max, 5.5, 0.55),
       ("after, lower", "after_min", min, 8.3, 0.83), ("after, upper", "after_min", max, 21.2, 2.12)]
print("Bounds of the range over scans, against the expected values (tolerance):")
for name, col, f, v, tol in exp:
    a, b = f(q[col]), f(sp[col])
    print(f"  {name}: expected {v} +- {tol}; 14 scans {a:.3f} ({'within' if abs(a - v) <= tol else 'outside'}); "
          f"all 19 files {b:.3f} ({'within' if abs(b - v) <= tol else 'outside'})")
scan  in_paper  before_min  after_min
 4-7      True       0.145     21.193
 4-9     False       0.146      8.255
4-10     False       3.327     18.016
 5-3     False       5.539     15.804
 5-6      True       3.271     18.077
 5-7      True       3.368     17.984
 6-2      True       4.841     16.464
 6-4      True       3.234     18.100
 6-6      True       0.147     21.191
 6-7      True       3.705     17.634
 7-3      True       0.147     21.187
 7-4      True       3.243     18.092
 7-5      True       3.527     17.814
 8-5      True       3.375     17.934
 8-7     False       0.147     21.174
 8-9     False       4.356     16.985
 9-3      True       3.108      9.133
 9-4      True       3.305      8.926
 9-6      True       3.179     11.087
14 scans: before 0.145-4.841 min, after 8.93-21.19 min
All within the widened ranges: True
Bounds of the range over scans, against the expected values (tolerance):
  before, lower: expected 0.15 +- 0.015; 14 scans 0.145 (within); all 19 files 0.145 (within)
  before, upper: expected 5.5 +- 0.55; 14 scans 4.841 (outside); all 19 files 5.539 (within)
  after, lower: expected 8.3 +- 0.83; 14 scans 8.926 (within); all 19 files 8.255 (within)
  after, upper: expected 21.2 +- 2.12; 14 scans 21.193 (within); all 19 files 21.193 (within)

Result: partially reproduced. Over the 14 scans the period before the first stimulus is 0.145 to 4.84 min and the period after the last is 8.93 to 21.19 min, all inside the widened ranges, so the criterion's test for each scan holds. The bounds of the range over the 14 scans do not all match the paper's: the longest period before the stimulus is 4.84 min, outside 5.5 +- 0.55 min, while the other three bounds are within tolerance. Over all 19 files the four bounds are 0.145, 5.54, 8.26 and 21.19 min, all within tolerance of the paper's 0.15, 5.5, 8.3 and 21.2 min, so the paper's range was probably taken over all 19 recordings. The dataset does not say whether the monitor was on or off in these periods.

behaviour-recorded¶

Treadmill velocity, and the pupil position and diameter from video of the left eye, were recorded throughout each scan.

Criterion: all 14 scans hold treadmill velocity and pupil position and diameter covering the imaging period (first to last imaging frame; gaps allowed), each scan's treadmill rate in 51.3 to 110 Hz and the eye camera rate within 18 to 22 Hz. The share of eye frames with a pupil fit is reported.

In [11]:
rows = []
for k in sorted(M):
    m = M[k]
    ts = m["frame_ts"][1]
    span = ts[-1] - ts[0]
    tr, ey = m["treadmill"], m["eye"]
    cover = lambda s: (min(s["t1"], ts[-1]) - max(s["t0"], ts[0])) / span
    rows.append({"scan": f"{k[0]}-{k[1]}", "in_paper": k in PAPER_SCANS,
                 "treadmill_hz": round(tr["rate_hz"], 1), "treadmill_cover": round(cover(tr), 4),
                 "eye_hz": round(ey["rate_hz"], 2), "eye_cover": round(cover(ey), 4),
                 "eye_start_after_first_frame_s": round(ey["t0"] - ts[0], 2),
                 "eye_end_before_last_frame_s": round(ts[-1] - ey["t1"], 1),
                 "eye_max_gap_s": round(ey["max_gap_s"], 2), "pupil_fit": round(ey["pupil_fit_fraction"], 3),
                 "minor_radius_same_times": ey["minor_ts_same"]})
bh = pd.DataFrame(rows)
print(bh.to_string(index=False))
q = bh[bh.in_paper]
print(f"\n14 scans: treadmill {q.treadmill_hz.min()}-{q.treadmill_hz.max()} Hz, eye {q.eye_hz.min()}-{q.eye_hz.max()} Hz")
print("Treadmill covers the imaging period in", int((q.treadmill_cover >= 0.999).sum()), "of 14;",
      "eye covers it (to within 1 s at each end) in",
      int(((q.eye_start_after_first_frame_s < 1) & (q.eye_end_before_last_frame_s < 1)).sum()), "of 14")
print("Pupil fit share over the 14 scans:", q.pupil_fit.min(), "to", q.pupil_fit.max())
o = bh[~bh.in_paper]
late = o[(o.eye_start_after_first_frame_s >= 1) | (o.eye_end_before_last_frame_s >= 1)]
print(f"Files outside the 14 where the eye tracking misses the imaging period by 1 s or more at either end: "
      f"{len(late)} of {len(o)}")
print(late[["scan", "eye_start_after_first_frame_s", "eye_end_before_last_frame_s", "eye_cover"]].to_string(index=False))
print("Every eye and pupil series in file 4-7 (time stamps, s):")
print(pd.DataFrame(M[(4, 7)]["eye_series"]).round(1).to_string(index=False))
scan  in_paper  treadmill_hz  treadmill_cover  eye_hz  eye_cover  eye_start_after_first_frame_s  eye_end_before_last_frame_s  eye_max_gap_s  pupil_fit  minor_radius_same_times
 4-7      True          76.1              1.0   20.01     0.8476                           0.14                        967.9           0.45      0.849                     True
 4-9     False          72.0              1.0   19.99     0.9990                           1.77                          3.9           0.67      0.981                     True
4-10     False          82.9              1.0   20.01     0.4861                           0.16                       3263.8           0.45      0.714                     True
 5-3     False          66.9              1.0   20.01     0.2784                           0.14                       4582.3           0.32      0.916                     True
 5-6      True          63.7              1.0   19.98     1.0000                           0.12                         -0.3           0.55      0.996                     True
 5-7      True          65.5              1.0   19.97     1.0000                           0.11                         -0.3           0.57      0.977                     True
 6-2      True          57.0              1.0   20.01     1.0000                           0.12                         -0.3           0.72      0.998                     True
 6-4      True          58.9              1.0   20.00     1.0000                           0.12                         -0.2           0.63      0.993                     True
 6-6      True         100.1              1.0   20.01     1.0000                           0.08                         -0.2           0.50      0.986                     True
 6-7      True          99.9              1.0   20.01     1.0000                           0.14                         -0.3           0.50      0.973                     True
 7-3      True         100.0              1.0   20.01     1.0000                           0.06                         -0.2           0.20      0.978                     True
 7-4      True         100.0              1.0   20.01     1.0000                           0.17                         -0.3           0.30      0.999                     True
 7-5      True         100.2              1.0   20.00     1.0000                           0.09                         -0.2           0.20      0.996                     True
 8-5      True         100.1              1.0   20.01     1.0000                           0.06                         -0.2           0.52      0.994                     True
 8-7     False          99.7              1.0   20.01     0.3507                           0.12                       4124.0           0.41      0.936                     True
 8-9     False          99.8              1.0   20.01     0.9990                           0.10                          6.2           0.45      0.631                     True
 9-3      True         100.2              1.0   20.00     1.0000                           0.23                         -0.2           0.50      0.978                     True
 9-4      True          98.8              1.0   19.99     1.0000                           0.05                          0.1           0.45      0.991                     True
 9-6      True          99.7              1.0   19.96     1.0000                           0.01                         -0.1           0.45      0.966                     True

14 scans: treadmill 57.0-100.2 Hz, eye 19.96-20.01 Hz
Treadmill covers the imaging period in 14 of 14; eye covers it (to within 1 s at each end) in 13 of 14
Pupil fit share over the 14 scans: 0.849 to 0.999
Files outside the 14 where the eye tracking misses the imaging period by 1 s or more at either end: 5 of 5
scan  eye_start_after_first_frame_s  eye_end_before_last_frame_s  eye_cover
 4-9                           1.77                          3.9     0.9990
4-10                           0.16                       3263.8     0.4861
 5-3                           0.14                       4582.3     0.2784
 8-7                           0.12                       4124.0     0.3507
 8-9                           0.10                          6.2     0.9990
Every eye and pupil series in file 4-7 (time stamps, s):
                          series      n   t0     t1
        EyeTracking/eye_position 104227 10.3 5392.9
PupilTracking/pupil_major_radius 104227 10.3 5392.9
PupilTracking/pupil_minor_radius 104227 10.3 5392.9

Result: partially reproduced. All 14 scans hold treadmill velocity (57.0 to 100.2 Hz) and pupil position and radii (19.96 to 20.01 Hz), and the treadmill covers the imaging period in all 14. The eye tracking covers it in 13: in scan 4-7 it ends 968 s (about 16 min) before the last imaging frame. File 4-7 holds one eye position series and two pupil radius series, all ending at 5,392.9 s, so no other series covers the rest of the scan. Elsewhere the eye tracking starts within 0.23 s of the first frame. A pupil was fitted in 85% to 99.9% of eye frames. By the same 1 s rule, the eye tracking misses the imaging period in all 5 files outside the paper's 14: it ends 3,264 to 4,582 s early in 4-10, 5-3 and 8-7, and 3.9 s and 6.2 s early in 4-9 and 8-9 (in 4-9 it also starts 1.77 s late).

monet2-directions¶

Each 15 s Monet2 block holds 16 equal periods of motion, one for each of 16 directions between 0 and 360 degrees, at 42.8 degrees per second.

Criterion: every Monet2 trial has 16 distinct directions in 0 to 360 degrees and periods within tolerance of 15 s / 16 (843.75 to 1,031.25 ms); spacing reported; speed checked only if given.

The Monet2 table gives num_directions and the duration of each trial but not the direction of each period or its onset. Both are measured here from the stimulus video stored in each file (microns.monet2_trial_directions): the frame-to-frame shift by phase correlation gives the boundaries where the motion direction changes, and the period is estimated as the slope of the boundary times on their index. The direction of each of the 16 equal periods is the direction of the global optical flow over the period (microns.period_directions_flow, the gradient method of Lucas and Kanade). The first round of this attempt took the direction from the summed phase correlation shifts; the sub-pixel peak of the phase correlation is pulled towards whole-pixel shifts, which bends directions towards the axes and diagonals. It is shown here for comparison. This is a measurement made for this attempt, not the paper's method, which read the directions from its stimulus tables. All 40 trials of each of the 14 scans are measured.

In [12]:
started = time.time()
N_SPLIT = 4
jobs, owners = [], []
for k in PAPER_SCANS:
    js = microns.monet2_jobs(AID[k], M[k], N_SPLIT)
    jobs += js
    owners += [k] * len(js)
res = microns.monet2_directions_all(jobs, workers=12)
monet = {}
for k in PAPER_SCANS:
    monet[k] = microns.gather_monet2([r for r, o in zip(res, owners) if o == k], N_SPLIT)
print(f"measured {sum(len(v) for v in monet.values())} Monet2 trials in {time.time() - started:.0f} s")
measured 560 Monet2 trials in 353 s
In [13]:
rows = []
for k in PAPER_SCANS:
    mo = M[k]["monet2"].sort_values("start_time").reset_index(drop=True)
    for i, r in enumerate(monet[k]):
        period, offset = microns.boundary_period(r["boundaries"], 15 / 16)
        d, dpc = r["directions"], r["directions_pc"]
        snapped = np.round(d / 22.5) * 22.5 % 360
        snapped_pc = np.round(dpc / 22.5) * 22.5 % 360
        rows.append({"scan": f"{k[0]}-{k[1]}", "trial": i, "seed": mo.rng_seed[i], "num_directions": mo.num_directions[i],
                     "duration_s": mo.duration[i], "video_hz": r["video_rate_hz"], "boundaries": len(r["boundaries"]),
                     "period_ms": 1000 * period, "first_boundary_offset_ms": 1000 * offset,
                     "distinct": microns.n_distinct_directions(d), "distinct_on_grid": len(set(snapped)),
                     "dev_from_grid_deg": np.max(np.abs((d - snapped + 180) % 360 - 180)),
                     "distinct_pc": microns.n_distinct_directions(dpc), "distinct_on_grid_pc": len(set(snapped_pc)),
                     "dev_from_grid_deg_pc": np.max(np.abs((dpc - snapped_pc + 180) % 360 - 180))})
md2 = pd.DataFrame(rows)
print("num_directions in the table:", sorted(md2.num_directions.unique()), "; durations:", sorted(md2.duration_s.unique()),
      "; video rate:", md2.video_hz.round(2).min(), "-", md2.video_hz.round(2).max(), "Hz")
print("Boundaries found per trial (15 expected inside a trial):", md2.boundaries.value_counts().sort_index().to_dict())
print(f"Period from the boundaries (ms): median {md2.period_ms.median():.1f}, range {md2.period_ms.min():.1f}-{md2.period_ms.max():.1f};"
      f" trials within 843.75-1031.25 ms: {int(md2.period_ms.between(843.75, 1031.25).sum())} of {len(md2)}")
for label, sfx in (("optical flow (used)", ""), ("phase correlation (first round)", "_pc")):
    print(f"{label}:")
    print(f"  distinct directions per trial (10 degree tolerance): {md2['distinct' + sfx].value_counts().to_dict()}")
    print(f"  distinct directions after snapping to the 22.5 degree grid: {md2['distinct_on_grid' + sfx].value_counts().to_dict()}")
    dev = md2["dev_from_grid_deg" + sfx]
    print(f"  largest deviation of a trial's directions from the grid (deg): median over trials {dev.median():.1f}, "
          f"95th percentile {dev.quantile(0.95):.1f}, max {dev.max():.1f}")
all_dev = np.concatenate([np.abs((np.asarray(r["directions"]) - np.round(np.asarray(r["directions"]) / 22.5) * 22.5 + 180) % 360 - 180)
                          for k in PAPER_SCANS for r in monet[k]])
print(f"Optical flow, deviation of each of the {len(all_dev)} periods from the grid (deg): percentiles 50/95/99/100 "
      f"{np.round(np.percentile(all_dev, [50, 95, 99, 100]), 1).tolist()}; periods more than 6.25 deg off "
      f"(the least that brings two grid directions within 10 deg of each other): {int((all_dev > 6.25).sum())}")
# Every trial that the phase correlation (first round) measured with fewer than 16 directions, by either rule
flag = md2[(md2.distinct_pc < 16) | (md2.distinct_on_grid_pc < 16)]
print(f"Trials the phase correlation measured with fewer than 16 directions: {len(flag)} "
      f"({int((md2.distinct_pc < 16).sum())} by the 10 degree rule, {int((md2.distinct_on_grid_pc < 16).sum())} on the grid)")
print(flag[["scan", "trial", "seed", "distinct_pc", "distinct_on_grid_pc", "dev_from_grid_deg_pc",
            "distinct", "distinct_on_grid", "dev_from_grid_deg"]].round(1).to_string(index=False))
missing = lambda d: [float(g) for g in sorted(set(np.arange(16) * 22.5) - set(np.round(d / 22.5) * 22.5 % 360))]
for _, f in flag.iterrows():
    r = monet[tuple(int(x) for x in f.scan.split("-"))][f.trial]
    print(f"scan {f.scan} seed {f.seed}, trial {f.trial}, directions of the 16 periods in order (deg):")
    print("  optical flow:     ", [round(float(x), 1) for x in r["directions"]])
    print("  phase correlation:", [round(float(x), 1) for x in r["directions_pc"]])
    print("  grid points not hit, optical flow:", missing(r["directions"]), "; phase correlation:", missing(r["directions_pc"]))
# Same seed, same directions? Each seed is shown twice in a scan, and 10 seeds in every scan.
agree = []
for k in PAPER_SCANS:
    mo = M[k]["monet2"].sort_values("start_time").reset_index(drop=True)
    for s in mo.rng_seed.unique():
        ii = np.where(mo.rng_seed == s)[0]
        a, b = (np.round(monet[k][i]["directions"] / 22.5) % 16 for i in ii[:2])
        agree.append(np.mean(a == b))
print(f"Repeats of a seed within a scan with the same 16 snapped directions: {np.mean(np.array(agree) == 1):.3f} of {len(agree)}")
print(md2[md2.distinct < 16].to_string(index=False) if (md2.distinct < 16).any() else "optical flow: every trial has 16 distinct directions")
num_directions in the table: [np.int64(16)] ; durations: [np.float64(15.0)] ; video rate: 60.0 - 60.0 Hz
Boundaries found per trial (15 expected inside a trial): {14: 10, 15: 516, 16: 34}
Period from the boundaries (ms): median 937.1, range 929.6-945.4; trials within 843.75-1031.25 ms: 560 of 560
optical flow (used):
  distinct directions per trial (10 degree tolerance): {16: 560}
  distinct directions after snapping to the 22.5 degree grid: {16: 560}
  largest deviation of a trial's directions from the grid (deg): median over trials 4.0, 95th percentile 7.8, max 7.9
phase correlation (first round):
  distinct directions per trial (10 degree tolerance): {16: 556, 15: 4}
  distinct directions after snapping to the 22.5 degree grid: {16: 554, 15: 6}
  largest deviation of a trial's directions from the grid (deg): median over trials 7.0, 95th percentile 9.3, max 11.0
Optical flow, deviation of each of the 8960 periods from the grid (deg): percentiles 50/95/99/100 [1.3, 4.0, 5.0, 7.9]; periods more than 6.25 deg off (the least that brings two grid directions within 10 deg of each other): 36
Trials the phase correlation measured with fewer than 16 directions: 8 (4 by the 10 degree rule, 6 on the grid)
scan  trial   seed  distinct_pc  distinct_on_grid_pc  dev_from_grid_deg_pc  distinct  distinct_on_grid  dev_from_grid_deg
 5-6     10 7637.0           15                   15                  10.1        16                16                4.5
 5-6     26 7637.0           15                   15                  10.1        16                16                4.5
 6-7      3 5145.0           16                   15                  10.9        16                16                3.2
 6-7     11 2618.0           16                   15                  10.6        16                16                4.8
 6-7     23 5145.0           16                   15                  10.9        16                16                3.2
 6-7     35 2618.0           16                   15                  10.6        16                16                4.8
 9-6      9 6154.0           15                   16                   8.4        16                16                5.1
 9-6     21 6154.0           15                   16                   8.4        16                16                5.1
scan 5-6 seed 7637.0, trial 10, directions of the 16 periods in order (deg):
  optical flow:      [319.5, 111.3, 291.4, 336.8, 0.7, 161.4, 224.6, 270.6, 180.1, 20.8, 135.6, 41.2, 200.5, 65.9, 90.4, 244.5]
  phase correlation: [318.9, 124.9, 299.3, 338.6, 357.5, 154.6, 224.8, 271.1, 180.0, 20.8, 134.1, 47.5, 203.2, 64.3, 88.9, 244.4]
  grid points not hit, optical flow: [] ; phase correlation: [112.5]
scan 5-6 seed 7637.0, trial 26, directions of the 16 periods in order (deg):
  optical flow:      [319.5, 111.3, 291.4, 336.8, 0.7, 161.4, 224.6, 270.6, 180.1, 20.8, 135.6, 41.2, 200.5, 65.9, 90.4, 244.5]
  phase correlation: [318.9, 124.9, 299.3, 338.6, 357.5, 154.6, 224.8, 271.1, 180.0, 20.8, 134.1, 47.5, 203.2, 64.3, 88.9, 244.4]
  grid points not hit, optical flow: [] ; phase correlation: [112.5]
scan 6-7 seed 5145.0, trial 3, directions of the 16 periods in order (deg):
  optical flow:      [65.3, 202.2, 22.1, 46.8, 1.5, 224.1, 317.3, 290.1, 337.1, 248.8, 138.2, 159.1, 112.2, 91.4, 268.8, 181.0]
  phase correlation: [63.1, 203.8, 22.0, 41.7, 0.7, 225.8, 319.2, 296.9, 333.0, 238.8, 134.5, 157.2, 124.1, 92.8, 273.8, 179.2]
  grid points not hit, optical flow: [] ; phase correlation: [112.5]
scan 6-7 seed 2618.0, trial 11, directions of the 16 periods in order (deg):
  optical flow:      [160.0, 269.6, 296.1, 20.4, 224.0, 40.2, 336.4, 134.6, 90.2, 67.5, 177.1, 3.4, 245.1, 316.4, 207.0, 114.7]
  phase correlation: [154.9, 268.9, 304.4, 25.2, 224.0, 39.3, 335.5, 138.7, 89.5, 58.6, 184.2, 1.6, 244.6, 319.0, 203.9, 113.6]
  grid points not hit, optical flow: [] ; phase correlation: [292.5]
scan 6-7 seed 5145.0, trial 23, directions of the 16 periods in order (deg):
  optical flow:      [65.3, 202.2, 22.1, 46.8, 1.5, 224.1, 317.3, 290.1, 337.1, 248.8, 138.2, 159.1, 112.2, 91.4, 268.8, 181.0]
  phase correlation: [63.1, 203.8, 22.0, 41.7, 0.7, 225.8, 319.2, 296.9, 333.0, 238.8, 134.5, 157.2, 124.1, 92.8, 273.8, 179.2]
  grid points not hit, optical flow: [] ; phase correlation: [112.5]
scan 6-7 seed 2618.0, trial 35, directions of the 16 periods in order (deg):
  optical flow:      [160.0, 269.6, 296.1, 20.4, 224.0, 40.2, 336.4, 134.6, 90.2, 67.5, 177.1, 3.4, 245.1, 316.4, 207.0, 114.7]
  phase correlation: [154.9, 268.9, 304.4, 25.2, 224.0, 39.3, 335.5, 138.7, 89.5, 58.6, 184.2, 1.6, 244.6, 319.0, 203.9, 113.6]
  grid points not hit, optical flow: [] ; phase correlation: [292.5]
scan 9-6 seed 6154.0, trial 9, directions of the 16 periods in order (deg):
  optical flow:      [158.1, 219.9, 22.1, 313.0, 1.0, 46.1, 177.3, 135.0, 338.5, 247.9, 202.6, 91.3, 112.7, 65.0, 271.3, 293.6]
  phase correlation: [155.8, 224.7, 26.5, 316.0, 3.0, 39.4, 188.4, 133.7, 336.3, 244.6, 196.8, 93.4, 116.7, 59.8, 270.6, 300.1]
  grid points not hit, optical flow: [] ; phase correlation: []
scan 9-6 seed 6154.0, trial 21, directions of the 16 periods in order (deg):
  optical flow:      [158.1, 219.9, 22.1, 313.0, 1.0, 46.1, 177.3, 135.0, 338.5, 247.9, 202.6, 91.3, 112.7, 65.0, 271.3, 293.6]
  phase correlation: [155.8, 224.7, 26.5, 316.0, 3.0, 39.4, 188.4, 133.7, 336.3, 244.6, 196.8, 93.4, 116.7, 59.8, 270.6, 300.1]
  grid points not hit, optical flow: [] ; phase correlation: []
Repeats of a seed within a scan with the same 16 snapped directions: 1.000 of 280
optical flow: every trial has 16 distinct directions
In [14]:
k = PAPER_SCANS[0]
r = monet[k][0]
fig, ax = plt.subplots(1, 2, figsize=(11, 3.2))
ax[0].step(np.arange(16) * 15 / 16, r["directions"], where="post")
for b in r["boundaries"]:
    ax[0].axvline(b, color="0.8", lw=0.8)
ax[0].set(xlabel="time from trial onset (s)", ylabel="measured direction (deg)", title=f"scan {k[0]}-{k[1]}, first Monet2 trial")
ax[1].hist(md2.period_ms, bins=40)
ax[1].set(xlabel="period from boundaries (ms)", ylabel="trials", title=f"{len(md2)} trials, range {md2.period_ms.min():.0f}-{md2.period_ms.max():.0f} ms")
plt.tight_layout()
No description has been provided for this image

Result: reproduced. The table gives 16 directions and 15 s for every Monet2 trial. From the video, the period fitted to the direction changes is 929.6 to 945.4 ms (median 937.1), within tolerance in all 560 trials (40 in each of the 14 scans). With the direction of each period taken from the optical flow, all 560 trials have 16 distinct directions under both measures (10 degree tolerance, and after snapping to the 22.5 degree grid). The measured directions lie within 7.9 degrees of the grid (99% of the 8,960 periods within 5.0 degrees), and the two showings of a seed give the same directions in all 280 pairs. The phase correlation estimate of the first round gave 15 distinct directions in 4 trials (10 degree rule) or 6 (grid), with deviations up to 11.0 degrees; these are 8 trials, both showings of seeds 7637 (scan 5-6), 5145 and 2618 (scan 6-7) and 6154 (scan 9-6). In all 8 the optical flow hits all 16 grid directions (in scan 5-6, seed 7637, the period the phase correlation put at 124.9 degrees measures 111.3, next to the missing 112.5). The spacing is the 22.5 degree grid. The speed (42.8 degrees per second) is not given in the dataset and is not tested. This rests on directions measured from the video, a method of this attempt.

Responses to the oracle clips and to Monet2¶

The next two claims use the traces. The dataset holds fluorescence traces only; the deconvolved traces the paper uses are not in it, so fluorescence takes their place (a deviation). For every mask of the 14 scans, the cell below reads the frames of the 10 oracle repeats and of the 40 Monet2 trials (microns.field_responses), and keeps per mask: the oracle score under the two readings, the mean pairwise correlation between single repeats, and the mean response in the central 500 ms of each direction period, over all 40 trials and over each half of them. Frames are taken from each field's own time stamps; the per-unit delay within a frame that the paper's code adds is not in the dataset and is not applied.

To check that the traces and the stimulus tables share a clock, the Monet2 windows are also shifted by -2, -1, -0.5, +0.5, +1 and +2 direction periods, and the split-half reliability of the direction tuning (correlation over the 16 directions between the first and second 20 trials) is computed at each shift. If the clocks agree, it should be highest at a small positive shift (the response delay of GCaMP6s fluorescence) and fall at whole-period shifts, which assign the wrong direction to each window.

In [15]:
started = time.time()
jobs, owners = [], []
for k in PAPER_SCANS:
    dirs = np.array([r["directions"] for r in monet[k]])
    js = microns.field_jobs(AID[k], M[k], dirs)
    jobs += js
    owners += [(k, j[1]) for j in js]
out = microns.all_field_responses(jobs, workers=12)
R = {o: x for o, x in zip(owners, out)}
print(f"read {len(out)} fields of {len(PAPER_SCANS)} scans in {time.time() - started:.0f} s; "
      f"frames read per field: {min(x['n_frames_read'] for x in out)}-{max(x['n_frames_read'] for x in out)}; "
      f"frames per 500 ms window: {sorted(set(f for x in out for f in x['frames_per_window']))}")
units = []
for (k, fl), x in R.items():
    F = M[k]["fields"].set_index("field").loc[fl]
    n = x["n_rois"]
    units.append(pd.DataFrame({"scan": f"{k[0]}-{k[1]}", "session": k[0], "scan_idx": k[1], "field": fl, "roi": np.arange(n),
                               "score_a": x["score_a"], "score_b": x["score_b"], "pairwise": x["pairwise"],
                               "score_a_resid": x["score_a_resid"]}))
units = pd.concat(units, ignore_index=True)
print("units (all masks):", len(units))
read 104 fields of 14 scans in 93 s; frames read per field: 7674-11785; frames per 500 ms window: [3, 4, 5]
units (all masks): 125413
In [16]:
# mask type of every unit, from the metadata read above (the EM fields keep it; read the others here)
import h5py
def mask_types(asset_id, n_fields):
    f = microns.open_asset(asset_id)
    out = {i: np.array([microns._str(t) for t in f[f"processing/ophys/ImageSegmentation/PlaneSegmentation{i}/mask_type"][:]])
           for i in range(1, n_fields + 1)}
    f.close()
    return out
mtypes = {k: mask_types(AID[k], len(M[k]["fields"])) for k in PAPER_SCANS}
units["mask_type"] = np.concatenate([mtypes[(s, c)][fl] for (s, c, fl), g in units.groupby(["session", "scan_idx", "field"], sort=False)])
print(units.mask_type.value_counts().to_dict())
lag = pd.DataFrame([{"scan": f"{k[0]}-{k[1]}", "field": fl, **x["lag_reliability"]} for (k, fl), x in R.items()])
lag_cols = [c for c in lag.columns if c not in ("scan", "field")]
print("Clock check: median over masks of the split-half tuning reliability, by shift of the Monet2 windows (s).")
print("Median over the fields of each scan:")
print(lag.groupby("scan", sort=False)[lag_cols].median().round(3).to_string())
best = lag[lag_cols].idxmax(axis=1)
print("Shift with the highest reliability, count of fields:", best.value_counts().to_dict())
{'soma': 115372, 'artifact': 10041}
Clock check: median over masks of the split-half tuning reliability, by shift of the Monet2 windows (s).
Median over the fields of each scan:
      -1.875  -0.9375  -0.46875    0.0   0.46875   0.9375   1.875
scan                                                             
4-7    0.145    0.180     0.142  0.311     0.487    0.378   0.183
5-6    0.068    0.066     0.066  0.163     0.326    0.242   0.110
5-7    0.078    0.089     0.106  0.210     0.383    0.318   0.172
6-2    0.079    0.090     0.093  0.224     0.392    0.283   0.115
6-4    0.099    0.116     0.105  0.215     0.352    0.247   0.156
6-6    0.011   -0.014     0.017  0.149     0.298    0.197   0.071
6-7    0.051    0.077     0.059  0.116     0.261    0.165   0.083
7-3    0.077    0.078     0.097  0.273     0.425    0.305   0.154
7-4    0.118    0.135     0.140  0.291     0.442    0.336   0.178
7-5    0.019    0.025     0.053  0.165     0.279    0.195   0.104
8-5    0.038    0.059     0.065  0.173     0.332    0.230   0.107
9-3    0.069    0.073     0.069  0.193     0.390    0.308   0.152
9-4    0.100    0.140     0.151  0.244     0.370    0.267   0.122
9-6    0.062    0.049     0.044  0.147     0.229    0.170   0.065
Shift with the highest reliability, count of fields: {0.46875: 104}

The reliability of the direction tuning is highest when the windows are shifted by +0.47 s in every scan, and lower at whole-period shifts either way, which put each window on a neighbouring direction. The lag is the slow rise and decay of GCaMP6s fluorescence. This shows that the traces and the Monet2 table share a clock to within a fraction of a period in every scan, with the stimulus leading the response, as it must.

fig3f-oracle-reliability¶

Neurons differ in how reliably they respond to the repeated oracle clips; units with high oracle scores give repeat-to-repeat responses that are consistent, and units with low scores do not.

Criterion: the oracle score per unit is the jackknife mean of correlations between each oracle trial and the mean of the remaining trials, (a) one trial = one repeat of the 6 clips (10 trials), (b) each clip scored apart over its 10 repeats and the 6 scores averaged. Units at the 99th, 79th and 34th percentiles of the score are selected and their rasters shown. Reproduced when, under both readings, the score can be computed for the units of the 14 scans and the three selected units' scores increase from the 34th to the 99th percentile with the 99th above 0. Variant: the mean pairwise correlation between single repeats of the 100 units nearest each percentile.

Units are the somatic masks (the paper's functional units); all masks are shown too. The score is computed on fluorescence (see above).

In [17]:
soma = units[units.mask_type == "soma"]
PCTS = (34, 79, 99)
for reading, col in (("a", "score_a"), ("b", "score_b")):
    for name, u in (("somatic masks", soma), ("all masks", units)):
        s = u[col]
        print(f"reading ({reading}), {name}: {s.notna().sum()} of {len(u)} units scored; "
              f"percentiles 1/34/50/79/99: {np.round(np.nanpercentile(s, [1, 34, 50, 79, 99]), 3).tolist()}; "
              f"share below 0: {np.mean(s < 0):.3f}")
picked = {}
for reading, col in (("a", "score_a"), ("b", "score_b")):
    s = soma[col]
    rows = []
    for p in PCTS:
        target = np.nanpercentile(s, p)
        i = (s - target).abs().idxmin()
        near = soma.loc[(s - target).abs().nsmallest(100).index]
        rows.append({"percentile": p, "unit": f"{soma.scan[i]} field {soma.field[i]} roi {soma.roi[i]}",
                     "score": round(s[i], 3), "pairwise_r_of_unit": round(soma.pairwise[i], 3),
                     "pairwise_r_100_nearest_mean": round(near.pairwise.mean(), 3)})
        picked[(reading, p)] = i
    t = pd.DataFrame(rows)
    print(f"\nreading ({reading}), selected units:")
    print(t.to_string(index=False))
    sc_ = t.score.values
    print(f"  increasing from 34th to 99th: {bool(np.all(np.diff(sc_) > 0))}; 99th above 0: {bool(sc_[-1] > 0)}")
# Shared signal: artifact masks alone, the mean trace of each field, and each mask with its fit on
# the field's mean trace removed (fluorescence holds neuropil and any light from the monitor).
art = units[units.mask_type == "artifact"]
print(f"\nreading (a), artifact masks only: {art.score_a.notna().sum()} scored; percentiles 34/79/99: "
      f"{np.round(np.nanpercentile(art.score_a, PCTS), 3).tolist()}; somatic: {np.round(np.nanpercentile(soma.score_a, PCTS), 3).tolist()}")
fm = np.array([x["score_field_mean"] for x in R.values()])
print(f"reading (a), score of the mean trace over all masks of a field: median {np.median(fm):.3f}, range {fm.min():.3f}-{fm.max():.3f} ({len(fm)} fields)")
for name, u in (("somatic", soma), ("artifact", art)):
    print(f"reading (a), {name} masks with the field's mean trace regressed out: percentiles 34/79/99 "
          f"{np.round(np.nanpercentile(u.score_a_resid, PCTS), 3).tolist()}; share below 0: {np.mean(u.score_a_resid < 0):.3f}")
s = soma.score_a_resid
rows = []
for p_ in PCTS:
    i = (s - np.nanpercentile(s, p_)).abs().idxmin()
    rows.append(round(float(s[i]), 3))
print(f"  units at the 34th, 79th and 99th percentiles of the residual score: {rows}; increasing: {bool(np.all(np.diff(rows) > 0))}")
print("\nCorrelation between the two readings over somatic masks:", round(soma[["score_a", "score_b"]].corr().iloc[0, 1], 3))
print("Median score (a) by scan:")
print(soma.groupby("scan", sort=False).score_a.agg(["count", "median", lambda x: np.nanpercentile(x, 99)]).round(3).to_string())
reading (a), somatic masks: 115372 of 115372 units scored; percentiles 1/34/50/79/99: [0.024, 0.208, 0.266, 0.396, 0.702]; share below 0: 0.004
reading (a), all masks: 125413 of 125413 units scored; percentiles 1/34/50/79/99: [0.023, 0.21, 0.268, 0.397, 0.698]; share below 0: 0.004
reading (b), somatic masks: 115372 of 115372 units scored; percentiles 1/34/50/79/99: [0.022, 0.151, 0.19, 0.275, 0.465]; share below 0: 0.003
reading (b), all masks: 125413 of 125413 units scored; percentiles 1/34/50/79/99: [0.022, 0.153, 0.192, 0.277, 0.465]; share below 0: 0.003

reading (a), selected units:
 percentile                unit  score  pairwise_r_of_unit  pairwise_r_100_nearest_mean
         34 5-7 field 1 roi 378  0.208               0.086                        0.084
         79 9-4 field 3 roi 665  0.396               0.187                        0.203
         99 6-6 field 5 roi 438  0.702               0.528                        0.524
  increasing from 34th to 99th: True; 99th above 0: True

reading (b), selected units:
 percentile                unit  score  pairwise_r_of_unit  pairwise_r_100_nearest_mean
         34 7-3 field 5 roi 333  0.151               0.067                        0.096
         79   6-4 field 5 roi 4  0.275               0.136                        0.205
         99 5-7 field 1 roi 668  0.465               0.617                        0.394
  increasing from 34th to 99th: True; 99th above 0: True

reading (a), artifact masks only: 10041 scored; percentiles 34/79/99: [0.235, 0.402, 0.65]; somatic: [0.208, 0.396, 0.702]
reading (a), score of the mean trace over all masks of a field: median 0.499, range 0.161-0.780 (104 fields)
reading (a), somatic masks with the field's mean trace regressed out: percentiles 34/79/99 [0.2, 0.38, 0.665]; share below 0: 0.004
reading (a), artifact masks with the field's mean trace regressed out: percentiles 34/79/99 [0.229, 0.38, 0.613]; share below 0: 0.006
  units at the 34th, 79th and 99th percentiles of the residual score: [0.2, 0.38, 0.665]; increasing: True

Correlation between the two readings over somatic masks: 0.804
Median score (a) by scan:
      count  median  <lambda_0>
scan                           
4-7    7493   0.348       0.715
5-6    8592   0.297       0.782
5-7    8138   0.259       0.707
6-2    8158   0.287       0.704
6-4    8221   0.309       0.745
6-6    7971   0.255       0.750
6-7    7887   0.213       0.633
7-3    8618   0.247       0.673
7-4   11201   0.208       0.515
7-5    8194   0.283       0.724
8-5    9941   0.288       0.709
9-3    7973   0.277       0.671
9-4    7855   0.232       0.629
9-6    5130   0.253       0.606
In [18]:
fig, axes = plt.subplots(2, 3, figsize=(13, 5.5))
for row, reading in enumerate(("a", "b")):
    for col, p in enumerate(PCTS[::-1]):
        i = picked[(reading, p)]
        k = (int(soma.session[i]), int(soma.scan_idx[i]))
        on, nfr, _ = microns.oracle_setup(M[k])
        tr = microns.oracle_trace(AID[k], int(soma.field[i]), int(soma.roi[i]), M[k]["frame_ts"][int(soma.field[i])], on, nfr)
        z = (tr - tr.min(axis=1, keepdims=True)) / np.ptp(tr, axis=1, keepdims=True)
        ax = axes[row, col]
        ax.imshow(z, aspect="auto", cmap="Greys", interpolation="nearest", vmin=0, vmax=1)
        ax.set(title=f"({reading}) {p}th pct, score {soma.loc[i, 'score_' + reading]:.2f}", xlabel="frame in repeat", ylabel="repeat")
plt.suptitle("Oracle rasters, each repeat min-max normalised (fluorescence)")
plt.tight_layout()
No description has been provided for this image

Result: reproduced. Under both readings the score is computed for all 115,372 somatic masks of the 14 scans. The units at the 34th, 79th and 99th percentiles have scores 0.208, 0.396 and 0.702 under reading (a) and 0.151, 0.275 and 0.465 under reading (b), increasing and above 0. In the variant, the mean pairwise correlation between single repeats of the 100 units nearest each percentile rises from 0.084 to 0.203 to 0.524 (a) and from 0.096 to 0.205 to 0.394 (b). The criterion is met by any score whose 99th percentile is above 0, so it tests little. The 10,041 artifact masks score as high as the somatic ones (0.235, 0.402 and 0.650 at the three percentiles), and the mean trace of a field scores 0.499 (median over 104 fields). Removing each mask's fit on its field's mean trace barely changes the scores (somatic 0.200, 0.380 and 0.665; artifact 0.229, 0.380 and 0.613), so a signal shared across the field does not drive them. What makes the artifact masks score as high is not shown here; a local neuropil signal or real neurites could account for it. The score on fluorescence therefore cannot separate the reliability of a neuron from that of what else its mask holds. The outcome rests on fluorescence in place of the deconvolved traces.

fig3g-direction-tuning¶

Monet2 drives direction-tuned responses; neurons can be found preferring each of the 16 directions, and their trial-averaged responses peak at their preferred direction.

Criterion: per unit, the response in the central 500 ms of each direction period, averaged over the 40 Monet2 trials; preferred direction (a) the direction with the highest mean response, (b) the peak of a von Mises function fitted to the 16 mean responses. Reproduced when, under both readings, every one of the 16 directions is preferred by at least 5 units. Variants: the 5 units per direction with the highest preference index are shown, and a split-half check (preferred direction from the first 20 trials, response from the last 20) is reported.

Fluorescence has an arbitrary offset, so the ratio of the preferred response to the mean is not defined for it; the preference index used to rank units is the preferred response minus the mean over directions, divided by the standard deviation over directions (a deviation from the claims file's ratio, which only chooses the units shown). The direction of each period comes from the video (monet2-directions). Units are the somatic masks.

In [19]:
order = [(k, fl) for (k, fl) in R]   # same order as `units`
by_dir = np.concatenate([R[o]["by_dir"] for o in order], axis=2)           # (3, 16, units)
by_late = np.concatenate([R[o]["by_dir_late"] for o in order], axis=2)
is_soma = (units.mask_type == "soma").values
grid = np.arange(16) * 22.5

def preferred(resp):
    """resp (16, n): (a) argmax direction, (b) von Mises peak snapped to the grid."""
    ok = np.all(np.isfinite(resp), axis=0)
    a = np.where(ok, np.nanargmax(np.where(np.isfinite(resp), resp, -np.inf), axis=0), -1)
    mu = microns.von_mises_peak(resp.T)
    b = np.where(np.isfinite(mu), np.round(mu / 22.5).astype(int) % 16, -1)
    return a, b, ok

counts = {}
for name, arr in (("central 500 ms", by_dir), ("shifted +0.47 s", by_late)):
    a, b, ok = preferred(arr[0][:, is_soma])
    ca = np.bincount(a[a >= 0], minlength=16)
    cb = np.bincount(b[b >= 0], minlength=16)
    counts[name] = (ca, cb)
    print(f"{name}: {ok.sum()} of {is_soma.sum()} somatic masks with all 16 responses")
    print("  (a) units per preferred direction:", dict(zip(grid, ca)))
    print("  (b) units per preferred direction:", dict(zip(grid, cb)))
    print(f"  fewest units on a direction: (a) {ca.min()}, (b) {cb.min()}; every direction >= 5: {bool(ca.min() >= 5 and cb.min() >= 5)}")
# Reference without tuning: the difference between the two halves of the trials cancels the tuning
# both halves share and leaves trial-to-trial variability; its argmax counts per direction.
diff = by_dir[1][:, is_soma] - by_dir[2][:, is_soma]
cn = np.bincount(np.nanargmax(np.where(np.isfinite(diff), diff, -np.inf), axis=0), minlength=16)
print(f"reference, argmax of first-half minus second-half responses: fewest units on a direction {cn.min()}, most {cn.max()}")
central 500 ms: 115372 of 115372 somatic masks with all 16 responses
  (a) units per preferred direction: {np.float64(0.0): np.int64(6998), np.float64(22.5): np.int64(6609), np.float64(45.0): np.int64(7352), np.float64(67.5): np.int64(5205), np.float64(90.0): np.int64(8467), np.float64(112.5): np.int64(7821), np.float64(135.0): np.int64(5556), np.float64(157.5): np.int64(6413), np.float64(180.0): np.int64(10606), np.float64(202.5): np.int64(6994), np.float64(225.0): np.int64(6694), np.float64(247.5): np.int64(7736), np.float64(270.0): np.int64(11747), np.float64(292.5): np.int64(5635), np.float64(315.0): np.int64(5471), np.float64(337.5): np.int64(6068)}
  (b) units per preferred direction: {np.float64(0.0): np.int64(6799), np.float64(22.5): np.int64(6480), np.float64(45.0): np.int64(6815), np.float64(67.5): np.int64(5142), np.float64(90.0): np.int64(9173), np.float64(112.5): np.int64(8140), np.float64(135.0): np.int64(5495), np.float64(157.5): np.int64(6532), np.float64(180.0): np.int64(11475), np.float64(202.5): np.int64(6664), np.float64(225.0): np.int64(5683), np.float64(247.5): np.int64(8206), np.float64(270.0): np.int64(12231), np.float64(292.5): np.int64(5792), np.float64(315.0): np.int64(5084), np.float64(337.5): np.int64(5661)}
  fewest units on a direction: (a) 5205, (b) 5084; every direction >= 5: True
shifted +0.47 s: 115372 of 115372 somatic masks with all 16 responses
  (a) units per preferred direction: {np.float64(0.0): np.int64(7351), np.float64(22.5): np.int64(6413), np.float64(45.0): np.int64(6081), np.float64(67.5): np.int64(5031), np.float64(90.0): np.int64(9311), np.float64(112.5): np.int64(7522), np.float64(135.0): np.int64(5073), np.float64(157.5): np.int64(7321), np.float64(180.0): np.int64(11414), np.float64(202.5): np.int64(6948), np.float64(225.0): np.int64(5919), np.float64(247.5): np.int64(7511), np.float64(270.0): np.int64(13239), np.float64(292.5): np.int64(5492), np.float64(315.0): np.int64(5496), np.float64(337.5): np.int64(5250)}
  (b) units per preferred direction: {np.float64(0.0): np.int64(7711), np.float64(22.5): np.int64(6413), np.float64(45.0): np.int64(5551), np.float64(67.5): np.int64(4698), np.float64(90.0): np.int64(9709), np.float64(112.5): np.int64(7671), np.float64(135.0): np.int64(5086), np.float64(157.5): np.int64(6964), np.float64(180.0): np.int64(12762), np.float64(202.5): np.int64(6518), np.float64(225.0): np.int64(5105), np.float64(247.5): np.int64(7742), np.float64(270.0): np.int64(13877), np.float64(292.5): np.int64(5697), np.float64(315.0): np.int64(4883), np.float64(337.5): np.int64(4985)}
  fewest units on a direction: (a) 5031, (b) 4698; every direction >= 5: True
reference, argmax of first-half minus second-half responses: fewest units on a direction 6153, most 8217
In [20]:
resp = by_dir[0][:, is_soma]
sd = np.nanstd(resp, axis=0)
pi = (np.nanmax(resp, axis=0) - np.nanmean(resp, axis=0)) / sd
a_pref, b_pref, ok = preferred(resp)
sel = []
for d in range(16):
    idx = np.where((a_pref == d) & np.isfinite(pi))[0]
    sel += list(idx[np.argsort(pi[idx])[::-1][:5]])
sel = np.array(sel)
# split-half: preferred direction from the first 20 trials, response from the last 20
first, second = by_dir[1][:, is_soma], by_dir[2][:, is_soma]
pref1 = np.nanargmax(np.where(np.isfinite(first), first, -np.inf), axis=0)
at_pref = second[pref1, np.arange(second.shape[1])]
others = (np.nansum(second, axis=0) - at_pref) / 15
above = at_pref > others
print(f"Split half, all somatic masks: response at the first-half preferred direction exceeds the mean of the other 15 "
      f"in the second half for {np.mean(above[ok]):.3f} of {ok.sum()} (0.5 expected without tuning)")
print(f"Split half, the 80 units shown: {np.mean(above[sel]):.3f}; the same 80 with the preferred direction "
      f"of the first half equal to that of all trials: {np.mean(pref1[sel] == a_pref[sel]):.3f}")
# The 80 units shown are chosen on all 40 trials, second half included, so their split-half share is
# raised by the selection. The same choice made on the first half alone is free of that.
pi1 = (np.nanmax(first, axis=0) - np.nanmean(first, axis=0)) / np.nanstd(first, axis=0)
sel1 = []
for d in range(16):
    idx = np.where((pref1 == d) & np.isfinite(pi1) & ok)[0]
    sel1 += list(idx[np.argsort(pi1[idx])[::-1][:5]])
sel1 = np.array(sel1)
print(f"Split half, 80 units chosen the same way on the first half only: {np.mean(above[sel1]):.3f}")
# A stimulus-locked signal shared by the masks of a field would also pass this test. Two checks:
# the artifact masks, and how many masks of a field share one first-half preferred direction.
fa, sa = by_dir[1][:, ~is_soma], by_dir[2][:, ~is_soma]
ok_a = np.all(np.isfinite(by_dir[0][:, ~is_soma]), axis=0)
pa = np.nanargmax(np.where(np.isfinite(fa), fa, -np.inf), axis=0)
at_a = sa[pa, np.arange(sa.shape[1])]
above_a = at_a > (np.nansum(sa, axis=0) - at_a) / 15
print(f"Split half, artifact masks: {np.mean(above_a[ok_a]):.3f} of {ok_a.sum()}")
fid = (units.scan.astype(str) + " " + units.field.astype(str)).values[is_soma]
mode_share = pd.Series(pref1[ok]).groupby(fid[ok]).agg(lambda x: x.value_counts(normalize=True).iloc[0])
print(f"Share of a field's somatic masks on the field's most common first-half preferred direction: median "
      f"{mode_share.median():.3f}, range {mode_share.min():.3f}-{mode_share.max():.3f} over {len(mode_share)} fields "
      f"(1/16 = 0.0625 if spread evenly)")
pi_split = (at_pref - others) / sd
print(f"Second-half preference index of the 80 units: median {np.nanmedian(pi_split[sel]):.2f}; of all units: {np.nanmedian(pi_split[ok]):.2f}")
print(f"Preference index (all trials) of the 80 units: {np.nanmin(pi[sel]):.2f}-{np.nanmax(pi[sel]):.2f}")
u_sel = units[is_soma].iloc[sel]
print("Scans of the 80 units:", u_sel.scan.value_counts().to_dict())
z = (resp[:, sel] - np.nanmean(resp[:, sel], axis=0)) / sd[sel]
fig, ax = plt.subplots(1, 2, figsize=(12, 5))
im = ax[0].imshow(z.T, aspect="auto", cmap="viridis", interpolation="nearest")
ax[0].set(xlabel="direction (deg)", ylabel="unit (5 per preferred direction)", title="trial-averaged response, z over directions",
          xticks=range(0, 16, 4), xticklabels=[f"{g:.0f}" for g in grid[::4]])
plt.colorbar(im, ax=ax[0])
ax[1].bar(grid, counts["central 500 ms"][0], width=10, label="(a) argmax")
ax[1].bar(grid + 10, counts["central 500 ms"][1], width=10, label="(b) von Mises")
ax[1].set(xlabel="preferred direction (deg)", ylabel="somatic masks", title="units per preferred direction")
ax[1].legend()
plt.tight_layout()
print("z range shown:", np.round(np.nanmin(z), 2), np.round(np.nanmax(z), 2))
Split half, all somatic masks: response at the first-half preferred direction exceeds the mean of the other 15 in the second half for 0.629 of 115372 (0.5 expected without tuning)
Split half, the 80 units shown: 0.912; the same 80 with the preferred direction of the first half equal to that of all trials: 0.912
Split half, 80 units chosen the same way on the first half only: 0.637
Split half, artifact masks: 0.659 of 10041
Share of a field's somatic masks on the field's most common first-half preferred direction: median 0.113, range 0.086-0.177 over 104 fields (1/16 = 0.0625 if spread evenly)
Second-half preference index of the 80 units: median 2.87; of all units: 0.44
Preference index (all trials) of the 80 units: 3.15-3.73
Scans of the 80 units: {'9-4': 12, '6-7': 10, '9-3': 7, '7-4': 7, '6-6': 7, '9-6': 6, '5-7': 5, '5-6': 5, '7-3': 5, '8-5': 5, '4-7': 4, '6-4': 3, '7-5': 2, '6-2': 2}
z range shown: -1.63 3.73
No description has been provided for this image

Result: reproduced. Under both readings every one of the 16 directions is preferred by at least 5,084 somatic masks (reading a, at least 5,205), and by at least 4,698 with the windows shifted by +0.47 s. The count tests little with this many units: the argmax of the difference between the two halves of the trials, which holds no shared tuning, also puts thousands of units on every direction (see the reference line above). The split-half variant gives the evidence of tuning: over all somatic masks, the response at the first-half preferred direction exceeds the mean of the other 15 in the second half for 62.9% (0.5 without tuning), and for 63.7% of 80 units chosen on the first half alone. The 80 units shown are chosen on all 40 trials, second half included, so their 91.2% is raised by the selection and is not evidence. The test does not separate the tuning of neurons from a stimulus-locked signal that masks share: the artifact masks pass it as often (65.9% of 10,041), and the most common first-half preferred direction of a field holds a median 11.3% of its somatic masks (range 8.6% to 17.7% over 104 fields), against 6.25% if the preferences were spread evenly. The split-half reliability of the tuning also peaks at a shift of +0.47 s in all 104 fields (clock check above). The outcome rests on fluorescence and on directions measured from the video.

EM identifiers held by the dataset (variant for manual-coreg-19181)¶

The claim manual-coreg-19181 is blocked: its table, CAVE coregistration_manual_v4, needs an account token. The claims file names the EM identifiers the dataset holds in its plane segmentation columns as a variant; they are counted here. The description says they were taken from CAVE on 2023-02-12, and pt_root_id is described as from the "Proofread Segmentation (v343)".

In [21]:
rows = []
roots = []
for k in PAPER_SCANS:
    F = M[k]["fields"]
    for _, f in F.iterrows():
        if f.has_em_columns:
            r = f.root_ids[np.isfinite(f.root_ids)]
            roots.append(pd.DataFrame({"scan": f"{k[0]}-{k[1]}", "field": f.field, "root": r.astype(np.int64)}))
        rows.append({"scan": f"{k[0]}-{k[1]}", "field": f.field, "em_columns": f.has_em_columns,
                     "rois": f.n_rois, "with_root_id": int(f.n_with_root_id) if f.has_em_columns else 0})
emc = pd.DataFrame(rows)
roots = pd.concat(roots)
print(emc.groupby("scan", sort=False)[["rois", "with_root_id"]].sum().assign(
    fields_with_columns=emc.groupby("scan", sort=False).em_columns.sum()).to_string())
print(f"\n14 scans: {int(emc.with_root_id.sum())} ROIs with an EM root id, {roots.root.nunique()} distinct root ids; "
      f"fields with EM columns {int(emc.em_columns.sum())} of {len(emc)}; "
      f"ROIs in those fields {int(emc[emc.em_columns].rois.sum())} of {int(emc.rois.sum())}")
print("ROIs with root id 0 (no segment):", int((roots.root == 0).sum()))
multi = roots[roots.root > 0].groupby("root").scan.nunique()
print("Root ids other than 0 found in more than one scan:", int((multi > 1).sum()))
print("Paper: 19,181 ROIs matched to 15,439 EM neurons (coregistration_manual_v4).")
       rois  with_root_id  fields_with_columns
scan                                          
4-7    8395           685                    4
5-6    9254           836                    4
5-7    8862           747                    4
6-2    8756           786                    4
6-4    8826           891                    4
6-6    8551           702                    4
6-7    8602           543                    4
7-3    9296           490                    4
7-4   12675           872                    4
7-5    9034           426                    4
8-5   10579           646                    4
9-3    8661           584                    3
9-4    8415           640                    3
9-6    5507            15                    2

14 scans: 8863 ROIs with an EM root id, 8326 distinct root ids; fields with EM columns 52 of 104; ROIs in those fields 60263 of 125413
ROIs with root id 0 (no segment): 2
Root ids other than 0 found in more than one scan: 395
Paper: 19,181 ROIs matched to 15,439 EM neurons (coregistration_manual_v4).

Only 52 of the 104 fields, one of the two fields at each depth, have EM columns; they hold 8,863 ROIs with a root id (8,326 distinct values, among them 0, the id of no segment, in 2 ROIs; 395 non-zero root ids occur in more than one scan). The other half of each scan's fields has no EM identifiers. These counts are therefore not comparable with the paper's 19,181 and 15,439, and decide nothing.

Matched units in two scans (variant for ed4-matched-higher-signal-correlation)¶

The claim ed4-matched-higher-signal-correlation is blocked with manual-coreg-19181, whose table it needs. As a variant that decides nothing, the test of the claims file is run here on the matches the dataset itself holds: masks of different scans whose pt_root_id is the same EM neuron. Only reading (b) of "nearest" can be run (the nearest mask in the same field, by the 2D centroid of the image masks), as the dataset has no stack coordinates. For every pair of scans in which a root id occurs, the matched signal correlation is the Pearson correlation between the two masks' trial-averaged responses to the 6 oracle clips (each clip resampled to a common 0.25 s grid, clips in one fixed order); the two controls pair each matched mask with the somatic mask nearest to the other matched mask in its field, among masks without that root id (Methods, Evaluating manual matches, as the claims file gives it). Signal is fluorescence, and the oracle score is reading (a) of fig3f. The files store pt_root_id as a 64-bit float, which cannot hold every 18-digit root id exactly, so the EM cell body points (pt_position) of the two masks of a pair are compared too, and the results are also given for pairs whose points lie within 10 um, about the size of a cell body (a standard choice).

In [22]:
from scipy.stats import wilcoxon
started = time.time()
em_keys = [(k, int(fl)) for k in PAPER_SCANS for fl in M[k]["fields"].field[M[k]["fields"].has_em_columns]]
cent = dict(zip(em_keys, microns.all_field_centroids([(AID[k], fl) for k, fl in em_keys], workers=8)))
canon = sorted(orders[PAPER_SCANS[0]])                       # one fixed order of the 6 oracle clips

def signal(k, fl, roi):
    pos = [orders[k].index(h) for h in canon]
    return R[(k, fl)]["clip_means"][pos, :, roi].ravel()

score = units.set_index(["session", "scan_idx", "field", "roi"]).score_a
rows = []
for k, fl in em_keys:
    F = M[k]["fields"].set_index("field").loc[fl]
    for roi, (root, mt) in enumerate(zip(F.root_ids, F.mask_type_arr)):
        if np.isfinite(root) and root > 0:
            rows.append({"k": k, "field": fl, "roi": roi, "root": int(root), "soma": mt == "soma", "pos": F.pt_position[roi]})
em_units = pd.DataFrame(rows)
multi_roots = em_units.groupby("root").k.nunique().loc[lambda v: v > 1].index
n_zero = sum(int((M[k]["fields"].set_index("field").loc[fl].root_ids == 0).sum()) for k, fl in em_keys)
print(f"masks with pt_root_id 0 (no segment), left out: {n_zero}")
print("pt_root_id is stored as", sorted({M[k]["fields"].set_index("field").loc[fl].root_id_dtype for k, fl in em_keys}),
      "; float64 holds integers exactly only up to 2**53, and root ids near", f"{em_units.root.min():.3g}", "are spaced by",
      np.spacing(float(em_units.root.min())), "in float64")
print(f"EM fields {len(em_keys)}; masks with a root id {len(em_units)}; root ids in two or more scans {len(multi_roots)}")

def nearest_other(k, fl, roi, root):
    F = M[k]["fields"].set_index("field").loc[fl]
    c = cent[(k, fl)]
    ok = (F.mask_type_arr == "soma") & ~(F.root_ids == root)
    ok[roi] = False
    d = np.hypot(*(c - c[roi]).T)
    d[~ok] = np.inf
    return int(np.argmin(d)), float(d.min())

pairs = []
for root in multi_roots:
    g = em_units[em_units.root == root]
    for i, a in enumerate(g.itertuples()):
        for b in g.iloc[i + 1:].itertuples():
            if a.k == b.k:
                continue
            ca, da = nearest_other(b.k, b.field, b.roi, root)     # control in scan B, near the matched mask B
            cb, db = nearest_other(a.k, a.field, a.roi, root)     # control in scan A, near the matched mask A
            sa, sb = signal(a.k, a.field, a.roi), signal(b.k, b.field, b.roi)
            pairs.append({"root": root, "A": f"{a.k[0]}-{a.k[1]}", "B": f"{b.k[0]}-{b.k[1]}",
                          "matched": np.corrcoef(sa, sb)[0, 1],
                          "control_A": np.corrcoef(sa, signal(b.k, b.field, ca))[0, 1],
                          "control_B": np.corrcoef(sb, signal(a.k, a.field, cb))[0, 1],
                          "control_dist_px": max(da, db), "em_pos_dist_um": float(np.linalg.norm((a.pos - b.pos) * [0.004, 0.004, 0.04])),
                          "score_A": score[(a.k[0], a.k[1], a.field, a.roi)], "score_B": score[(b.k[0], b.k[1], b.field, b.roi)],
                          "both_soma": a.soma and b.soma})
ed4 = pd.DataFrame(pairs)
print(f"distance between the EM cell body points (pt_position) of the two masks of a pair (um): percentiles 50/75/95/100 "
      f"{np.round(np.percentile(ed4.em_pos_dist_um, [50, 75, 95, 100]), 2).tolist()}; pairs more than 10 um apart (about a cell body): "
      f"{int((ed4.em_pos_dist_um > 10).sum())} of {len(ed4)}")
print(f"pairs of scans for one root id: {len(ed4)} (roots {ed4.root.nunique()}); both masks somatic in {int(ed4.both_soma.sum())}; "
      f"distance to the control (px): median {ed4.control_dist_px.median():.1f}, max {ed4.control_dist_px.max():.1f}; {time.time() - started:.0f} s")
near = ed4.em_pos_dist_um <= 10
high = (ed4.score_A > 0.2) & (ed4.score_B > 0.2)
for name, sub in (("all pairs", ed4), ("both oracle scores above 0.2", ed4[high]),
                  ("EM points within 10 um", ed4[near]), ("EM points within 10 um, both scores above 0.2", ed4[near & high])):
    beats = (sub.matched > sub.control_A) & (sub.matched > sub.control_B)
    dA, dB = sub.matched - sub.control_A, sub.matched - sub.control_B
    print(f"{name}: n = {len(sub)}; matched r median {sub.matched.median():.3f}, controls {sub.control_A.median():.3f} and {sub.control_B.median():.3f}; "
          f"matched above both controls in {beats.mean():.3f}; median difference {dA.median():.3f} and {dB.median():.3f}; "
          f"Wilcoxon p {wilcoxon(dA).pvalue:.2g} and {wilcoxon(dB).pvalue:.2g}")
masks with pt_root_id 0 (no segment), left out: 2
pt_root_id is stored as ['float64'] ; float64 holds integers exactly only up to 2**53, and root ids near 8.65e+17 are spaced by 128.0 in float64
EM fields 52; masks with a root id 8861; root ids in two or more scans 395
distance between the EM cell body points (pt_position) of the two masks of a pair (um): percentiles 50/75/95/100 [0.84, 2.06, 161.09, 547.46]; pairs more than 10 um apart (about a cell body): 50 of 538
pairs of scans for one root id: 538 (roots 395); both masks somatic in 538; distance to the control (px): median 6.4, max 14.3; 63 s
all pairs: n = 538; matched r median 0.585, controls 0.239 and 0.233; matched above both controls in 0.719; median difference 0.273 and 0.273; Wilcoxon p 2e-60 and 1.9e-51
both oracle scores above 0.2: n = 399; matched r median 0.664, controls 0.298 and 0.260; matched above both controls in 0.777; median difference 0.303 and 0.353; Wilcoxon p 2e-50 and 3.3e-44
EM points within 10 um: n = 488; matched r median 0.612, controls 0.270 and 0.242; matched above both controls in 0.752; median difference 0.284 and 0.291; Wilcoxon p 1.4e-59 and 4.3e-54
EM points within 10 um, both scores above 0.2: n = 368; matched r median 0.679, controls 0.318 and 0.270; matched above both controls in 0.804; median difference 0.311 and 0.362; Wilcoxon p 5.2e-50 and 7.1e-47

These matches are the dataset's own EM identifiers, which cover one field at each depth, not the paper's coregistration_manual_v4 table, so the claim stays blocked and these numbers decide nothing. Over 538 pairs of scans (395 root ids), the matched signal correlation (median 0.585) is above both controls (medians 0.239 and 0.233) in 71.9% of pairs, more than half, and the median differences, 0.273 and 0.273, grow to 0.303 and 0.353 when both masks score above 0.2 (77.7% of 399 pairs above both controls; Wilcoxon p below 1e-43 throughout). In 50 of the 538 pairs the two masks' EM cell body points lie more than 10 um apart, so the same root id does not always mark the same cell body; without them the result is the same (75.2% of 488 pairs; 80.4% of 368 with both scores above 0.2). On these matches the claims file's test would pass under reading (b).

nuclei-144120¶

Nuclei were automatically segmented in subvolume 65, 144,120 in all.

Data elsewhere: the nucleus detection table nucleus_detection_v0, as released on the BossDB open data bucket (iarpa_microns/minnie/minnie65/nucleus_detection/nucleus_detection_v0.csv, ETag 6a269d25a4b065cdc44be1d61b3ff2c4, modified 2021-07-01). The file has no header row; every line is one detection.

In [23]:
BOSS = "https://bossdb-open-data.s3.amazonaws.com/iarpa_microns/minnie/minnie65/"
nuc = pd.read_csv(BOSS + "nucleus_detection/nucleus_detection_v0.csv", header=None,
                  names=["id", "valid", "pt_supervoxel_id", "pt_root_id", "x", "y", "z", "volume_um3"])
print("rows (detections):", len(nuc), "; distinct ids:", nuc.id.nunique(), "; valid values:", nuc.valid.value_counts().to_dict())
print("z range (40 nm sections):", nuc.z.min(), "-", nuc.z.max(), "; volume (um3) median", nuc.volume_um3.median().round(1))
print("detections with a root id:", int((nuc.pt_root_id != 0).sum()))
rows (detections): 144120 ; distinct ids: 144120 ; valid values: {'t': 144120}
z range (40 nm sections): 14834 - 27871 ; volume (um3) median 227.8
detections with a root id: 129816

Result: reproduced. The released table holds 144,120 nucleus detections, the paper's count, all in subvolume 65 by their z positions (above section 14,816).

cell-classification¶

An SVM on nucleus features detects neurons with 96.9% precision and 99.6% recall, predicting 82,247 neurons in subvolume 65, of which 69,957 have well-segmented somata; a multilayer perceptron separates excitatory from inhibitory neurons with 97% accuracy (96% precision and 94% recall for inhibitory calls).

Criterion: counts from the released tables; accuracies only if the manual labels are public. Tables (BossDB, minnie65/cell_types/, modified 2021-07-27): nucleus_neuron_svm.csv (the SVM calls), allen_soma_ei_class_model_v1.csv (the excitatory and inhibitory calls of the well-segmented subset) and allen_visp_column_soma_coarse_types_v1.csv (manual labels of the 100 um column). None has a header row.

In [24]:
svm = pd.read_csv(BOSS + "cell_types/nucleus_neuron_svm.csv", header=None,
                  names=["id", "valid", "pt_supervoxel_id", "pt_root_id", "x", "y", "z", "classification_system", "cell_type"])
ei = pd.read_csv(BOSS + "cell_types/allen_soma_ei_class_model_v1.csv", header=None,
                 names=["id", "valid", "classification_system", "cell_type", "x", "y", "z"])
col = pd.read_csv(BOSS + "cell_types/allen_visp_column_soma_coarse_types_v1.csv", header=None,
                  names=["id", "valid", "classification_system", "cell_type", "x", "y", "z"])
print("SVM table:", len(svm), "rows,", svm.id.nunique(), "distinct ids; calls", svm.cell_type.value_counts().to_dict())
svm65 = svm[svm.id.isin(nuc.id)]
print("SVM rows that are detections of nucleus_detection_v0:", len(svm65), "; calls", svm65.cell_type.value_counts().to_dict())
other = svm[~svm.id.isin(nuc.id)]
print("SVM rows not in nucleus_detection_v0:", len(other), "; calls", other.cell_type.value_counts().to_dict())
print("E/I table (well-segmented neurons):", len(ei), "rows; classes", ei.cell_type.value_counts().to_dict(),
      "; ids among the SVM neurons:", int(ei.id.isin(svm65[svm65.cell_type == 'neuron'].id).sum()))
print("Column labels:", len(col), "rows;", col.classification_system.value_counts().to_dict())
SVM table: 171815 rows, 171815 distinct ids; calls {'neuron': 89062, 'not-neuron': 82753}
SVM rows that are detections of nucleus_detection_v0: 144117 ; calls {'neuron': 89056, 'not-neuron': 55061}
SVM rows not in nucleus_detection_v0: 27698 ; calls {'not-neuron': 27692, 'neuron': 6}
E/I table (well-segmented neurons): 69957 rows; classes {'excitatory': 61926, 'inhibitory': 8031} ; ids among the SVM neurons: 69957
Column labels: 2204 rows; {'aibs_coarse_excitatory': 1185, 'aibs_coarse_nonneuronal': 679, 'aibs_coarse_error': 172, 'aibs_coarse_inhibitory': 162, 'aibs_coarse_unclear': 3, 'aibs_coarse_neuron': 3}
In [25]:
# SVM against the manual column labels, matched by position (the column points sit on the nucleus
# centroids; distance shown). Neuron = excitatory, inhibitory or neuron; not neuron = nonneuronal;
# error and unclear labels are left out.
from scipy.spatial import cKDTree
nm = lambda df: np.c_[df.x * 4, df.y * 4, df.z * 40] / 1000.0          # um, from 4 x 4 x 40 nm voxels
dist, ix = cKDTree(nm(svm)).query(nm(col))
col = col.assign(svm_call=svm.cell_type.values[ix], match_um=dist)
print("Distance from each column point to the nearest SVM nucleus (um): max", round(dist.max(), 3))
truth = col.classification_system.map({"aibs_coarse_excitatory": True, "aibs_coarse_inhibitory": True,
                                       "aibs_coarse_neuron": True, "aibs_coarse_nonneuronal": False})
use = truth.notna()
t, p = truth[use].astype(bool), (col.svm_call[use] == "neuron")
tp, fp, fn = int((p & t).sum()), int((p & ~t).sum()), int((~p & t).sum())
print(f"{use.sum()} labelled column cells: precision {tp / (tp + fp):.3f}, recall {tp / (tp + fn):.3f} (tp {tp}, fp {fp}, fn {fn})")
# E/I calls of the model against the manual labels of the column (training data of the model)
m_ei = col[col.classification_system.isin(["aibs_coarse_excitatory", "aibs_coarse_inhibitory"])]
dist2, ix2 = cKDTree(nm(ei)).query(nm(m_ei))
near = dist2 < 1.0
pred_inh = ei.cell_type.values[ix2][near] == "inhibitory"
true_inh = (m_ei.classification_system == "aibs_coarse_inhibitory").values[near]
print(f"Column neurons found in the E/I table: {near.sum()} of {len(m_ei)}; agreement {np.mean(pred_inh == true_inh):.3f}; "
      f"inhibitory precision {np.sum(pred_inh & true_inh) / max(1, pred_inh.sum()):.3f}, recall {np.sum(pred_inh & true_inh) / max(1, true_inh.sum()):.3f}")
Distance from each column point to the nearest SVM nucleus (um): max 0.0
2029 labelled column cells: precision 0.955, recall 0.996 (tp 1345, fp 63, fn 5)
Column neurons found in the E/I table: 1314 of 1347; agreement 0.982; inhibitory precision 0.986, recall 0.868

Result: reproduced. Of the 144,120 nuclei of subvolume 65, the SVM table calls 89,056 neurons (82,247 in the paper, within the 10% tolerance; the full SVM table has 171,815 rows, 27,698 of them not in the nucleus table and nearly all non-neurons). The excitatory and inhibitory table holds 69,957 neurons, the paper's count of well-segmented neurons. Against the manual labels of the column, the SVM has precision 95.5% and recall 99.6% (paper 96.9% and 99.6%). The multilayer perceptron's accuracy was measured by the paper on 350 further cells that are not released; on the column cells, which were its training and validation data, its calls agree with the labels for 98.2%, which is not the paper's test.

Summary¶

Claim Data Outcome
scans-14-p75-p81 dataset reproduced
scan-configurations dataset reproduced
masks-125413-somatic-115372 dataset reproduced (exact counts)
unique-neurons-75909 dataset blocked: data missing (no stack coordinates)
area-split-half-visp dataset blocked: metadata insufficient (no area per mask)
z-drift-under-10um dataset blocked: data missing (no structural stack)
structural-stack dataset blocked: data missing
stimulus-composition dataset reproduced
stimulus-conserved-across-scans dataset partially reproduced (one Trippy seed in two scans)
oracle-same-order dataset reproduced
spontaneous-periods dataset partially reproduced (every scan within the ranges; the longest period before the stimulus, 4.84 min, falls short of 5.5 min)
monet2-directions dataset reproduced (directions measured from the video by optical flow)
fig3f-oracle-reliability dataset reproduced (on fluorescence)
fig3g-direction-tuning dataset reproduced (on fluorescence)
behaviour-recorded dataset partially reproduced (eye tracking of scan 4-7 ends 16 min early)
photodiode-89pct dataset blocked: data missing
manual-coreg-19181, ed4-matched-higher-signal-correlation, fig6k-target-responses with_inputs blocked: coregistration_manual_v4 is a CAVE table that needs an account token (the dataset's own EM identifiers are counted, and the ed4 test is run on them, above)
nuclei-144120 elsewhere reproduced
cell-classification elsewhere reproduced
synapse-detection elsewhere partially reproduced (both counts within tolerance; precision, recall and partner accuracy not tested; em_volume_counts.ipynb)
em-sections elsewhere partially reproduced (share of subvolume 35 within tolerance; 19,952 sections with data against 19,974; em_volume_counts.ipynb)
13 other EM and co-registration claims elsewhere blocked: the CAVE tables they test, or that choose their neurons, need an account token, and no public copy was found; fiducial-transform-residual names no table

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.

Every claim tested is reproduced or partially reproduced. The three partial outcomes on the dataset are about its content: one Trippy seed shown in two scans, eye tracking that stops early in scan 4-7, and a longest period before the stimulus of 4.84 min over the 14 scans against the paper's 5.5 min (over all 19 files the four bounds match the paper's). The two EM claims tested in em_volume_counts.ipynb are partially reproduced: the share of subvolume 35 matches but the region with data holds 19,952 sections against 19,974, and both synapse counts match while precision, recall and partner accuracy are not tested.

How the analysis differs from the paper's.

  • The 14 scans were identified from the image of Extended Data Table 1; the dataset holds 19 and does not mark them.
  • Fluorescence traces take the place of the deconvolved traces, which are not in the dataset; the per-unit delay within a frame is not applied.
  • The Monet2 direction of each period was measured from the stimulus video by optical flow, as the dataset does not give it.
  • The oracle score on fluorescence does not separate a neuron from what else its mask holds: artifact masks score as high as somatic ones, also after the field's mean trace is regressed out. In the Fig. 3g split-half test, too, artifact masks pass as often as somatic ones.
  • The preference index used to choose the 80 units shown in Fig. 3g is a difference over the standard deviation, not the ratio of the claims file, which is undefined for fluorescence.
  • The EM claims tested use the 2021 releases of the nucleus and cell type tables on BossDB, not CAVE.
  • In em_volume_counts.ipynb, the first and last sections are read from the imagery on a grid of sampled chunks, and the synapse table of subvolume 65 is the export of version 1181.

What was found about the dataset.

  • It holds all 19 completed scans; nothing marks the 14 the paper describes.
  • The EM identifiers (pt_root_id and others) are present only in one of the two fields at each depth (52 of 104 fields of the 14 scans), and in none of 4 files.
  • The deconvolved traces, the stack coordinates and area of each mask, the oracle scores, the Monet2 directions per period and the photodiode signal are not in it.
  • The imaging_rate of every imaging plane is NaN, every plane's location lists all four areas, and the subject age is the range P75D/P81D in every file.
  • In scan 4-7 the eye tracking ends 968 s before the last imaging frame. In all five files outside the paper's 14 it misses the imaging period too, by 54 to 76 min in three and by seconds in two.
  • The treadmill and eye time stamps contain missing (NaN) values in several files.
  • On BossDB, the imagery of subvolume 65 ends at section 27,882, 22 sections before the paper's 27,904 (em_volume_counts.ipynb).
  • pt_root_id is stored as a 64-bit float, which cannot hold every 18-digit root id exactly (spacing 128 near 8.65e17); in 50 of 538 pairs of masks that share a root id across scans, the EM cell body points lie more than 10 um apart.