benchdash / validate_data.py
josephsoo's picture
Remove release manifest bookkeeping
cfd9435
Raw
History Blame Contribute Delete
33.5 kB
#!/usr/bin/env python3
"""Validate the bundled BEND-BCI Space tables and their paper provenance.
The local checks run in the standalone Hugging Face Space. Passing
``--canonical-root`` additionally compares every manuscript-facing summary
against ``paper/results`` in the main benchmark repository.
"""
from __future__ import annotations
import argparse
from pathlib import Path
from typing import Iterable
import numpy as np
import pandas as pd
DATASETS = {
"monkey",
"allen_neuropixels",
"speech",
"mc_pacman",
"ratinabox",
}
CONSISTENCY_DATASETS = DATASETS - {"mc_pacman"}
LATENT_COORDINATE_SPACE = "per_session_whitened_reference_aligned_3d"
FIGURE5_TARGET_SESSION = "sub-C_ses-CO-20150716_behavior+ecephys"
# Coverage is defined in Supplementary Table 2.
EXPECTED_COVERAGE = {
"clean_prediction_summary.csv": (115, 112),
"robustness_summary.csv": (115, 112),
"scalability_summary.csv": (115, 112),
"consistency_summary.csv": (54, 46),
"neuron_shap_summary.csv": (106, 106),
"trial_shapley_summary.csv": (81, 81),
"trial_shapley_retrain_summary.csv": (99, 99),
}
REQUIRED_COLUMNS = {
"dataset_overview.csv": {
"dataset", "dataset_name", "species", "task", "array_shape",
"target", "score", "bin_ms", "recordings", "source_label",
"source_url", "example_trial_index", "example_features_shown",
},
"dataset_example_neural.csv": {
"dataset", "trial_index", "time_index", "time_ms",
"feature_display_index", "feature_index", "neural_value",
},
"dataset_example_targets.csv": {
"dataset", "trial_index", "time_index", "time_ms", "target_0",
"target_1", "target_label",
},
"dataset_targets.csv": {
"dataset", "trial_index", "trial_id", "condition_id", "target_label",
"time_index", "time_ms", "target_0", "target_1", "is_example",
},
"feature_example_raster.csv": {
"dataset", "trial_index", "display_index", "feature_index",
"feature_group", "validation_value", "group_order", "n_time",
"t0_index", "bin_ms",
},
"clean_prediction_summary.csv": {
"model", "dataset", "status", "metric", "score", "decoder",
},
"robustness_summary.csv": {
"model", "dataset", "status", "metric", "noise_levels", "scores",
"raw_auc",
},
"scalability_summary.csv": {
"model", "dataset", "status", "training_time_sec",
"inference_time_sec", "peak_ram_gb", "peak_vram_gb",
},
"consistency_summary.csv": {
"model", "dataset", "is_active_model", "mean_r2", "n_sessions",
"sessions", "latent_dim", "scoring_modes", "normalizations",
},
"neuron_shap_summary.csv": {
"model", "dataset", "is_active_model", "auc", "spearman_corr",
"shap_mean_value", "shap_min_value", "shap_max_value",
"shap_fraction_positive", "shap_fraction_negative",
},
"neuron_attributions.csv": {
"model", "dataset", "feature_index", "feature_group",
"signed_attribution", "attribution_rank", "attribution_bin",
"validation_value",
},
"trial_shapley_summary.csv": {
"model", "dataset", "is_active_model", "analysis", "perturbation_auc",
"rotation_angle_deg", "rotation_subspace_dim_spec",
"trial_selection_mode", "converged", "shapley_mean_value",
"shapley_min_value", "shapley_max_value",
"shapley_fraction_positive", "shapley_fraction_negative",
},
"trial_shapley_retrain_summary.csv": {
"analysis", "model", "is_active_model", "condition", "metric", "score",
},
"trial_historical_trajectories.csv": {
"model", "target_session", "trial_index", "trial_id",
"direction_index", "direction_label", "time_index", "target_x",
"target_y", "current_only_x", "current_only_y",
"historical_selected_x", "historical_selected_y", "current_only_r2",
"historical_selected_r2",
},
"latent_samples.csv": {
"model", "dataset", "session", "session_label", "x", "y", "z",
"condition", "trial_index", "time_index", "eval_time_index",
"coordinate_space", "reference_session", "alignment", "landmark_type",
"n_alignment_landmarks", "is_reference", "session_order",
},
"latent_trajectories.csv": {
"model", "dataset", "session", "session_label", "x", "y", "z",
"condition", "time_index", "eval_time_index", "n_points",
"coordinate_space", "reference_session", "alignment", "landmark_type",
"n_alignment_landmarks", "is_reference", "session_order",
},
}
UNIQUE_KEYS = {
"dataset_overview.csv": ["dataset"],
"dataset_example_neural.csv": ["dataset", "time_index", "feature_display_index"],
"dataset_example_targets.csv": ["dataset", "time_index"],
"dataset_targets.csv": ["dataset", "trial_index", "time_index"],
"feature_example_raster.csv": ["dataset", "feature_index"],
"clean_prediction_summary.csv": ["model", "dataset"],
"robustness_summary.csv": ["model", "dataset"],
"scalability_summary.csv": ["model", "dataset"],
"consistency_summary.csv": ["model", "dataset"],
"neuron_shap_summary.csv": ["model", "dataset"],
"neuron_attributions.csv": ["model", "dataset", "feature_index"],
"trial_shapley_summary.csv": ["model", "dataset"],
"trial_shapley_retrain_summary.csv": ["analysis", "model", "condition"],
}
CANONICAL_NAMES = {
"clean_prediction_summary.csv": "metrics_summary.csv",
"robustness_summary.csv": "robustness_summary.csv",
"scalability_summary.csv": "scalability_summary.csv",
"consistency_summary.csv": "consistency_summary.csv",
"neuron_shap_summary.csv": "neuron_shap_summary.csv",
"trial_shapley_summary.csv": "trial_shapley_summary.csv",
"trial_shapley_retrain_summary.csv": "trial_shapley_retrain_summary.csv",
}
class ValidationError(RuntimeError):
"""Raised when Space data violates its manuscript-facing contract."""
def _active_mask(frame: pd.DataFrame) -> pd.Series:
if "status" in frame.columns:
return frame["status"].fillna("").eq("present")
if "is_active_model" in frame.columns:
return frame["is_active_model"].astype(str).str.lower().eq("true")
return pd.Series(True, index=frame.index)
def _require(condition: bool, message: str, errors: list[str]) -> None:
if not condition:
errors.append(message)
def _same_values(left: pd.DataFrame, right: pd.DataFrame) -> None:
pd.testing.assert_frame_equal(
left.reset_index(drop=True),
right.reset_index(drop=True),
check_dtype=False,
check_exact=True,
check_categorical=False,
)
def _validate_latent_cell(
frame: pd.DataFrame,
*,
table_label: str,
model: str,
dataset: str,
sessions: list[str],
landmark_type: str,
errors: list[str],
) -> None:
"""Validate reference/alignment metadata for one method-dataset cell."""
cell = frame[
frame["model"].astype(str).eq(model)
& frame["dataset"].astype(str).eq(dataset)
]
for session_order, session in enumerate(sessions):
session_rows = cell[cell["session"].astype(str).eq(session)]
if session_rows.empty:
errors.append(f"{table_label}: missing {model}/{dataset}/{session}")
continue
expected_reference = "true" if session_order == 0 else "false"
observed_reference = set(
session_rows["is_reference"].dropna().astype(str).str.lower()
)
_require(
observed_reference == {expected_reference},
f"{table_label}: {model}/{dataset}/{session} is_reference "
f"values {sorted(observed_reference)}",
errors,
)
expected_alignment = "identity" if session_order == 0 else "proper_similarity_procrustes"
observed_alignment = set(session_rows["alignment"].dropna().astype(str))
_require(
observed_alignment == {expected_alignment},
f"{table_label}: {model}/{dataset}/{session} alignment "
f"values {sorted(observed_alignment)}",
errors,
)
observed_reference_sessions = set(
session_rows["reference_session"].dropna().astype(str)
)
_require(
observed_reference_sessions == {sessions[0]},
f"{table_label}: {model}/{dataset}/{session} reference metadata "
f"{sorted(observed_reference_sessions)}",
errors,
)
observed_landmarks = set(session_rows["landmark_type"].dropna().astype(str))
_require(
observed_landmarks == {landmark_type},
f"{table_label}: {model}/{dataset}/{session} landmarks "
f"{sorted(observed_landmarks)}",
errors,
)
orders = pd.to_numeric(session_rows["session_order"], errors="coerce")
_require(
orders.notna().all()
and np.isfinite(orders.to_numpy()).all()
and orders.eq(session_order).all(),
f"{table_label}: {model}/{dataset}/{session} has invalid session_order",
errors,
)
landmark_counts = pd.to_numeric(
session_rows["n_alignment_landmarks"], errors="coerce"
)
_require(
landmark_counts.notna().all()
and np.isfinite(landmark_counts.to_numpy()).all()
and landmark_counts.ge(3).all(),
f"{table_label}: {model}/{dataset}/{session} has invalid landmark count",
errors,
)
def validate_local(data_dir: Path) -> dict[str, pd.DataFrame]:
"""Validate schemas, coverage, uniqueness, and fixed analysis conventions."""
frames: dict[str, pd.DataFrame] = {}
errors: list[str] = []
for name, columns in REQUIRED_COLUMNS.items():
path = data_dir / name
if not path.exists():
errors.append(f"missing required table: {path}")
continue
frame = pd.read_csv(path, low_memory=False)
missing = sorted(columns - set(frame.columns))
_require(not missing, f"{name}: missing columns {missing}", errors)
if missing:
continue
frames[name] = frame
for name, (n_rows, n_active) in EXPECTED_COVERAGE.items():
if name not in frames:
continue
frame = frames[name]
_require(len(frame) == n_rows, f"{name}: expected {n_rows} rows, found {len(frame)}", errors)
active = int(_active_mask(frame).sum())
_require(active == n_active, f"{name}: expected {n_active} available rows, found {active}", errors)
for name, keys in UNIQUE_KEYS.items():
if name not in frames or not set(keys).issubset(frames[name].columns):
continue
duplicates = frames[name].duplicated(keys, keep=False)
_require(not duplicates.any(), f"{name}: duplicate keys for {keys}", errors)
for name in ("clean_prediction_summary.csv", "robustness_summary.csv", "scalability_summary.csv"):
if name in frames:
observed = set(frames[name]["dataset"].dropna().astype(str))
_require(observed == DATASETS, f"{name}: dataset set is {sorted(observed)}", errors)
for name in (
"dataset_overview.csv",
"dataset_example_neural.csv",
"dataset_example_targets.csv",
"dataset_targets.csv",
"feature_example_raster.csv",
):
if name in frames:
observed = set(frames[name]["dataset"].dropna().astype(str))
_require(observed == DATASETS, f"{name}: dataset set is {sorted(observed)}", errors)
if "dataset_overview.csv" in frames:
overview = frames["dataset_overview.csv"]
_require(len(overview) == 5, "dataset overview: expected five rows", errors)
shown = pd.to_numeric(overview["example_features_shown"], errors="coerce")
_require(
shown.notna().all() and shown.between(1, 80).all(),
"dataset overview: invalid example feature counts",
errors,
)
if "dataset_example_neural.csv" in frames:
examples = frames["dataset_example_neural.csv"]
values = pd.to_numeric(examples["neural_value"], errors="coerce")
_require(
values.notna().all() and np.isfinite(values.to_numpy()).all(),
"dataset examples: neural values must be finite",
errors,
)
trials_per_dataset = examples.groupby("dataset")["trial_index"].nunique()
_require(
trials_per_dataset.eq(1).all(),
"dataset examples: expected one trial per dataset",
errors,
)
if "dataset_example_targets.csv" in frames:
targets = frames["dataset_example_targets.csv"]
trials_per_dataset = targets.groupby("dataset")["trial_index"].nunique()
_require(
trials_per_dataset.eq(1).all(),
"dataset targets: expected one trial per dataset",
errors,
)
classification = targets[
targets["dataset"].isin({"allen_neuropixels", "speech"})
]
_require(
len(classification) == 2 and classification["target_label"].notna().all(),
"dataset targets: classification labels are missing",
errors,
)
if "dataset_targets.csv" in frames:
targets = frames["dataset_targets.csv"]
expected_rows = {
"monkey": 15_950,
"allen_neuropixels": 598,
"speech": 168,
"mc_pacman": 40_544,
"ratinabox": 15_000,
}
observed_rows = targets.groupby("dataset").size().to_dict()
_require(
observed_rows == expected_rows,
f"all targets: row counts are {observed_rows}",
errors,
)
for column in ["trial_index", "condition_id", "time_index"]:
values = pd.to_numeric(targets[column], errors="coerce")
_require(
values.notna().all() and np.allclose(values, np.round(values)),
f"all targets: invalid {column} values",
errors,
)
example_mask = targets["is_example"].astype(str).str.lower().eq("true")
example_trials = targets.loc[example_mask].groupby("dataset")[
"trial_index"
].nunique()
_require(
example_trials.reindex(sorted(DATASETS)).eq(1).all(),
"all targets: expected one highlighted trial per dataset",
errors,
)
classification = targets[targets["dataset"].isin({"allen_neuropixels", "speech"})]
class_sets = classification.groupby("dataset")["condition_id"].apply(
lambda values: set(pd.to_numeric(values, errors="coerce").astype(int))
)
_require(
class_sets.map(lambda values: values == set(range(8))).all()
and classification["target_label"].notna().all(),
"all targets: classification labels or class coverage differ",
errors,
)
continuous = targets[targets["dataset"].isin({"monkey", "mc_pacman", "ratinabox"})]
target_0 = pd.to_numeric(continuous["target_0"], errors="coerce")
_require(
target_0.notna().all() and np.isfinite(target_0.to_numpy()).all(),
"all targets: continuous target_0 values must be finite",
errors,
)
two_dimensional = continuous[continuous["dataset"].isin({"monkey", "ratinabox"})]
target_1 = pd.to_numeric(two_dimensional["target_1"], errors="coerce")
_require(
target_1.notna().all() and np.isfinite(target_1.to_numpy()).all(),
"all targets: two-dimensional target values must be finite",
errors,
)
if "feature_example_raster.csv" in frames:
raster = frames["feature_example_raster.csv"]
value_columns = sorted(
column for column in raster.columns if column.startswith("value_")
)
_require(
len(value_columns) == 300,
"feature raster: expected 300 time-value columns",
errors,
)
for dataset, group in raster.groupby("dataset"):
n_time = pd.to_numeric(group["n_time"], errors="coerce")
_require(
n_time.notna().all() and n_time.nunique() == 1,
f"feature raster: inconsistent time length for {dataset}",
errors,
)
if n_time.notna().all():
active_columns = value_columns[: int(n_time.iloc[0])]
active = group[active_columns].apply(pd.to_numeric, errors="coerce")
_require(
active.notna().all().all()
and np.isfinite(active.to_numpy(dtype=float)).all(),
f"feature raster: nonfinite activity for {dataset}",
errors,
)
if all(
name in frames
for name in (
"dataset_overview.csv",
"dataset_example_neural.csv",
"dataset_example_targets.csv",
"dataset_targets.csv",
)
):
overview_trials = frames["dataset_overview.csv"].set_index("dataset")[
"example_trial_index"
].astype(int)
neural_trials = frames["dataset_example_neural.csv"].groupby("dataset")[
"trial_index"
].first().astype(int)
target_trials = frames["dataset_example_targets.csv"].groupby("dataset")[
"trial_index"
].first().astype(int)
all_targets = frames["dataset_targets.csv"]
all_target_mask = all_targets["is_example"].astype(str).str.lower().eq("true")
all_target_trials = all_targets.loc[all_target_mask].groupby("dataset")[
"trial_index"
].first().astype(int)
_require(
overview_trials.equals(neural_trials.reindex(overview_trials.index))
and overview_trials.equals(target_trials.reindex(overview_trials.index))
and overview_trials.equals(all_target_trials.reindex(overview_trials.index)),
"dataset examples: manifest, neural and target trial indices differ",
errors,
)
if "clean_prediction_summary.csv" in frames:
prediction = frames["clean_prediction_summary.csv"]
_require(prediction["model"].nunique() == 23, "prediction: expected 23 methods", errors)
metrics = set(prediction.loc[_active_mask(prediction), "metric"].dropna())
_require(metrics == {"accuracy", "r2"}, f"prediction: unexpected metrics {sorted(metrics)}", errors)
if "consistency_summary.csv" in frames:
consistency = frames["consistency_summary.csv"]
active = consistency.loc[_active_mask(consistency)]
observed = set(active["dataset"].dropna().astype(str))
_require(observed == CONSISTENCY_DATASETS, f"consistency: dataset set is {sorted(observed)}", errors)
norms = set(active["normalizations"].dropna().astype(str))
_require(norms == {"per_session_whitening"}, f"consistency: unexpected normalization {sorted(norms)}", errors)
if "neuron_shap_summary.csv" in frames:
feature = frames["neuron_shap_summary.csv"]
allen = feature[feature["dataset"].eq("allen_neuropixels")]
_require(not allen.empty and allen["spearman_corr"].notna().all(), "feature attribution: Allen Spearman values missing", errors)
other = feature[~feature["dataset"].eq("allen_neuropixels")]
_require(other["auc"].notna().all(), "feature attribution: ROC-AUC values missing", errors)
_require(feature["shap_min_value"].lt(0).any(), "feature attribution: signed negative values absent", errors)
if "neuron_attributions.csv" in frames:
features = frames["neuron_attributions.csv"]
observed = set(features["dataset"].dropna().astype(str))
_require(observed == DATASETS, f"neuron attributions: dataset set is {sorted(observed)}", errors)
_require(
features[["model", "dataset"]].drop_duplicates().shape[0] == 106,
"neuron attributions: expected 106 method-dataset pairs",
errors,
)
values = pd.to_numeric(features["signed_attribution"], errors="coerce")
_require(
values.notna().all() and np.isfinite(values.to_numpy()).all(),
"neuron attributions: signed values must be finite",
errors,
)
_require(
values.lt(0).any() and values.gt(0).any(),
"neuron attributions: expected positive and negative signed values",
errors,
)
_require(
set(features["attribution_bin"].dropna().astype(str))
== {"Top", "Middle", "Bottom", "Tied"},
"neuron attributions: invalid rank bins",
errors,
)
if "neuron_shap_summary.csv" in frames:
expected_counts = (
frames["neuron_shap_summary.csv"]
.set_index(["model", "dataset"])["shap_n_values"]
.astype(int)
)
observed_counts = features.groupby(["model", "dataset"]).size()
_require(
observed_counts.equals(expected_counts.reindex(observed_counts.index)),
"neuron attributions: feature counts differ from summary",
errors,
)
if "feature_example_raster.csv" in frames:
raster = frames["feature_example_raster.csv"]
raster_counts = raster.groupby("dataset")["feature_index"].nunique()
attribution_counts = features.groupby("dataset")["feature_index"].nunique()
_require(
raster_counts.equals(attribution_counts.reindex(raster_counts.index)),
"feature raster: feature counts differ from attributions",
errors,
)
raster_groups = raster[["dataset", "feature_index", "feature_group"]]
attribution_groups = features[
["dataset", "feature_index", "feature_group"]
].drop_duplicates()
merged = raster_groups.merge(
attribution_groups,
on=["dataset", "feature_index"],
how="outer",
suffixes=("_raster", "_attribution"),
indicator=True,
)
_require(
merged["_merge"].eq("both").all()
and merged["feature_group_raster"].eq(
merged["feature_group_attribution"]
).all(),
"feature raster: group labels differ from attributions",
errors,
)
if "trial_shapley_summary.csv" in frames:
trial = frames["trial_shapley_summary.csv"]
_require(set(trial["analysis"].dropna()) == {"subspace_rotation"}, "trial valuation: noncanonical analysis", errors)
angles = set(pd.to_numeric(trial["rotation_angle_deg"], errors="coerce").dropna())
_require(angles == {75.0}, f"trial valuation: rotation angles {sorted(angles)}", errors)
dims = set(trial["rotation_subspace_dim_spec"].dropna().astype(str))
_require(dims == {"full"}, f"trial valuation: subspace specs {sorted(dims)}", errors)
modes = set(trial["trial_selection_mode"].dropna().astype(str))
_require(modes == {"random"}, f"trial valuation: selection modes {sorted(modes)}", errors)
_require(trial["perturbation_auc"].notna().all(), "trial valuation: detection AUC missing", errors)
_require(trial["shapley_min_value"].lt(0).any(), "trial valuation: signed negative values absent", errors)
if "trial_shapley_retrain_summary.csv" in frames:
retrain = frames["trial_shapley_retrain_summary.csv"]
expected = {
"within_session_cleaning": {"mixed_full", "data_shapley", "oracle"},
"cross_session_old_trial_selection": {
"target_only", "all_sessions", "oldonly_dshap_negative_removal",
},
}
observed = {
analysis: set(group["condition"].dropna().astype(str))
for analysis, group in retrain.groupby("analysis")
}
_require(observed == expected, f"trial retraining: conditions {observed}", errors)
if "trial_historical_trajectories.csv" in frames:
historical = frames["trial_historical_trajectories.csv"]
_require(
len(historical) == 135 * 35,
"historical trajectories: expected 135 trials × 35 time bins",
errors,
)
keys = ["trial_index", "time_index"]
_require(
not historical.duplicated(keys, keep=False).any(),
f"historical trajectories: duplicate keys for {keys}",
errors,
)
_require(
set(historical["model"].dropna().astype(str)) == {"rnn"},
"historical trajectories: expected the Figure 5e RNN example",
errors,
)
_require(
set(historical["target_session"].dropna().astype(str))
== {FIGURE5_TARGET_SESSION},
"historical trajectories: unexpected target session",
errors,
)
_require(
historical["trial_index"].nunique() == 135
and historical["trial_id"].nunique() == 135,
"historical trajectories: expected 135 held-out trials and trial IDs",
errors,
)
time_counts = historical.groupby("trial_index")["time_index"].nunique()
_require(
len(time_counts) == 135 and time_counts.eq(35).all(),
"historical trajectories: every trial must contain 35 time bins",
errors,
)
direction_indices = pd.to_numeric(
historical["direction_index"], errors="coerce"
)
_require(
direction_indices.notna().all()
and set(direction_indices.astype(int)) == set(range(8)),
"historical trajectories: expected all eight reach directions",
errors,
)
for column in (
"target_x",
"target_y",
"current_only_x",
"current_only_y",
"historical_selected_x",
"historical_selected_y",
"current_only_r2",
"historical_selected_r2",
):
values = pd.to_numeric(historical[column], errors="coerce")
_require(
values.notna().all() and np.isfinite(values.to_numpy()).all(),
f"historical trajectories: non-finite {column} values",
errors,
)
_require(
historical["current_only_r2"].nunique() == 1
and historical["historical_selected_r2"].nunique() == 1,
"historical trajectories: expected one R² value per condition",
errors,
)
for name in ("latent_samples.csv", "latent_trajectories.csv"):
if name not in frames:
continue
latent = frames[name]
observed = set(latent["dataset"].dropna().astype(str))
_require(observed.issubset(CONSISTENCY_DATASETS), f"{name}: unsupported datasets {sorted(observed - CONSISTENCY_DATASETS)}", errors)
for column in ("x", "y", "z"):
values = pd.to_numeric(latent[column], errors="coerce")
_require(
values.notna().all() and np.isfinite(values.to_numpy()).all(),
f"{name}: non-finite {column} values",
errors,
)
for column in (
"coordinate_space",
"reference_session",
"alignment",
"landmark_type",
"n_alignment_landmarks",
"is_reference",
"session_order",
):
_require(
latent[column].notna().all(),
f"{name}: missing {column} values",
errors,
)
spaces = set(latent["coordinate_space"].dropna().astype(str))
_require(
spaces == {LATENT_COORDINATE_SPACE},
f"{name}: coordinate spaces {sorted(spaces)}",
errors,
)
if {
"consistency_summary.csv", "latent_samples.csv", "latent_trajectories.csv"
}.issubset(frames):
consistency = frames["consistency_summary.csv"]
consistency = consistency.loc[_active_mask(consistency)].copy()
samples = frames["latent_samples.csv"]
trajectories = frames["latent_trajectories.csv"]
expected_sample_sessions: set[tuple[str, str, str]] = set()
expected_trajectory_sessions: set[tuple[str, str, str]] = set()
for row in consistency.itertuples(index=False):
sessions = (
[]
if pd.isna(row.sessions)
else [item.strip() for item in str(row.sessions).split(";") if item.strip()]
)
if not sessions:
errors.append(f"consistency: {row.model}/{row.dataset} has no sessions")
continue
try:
expected_n_sessions = int(row.n_sessions)
except (TypeError, ValueError):
expected_n_sessions = -1
_require(
len(sessions) == expected_n_sessions,
f"consistency: {row.model}/{row.dataset} lists {len(sessions)} "
f"sessions but n_sessions={row.n_sessions}",
errors,
)
expected_sample_sessions.update(
(str(row.model), str(row.dataset), session) for session in sessions
)
if str(row.dataset) != "ratinabox":
expected_trajectory_sessions.update(
(str(row.model), str(row.dataset), session) for session in sessions
)
_validate_latent_cell(
samples,
table_label="latent samples",
model=str(row.model),
dataset=str(row.dataset),
sessions=sessions,
landmark_type=str(row.scoring_modes),
errors=errors,
)
if str(row.dataset) != "ratinabox":
_validate_latent_cell(
trajectories,
table_label="latent trajectories",
model=str(row.model),
dataset=str(row.dataset),
sessions=sessions,
landmark_type=str(row.scoring_modes),
errors=errors,
)
observed_sample_sessions = set(
samples[["model", "dataset", "session"]].astype(str).itertuples(index=False, name=None)
)
observed_trajectory_sessions = set(
trajectories[["model", "dataset", "session"]].astype(str).itertuples(index=False, name=None)
)
_require(
observed_sample_sessions == expected_sample_sessions,
"latent samples: active consistency session coverage differs",
errors,
)
_require(
observed_trajectory_sessions == expected_trajectory_sessions,
"latent trajectories: applicable consistency session coverage differs",
errors,
)
_require(
len(observed_sample_sessions) == 173,
f"latent samples: expected 173 sessions, found {len(observed_sample_sessions)}",
errors,
)
if errors:
raise ValidationError("\n".join(f"- {item}" for item in errors))
return frames
def validate_canonical(data_dir: Path, canonical_root: Path) -> None:
"""Require Space summaries to equal the current paper result tables."""
results_dir = canonical_root / "paper" / "results"
errors: list[str] = []
for dashboard_name, paper_name in CANONICAL_NAMES.items():
dashboard_path = data_dir / dashboard_name
paper_path = results_dir / paper_name
if not dashboard_path.exists():
errors.append(f"missing dashboard table: {dashboard_path}")
continue
if not paper_path.exists():
errors.append(f"missing canonical table: {paper_path}")
continue
try:
_same_values(pd.read_csv(dashboard_path), pd.read_csv(paper_path))
except AssertionError as exc:
first_line = str(exc).splitlines()[0] if str(exc) else "values differ"
errors.append(f"{dashboard_name} != {paper_name}: {first_line}")
if errors:
raise ValidationError("\n".join(f"- {item}" for item in errors))
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--data-dir", type=Path, default=Path(__file__).resolve().parent / "data")
parser.add_argument(
"--canonical-root",
type=Path,
help="Main benchmark repository root; enables exact paper/results comparisons.",
)
return parser
def main(argv: Iterable[str] | None = None) -> int:
args = build_parser().parse_args(argv)
frames = validate_local(args.data_dir)
if args.canonical_root is not None:
validate_canonical(args.data_dir, args.canonical_root.resolve())
print(f"Validated {len(frames)} BEND-BCI Space tables in {args.data_dir}")
if args.canonical_root is not None:
print("Canonical paper/results comparison passed")
return 0
if __name__ == "__main__":
raise SystemExit(main())