Clark and Nolan (2024) on dandiset 000943¶

Paper. Clark H, Nolan MF (2024). Task-anchored grid cell firing is selectively associated with successful path integration-dependent behaviour. eLife 12:RP89356. doi:10.7554/eLife.89356. The claims file was written from the version of record (reviewed preprint version 3, 28 March 2024, full text from Europe PMC PMC10977970) with its supplementary files: the summary table of recorded cells per mouse and the MDAR checklist.

Dandiset. DANDI:000943, version 0.240614.0754. The paper cites no DANDI version: its data statement points to Edinburgh DataShare (doi:10.7488/ds/7684). 0.240614.0754 is the only published version, so it is pinned. The draft has the same 734 assets and the same total size (checked below), so no other version could match the claims file better.

What the claims need and what the dandiset holds. Every one of the 31 claims rests on single units: grid cells are classified from the units' open arena firing, and every later claim uses those cells or their firing on the virtual track. The paper sorted spikes with MountainSort (Methods, Spike sorting: "Spikes were isolated from electrophysiological data using an automated pipeline based on MountainSort (v0.11.5 and dependencies) ... Recordings from the open-field and virtual reality tasks were concatenated for spike sorting"). The dandiset holds only the raw tetrode signal and the position series of each session; no file has a units table or any processed data (shown below). Testing any claim therefore means spike sorting the raw signal of every recording day, about 976 GB. This notebook surveys the dandiset, checks its description against the values, measures how fast the raw signal streams, and from that gives the estimate on which every claim is blocked as too_large.

Claim Status
grid-cell-count, supp1-recording-summary, grid-field-size-spacing blocked, too_large (needs sorted units)
fig2-session-level-classes, fig2e-nongrid-classes, fig2f-spatial-information, fig2f-firing-rate-similar, fig2f-periodogram-features-similar, fig2f-aperiodic-low-rate blocked, too_large
fig3f-switching-within-session, fig3f-session-class-bias, fig3g-trial-spatial-information, fig3g-trial-firing-rate-similar, fig3s4-stable-cells, fig3s5-grid-vs-nongrid-modes, fig3s6-block-lengths, fig3s7-trial-type-ratio blocked, too_large
fig4e-grid-agreement-higher, fig4e-agreement-above-shuffle, fig4f-nongrid-spatial-information blocked, too_large
fig6b-stable-beaconed-similar, fig6b-stable-uncued-reduced, fig6a-stable-stop-density, fig6d-epochs-beaconed-similar, fig6d-epochs-nonbeaconed-reduced, fig6d-epochs-probe, fig6c-epoch-stop-density, fig6e-hit-try-run, fig6f-odds-ratio, fig6s1-speed-profiles, fig6s2-template-method blocked, too_large

No claim is tested. The session and trial counts of the summary table (claim supp1-recording-summary) need no units and are compared with the dandiset in the data check, but that claim also needs the cell and grid cell counts, so it stays blocked.

In [1]:
import time
import warnings

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

import clark2024 as c

pd.set_option("display.width", 220)
pd.set_option("display.max_columns", 40)
pd.set_option("display.max_rows", 400)
warnings.filterwarnings("ignore")

started = time.time()
info = c.version_info()
assets = c.list_assets()
print(f"dandiset {c.DANDISET_ID}, version {c.DANDISET_VERSION}: {len(assets)} assets, "
      f"{sum(a['size'] for a in assets) / 1e9:.1f} GB")
for v in info["versions"]:
    print(f"  version {v['version']:>14}: {v['asset_count']} assets, {v['size'] / 1e9:.1f} GB, "
          f"status {v['status']}, modified {v['modified'][:19]}")
meta = info["meta"]
print("\nDescription:", meta["description"])
print("variableMeasured:", [x["value"] if isinstance(x, dict) else x for x in meta["assetsSummary"].get("variableMeasured", [])])
print("numberOfSubjects:", meta["assetsSummary"].get("numberOfSubjects"))
dandiset 000943, version 0.240614.0754: 734 assets, 976.2 GB
  version          draft: 734 assets, 976.2 GB, status Published, modified 2024-06-14T07:54:16
  version  0.240614.0754: 734 assets, 976.2 GB, status Valid, modified 2024-06-14T07:54:17

Description: This dataset contains electrophysiological recordings alongside the synchronised behavioural data for mice running in an open field and virtual linear track environments. Movable tetrodes were targeted to the medial entorhinal cortex. Further details can be found here (https://doi.org/10.7554/eLife.89356.2).
variableMeasured: ['ElectrodeGroup', 'ElectricalSeries']
numberOfSubjects: 12
In [2]:
# One streamed open per file: metadata, the contents of every group that could hold derived data,
# and a summary of the position series (read whole; it is small).
t = time.time()
survey = pd.DataFrame(c.map_files(c.survey_file, assets, workers=16))
print(f"surveyed {len(survey)} files in {time.time() - t:.0f} s; files that failed to open: "
      f"{int(survey['error'].notna().sum()) if 'error' in survey else 0}")
survey["day"] = survey.session_id.str.split("_").str[1]
vr = survey[survey.session_description == "vr"].copy()
of = survey[survey.session_description == "openfield"].copy()
surveyed 734 files in 83 s; files that failed to open: 0

Data check¶

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

In [3]:
print("Files by mouse and session type (session_description):")
print(pd.crosstab(survey.mouse, survey.session_description, margins=True).to_string())

print("\nWhat the files hold (every file):")
contents = survey.contents.map(lambda d: str({k: v for k, v in d.items()}))
print(contents.value_counts().to_string())
n_units = sum(d["units"] is not None for d in survey.contents)
n_proc = sum(bool(d["processing"]) for d in survey.contents)
n_intervals = sum(d["intervals"] is not None for d in survey.contents)
print(f"files with a units table: {n_units}; with any processing module: {n_proc}; with intervals (trials): {n_intervals}")

print("\nRaw signal: channels", sorted(survey.es_channels.unique()), "; rate (Hz)", sorted(survey.es_rate.unique()),
      "; conversion", sorted(survey.es_conversion.unique()), sorted(survey.es_unit.unique()))
dur = survey.es_samples / survey.es_rate / 60
print("Session length from the raw signal (min) by type:")
print(dur.groupby(survey.session_description).describe().round(1).to_string())
print("Position series description (identical in every file):", survey.pos_description.unique().tolist())
print("Position columns by type:", survey.groupby("session_description").pos_shape.agg(lambda s: sorted({p[1] for p in s})).to_dict())
Files by mouse and session type (session_description):
session_description  openfield   vr  All
mouse                                   
M1                          41   11   52
M10                         25   25   50
M11                         45   45   90
M12                         30   30   60
M13                         32   29   61
M14                         45   45   90
M15                         33   21   54
M2                          21   29   50
M3                          29   35   64
M4                          29   30   59
M6                          22   22   44
M7                          27   33   60
All                        379  355  734

What the files hold (every file):
contents
{'processing': [], 'analysis': [], 'stimulus': ['presentation', 'templates'], 'intervals': None, 'units': None, 'scratch': None, 'acquisition': ['ElectricalSeries', 'position']}    734
files with a units table: 0; with any processing module: 0; with intervals (trials): 0

Raw signal: channels [np.int64(16)] ; rate (Hz) [np.float64(30000.0)] ; conversion [np.float64(1.9499999999999999e-07)] ['volts']
Session length from the raw signal (min) by type:
                     count  mean  std   min   25%   50%   75%   max
session_description                                                
openfield            379.0  30.4  2.1  23.3  29.7  30.2  30.9  52.9
vr                   355.0  30.6  3.2  11.3  29.7  30.2  31.1  42.6
Position series description (identical in every file): ['position: cm, speed: cm/s, angle: degrees, trials: non-zero indexed, trial types: 0-beaconed, 1-nonbeaconed, 2-probe']
Position columns by type: {'openfield': [9], 'vr': [3]}
In [4]:
# The free-text metadata of one VR file and one open arena file, which the checks below refer to.
import textwrap
by_path = {a["path"]: a for a in assets}
example = [vr.sort_values("path").path.iloc[0], of.sort_values("path").path.iloc[0]]
texts = c.map_files(c.file_text, [by_path[p] for p in example], workers=2)
for t in texts:
    print(f"== {t['path']}")
    for k, v in t.items():
        if k != "path":
            print(textwrap.fill(f"{k}: {v}", 120, subsequent_indent="    "))
print("\ngeneral/notes identical in the two files:", texts[0]["general/notes"] == texts[1]["general/notes"],
      "; experiment_description identical:", texts[0]["general/experiment_description"] == texts[1]["general/experiment_description"])
== sub-M1/sub-M1_ses-M1-D1-2020-08-03-16-11-14_ecephys.nwb
session_description: vr
general/notes: Experimental days involved recording from mice in the open arena and then in the virtual location memory
    task. On a few days this order was reversed without apparent effects on the results obtained. Mice were collected
    from the holding room 30 - 60 minutes before recording, were handled for 5 - 10 minutes, weighed and placed for 10 -
    20 minutes in a cage containing objects and a running wheel. Between recording sessions mice were placed back in the
    object-filled playground for 30 minutes. Tetrodes were typically lowered by 50 - 100 µm after each session. The open
    arena consisted of a metal box with a square floor area, removable metal walls, metal frame (Frame parts from Kanya
    UK, C01-1, C20-10, A33-12, B49-75, B48-75, A39-31, ALU3), and an A4-sized cue card in the middle of one of the metal
    walls. For the open field exploration session, mice were placed in the open arena while tethered via an ultrathin
    SPI cable and custom-build commutator and left unprompted for 30 minutes to freely move around. For the location
    memory task mice were trained to obtain rewards at a location on the virtual linear track. Mice were head-fixed
    using a RIVETS clamp (Ronal Tool Company, Inc) and ran on a cylindrical treadmill fitted with a rotary encoder
    (Pewatron). Virtual tracks, generated using Blender3D (blender.com) had a range of lengths, in the published journal
    article only sessions with 200cm track lengths are used. This consisted of a 60 cm track zone, a 20 cm reward zone,
    a second 60 cm track zone and a 60 cm black box to separate successive trials. In sessions with track lengths longer
    than 200cm, Xcm length tracks consisted of a Zcm length track zone, a 20 cm reward zone, a 60 cm track zone and a 60
    cm black box to seperate successive trials. The length of Z can be derived from the track length using the equations
    Z = X-(60+60+20). In actuality, the trial starts in the middle of the blackbox region therefore it can be thought of
    as 2 successive 30 cm black box regions or a single 60 cm region that spans across two trials, this explains how the
    black box is visualised within the research article. The distance visible ahead of the mouse was 50 cm. The reward
    zone was either marked by distinct vertical green and black bars on beaconed trials, or was not marked by a visual
    cue at all on non-beaconed or probe trials. A feeding tube placed in front of the animal dispensed soy milk rewards
    (5 - 10 µl per reward) if the mouse stopped in the reward zone, however was not dispensed on probe trials. A stop
    was registered in Blender3D if the speed of the mouse dropped below 4.7 cm/s. Speed was calculated on a rolling
    basis from the previous 100 ms at a rate of 60 Hz. Trials were delivered in repeating blocks throughout a recording
    session. For example, 3 beaconed trials (B) followed by 2 non-beaconed trials (N) with the order repeating until the
    end of the session. To encourage learning and engagement in both beaconed and non-beaconed trials, the first day of
    training typically used a trial type ratio of 3 beaconed trials to 1 non-beaconed trials. As training progressed we
    then increased the proportion of non-beaconed trials up to a ratio of 1 beaconed trial to 4 non-beaconed trials.
    Examples of trial blocks include BBBBN, BBBN, BBN, BN, BNN, BNNN, BBNNN and BBNNNNNNN where each character indicates
    the trial type of each trial within a block. In some sessions we replaced single non-beaconed trials in trial blocks
    with a probe trial (P). Examples of trial blocks with probe trials include BBBBNBBBBP, BBBNBBBP, BBNBBP and
    BBNNNNNNNP. For more details, see https://doi.org/10.7554/eLife.89356.2
general/experiment_description: Extracellular recording of mice medial entorhinal cortex during free exploration in a
    100 x 100 cm open field and a virtual reality location memory task
general/protocol: see https://doi.org/10.7554/eLife.89356.2
general/session_id: M1_D1_2020-08-03_16-11-14
== sub-M1/sub-M1_ses-M1-D1-2020-08-03-16-49-15_ecephys.nwb
session_description: openfield
general/notes: Experimental days involved recording from mice in the open arena and then in the virtual location memory
    task. On a few days this order was reversed without apparent effects on the results obtained. Mice were collected
    from the holding room 30 - 60 minutes before recording, were handled for 5 - 10 minutes, weighed and placed for 10 -
    20 minutes in a cage containing objects and a running wheel. Between recording sessions mice were placed back in the
    object-filled playground for 30 minutes. Tetrodes were typically lowered by 50 - 100 µm after each session. The open
    arena consisted of a metal box with a square floor area, removable metal walls, metal frame (Frame parts from Kanya
    UK, C01-1, C20-10, A33-12, B49-75, B48-75, A39-31, ALU3), and an A4-sized cue card in the middle of one of the metal
    walls. For the open field exploration session, mice were placed in the open arena while tethered via an ultrathin
    SPI cable and custom-build commutator and left unprompted for 30 minutes to freely move around. For the location
    memory task mice were trained to obtain rewards at a location on the virtual linear track. Mice were head-fixed
    using a RIVETS clamp (Ronal Tool Company, Inc) and ran on a cylindrical treadmill fitted with a rotary encoder
    (Pewatron). Virtual tracks, generated using Blender3D (blender.com) had a range of lengths, in the published journal
    article only sessions with 200cm track lengths are used. This consisted of a 60 cm track zone, a 20 cm reward zone,
    a second 60 cm track zone and a 60 cm black box to separate successive trials. In sessions with track lengths longer
    than 200cm, Xcm length tracks consisted of a Zcm length track zone, a 20 cm reward zone, a 60 cm track zone and a 60
    cm black box to seperate successive trials. The length of Z can be derived from the track length using the equations
    Z = X-(60+60+20). In actuality, the trial starts in the middle of the blackbox region therefore it can be thought of
    as 2 successive 30 cm black box regions or a single 60 cm region that spans across two trials, this explains how the
    black box is visualised within the research article. The distance visible ahead of the mouse was 50 cm. The reward
    zone was either marked by distinct vertical green and black bars on beaconed trials, or was not marked by a visual
    cue at all on non-beaconed or probe trials. A feeding tube placed in front of the animal dispensed soy milk rewards
    (5 - 10 µl per reward) if the mouse stopped in the reward zone, however was not dispensed on probe trials. A stop
    was registered in Blender3D if the speed of the mouse dropped below 4.7 cm/s. Speed was calculated on a rolling
    basis from the previous 100 ms at a rate of 60 Hz. Trials were delivered in repeating blocks throughout a recording
    session. For example, 3 beaconed trials (B) followed by 2 non-beaconed trials (N) with the order repeating until the
    end of the session. To encourage learning and engagement in both beaconed and non-beaconed trials, the first day of
    training typically used a trial type ratio of 3 beaconed trials to 1 non-beaconed trials. As training progressed we
    then increased the proportion of non-beaconed trials up to a ratio of 1 beaconed trial to 4 non-beaconed trials.
    Examples of trial blocks include BBBBN, BBBN, BBN, BN, BNN, BNNN, BBNNN and BBNNNNNNN where each character indicates
    the trial type of each trial within a block. In some sessions we replaced single non-beaconed trials in trial blocks
    with a probe trial (P). Examples of trial blocks with probe trials include BBBBNBBBBP, BBBNBBBP, BBNBBP and
    BBNNNNNNNP. For more details, see https://doi.org/10.7554/eLife.89356.2
general/experiment_description: Extracellular recording of mice medial entorhinal cortex during free exploration in a
    100 x 100 cm open field and a virtual reality location memory task
general/protocol: see https://doi.org/10.7554/eLife.89356.2
general/session_id: M1_D1_2020-08-03_16-49-15

general/notes identical in the two files: True ; experiment_description identical: True

The notes say that "in the published journal article only sessions with 200cm track lengths are used", that a stop is registered when speed drops below 4.7 cm/s, computed over the previous 100 ms, and the experiment description gives "a 100 x 100 cm open field" (the notes themselves describe the arena only as "a metal box with a square floor area"). Both texts are the same in the VR and the open arena file, both of M1. These are the sources of the track length, stop speed and arena size used below.

The dandiset's metadata lists 12 subjects and only ElectrodeGroup and ElectricalSeries as variables measured, which the files bear out: each of the 734 files holds the raw 16-channel signal at 30 kHz and a position series, and nothing else. There is no units table, no processing module and no trials table in any file, so the spike sorting, the units' quality metrics, the reward times and the trial table that the paper's analysis uses are not in the dandiset. The position description, identical in every file, names position, speed, angle, trials and trial types, which describes neither the three columns of the VR files (no speed or angle column) nor the nine columns of the open arena files.

In [5]:
# Virtual track sessions: columns are position (cm), trial number and trial type (description above).
print("VR position: sample interval (s)", sorted(vr.dt.round(6).unique()), "; first timestamp", sorted(vr.t_first.unique()))
print("trial numbers start at 1 in every file:", bool((vr.trial_first == 1).all()),
      "; never decrease:", bool(vr.trial_monotonic.all()),
      "; each trial one contiguous run:", bool((vr.n_trials == vr.n_trial_runs).all()))
print("trial type codes present:", sorted({t for s in vr.trial_types_present for t in s}))
print("position range (cm): min", round(vr.x_min.min(), 4), "max", round(vr.x_max.max(), 2))
v200 = vr[vr.track_length == c.TRACK_LENGTH_CM]
print(f"position range (cm) in the {len(v200)} sessions on the 200 cm track: min", round(v200.x_min.min(), 4),
      "max", round(v200.x_max.max(), 2), "; per-session maximum from", round(v200.x_max.min(), 2))
print("\nTrack length (median of the per-trial maximum position, to 10 cm), sessions by mouse:")
print(pd.crosstab(vr.mouse, vr.track_length, margins=True).to_string())
gap = vr.t_last - vr.es_duration
print(f"\nPosition end minus raw signal end (s): median {gap.median():.4f}, "
      f"within 0.1 s in {(gap.abs() < 0.1).sum()} of {len(vr)} files")
print(vr.loc[gap.abs() >= 0.1, ["path", "es_duration", "t_last", "n_trials"]].round(1).to_string(index=False))
print("\nExample trial type sequences (first 30 trials):")
print(vr.groupby("mouse").trial_type_sequence.first().to_string())
print("\nSessions with probe trials:", int((vr.n_probe > 0).sum()), "of", len(vr))
print("median speed over each session (cm/s): range", round(vr.median_speed.min(), 1), "to", round(vr.median_speed.max(), 1))
VR position: sample interval (s) [np.float64(0.001)] ; first timestamp [np.float64(0.0)]
trial numbers start at 1 in every file: True ; never decrease: True ; each trial one contiguous run: True
trial type codes present: [0, 1, 2]
position range (cm): min 0.0 max 342.0
position range (cm) in the 326 sessions on the 200 cm track: min 0.0 max 200.0 ; per-session maximum from 199.99

Track length (median of the per-trial maximum position, to 10 cm), sessions by mouse:
track_length  200.0  230.0  270.0  340.0  All
mouse                                        
M1               11      0      0      0   11
M10              25      0      0      0   25
M11              36      2      5      2   45
M12              28      0      2      0   30
M13              29      0      0      0   29
M14              37      1      5      2   45
M15              21      0      0      0   21
M2               29      0      0      0   29
M3               30      0      5      0   35
M4               30      0      0      0   30
M6               22      0      0      0   22
M7               28      0      5      0   33
All             326      3     22      4  355

Position end minus raw signal end (s): median -0.0007, within 0.1 s in 353 of 355 files
                                                       path  es_duration  t_last  n_trials
sub-M13/sub-M13_ses-M13-D27-2021-06-15-11-43-42_ecephys.nwb       2167.6   687.5      21.0
 sub-M15/sub-M15_ses-M15-D5-2021-05-14-12-46-13_ecephys.nwb       1778.6   881.4      14.0

Example trial type sequences (first 30 trials):
mouse
M1     NBBBBNBBBBNBBBBNBBBBNBBBBNBBBB
M10       BNBBNBBNBBNBBNBBNBBNBBNBBNB
M11                           BBBNBBN
M12    NBBPBBNBBNBBPBBNBBNBBPBBNBBNBB
M13               BBBNNBBBBBNBBBNBBNB
M14            BNNBBBNBBBBNBBBBNBBNNN
M15                          BBBNBBNB
M2         BBNBBBBBNBBBBNBBBBNBBBBNBB
M3     NBBBBPBBBBBNBBBBPBBBBNBBBBBPBB
M4                            BBBBNBB
M6     BNBBBPBBBNBBBPBBBNBBBPBBBNBBBP
M7     BBBBNBBBBNBBBBNBBBBNBBBBBBNBBB

Sessions with probe trials: 105 of 355
median speed over each session (cm/s): range 0.0 to 70.7

The VR trial columns behave as the description says: trials numbered from 1, each a contiguous run, trial types 0, 1 and 2, and position from 0 to 200 cm in the 326 sessions on the paper's 200 cm track (the range over all VR files is 0 to 342 cm). Most sessions use the 200 cm track; 29 use longer tracks (230 to 340 cm), which the files' notes say the paper does not use. In all but two VR files the position series ends within 0.1 s of the raw signal (median 0.7 ms early); in two (listed) it stops part way through, so the rest of the recording has no behaviour. The trial type sequences show repeating blocks as the paper describes.

In [6]:
# The paper's summary table (Supplementary file 1): sessions and trials per session per mouse.
paper_table = pd.DataFrame({
    "mouse": ["M1", "M3", "M6", "M7", "M10", "M11", "M12", "M13", "M14"],
    "paper_sessions": [11, 26, 21, 23, 16, 35, 21, 25, 37],
    "paper_trials_mean": [45.5, 270.3, 172.3, 268.1, 74.3, 173.1, 244.4, 47.5, 316.4],
    "paper_trials_sd": [33.6, 93.5, 99.8, 137.8, 34.8, 126.1, 68.5, 25.1, 148.2],
}).set_index("mouse")
# Sessions the paper's code can use: 200 cm track and at least 10 trials (claims file, opening comment).
use = vr[(vr.track_length == c.TRACK_LENGTH_CM) & (vr.n_trials >= c.MIN_TRIALS)]
mine = use.groupby("mouse").n_trials.agg(sessions="size", trials_mean="mean", trials_sd="std")
# The paper sorted open arena and VR recordings of a day together, so a VR session without an open arena
# session on the same day cannot give grid cells.
paired_days = set(of.groupby(["mouse", "day"]).size().index)
use_paired = use[[(m, d) in paired_days for m, d in zip(use.mouse, use.day)]]
mine["sessions_with_open_arena_same_day"] = use_paired.groupby("mouse").size()
cmp = paper_table.join(mine).round(1)
print(cmp.to_string())
print(f"\nNine mice: paper {paper_table.paper_sessions.sum()} sessions; dandiset {int(cmp.sessions.sum())} "
      f"200 cm sessions with >= 10 trials, {int(cmp.sessions_with_open_arena_same_day.sum())} of them with an open arena session the same day")
print("Mean over the nine mice of each mouse's mean trials per session: paper 179.1; dandiset",
      round(cmp.trials_mean.mean(), 1))
others = mine.drop(paper_table.index, errors="ignore")
print("\nMice in the dandiset but not in the paper's table (the code drops M2, M4 and M15, which had no grid cells):")
print(others.round(1).to_string())
print("\nDays by session types held (o = open arena, v = VR):")
print(survey.groupby(["mouse", "day"]).session_description.agg(lambda s: "".join(sorted(x[0] for x in s))).value_counts().to_string())
       paper_sessions  paper_trials_mean  paper_trials_sd  sessions  trials_mean  trials_sd  sessions_with_open_arena_same_day
mouse                                                                                                                         
M1                 11               45.5             33.6        11         58.0       38.2                                 11
M3                 26              270.3             93.5        30        259.2      109.1                                 29
M6                 21              172.3             99.8        21        183.3      102.1                                 21
M7                 23              268.1            137.8        28        271.7      144.3                                 27
M10                16               74.3             34.8        25        106.5       74.3                                 25
M11                35              173.1            126.1        35        165.1      127.7                                 35
M12                21              244.4             68.5        28        288.1       86.8                                 28
M13                25               47.5             25.1        27         44.5       24.1                                 26
M14                37              316.4            148.2        37        278.9      155.8                                 37

Nine mice: paper 215 sessions; dandiset 242 200 cm sessions with >= 10 trials, 239 of them with an open arena session the same day
Mean over the nine mice of each mouse's mean trials per session: paper 179.1; dandiset 183.9

Mice in the dandiset but not in the paper's table (the code drops M2, M4 and M15, which had no grid cells):
       sessions  trials_mean  trials_sd  sessions_with_open_arena_same_day
mouse                                                                     
M15          12         31.4       12.7                                 12
M2           29         78.9       46.7                                 21
M4           29        189.7       86.4                                 28

Days by session types held (o = open arena, v = VR):
session_description
ov    331
o      48
v      24

The dandiset has 242 usable 200 cm sessions for the nine mice against the paper's 215, a difference of 27 that is outside the claim's tolerance of 21.5 (10% of 215). Per mouse, five of the nine equal or are within 10% of the paper's table (M1, M6, M11, M13, M14); for M3, M7, M10 and M12 the dandiset has more, by 15% (M3), 22% (M7), 56% (M10) and 33% (M12). The paper counts only sessions in which it kept units, which this dandiset cannot show, so the excess is consistent with sessions that yielded no kept units but is not shown to be that. The mean trials per session differ from the table by up to 43% for single mice (M10), and their mean over the nine mice (183.9 against 179.1) is within 3% of the table's. This is a data check and does not test the claim, which also needs cell and grid cell counts. Twelve mice are in the dandiset; the three not in the paper's table (M2, M4, M15) are those the paper's code drops for having no grid cells.

In [7]:
# Open arena sessions: nine undescribed columns. Their ranges suggest what they are.
cmin = pd.DataFrame(of.col_min.tolist()); cmax = pd.DataFrame(of.col_max.tolist())
print("Open arena position: sample interval (s)", sorted(of.dt.round(6).unique()))
print("Per-column range over sessions, median of the session minimum and maximum:")
print(pd.DataFrame({"min_min": cmin.min(), "min_median": cmin.median(), "max_median": cmax.median(), "max_min": cmax.min(),
                    "max_max": cmax.max()}).round(2).to_string())
print("NaN fraction, largest over sessions, per column:", np.round(np.array(of.col_nan.tolist()).max(axis=0), 3).tolist())
of["x_extent"] = cmax[1].values - cmin[1].values
of["y_extent"] = cmax[3].values - cmin[3].values
small = of[(of.x_extent < 90) & (of.y_extent < 90)]
print(f"\nSessions whose columns 1 and 3 span less than 90 cm in both: {len(small)}, by mouse:",
      small.mouse.value_counts().to_dict())
print("their extents (cm): x", round(small.x_extent.min(), 1), "to", round(small.x_extent.max(), 1),
      "; y", round(small.y_extent.min(), 1), "to", round(small.y_extent.max(), 1))
rest = of.drop(small.index)
print("other sessions: x", round(rest.x_extent.min(), 1), "to", round(rest.x_extent.max(), 1),
      "; y", round(rest.y_extent.min(), 1), "to", round(rest.y_extent.max(), 1))
gap = of.t_last - of.es_duration
print(f"\nPosition end minus raw signal end (s): median {gap.median():.2f}, 5th to 95th percentile "
      f"{gap.quantile(0.05):.2f} to {gap.quantile(0.95):.2f}; more than 10 s apart in {(gap.abs() > 10).sum()} of {len(of)} files"
      f" (largest {gap.max():.1f} s longer, {-gap.min():.1f} s shorter)")
Open arena position: sample interval (s) [np.float64(0.033333)]
Per-column range over sessions, median of the session minimum and maximum:
   min_min  min_median  max_median  max_min   max_max
0     0.00        1.13     1816.73  1409.80   3225.80
1     0.00        0.00      101.99    67.50    103.69
2     0.00        0.00      448.77   296.99    456.23
3     0.00        0.00      100.25    67.63    103.20
4     0.00        0.00      441.11   297.57    454.09
5  -180.00     -179.99      180.00   179.95    180.00
6    33.43     1793.02     5656.57  1983.43  37697.43
7     0.00        0.00       70.48    32.09    131.62
8   -17.93        0.07     1814.07  1402.80   3225.23
NaN fraction, largest over sessions, per column: [0.0, 0.008, 0.008, 0.008, 0.008, 0.008, 0.0, 0.008, 0.0]

Sessions whose columns 1 and 3 span less than 90 cm in both: 38, by mouse: {'M2': 20, 'M1': 18}
their extents (cm): x 67.5 to 89.4 ; y 67.6 to 89.8
other sessions: x 89.3 to 103.7 ; y 88.0 to 103.2

Position end minus raw signal end (s): median 2.87, 5th to 95th percentile 1.20 to 9.49; more than 10 s apart in 20 of 379 files (largest 451.9 s longer, 178.2 s shorter)

The open arena position has nine columns that the file does not describe. From their ranges, columns 1 and 3 look like x and y in cm, columns 2 and 4 like the same in camera pixels, column 5 like head direction in degrees, column 7 like speed in cm/s, and columns 0 and 8 like times; column 6 is not identified (its session minima run from 33.4, its maxima up to 37,697). The file's experiment description gives a 100 x 100 cm arena; most sessions span 88.0 to 103.7 cm in x and y, but 38 open arena sessions, 18 of M1's 41 and 20 of M2's 21, span less than 90 cm in both, from 67.5 to 89.8 cm. The two ranges overlap a little, but the smallest extents are far below 100 cm, so either the arena was smaller in some sessions or the scaling differs. The position series usually runs a few seconds past the end of the raw signal (median above), so its last samples have no neural data.

Even with sorted units, the open arena rate maps on which grid cells are classified would rest on these inferred column meanings, and for 18 sessions of M1 (one of the paper's nine mice) and 20 of M2 on extents of 67.5 to 89.8 cm whose scaling the files do not explain. A re-sort could therefore still meet a metadata_insufficient block for the open arena claims; this is not tested here, as the units are missing first.

In [8]:
# Recordings deposited twice: identical position series, or identical raw blocks in files with the same
# number of raw samples.
print("VR files with an identical position series:", int(vr.md5.duplicated(keep=False).sum()))
print("open arena files with an identical position series:", int(of.md5.duplicated(keep=False).sum()))
same_len = survey[survey.es_samples.duplicated(keep=False)]
print(f"files sharing a raw sample count with another file: {len(same_len)} "
      f"({same_len.es_samples.nunique()} distinct counts)")
digests = pd.DataFrame(c.map_files(c.raw_block_digest, [a for a in assets if a["path"] in set(same_len.path)], workers=16))
same_len = same_len.merge(digests, on="path", suffixes=("", "_raw"))
dup_raw = same_len[same_len.duplicated(["es_samples", "md5_raw"], keep=False)]
print("of these, files whose 1 s raw block at sample 3,000,000 matches another file's:", len(dup_raw))
print("median SD of those raw blocks (ADC units):", round(float(same_len["std"].median()), 1))
print("same-count pairs within one mouse:", int(same_len.groupby("es_samples").mouse.nunique().eq(1).sum()))
VR files with an identical position series: 0
open arena files with an identical position series: 0
files sharing a raw sample count with another file: 74 (36 distinct counts)
of these, files whose 1 s raw block at sample 3,000,000 matches another file's: 0
median SD of those raw blocks (ADC units): 590.9
same-count pairs within one mouse: 2

No recording is deposited twice: no two position series are identical, and the files that share a raw sample count (Open Ephys writes in blocks of 1024 samples, so equal counts are common) hold different raw signals.

Shared clock of the raw signal and the position series¶

The files hold no spikes and no trial table, so the shared clock is checked between the raw signal and the position series, both starting at 0 s. Multi-unit activity (threshold crossings, as in the helper) is counted in 100 ms bins and compared with running speed: in the medial entorhinal cortex activity rises with running speed, so if the two series share a clock the correlation peaks at zero lag and the contrast between running and stopped bins is largest with the labels unshifted. Six VR sessions (one per mouse, six mice) and three open arena sessions are checked, five minutes from 300 s each.

In [9]:
rng = np.random.default_rng(943)
check_vr = []
for m in ["M3", "M7", "M11", "M12", "M13", "M14"]:
    pool = use[use.mouse == m].sort_values("path")
    check_vr.append(pool.iloc[rng.integers(len(pool))].path)
pool_of = of[~of.mouse.isin(["M1", "M2"])].sort_values("path")
check_of = list(pool_of.iloc[rng.choice(len(pool_of), 3, replace=False)].path)
t = time.time()
mua = c.map_files(c.mua_check, [by_path[p] for p in check_vr + check_of], workers=3)
print(f"read and filtered {len(mua)} five-minute raw blocks in {time.time() - t:.0f} s")
shifts = np.array([-50, -20, -10, -5, -2, -1, 0, 1, 2, 5, 10, 20, 50])
rows = []
for p, r in zip(check_vr + check_of, mua):
    lags, cc = c.lagged_corr(r["mua"], r["speed"], 50)
    con = c.shifted_contrast(r["mua"], r["speed"], shifts)
    rows.append({"file": p.split("/")[1][:-12], "type": "vr" if p in check_vr else "openfield",
                 "r_lag0": cc[50], "peak_lag_s": lags[np.argmax(cc)] * 0.1, "r_peak": cc.max(),
                 "r_at_+-5s": np.round([cc[0], cc[-1]], 3).tolist(),
                 "run/stop_lag0": con[shifts == 0][0], "best_shift_s": shifts[np.argmax(con)] * 0.1,
                 "run/stop_at_+-5s": np.round([con[0], con[-1]], 3).tolist(),
                 "stopped_bins": int((r["speed"] < c.STOP_SPEED_CM_S).sum()), "bins": len(r["speed"])})
clock = pd.DataFrame(rows)
print(clock.round(3).to_string(index=False))
read and filtered 9 five-minute raw blocks in 43 s
                                   file      type  r_lag0  peak_lag_s  r_peak       r_at_+-5s  run/stop_lag0  best_shift_s run/stop_at_+-5s  stopped_bins  bins
  sub-M3_ses-M3-D27-2020-12-04-15-10-38        vr  -0.198         1.8   0.302  [0.22, -0.174]          0.617           2.0   [1.291, 0.992]           536  3000
   sub-M7_ses-M7-D7-2020-11-06-16-14-44        vr   0.200         0.1   0.207  [0.111, 0.152]          1.192           0.1   [1.087, 1.135]          1578  3000
sub-M11_ses-M11-D12-2021-05-25-09-49-23        vr   0.123        -0.3   0.147 [-0.014, 0.036]          1.219          -0.1    [0.984, 1.04]          2331  3000
sub-M12_ses-M12-D17-2021-06-01-11-08-39        vr  -0.134         1.7   0.193 [-0.049, -0.06]          0.791           2.0    [0.876, 0.92]           738  3000
sub-M13_ses-M13-D13-2021-05-26-10-56-26        vr   0.093         0.0   0.093  [0.005, 0.014]          1.066          -0.1   [1.001, 1.002]          1410  3000
sub-M14_ses-M14-D34-2021-06-24-12-48-57        vr   0.129         0.4   0.149   [0.009, 0.04]          1.002           1.0   [1.028, 1.036]           537  3000
sub-M11_ses-M11-D35-2021-06-25-11-26-39 openfield   0.100         0.4   0.193 [-0.004, 0.033]          1.063           0.1   [1.002, 1.007]           617  3000
sub-M14_ses-M14-D22-2021-06-08-11-54-13 openfield   0.249         0.0   0.249  [0.036, 0.053]          1.173          -0.1   [1.065, 1.017]           485  3000
  sub-M3_ses-M3-D19-2020-11-22-15-25-38 openfield   0.327         0.3   0.343 [-0.005, 0.043]          1.272           0.0   [1.007, 1.003]          1003  3000
In [10]:
fig, axes = plt.subplots(1, 2, figsize=(11, 3.5))
for p, r in zip(check_vr + check_of, mua):
    lags, cc = c.lagged_corr(r["mua"], r["speed"], 50)
    axes[0].plot(lags * 0.1, cc, lw=1, label=p.split("/")[1][4:-12], ls="-" if p in check_vr else "--")
    axes[1].plot(shifts * 0.1, c.shifted_contrast(r["mua"], r["speed"], shifts), lw=1, marker=".")
axes[0].set(xlabel="lag of speed relative to multi-unit activity (s)", ylabel="Pearson r", title="MUA against speed")
axes[1].set(xlabel="shift of running/stopped labels (s)", ylabel="MUA running / stopped", title="Shifted labels")
axes[0].legend(fontsize=6)
plt.tight_layout(); plt.show()
print("plotted ranges: r from", round(min(c.lagged_corr(r['mua'], r['speed'], 50)[1].min() for r in mua), 3),
      "to", round(max(c.lagged_corr(r['mua'], r['speed'], 50)[1].max() for r in mua), 3))
print("sessions whose correlation peaks within 0.5 s of zero lag:", int((clock.peak_lag_s.abs() <= 0.5).sum()), "of", len(clock))
print("sessions with more activity running than stopped at no shift:", int((clock["run/stop_lag0"] > 1).sum()), "of", len(clock))
print("sessions whose running/stopped contrast is largest within 0.2 s of no shift:", int((clock.best_shift_s.abs() <= 0.2).sum()), "of", len(clock))
No description has been provided for this image
plotted ranges: r from -0.278 to 0.343
sessions whose correlation peaks within 0.5 s of zero lag: 7 of 9
sessions with more activity running than stopped at no shift: 7 of 9
sessions whose running/stopped contrast is largest within 0.2 s of no shift: 6 of 9

Speed changes over seconds, so this check resolves an offset of about half a second, not less. In seven of the nine sessions (the three open arena sessions and the M7, M11, M13 and M14 VR sessions) the correlation of multi-unit activity with speed peaks within 0.5 s of zero lag and is lower 5 s away (at most 0.15 there, against peaks of 0.09 to 0.34). In six of these, activity is higher when running than when stopped with the labels unshifted (ratios 1.06 to 1.27), and the ratio is largest within 0.1 s of no shift; in the M14 VR session the ratio is 1.00. In these sessions the raw signal and the position series are consistent with one clock, to within about half a second. In the M3 and M12 VR sessions activity is lower when running than when stopped and the correlation peaks near +1.8 s (the running/stopped contrast, with the same sign convention, is largest at +2.0 s); there the check cannot tell a response from an offset (stops on the track come with reward and licking, which can add to the signal), so the shared clock is not established for them. No claim is tested, so no outcome rests on this check.

Why every claim is blocked: the size of spike sorting¶

Every claim needs single units, which the dandiset does not hold. To make them as the paper did, the raw signal of each recording day (the open arena and VR files of the day together) would be streamed, filtered, whitened and sorted with MountainSort, then curated by isolation, noise overlap and peak SNR (Methods, Spike sorting). The cell below measures how fast the raw signal streams from the archive here, with 16 processes each reading 20 s of signal (about 19 MB) from a different file, and from that gives a lower bound on the time to read the raw signal of the recording days of the 239 paired 200 cm sessions of the nine mice. These are a superset of the paper's 215 sessions, which the dandiset does not mark, so the size is also scaled by 215/239 as an estimate for the paper's sessions alone. Sorting time comes on top of it and is not measured. The limit compared with is the protocol's: "CI fails a notebook over sixty" minutes (docs/protocol.md, Author, step 3, "Keep it fast"). The bound alone exceeds it, so a timed MountainSort run, which would only add to it, was not set up.

In [11]:
rate = c.streaming_rate([by_path[p] for p in use.sort_values("path").path.iloc[::20][:16]], workers=16)
print(f"read {rate['bytes'] / 1e6:.0f} MB in {rate['wall_s']:.1f} s: aggregate {rate['aggregate_mb_s']:.1f} MB/s "
      f"with {rate['workers']} processes; median single stream {rate['single_stream_mb_s']:.1f} MB/s")
nine = ["M1", "M3", "M6", "M7", "M10", "M11", "M12", "M13", "M14"]
days = use_paired[use_paired.mouse.isin(nine)][["mouse", "day"]].drop_duplicates()
need = survey.merge(days, on=["mouse", "day"])
gb_all = survey.size_gb.sum()
gb_need = need.size_gb.sum()
for label, gb in [("every file", gb_all), ("days of the nine mice with a usable VR session and an open arena session", gb_need)]:
    hours = gb * 1e3 / rate["aggregate_mb_s"] / 3600
    print(f"{label}: {gb:.0f} GB, {len(survey) if gb == gb_all else len(need)} files; reading alone at the measured rate "
          f"takes {hours:.1f} h (notebook limit {c.NOTEBOOK_LIMIT_MIN} min)")
# These days hold the 239 paired sessions, a superset of the paper's 215; scaled to 215 as a smaller bound.
n_paired = int(use_paired.mouse.isin(nine).sum())
gb_scaled = gb_need * paper_table.paper_sessions.sum() / n_paired
print(f"scaled by the paper's {paper_table.paper_sessions.sum()} of these {n_paired} sessions: {gb_scaled:.0f} GB; "
      f"reading alone takes {gb_scaled * 1e3 / rate['aggregate_mb_s'] / 3600:.1f} h")
print(f"\nnotebook so far: {(time.time() - started) / 60:.1f} min")
read 307 MB in 3.2 s: aggregate 95.8 MB/s with 16 processes; median single stream 9.8 MB/s
every file: 976 GB, 734 files; reading alone at the measured rate takes 2.8 h (notebook limit 60 min)
days of the nine mice with a usable VR session and an open arena session: 638 GB, 478 files; reading alone at the measured rate takes 1.8 h (notebook limit 60 min)
scaled by the paper's 215 of these 239 sessions: 574 GB; reading alone takes 1.7 h

notebook so far: 2.3 min

Summary¶

Claim Outcome
All 31 claims blocked, too_large

How the analysis differs from the paper's. No analysis of the claims was run. The paper's analysis starts from units sorted from the raw signal with MountainSort; the dandiset holds the raw signal and positions only, and sorting about 1 TB of raw signal, of which the reading alone takes more than the notebook limit (above), cannot be done here. The paper's data are deposited at Edinburgh DataShare (the paper's data availability statement); they are not used or inspected, as the protocol tests the dandiset.

What was found about the dandiset.

  • No file holds sorted units, a trials table, reward times or any processed data; every claim of the paper needs units.
  • The position series' description, the same in every file, fits neither the three VR columns nor the nine open arena columns; the open arena columns are not described.
  • 38 open arena sessions, 18 of M1 and 20 of M2, span 67.5 to 89.8 cm, against the 100 x 100 cm arena the files describe; the other sessions span 88.0 to 103.7 cm.
  • Two VR files have position for only part of the recording.
  • The usable 200 cm session counts differ from the paper's table for five of nine mice (by more than 10% for four), and the dandiset holds three mice the paper's table leaves out, as the paper's code drops them.
  • No recording is deposited twice. The raw signal and the position series are consistent with one clock in seven of nine sessions checked; in two VR sessions the check could not tell.