{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","pygments_lexer":"ipython3"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# rsna-knee-abnormality-detection — Kaggle runner\n\nIn **Session options** set **Accelerator = GPU** (T4 or P100).\nInternet **off**. Attach only this competition's data.\nThen **Restart session** and **Run All**.\nInteractive Run All uses the example test. For a leaderboard file, **Save Version** so Kaggle mounts the hidden test.\nThe first code cell wipes `/kaggle/working` (except hidden files), unpacks `src/`, and trains.\n\nSubmit `/kaggle/working/submission.csv` yourself.\nPaste `/kaggle/working/REPORT.md` and the public LB into chat.\n\n`EXPERIMENT_ID` defaults to `exp002`.\n","metadata":{}},{"cell_type":"code","source":"import shutil\nfrom pathlib import Path\n\nworking = Path('/kaggle/working')\nif not working.exists():\n    print('not on Kaggle; skip output wipe')\nelse:\n    removed = []\n    for child in sorted(working.iterdir()):\n        if child.name.startswith('.'):\n            continue\n        if child.is_dir():\n            shutil.rmtree(child)\n        else:\n            child.unlink()\n        removed.append(child.name)\n    print('cleared', len(removed), 'items from', working)\n    for name in removed:\n        print(' -', name)\n    if not removed:\n        print('working directory already empty')\n","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EXPERIMENT_ID = 'exp002'\nprint('experiment', EXPERIMENT_ID)\n","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\nfrom pathlib import Path\n\nBUNDLE = json.loads('{\"competition.yaml\": \"competition:\\\\n  name: \\\\\"RSNA Knee Abnormality Detection\\\\\"\\\\n  slug: \\\\\"rsna-knee-abnormality-detection\\\\\"\\\\n  url: \\\\\"https://www.kaggle.com/competitions/rsna-knee-abnormality-detection\\\\\"\\\\n  modality: \\\\\"multimodal\\\\\"\\\\n  deadline_utc: \\\\\"2026-10-22T23:59:00Z\\\\\"\\\\n\\\\ndata:\\\\n  train_file: \\\\\"data/raw/train.csv\\\\\"\\\\n  test_file: \\\\\"data/raw/test.csv\\\\\"\\\\n  sample_submission_file: \\\\\"data/raw/sample_submission.csv\\\\\"\\\\n  train_series_file: \\\\\"data/raw/train_series.csv\\\\\"\\\\n  test_series_file: \\\\\"data/raw/test_series.csv\\\\\"\\\\n  id_columns: [\\\\\"StudyInstanceUID\\\\\"]\\\\n  target_columns:\\\\n    - \\\\\"ACL\\\\\"\\\\n    - \\\\\"MCL\\\\\"\\\\n    - \\\\\"Medial Meniscus\\\\\"\\\\n    - \\\\\"Lateral Meniscus\\\\\"\\\\n    - \\\\\"Medial OA\\\\\"\\\\n    - \\\\\"Lateral OA\\\\\"\\\\n    - \\\\\"PF OA\\\\\"\\\\n    - \\\\\"Effusion\\\\\"\\\\n    - \\\\\"Synovitis\\\\\"\\\\n    - \\\\\"Baker\\'s\\\\\"\\\\n    - \\\\\"Contusion\\\\\"\\\\n    - \\\\\"Fracture\\\\\"\\\\n  group_columns: [\\\\\"StudyInstanceUID\\\\\"]\\\\n  time_column: null\\\\n\\\\nevaluation:\\\\n  metric: \\\\\"macro_roc_auc\\\\\"\\\\n  direction: \\\\\"maximize\\\\\"\\\\n  implementation: \\\\\"mean of sklearn.metrics.roc_auc_score over the 12 findings\\\\\"\\\\n\\\\nvalidation:\\\\n  strategy: \\\\\"study-level 5-fold; score expert-labeled studies only\\\\\"\\\\n  n_splits: 5\\\\n  seed: 42\\\\n  fold_version: \\\\\"v1\\\\\"\\\\n  fold_file: \\\\\"folds/v1.parquet\\\\\"\\\\n  leakage_risks:\\\\n    - \\\\\"Reports exist only in train; report text is invalid as a test-time feature\\\\\"\\\\n    - \\\\\"About 58 of 4407 training studies have expert labels; gold prevalence is not the corpus prior\\\\\"\\\\n    - \\\\\"Site and scanner IDs are not in the CSVs; report language is a weak site proxy\\\\\"\\\\n    - \\\\\"Series from the same study must never cross a fold boundary\\\\\"\\\\n    - \\\\\"A gold study must not be trained on its own report-extracted labels when it is in validation\\\\\"\\\\n\\\\nruntime:\\\\n  internet: false\\\\n  accelerator: \\\\\"gpu\\\\\"\\\\n  time_limit_minutes: 540\\\\n  execute_on: \\\\\"kaggle\\\\\"\\\\n\\\\nsubmission:\\\\n  format: \\\\\"csv\\\\\"\\\\n  prediction_columns:\\\\n    - \\\\\"ACL\\\\\"\\\\n    - \\\\\"MCL\\\\\"\\\\n    - \\\\\"Medial Meniscus\\\\\"\\\\n    - \\\\\"Lateral Meniscus\\\\\"\\\\n    - \\\\\"Medial OA\\\\\"\\\\n    - \\\\\"Lateral OA\\\\\"\\\\n    - \\\\\"PF OA\\\\\"\\\\n    - \\\\\"Effusion\\\\\"\\\\n    - \\\\\"Synovitis\\\\\"\\\\n    - \\\\\"Baker\\'s\\\\\"\\\\n    - \\\\\"Contusion\\\\\"\\\\n    - \\\\\"Fracture\\\\\"\\\\n  allowed_daily_submissions: 5\\\\n\\\\ntracking:\\\\n  primary_experiment_log: \\\\\"experiments/EXPERIMENTS.md\\\\\"\\\\n  submission_log: \\\\\"submissions/SUBMISSIONS.md\\\\\"\\\\n\", \"src/__init__.py\": \"\\\\\"\\\\\"\\\\\"Competition pipeline. Import from Kaggle, Colab, or a local clone.\\\\\"\\\\\"\\\\\"\\\\n\", \"src/config.py\": \"\\\\\"\\\\\"\\\\\"Load the competition contract from YAML.\\\\\"\\\\\"\\\\\"\\\\n\\\\nfrom __future__ import annotations\\\\n\\\\nfrom pathlib import Path\\\\nfrom typing import Any\\\\n\\\\nimport yaml\\\\n\\\\nfrom runtime import find_src_dir\\\\n\\\\n\\\\ndef load_competition_config(path: Path | None = None) -> dict[str, Any]:\\\\n    \\\\\"\\\\\"\\\\\"Load competition.yaml next to src/ unless an explicit path is given.\\\\\"\\\\\"\\\\\"\\\\n    config_path = path or find_src_dir().parent / \\\\\"competition.yaml\\\\\"\\\\n    if not config_path.is_file():\\\\n        raise FileNotFoundError(f\\\\\"Missing competition config: {config_path}\\\\\")\\\\n    loaded = yaml.safe_load(config_path.read_text(encoding=\\\\\"utf-8\\\\\"))\\\\n    if not isinstance(loaded, dict):\\\\n        raise ValueError(f\\\\\"Invalid YAML mapping in {config_path}\\\\\")\\\\n    return loaded\\\\n\", \"src/constants.py\": \"\\\\\"\\\\\"\\\\\"Shared competition identifiers. Keep the target order identical to Kaggle.\\\\\"\\\\\"\\\\\"\\\\n\\\\nfrom __future__ import annotations\\\\n\\\\nID_COL = \\\\\"StudyInstanceUID\\\\\"\\\\nREPORT_COL = \\\\\"Report\\\\\"\\\\nSERIES_ID_COL = \\\\\"SeriesInstanceUID\\\\\"\\\\nPLANE_COL = \\\\\"Anatomical_Plane\\\\\"\\\\nFLUID_COL = \\\\\"Fluid_Sensitive\\\\\"\\\\nFAT_COL = \\\\\"Fat_Suppression\\\\\"\\\\n\\\\nTARGETS: tuple[str, ...] = (\\\\n    \\\\\"ACL\\\\\",\\\\n    \\\\\"MCL\\\\\",\\\\n    \\\\\"Medial Meniscus\\\\\",\\\\n    \\\\\"Lateral Meniscus\\\\\",\\\\n    \\\\\"Medial OA\\\\\",\\\\n    \\\\\"Lateral OA\\\\\",\\\\n    \\\\\"PF OA\\\\\",\\\\n    \\\\\"Effusion\\\\\",\\\\n    \\\\\"Synovitis\\\\\",\\\\n    \\\\\"Baker\\'s\\\\\",\\\\n    \\\\\"Contusion\\\\\",\\\\n    \\\\\"Fracture\\\\\",\\\\n)\\\\n\\\\nPLANES: tuple[str, ...] = (\\\\\"Axial\\\\\", \\\\\"Coronal\\\\\", \\\\\"Sagittal\\\\\")\\\\nIMAGE_SLOTS: tuple[tuple[str, int], ...] = (\\\\n    (\\\\\"Sagittal\\\\\", 1),\\\\n    (\\\\\"Sagittal\\\\\", 0),\\\\n    (\\\\\"Coronal\\\\\", 1),\\\\n    (\\\\\"Coronal\\\\\", 0),\\\\n    (\\\\\"Axial\\\\\", 1),\\\\n    (\\\\\"Axial\\\\\", 0),\\\\n)\\\\nFOLD_SEED = 42\\\\nN_SPLITS = 5\\\\nIMAGE_SIZE = 128\\\\nSLICE_QUANTS: tuple[float, ...] = (0.25, 0.5, 0.75)\\\\nCROP_MM = 140.0\\\\nIMAGE_EPOCHS = 4\\\\nIMAGE_BATCH = 16\\\\nIMAGE_LR = 1e-3\\\\n\", \"src/data.py\": \"\\\\\"\\\\\"\\\\\"Load RSNA knee CSV tables and enforce the submission contract.\\\\\"\\\\\"\\\\\"\\\\n\\\\nfrom __future__ import annotations\\\\n\\\\nfrom pathlib import Path\\\\n\\\\nimport numpy as np\\\\nimport pandas as pd\\\\n\\\\nfrom constants import ID_COL, REPORT_COL, TARGETS\\\\nfrom runtime import RuntimePaths, find_data_file\\\\n\\\\nSERIES_REQUIRED = (\\\\n    ID_COL,\\\\n    \\\\\"SeriesInstanceUID\\\\\",\\\\n    \\\\\"Fluid_Sensitive\\\\\",\\\\n    \\\\\"Fat_Suppression\\\\\",\\\\n    \\\\\"Anatomical_Plane\\\\\",\\\\n)\\\\n\\\\n\\\\ndef load_table(cfg: dict, paths: RuntimePaths, key: str) -> pd.DataFrame:\\\\n    path = find_data_file(cfg[\\\\\"data\\\\\"][key], paths)\\\\n    frame = pd.read_csv(path)\\\\n    if frame.empty:\\\\n        raise ValueError(f\\\\\"Empty table: {path}\\\\\")\\\\n    if ID_COL in frame.columns:\\\\n        frame[ID_COL] = frame[ID_COL].astype(str)\\\\n    return frame\\\\n\\\\n\\\\ndef gold_mask(train: pd.DataFrame) -> pd.Series:\\\\n    \\\\\"\\\\\"\\\\\"True where all twelve expert labels are present.\\\\\"\\\\\"\\\\\"\\\\n    missing = [col for col in TARGETS if col not in train.columns]\\\\n    if missing:\\\\n        raise ValueError(f\\\\\"Train missing target columns: {missing}\\\\\")\\\\n    return train[list(TARGETS)].notna().all(axis=1)\\\\n\\\\n\\\\ndef target_matrix(frame: pd.DataFrame) -> np.ndarray:\\\\n    values = frame[list(TARGETS)].to_numpy(dtype=float)\\\\n    return values\\\\n\\\\n\\\\ndef assert_train_schema(train: pd.DataFrame) -> None:\\\\n    required = [ID_COL, REPORT_COL, *TARGETS]\\\\n    missing = [col for col in required if col not in train.columns]\\\\n    if missing:\\\\n        raise ValueError(f\\\\\"Train missing columns: {missing}\\\\\")\\\\n    if train[ID_COL].duplicated().any():\\\\n        raise ValueError(\\\\\"Train StudyInstanceUID values are not unique\\\\\")\\\\n\\\\n\\\\ndef assert_series_schema(series: pd.DataFrame, *, split: str) -> None:\\\\n    missing = [col for col in SERIES_REQUIRED if col not in series.columns]\\\\n    if missing:\\\\n        raise ValueError(f\\\\\"{split} series missing columns: {missing}\\\\\")\\\\n    if series[ID_COL].isna().any() or series[\\\\\"SeriesInstanceUID\\\\\"].isna().any():\\\\n        raise ValueError(f\\\\\"{split} series contains null IDs\\\\\")\\\\n\\\\n\\\\ndef assert_test_schema(test: pd.DataFrame, sample: pd.DataFrame) -> None:\\\\n    if ID_COL not in test.columns:\\\\n        raise ValueError(\\\\\"Test missing StudyInstanceUID\\\\\")\\\\n    if test[ID_COL].duplicated().any():\\\\n        raise ValueError(\\\\\"Test StudyInstanceUID values are not unique\\\\\")\\\\n    leaked = [col for col in TARGETS if col in test.columns]\\\\n    if leaked:\\\\n        raise ValueError(f\\\\\"Test must not contain target columns: {leaked}\\\\\")\\\\n    expected = [ID_COL, *TARGETS]\\\\n    if list(sample.columns) != expected:\\\\n        raise ValueError(f\\\\\"Unexpected sample columns: {list(sample.columns)}\\\\\")\\\\n    if not sample[ID_COL].astype(str).equals(test[ID_COL].astype(str)):\\\\n        raise ValueError(\\\\\"Sample IDs do not match test IDs in value and order\\\\\")\\\\n    for col in TARGETS:\\\\n        if col in test.columns:\\\\n            raise ValueError(f\\\\\"Test leaked target column {col}\\\\\")\\\\n\\\\n\\\\ndef write_submission(\\\\n    sample: pd.DataFrame,\\\\n    probabilities: pd.DataFrame,\\\\n    path: Path,\\\\n) -> pd.DataFrame:\\\\n    \\\\\"\\\\\"\\\\\"Align predictions to the sample row order and write submission.csv.\\\\\"\\\\\"\\\\\"\\\\n    if list(probabilities.columns) != list(TARGETS):\\\\n        raise ValueError(f\\\\\"Prediction columns must be {list(TARGETS)}\\\\\")\\\\n    aligned = probabilities.reindex(sample[ID_COL].astype(str))\\\\n    if aligned.isna().any().any():\\\\n        missing = aligned.index[aligned.isna().any(axis=1)].tolist()\\\\n        raise ValueError(f\\\\\"Missing predictions for studies: {missing[:8]}\\\\\")\\\\n    values = aligned.to_numpy(dtype=float)\\\\n    if not np.isfinite(values).all():\\\\n        raise ValueError(\\\\\"Predictions must be finite\\\\\")\\\\n    if (values < 0.0).any() or (values > 1.0).any():\\\\n        raise ValueError(\\\\\"Predictions must lie in [0, 1]\\\\\")\\\\n\\\\n    submission = sample.copy()\\\\n    submission[ID_COL] = submission[ID_COL].astype(str)\\\\n    submission[list(TARGETS)] = values\\\\n    if list(submission.columns) != [ID_COL, *TARGETS]:\\\\n        raise ValueError(f\\\\\"Submission columns drifted: {list(submission.columns)}\\\\\")\\\\n    if not submission[ID_COL].equals(sample[ID_COL].astype(str)):\\\\n        raise ValueError(\\\\\"Submission IDs drifted from the sample file\\\\\")\\\\n    path.parent.mkdir(parents=True, exist_ok=True)\\\\n    submission.to_csv(path, index=False)\\\\n    return submission\\\\n\", \"src/dicom_io.py\": \"\\\\\"\\\\\"\\\\\"Map each study onto the six plane x fat-sat slots and decode DICOMs.\\\\\"\\\\\"\\\\\"\\\\n\\\\nfrom __future__ import annotations\\\\n\\\\nfrom concurrent.futures import ThreadPoolExecutor\\\\nfrom pathlib import Path\\\\n\\\\nimport numpy as np\\\\nimport pandas as pd\\\\n\\\\nfrom constants import (\\\\n    CROP_MM,\\\\n    FAT_COL,\\\\n    ID_COL,\\\\n    IMAGE_SIZE,\\\\n    IMAGE_SLOTS,\\\\n    PLANE_COL,\\\\n    SERIES_ID_COL,\\\\n    SLICE_QUANTS,\\\\n)\\\\n\\\\ntry:\\\\n    from PIL import Image\\\\nexcept ImportError:  # pragma: no cover\\\\n    Image = None  # type: ignore[assignment]\\\\n\\\\n\\\\ndef assign_slot_series(study_series: pd.DataFrame) -> list[str | None]:\\\\n    \\\\\"\\\\\"\\\\\"Pick one SeriesInstanceUID per IMAGE_SLOTS entry. Missing slots are None.\\\\\"\\\\\"\\\\\"\\\\n    if study_series.empty:\\\\n        return [None] * len(IMAGE_SLOTS)\\\\n    frame = study_series.copy()\\\\n    frame[SERIES_ID_COL] = frame[SERIES_ID_COL].astype(str)\\\\n    frame[PLANE_COL] = frame[PLANE_COL].astype(str)\\\\n    frame[FAT_COL] = (\\\\n        pd.to_numeric(frame[FAT_COL], errors=\\\\\"coerce\\\\\").fillna(0).astype(int)\\\\n    )\\\\n    chosen: list[str | None] = []\\\\n    for plane, fat in IMAGE_SLOTS:\\\\n        match = frame[(frame[PLANE_COL] == plane) & (frame[FAT_COL] == fat)]\\\\n        if match.empty:\\\\n            chosen.append(None)\\\\n            continue\\\\n        chosen.append(str(match.iloc[0][SERIES_ID_COL]))\\\\n    return chosen\\\\n\\\\n\\\\ndef _list_dicoms(series_dir: Path) -> list[Path]:\\\\n    if not series_dir.is_dir():\\\\n        return []\\\\n    files = [path for path in series_dir.iterdir() if path.suffix.lower() == \\\\\".dcm\\\\\"]\\\\n    return sorted(files)\\\\n\\\\n\\\\ndef _slice_position(ds) -> float:\\\\n    ipp = getattr(ds, \\\\\"ImagePositionPatient\\\\\", None)\\\\n    iop = getattr(ds, \\\\\"ImageOrientationPatient\\\\\", None)\\\\n    if ipp is not None and iop is not None:\\\\n        orientation = np.asarray(iop, dtype=float).reshape(2, 3)\\\\n        normal = np.cross(orientation[0], orientation[1])\\\\n        return float(np.dot(np.asarray(ipp, dtype=float), normal))\\\\n    return float(getattr(ds, \\\\\"InstanceNumber\\\\\", 0) or 0)\\\\n\\\\n\\\\ndef _read_header(path: Path):\\\\n    import pydicom\\\\n\\\\n    return pydicom.dcmread(str(path), stop_before_pixels=True, force=True)\\\\n\\\\n\\\\ndef _ordered_dicoms(series_dir: Path) -> list[Path]:\\\\n    files = _list_dicoms(series_dir)\\\\n    if len(files) <= 1:\\\\n        return files\\\\n    keyed: list[tuple[float, Path]] = []\\\\n    for path in files:\\\\n        try:\\\\n            header = _read_header(path)\\\\n            keyed.append((_slice_position(header), path))\\\\n        except Exception:\\\\n            keyed.append((0.0, path))\\\\n    keyed.sort(key=lambda item: item[0])\\\\n    return [path for _, path in keyed]\\\\n\\\\n\\\\ndef _window(pixels: np.ndarray) -> np.ndarray:\\\\n    finite = pixels[np.isfinite(pixels)]\\\\n    if finite.size == 0:\\\\n        return np.zeros_like(pixels, dtype=np.float32)\\\\n    lo, hi = np.percentile(finite, (1.0, 99.0))\\\\n    if hi <= lo:\\\\n        hi = lo + 1.0\\\\n    scaled = np.clip((pixels - lo) / (hi - lo), 0.0, 1.0)\\\\n    return scaled.astype(np.float32)\\\\n\\\\n\\\\ndef _decode_plane(path: Path) -> tuple[np.ndarray, float]:\\\\n    import pydicom\\\\n\\\\n    ds = pydicom.dcmread(str(path), force=True)\\\\n    pixels = np.asarray(ds.pixel_array)\\\\n    if pixels.ndim == 3:\\\\n        pixels = pixels[0]\\\\n    pixels = pixels.astype(np.float32)\\\\n    slope = float(getattr(ds, \\\\\"RescaleSlope\\\\\", 1.0) or 1.0)\\\\n    intercept = float(getattr(ds, \\\\\"RescaleIntercept\\\\\", 0.0) or 0.0)\\\\n    pixels = pixels * slope + intercept\\\\n    if str(getattr(ds, \\\\\"PhotometricInterpretation\\\\\", \\\\\"\\\\\")).upper() == \\\\\"MONOCHROME1\\\\\":\\\\n        pixels = pixels.max() - pixels\\\\n    spacing = 0.5\\\\n    pix_spacing = getattr(ds, \\\\\"PixelSpacing\\\\\", None)\\\\n    if pix_spacing is not None and len(pix_spacing) >= 1:\\\\n        spacing = float(pix_spacing[0]) or 0.5\\\\n    return _window(pixels), spacing\\\\n\\\\n\\\\ndef crop_resize(\\\\n    image: np.ndarray,\\\\n    spacing_mm: float,\\\\n    *,\\\\n    crop_mm: float = CROP_MM,\\\\n    out_size: int = IMAGE_SIZE,\\\\n) -> np.ndarray:\\\\n    \\\\\"\\\\\"\\\\\"Center-crop `crop_mm` millimetres and resize to a square.\\\\\"\\\\\"\\\\\"\\\\n    if image.ndim != 2:\\\\n        raise ValueError(\\\\\"Expected a 2D slice\\\\\")\\\\n    height, width = image.shape\\\\n    spacing = spacing_mm if spacing_mm > 1e-6 else 0.5\\\\n    crop_px = round(crop_mm / spacing)\\\\n    crop_px = max(8, min(crop_px, height, width))\\\\n    row0 = max(0, (height - crop_px) // 2)\\\\n    col0 = max(0, (width - crop_px) // 2)\\\\n    cropped = image[row0 : row0 + crop_px, col0 : col0 + crop_px]\\\\n    if Image is None:\\\\n        raise RuntimeError(\\\\\"Pillow is required to resize MRI slices\\\\\")\\\\n    scaled = (np.clip(cropped, 0.0, 1.0) * 255.0).astype(np.uint8)\\\\n    resized = Image.fromarray(scaled, mode=\\\\\"L\\\\\").resize(\\\\n        (out_size, out_size), Image.BILINEAR\\\\n    )\\\\n    return np.asarray(resized, dtype=np.uint8)\\\\n\\\\n\\\\ndef _series_slices(series_dir: Path, out_size: int) -> np.ndarray:\\\\n    ordered = _ordered_dicoms(series_dir)\\\\n    n_slices = len(SLICE_QUANTS)\\\\n    canvas = np.zeros((n_slices, out_size, out_size), dtype=np.uint8)\\\\n    if not ordered:\\\\n        return canvas\\\\n    for idx, quant in enumerate(SLICE_QUANTS):\\\\n        loc = round(quant * (len(ordered) - 1))\\\\n        loc = min(max(loc, 0), len(ordered) - 1)\\\\n        try:\\\\n            plane, spacing = _decode_plane(ordered[loc])\\\\n            canvas[idx] = crop_resize(plane, spacing, out_size=out_size)\\\\n        except Exception:\\\\n            continue\\\\n    return canvas\\\\n\\\\n\\\\ndef load_study_tensor(\\\\n    study_uid: str,\\\\n    study_series: pd.DataFrame,\\\\n    series_root: Path,\\\\n    *,\\\\n    out_size: int = IMAGE_SIZE,\\\\n) -> np.ndarray:\\\\n    \\\\\"\\\\\"\\\\\"Return uint8 tensor (slots * slices, H, W). Missing slots are zeros.\\\\\"\\\\\"\\\\\"\\\\n    n_slices = len(SLICE_QUANTS)\\\\n    tensor = np.zeros((len(IMAGE_SLOTS) * n_slices, out_size, out_size), dtype=np.uint8)\\\\n    slots = assign_slot_series(study_series)\\\\n    for slot_idx, series_uid in enumerate(slots):\\\\n        if series_uid is None:\\\\n            continue\\\\n        series_dir = series_root / str(study_uid) / str(series_uid)\\\\n        block = _series_slices(series_dir, out_size)\\\\n        start = slot_idx * n_slices\\\\n        tensor[start : start + n_slices] = block\\\\n    return tensor\\\\n\\\\n\\\\ndef load_split_tensors(\\\\n    studies: pd.Series,\\\\n    series: pd.DataFrame,\\\\n    series_root: Path,\\\\n    *,\\\\n    out_size: int = IMAGE_SIZE,\\\\n    workers: int = 8,\\\\n    limit: int | None = None,\\\\n) -> dict[str, np.ndarray]:\\\\n    \\\\\"\\\\\"\\\\\"Decode one tensor per study. `limit` is for smoke tests.\\\\\"\\\\\"\\\\\"\\\\n    ids = [str(uid) for uid in studies.astype(str).tolist()]\\\\n    if limit is not None:\\\\n        ids = ids[: int(limit)]\\\\n    grouped = {\\\\n        str(uid): frame\\\\n        for uid, frame in series.groupby(series[ID_COL].astype(str), sort=False)\\\\n    }\\\\n\\\\n    def _one(uid: str) -> tuple[str, np.ndarray]:\\\\n        frame = grouped.get(uid)\\\\n        if frame is None:\\\\n            empty = np.zeros(\\\\n                (len(IMAGE_SLOTS) * len(SLICE_QUANTS), out_size, out_size),\\\\n                dtype=np.uint8,\\\\n            )\\\\n            return uid, empty\\\\n        return uid, load_study_tensor(uid, frame, series_root, out_size=out_size)\\\\n\\\\n    out: dict[str, np.ndarray] = {}\\\\n    workers = max(1, int(workers))\\\\n    total = len(ids)\\\\n    if workers == 1 or total < 4:\\\\n        for idx, uid in enumerate(ids):\\\\n            if idx % 100 == 0:\\\\n                print(f\\\\\"[dicom_io] {idx}/{total}\\\\\", flush=True)\\\\n            key, tensor = _one(uid)\\\\n            out[key] = tensor\\\\n        return out\\\\n    with ThreadPoolExecutor(max_workers=workers) as pool:\\\\n        for idx, (uid, tensor) in enumerate(pool.map(_one, ids)):\\\\n            if idx % 100 == 0:\\\\n                print(f\\\\\"[dicom_io] {idx}/{total}\\\\\", flush=True)\\\\n            out[uid] = tensor\\\\n    return out\\\\n\", \"src/features.py\": \"\\\\\"\\\\\"\\\\\"Study-level features from series metadata. No pixels, no reports.\\\\\"\\\\\"\\\\\"\\\\n\\\\nfrom __future__ import annotations\\\\n\\\\nimport pandas as pd\\\\n\\\\nfrom constants import FAT_COL, FLUID_COL, ID_COL, PLANE_COL, PLANES\\\\n\\\\nSLOT_COLUMNS = [\\\\n    f\\\\\"n_{plane.lower()}_{\\'fs\\' if fat else \\'nfs\\'}\\\\\" for plane in PLANES for fat in (0, 1)\\\\n]\\\\n\\\\n\\\\ndef series_study_features(series: pd.DataFrame) -> pd.DataFrame:\\\\n    \\\\\"\\\\\"\\\\\"Aggregate each study\\'s MRI series into a fixed metadata vector.\\\\n\\\\n    Slot names follow the public 6-slot layout used by strong inference\\\\n    kernels: plane x fat-suppression. Counts, not pixels.\\\\n    \\\\\"\\\\\"\\\\\"\\\\n    required = [ID_COL, PLANE_COL, FLUID_COL, FAT_COL]\\\\n    missing = [col for col in required if col not in series.columns]\\\\n    if missing:\\\\n        raise ValueError(f\\\\\"Series table missing columns: {missing}\\\\\")\\\\n\\\\n    frame = series.copy()\\\\n    frame[ID_COL] = frame[ID_COL].astype(str)\\\\n    frame[PLANE_COL] = frame[PLANE_COL].astype(str)\\\\n    frame[FLUID_COL] = pd.to_numeric(frame[FLUID_COL], errors=\\\\\"coerce\\\\\").fillna(0)\\\\n    frame[FAT_COL] = pd.to_numeric(frame[FAT_COL], errors=\\\\\"coerce\\\\\").fillna(0)\\\\n\\\\n    grouped = frame.groupby(ID_COL, sort=True)\\\\n    out = pd.DataFrame({ID_COL: grouped.size().index})\\\\n    out = out.set_index(ID_COL)\\\\n    out[\\\\\"n_series\\\\\"] = grouped.size()\\\\n    out[\\\\\"n_fluid_sensitive\\\\\"] = grouped[FLUID_COL].sum()\\\\n    out[\\\\\"n_fat_suppression\\\\\"] = grouped[FAT_COL].sum()\\\\n    out[\\\\\"n_planes\\\\\"] = grouped[PLANE_COL].nunique()\\\\n    out[\\\\\"frac_fluid\\\\\"] = out[\\\\\"n_fluid_sensitive\\\\\"] / out[\\\\\"n_series\\\\\"].clip(lower=1)\\\\n    out[\\\\\"frac_fat\\\\\"] = out[\\\\\"n_fat_suppression\\\\\"] / out[\\\\\"n_series\\\\\"].clip(lower=1)\\\\n    out[\\\\\"fluid_equals_fat\\\\\"] = (\\\\n        (frame[FLUID_COL] == frame[FAT_COL]).groupby(frame[ID_COL]).mean()\\\\n    )\\\\n\\\\n    for plane in PLANES:\\\\n        key = f\\\\\"n_{plane.lower()}\\\\\"\\\\n        out[key] = (\\\\n            frame.loc[frame[PLANE_COL] == plane]\\\\n            .groupby(ID_COL)\\\\n            .size()\\\\n            .reindex(out.index)\\\\n        )\\\\n        for fat, tag in ((0, \\\\\"nfs\\\\\"), (1, \\\\\"fs\\\\\")):\\\\n            slot = f\\\\\"n_{plane.lower()}_{tag}\\\\\"\\\\n            mask = (frame[PLANE_COL] == plane) & (frame[FAT_COL] == fat)\\\\n            out[slot] = frame.loc[mask].groupby(ID_COL).size().reindex(out.index)\\\\n\\\\n    out = out.fillna(0.0)\\\\n    for slot in SLOT_COLUMNS:\\\\n        out[f\\\\\"has_{slot[2:]}\\\\\"] = (out[slot] > 0).astype(float)\\\\n    out[\\\\\"n_slots_filled\\\\\"] = out[SLOT_COLUMNS].gt(0).sum(axis=1).astype(float)\\\\n    return out.reset_index()\\\\n\\\\n\\\\ndef align_features(\\\\n    studies: pd.Series,\\\\n    features: pd.DataFrame,\\\\n) -> pd.DataFrame:\\\\n    \\\\\"\\\\\"\\\\\"Return the feature matrix in `studies` order. Missing studies are zeros.\\\\\"\\\\\"\\\\\"\\\\n    indexed = features.set_index(ID_COL)\\\\n    aligned = indexed.reindex(studies.astype(str))\\\\n    if bool(aligned.isna().all().all()):\\\\n        raise ValueError(\\\\\"No series features matched the requested studies\\\\\")\\\\n    numeric = aligned.fillna(0.0)\\\\n    if not numeric.index.equals(pd.Index(studies.astype(str))):\\\\n        raise ValueError(\\\\\"Feature alignment drifted from study order\\\\\")\\\\n    return numeric\\\\n\", \"src/folds.py\": \"\\\\\"\\\\\"\\\\\"Frozen study-level folds. Score only expert-labeled studies.\\\\\"\\\\\"\\\\\"\\\\n\\\\nfrom __future__ import annotations\\\\n\\\\nimport hashlib\\\\nfrom pathlib import Path\\\\n\\\\nimport numpy as np\\\\nimport pandas as pd\\\\n\\\\nfrom constants import FOLD_SEED, ID_COL, N_SPLITS, TARGETS\\\\nfrom data import gold_mask\\\\n\\\\n\\\\ndef _uid_fold(uid: str, n_splits: int) -> int:\\\\n    digest = hashlib.md5(uid.encode(\\\\\"utf-8\\\\\"), usedforsecurity=False).hexdigest()\\\\n    return int(digest, 16) % n_splits\\\\n\\\\n\\\\ndef iterative_multilabel_folds(\\\\n    y: np.ndarray,\\\\n    *,\\\\n    n_splits: int,\\\\n    seed: int,\\\\n) -> np.ndarray:\\\\n    \\\\\"\\\\\"\\\\\"Assign folds so rare positives are spread. y is binary (n, n_labels).\\\\\"\\\\\"\\\\\"\\\\n    if y.ndim != 2:\\\\n        raise ValueError(\\\\\"y must be a 2D label matrix\\\\\")\\\\n    n_rows = len(y)\\\\n    if n_rows < n_splits:\\\\n        raise ValueError(\\\\\"Need at least one labeled study per fold\\\\\")\\\\n\\\\n    rng = np.random.default_rng(seed)\\\\n    order = rng.permutation(n_rows)\\\\n    shuffled = y[order]\\\\n    assigned = np.full(n_rows, fill_value=-1, dtype=int)\\\\n    fold_label_counts = np.zeros((n_splits, y.shape[1]), dtype=int)\\\\n    fold_sizes = np.zeros(n_splits, dtype=int)\\\\n    remaining = list(range(n_rows))\\\\n\\\\n    while remaining:\\\\n        subset = shuffled[remaining]\\\\n        positives = subset.sum(axis=0).astype(float)\\\\n        positives[positives <= 0] = np.inf\\\\n        rarest = int(np.argmin(positives))\\\\n        if np.isfinite(positives[rarest]):\\\\n            candidates = [idx for idx in remaining if shuffled[idx, rarest] == 1]\\\\n        else:\\\\n            candidates = remaining\\\\n        chosen = candidates[0]\\\\n        fold = min(\\\\n            range(n_splits),\\\\n            key=lambda item: (\\\\n                int(fold_label_counts[item, rarest]),\\\\n                int(fold_sizes[item]),\\\\n                item,\\\\n            ),\\\\n        )\\\\n        assigned[chosen] = fold\\\\n        fold_label_counts[fold] += shuffled[chosen].astype(int)\\\\n        fold_sizes[fold] += 1\\\\n        remaining.remove(chosen)\\\\n\\\\n    if (assigned < 0).any():\\\\n        raise ValueError(\\\\\"Some labeled studies were not assigned a fold\\\\\")\\\\n    out = np.empty(n_rows, dtype=int)\\\\n    out[order] = assigned\\\\n    return out\\\\n\\\\n\\\\ndef build_folds(\\\\n    train: pd.DataFrame,\\\\n    *,\\\\n    n_splits: int = N_SPLITS,\\\\n    seed: int = FOLD_SEED,\\\\n) -> pd.DataFrame:\\\\n    \\\\\"\\\\\"\\\\\"One fold per training study. Gold studies are iteratively stratified.\\\\\"\\\\\"\\\\\"\\\\n    ids = train[ID_COL].astype(str)\\\\n    labeled = gold_mask(train)\\\\n    fold = np.full(len(train), fill_value=-1, dtype=int)\\\\n\\\\n    if int(labeled.sum()) < n_splits:\\\\n        raise ValueError(\\\\n            f\\\\\"Need at least {n_splits} expert-labeled studies to build {n_splits} folds\\\\\"\\\\n        )\\\\n    gold_y = train.loc[labeled, list(TARGETS)].to_numpy(dtype=float)\\\\n    if not np.isfinite(gold_y).all():\\\\n        raise ValueError(\\\\\"Gold label matrix contains NaNs\\\\\")\\\\n    fold[labeled.to_numpy()] = iterative_multilabel_folds(\\\\n        gold_y.astype(int),\\\\n        n_splits=n_splits,\\\\n        seed=seed,\\\\n    )\\\\n\\\\n    unlabeled_ids = ids.loc[~labeled]\\\\n    for idx, uid in unlabeled_ids.items():\\\\n        fold[idx] = _uid_fold(str(uid), n_splits)\\\\n\\\\n    if (fold < 0).any():\\\\n        raise ValueError(\\\\\"Some training studies were not assigned a fold\\\\\")\\\\n    return pd.DataFrame(\\\\n        {\\\\n            ID_COL: ids.to_numpy(),\\\\n            \\\\\"fold\\\\\": fold,\\\\n            \\\\\"has_labels\\\\\": labeled.astype(int).to_numpy(),\\\\n        }\\\\n    )\\\\n\\\\n\\\\ndef load_or_create_folds(\\\\n    train: pd.DataFrame,\\\\n    fold_path: Path,\\\\n    *,\\\\n    n_splits: int = N_SPLITS,\\\\n    seed: int = FOLD_SEED,\\\\n) -> pd.DataFrame:\\\\n    \\\\\"\\\\\"\\\\\"Return the frozen fold artifact, creating it once.\\\\\"\\\\\"\\\\\"\\\\n    expected_ids = set(train[ID_COL].astype(str))\\\\n    if fold_path.is_file():\\\\n        folds = pd.read_parquet(fold_path)\\\\n        required = [ID_COL, \\\\\"fold\\\\\", \\\\\"has_labels\\\\\"]\\\\n        if list(folds.columns) != required:\\\\n            raise ValueError(f\\\\\"Unexpected fold schema: {list(folds.columns)}\\\\\")\\\\n        folds[ID_COL] = folds[ID_COL].astype(str)\\\\n        if folds[ID_COL].duplicated().any():\\\\n            raise ValueError(\\\\\"Fold artifact has duplicate IDs\\\\\")\\\\n        if set(folds[ID_COL]) != expected_ids:\\\\n            raise ValueError(\\\\\"Fold artifact IDs do not match train\\\\\")\\\\n        if len(folds) != len(train):\\\\n            raise ValueError(\\\\\"Fold artifact row count does not match train\\\\\")\\\\n        return folds.sort_values(ID_COL).reset_index(drop=True)\\\\n\\\\n    folds = build_folds(train, n_splits=n_splits, seed=seed)\\\\n    fold_path.parent.mkdir(parents=True, exist_ok=True)\\\\n    folds.to_parquet(fold_path, index=False)\\\\n    return folds.sort_values(ID_COL).reset_index(drop=True)\\\\n\", \"src/image_train.py\": \"\\\\\"\\\\\"\\\\\"From-scratch 2.5D CNN on the six anatomical slots.\\\\\"\\\\\"\\\\\"\\\\n\\\\nfrom __future__ import annotations\\\\n\\\\nimport os\\\\nimport random\\\\nfrom collections.abc import Sequence\\\\n\\\\nos.environ.setdefault(\\\\\"CUDNN_CONV_WSCAP_DBG\\\\\", \\\\\"1024\\\\\")\\\\n\\\\nimport numpy as np\\\\nimport pandas as pd\\\\nimport torch\\\\nfrom torch import nn\\\\nfrom torch.utils.data import DataLoader, Dataset\\\\n\\\\nfrom constants import (\\\\n    IMAGE_BATCH,\\\\n    IMAGE_EPOCHS,\\\\n    IMAGE_LR,\\\\n    IMAGE_SIZE,\\\\n    IMAGE_SLOTS,\\\\n    SLICE_QUANTS,\\\\n    TARGETS,\\\\n)\\\\n\\\\nIN_CHANNELS = len(IMAGE_SLOTS) * len(SLICE_QUANTS)\\\\n\\\\n\\\\ndef seed_all(seed: int) -> None:\\\\n    random.seed(seed)\\\\n    np.random.seed(seed)\\\\n    torch.manual_seed(seed)\\\\n    if torch.cuda.is_available():\\\\n        torch.cuda.manual_seed_all(seed)\\\\n\\\\n\\\\ndef get_device() -> torch.device:\\\\n    if torch.cuda.is_available():\\\\n        return torch.device(\\\\\"cuda\\\\\")\\\\n    return torch.device(\\\\\"cpu\\\\\")\\\\n\\\\n\\\\nclass SlotCNN(nn.Module):\\\\n    def __init__(self, in_channels: int = IN_CHANNELS, n_out: int = len(TARGETS)):\\\\n        super().__init__()\\\\n        self.backbone = nn.Sequential(\\\\n            nn.Conv2d(in_channels, 32, 3, padding=1),\\\\n            nn.BatchNorm2d(32),\\\\n            nn.ReLU(inplace=True),\\\\n            nn.MaxPool2d(2),\\\\n            nn.Conv2d(32, 64, 3, padding=1),\\\\n            nn.BatchNorm2d(64),\\\\n            nn.ReLU(inplace=True),\\\\n            nn.MaxPool2d(2),\\\\n            nn.Conv2d(64, 128, 3, padding=1),\\\\n            nn.BatchNorm2d(128),\\\\n            nn.ReLU(inplace=True),\\\\n            nn.MaxPool2d(2),\\\\n            nn.Conv2d(128, 256, 3, padding=1),\\\\n            nn.BatchNorm2d(256),\\\\n            nn.ReLU(inplace=True),\\\\n            nn.AdaptiveAvgPool2d(1),\\\\n        )\\\\n        self.head = nn.Linear(256, n_out)\\\\n\\\\n    def forward(self, x: torch.Tensor) -> torch.Tensor:\\\\n        features = self.backbone(x).flatten(1)\\\\n        return self.head(features)\\\\n\\\\n\\\\nclass StudyTensorDataset(Dataset):\\\\n    def __init__(\\\\n        self,\\\\n        uids: Sequence[str],\\\\n        tensors: dict[str, np.ndarray],\\\\n        labels: np.ndarray | None = None,\\\\n    ):\\\\n        self.uids = [str(uid) for uid in uids]\\\\n        self.tensors = tensors\\\\n        self.labels = labels\\\\n        expected = (IN_CHANNELS, IMAGE_SIZE, IMAGE_SIZE)\\\\n        blank = np.zeros(expected, dtype=np.uint8)\\\\n        for uid in self.uids:\\\\n            array = self.tensors.get(uid, blank)\\\\n            if array.shape != expected:\\\\n                raise ValueError(\\\\n                    f\\\\\"Tensor {uid} has shape {array.shape}, expected {expected}\\\\\"\\\\n                )\\\\n\\\\n    def __len__(self) -> int:\\\\n        return len(self.uids)\\\\n\\\\n    def __getitem__(self, index: int) -> tuple[torch.Tensor, torch.Tensor]:\\\\n        uid = self.uids[index]\\\\n        blank = np.zeros((IN_CHANNELS, IMAGE_SIZE, IMAGE_SIZE), dtype=np.uint8)\\\\n        array = self.tensors.get(uid, blank)\\\\n        image = torch.from_numpy(array.astype(np.float32) / 255.0)\\\\n        if self.labels is None:\\\\n            target = torch.full((len(TARGETS),), float(\\\\\"nan\\\\\"))\\\\n        else:\\\\n            target = torch.from_numpy(self.labels[index].astype(np.float32))\\\\n        return image, target\\\\n\\\\n\\\\ndef _masked_bce(logits: torch.Tensor, target: torch.Tensor) -> torch.Tensor:\\\\n    mask = torch.isfinite(target)\\\\n    if not bool(mask.any()):\\\\n        return logits.sum() * 0.0\\\\n    return nn.functional.binary_cross_entropy_with_logits(\\\\n        logits[mask], target[mask].clamp(0.0, 1.0)\\\\n    )\\\\n\\\\n\\\\ndef train_slot_cnn(\\\\n    uids: Sequence[str],\\\\n    tensors: dict[str, np.ndarray],\\\\n    labels: np.ndarray,\\\\n    *,\\\\n    seed: int,\\\\n    epochs: int = IMAGE_EPOCHS,\\\\n    batch_size: int = IMAGE_BATCH,\\\\n    lr: float = IMAGE_LR,\\\\n    device: torch.device | None = None,\\\\n) -> SlotCNN:\\\\n    if labels.shape != (len(uids), len(TARGETS)):\\\\n        raise ValueError(\\\\\"Label matrix does not match the study list\\\\\")\\\\n    seed_all(seed)\\\\n    device = device or get_device()\\\\n    dataset = StudyTensorDataset(uids, tensors, labels)\\\\n    loader = DataLoader(\\\\n        dataset,\\\\n        batch_size=batch_size,\\\\n        shuffle=True,\\\\n        num_workers=0,\\\\n        drop_last=False,\\\\n    )\\\\n    model = SlotCNN().to(device)\\\\n    optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)\\\\n    model.train()\\\\n    use_amp = device.type == \\\\\"cuda\\\\\"\\\\n    scaler = torch.amp.GradScaler(\\\\\"cuda\\\\\") if use_amp else None\\\\n    for _epoch in range(epochs):\\\\n        for images, targets in loader:\\\\n            images = images.to(device, non_blocking=True)\\\\n            targets = targets.to(device, non_blocking=True)\\\\n            optimizer.zero_grad(set_to_none=True)\\\\n            with torch.amp.autocast(\\\\\"cuda\\\\\" if use_amp else \\\\\"cpu\\\\\", enabled=use_amp):\\\\n                logits = model(images)\\\\n                loss = _masked_bce(logits, targets)\\\\n            if scaler is None:\\\\n                loss.backward()\\\\n                optimizer.step()\\\\n            else:\\\\n                scaler.scale(loss).backward()\\\\n                scaler.step(optimizer)\\\\n                scaler.update()\\\\n    model.eval()\\\\n    return model\\\\n\\\\n\\\\n@torch.inference_mode()\\\\ndef predict_slot_cnn(\\\\n    model: SlotCNN,\\\\n    uids: Sequence[str],\\\\n    tensors: dict[str, np.ndarray],\\\\n    *,\\\\n    batch_size: int = IMAGE_BATCH,\\\\n    device: torch.device | None = None,\\\\n) -> pd.DataFrame:\\\\n    device = device or next(model.parameters()).device\\\\n    dataset = StudyTensorDataset(uids, tensors, labels=None)\\\\n    loader = DataLoader(dataset, batch_size=batch_size, shuffle=False, num_workers=0)\\\\n    model.eval()\\\\n    chunks: list[np.ndarray] = []\\\\n    use_amp = device.type == \\\\\"cuda\\\\\"\\\\n    for images, _targets in loader:\\\\n        images = images.to(device, non_blocking=True)\\\\n        with torch.amp.autocast(\\\\\"cuda\\\\\" if use_amp else \\\\\"cpu\\\\\", enabled=use_amp):\\\\n            logits = model(images)\\\\n            probs = torch.sigmoid(logits.float())\\\\n        chunks.append(probs.detach().cpu().numpy())\\\\n    stacked = np.concatenate(chunks, axis=0) if chunks else np.zeros((0, len(TARGETS)))\\\\n    clipped = np.clip(stacked, 0.0, 1.0)\\\\n    if not np.isfinite(clipped).all():\\\\n        raise ValueError(\\\\\"CNN produced non-finite probabilities\\\\\")\\\\n    frame = pd.DataFrame(\\\\n        clipped, index=[str(uid) for uid in uids], columns=list(TARGETS)\\\\n    )\\\\n    return frame\\\\n\", \"src/labels.py\": \"\\\\\"\\\\\"\\\\\"Expert labels plus a compact multilingual report extractor.\\\\n\\\\nThe extractor is training-only. Test studies have no reports. Gold labels\\\\nalways override extracted labels. This is a first-pass keyword matcher, not\\\\nthe large public rule system from the 0.941 inference kernels.\\\\n\\\\\"\\\\\"\\\\\"\\\\n\\\\nfrom __future__ import annotations\\\\n\\\\nimport re\\\\nimport unicodedata\\\\n\\\\nimport numpy as np\\\\nimport pandas as pd\\\\n\\\\nfrom constants import ID_COL, REPORT_COL, TARGETS\\\\nfrom data import gold_mask\\\\n\\\\n_PRE = str.maketrans(\\\\n    {\\\\n        \\\\\"\\\\u0131\\\\\": \\\\\"i\\\\\",\\\\n        \\\\\"\\\\u0130\\\\\": \\\\\"i\\\\\",\\\\n        \\\\\"I\\\\\": \\\\\"i\\\\\",\\\\n        \\\\\"\\\\u00df\\\\\": \\\\\"ss\\\\\",\\\\n        \\\\\"\\\\u00f8\\\\\": \\\\\"o\\\\\",\\\\n        \\\\\"\\\\u00e6\\\\\": \\\\\"ae\\\\\",\\\\n    }\\\\n)\\\\n\\\\nNEG_BEFORE = re.compile(\\\\n    r\\\\\"(?:\\\\\\\\bno\\\\\\\\b|\\\\\\\\bnot\\\\\\\\b|\\\\\\\\bwithout\\\\\\\\b|\\\\\\\\bnegative for\\\\\\\\b|\\\\\\\\babsence\\\\\\\\b|\\\\\"\\\\n    r\\\\\"\\\\\\\\bno evidence\\\\\\\\b|\\\\\\\\bfree of\\\\\\\\b|\\\\\\\\bsin\\\\\\\\b|\\\\\\\\bausencia\\\\\\\\b|\\\\\\\\bpas de\\\\\\\\b|\\\\\\\\bsans\\\\\\\\b|\\\\\"\\\\n    r\\\\\"\\\\\\\\bkeine?n?\\\\\\\\b|\\\\\\\\bohne\\\\\\\\b|\\\\\\\\bnicht\\\\\\\\b|\\\\\\\\bgeen\\\\\\\\b|\\\\\\\\bzonder\\\\\\\\b|\\\\\\\\bniet\\\\\\\\b|\\\\\"\\\\n    r\\\\\"\\\\\\\\byok\\\\\\\\b|\\\\\\\\bizlenmedi\\\\\\\\b|\\\\\\\\bsaptanamadi\\\\\\\\b|\\\\\\\\bden\\\\\\\\b|\\\\\\\\b\\\\u03c7\\\\u03c9\\\\u03c1\\\\u03b9\\\\u03c2\\\\\\\\b|\\\\\\\\b\\\\u0431\\\\u0435\\\\u0437\\\\\\\\b|\\\\\"\\\\n    r\\\\\"\\\\\\\\b\\\\u043d\\\\u044f\\\\u043c\\\\u0430\\\\\\\\b|\\\\\\\\bnema\\\\\\\\b|\\\\\\\\bbez\\\\\\\\b)\\\\\\\\s{0,40}$\\\\\",\\\\n    re.I,\\\\n)\\\\nNEG_AFTER = re.compile(\\\\n    r\\\\\"^\\\\\\\\s{0,24}(?:izlenmedi|yok|saptanamadi|gorulmedi|yoktur|degil)\\\\\\\\b\\\\\",\\\\n    re.I,\\\\n)\\\\n\\\\nANATOMY: dict[str, re.Pattern[str]] = {\\\\n    \\\\\"ACL\\\\\": re.compile(\\\\n        r\\\\\"anterior cruciate|\\\\\\\\bacl\\\\\\\\b|\\\\\\\\bcruzado anterior\\\\\\\\b|\\\\\\\\blca\\\\\\\\b|\\\\\"\\\\n        r\\\\\"vorderes kreuzband|voorste kruisband|\\\\\\\\bvkb\\\\\\\\b|on capraz|\\\\\\\\bocb\\\\\\\\b|\\\\\"\\\\n        r\\\\\"prednji krizni|\\\\u03c0\\\\u03c1\\\\u03bf\\\\u03c3\\\\u03b8\\\\u03b9\\\\\\\\w* \\\\u03c7\\\\u03b9\\\\u03b1\\\\u03c3\\\\u03c4|\\\\u043f\\\\u0440\\\\u0435\\\\u0434\\\\u043d\\\\u0430 \\\\u043a\\\\u0440\\\\u044a\\\\u0441\\\\u0442\\\\u043d\\\\u0430\\\\\",\\\\n        re.I,\\\\n    ),\\\\n    \\\\\"MCL\\\\\": re.compile(\\\\n        r\\\\\"medial collateral|\\\\\\\\bmcl\\\\\\\\b|colateral medial|ligamento colateral medial|\\\\\"\\\\n        r\\\\\"innenseitiges seitenband|mediale collaterale|ic yan bag|\\\\\\\\bicb\\\\\\\\b\\\\\",\\\\n        re.I,\\\\n    ),\\\\n    \\\\\"Medial Meniscus\\\\\": re.compile(\\\\n        r\\\\\"medial menisc|\\\\\\\\bmm\\\\\\\\b|menisco medial|menisque medial|\\\\\"\\\\n        r\\\\\"innenmeniskus|mediale meniscus|medial menisk\\\\\",\\\\n        re.I,\\\\n    ),\\\\n    \\\\\"Lateral Meniscus\\\\\": re.compile(\\\\n        r\\\\\"lateral menisc|menisco lateral|menisque lateral|\\\\\"\\\\n        r\\\\\"aussenmeniskus|laterale meniscus|lateral menisk\\\\\",\\\\n        re.I,\\\\n    ),\\\\n    \\\\\"Medial OA\\\\\": re.compile(\\\\n        r\\\\\"medial (?:compartment )?(?:oa|osteoarth|chondral|cartilage)|\\\\\"\\\\n        r\\\\\"gonarthrose medial|artrosis medial|medial kompartman\\\\\",\\\\n        re.I,\\\\n    ),\\\\n    \\\\\"Lateral OA\\\\\": re.compile(\\\\n        r\\\\\"lateral (?:compartment )?(?:oa|osteoarth|chondral|cartilage)|\\\\\"\\\\n        r\\\\\"gonarthrose lateral|artrosis lateral|lateral kompartman\\\\\",\\\\n        re.I,\\\\n    ),\\\\n    \\\\\"PF OA\\\\\": re.compile(\\\\n        r\\\\\"patellofemoral|\\\\\\\\bpfj\\\\\\\\b|\\\\\\\\bpf oa\\\\\\\\b|femoropatellar|artrosis patel|\\\\\"\\\\n        r\\\\\"retropatellar knorpel\\\\\",\\\\n        re.I,\\\\n    ),\\\\n    \\\\\"Effusion\\\\\": re.compile(\\\\n        r\\\\\"\\\\\\\\beffusion\\\\\\\\b|\\\\\\\\bderrame\\\\\\\\b|\\\\\\\\bversamento\\\\\\\\b|\\\\\\\\bepanchement\\\\\\\\b|\\\\\\\\berguss\\\\\\\\b|\\\\\"\\\\n        r\\\\\"\\\\\\\\befuzyon\\\\\\\\b|\\\\\\\\befusion\\\\\\\\b|\\\\\\\\b\\\\u03bf\\\\u03b9\\\\u03b4\\\\u03b7\\\\u03bc\\\\u03b1 \\\\u03b1\\\\u03c1\\\\u03b8\\\\u03c1|\\\\\\\\b\\\\u0438\\\\u0437\\\\u043b\\\\u0438\\\\u0432\\\\\\\\b\\\\\",\\\\n        re.I,\\\\n    ),\\\\n    \\\\\"Synovitis\\\\\": re.compile(\\\\n        r\\\\\"\\\\\\\\bsynovitis\\\\\\\\b|\\\\\\\\bsinovit\\\\\\\\b|\\\\\\\\bsynovite\\\\\\\\b|\\\\\\\\bsynovitis\\\\\\\\b|\\\\\\\\b\\\\u0441\\\\u0438\\\\u043d\\\\u043e\\\\u0432\\\\u0438\\\\u0442\\\\\\\\b\\\\\",\\\\n        re.I,\\\\n    ),\\\\n    \\\\\"Baker\\'s\\\\\": re.compile(\\\\n        r\\\\\"baker\\'?s?|\\\\\\\\bpopliteal cyst\\\\\\\\b|quiste de baker|kyste de baker|\\\\\"\\\\n        r\\\\\"bakerzyste|baker kisti\\\\\",\\\\n        re.I,\\\\n    ),\\\\n    \\\\\"Contusion\\\\\": re.compile(\\\\n        r\\\\\"\\\\\\\\bcontusion\\\\\\\\b|bone bruise|bone marrow edema|\\\\\\\\boedema\\\\\\\\b|\\\\\\\\bedema oseo\\\\\\\\b|\\\\\"\\\\n        r\\\\\"knochenmarkodem|kemik kontuzyon|kontuzyon\\\\\",\\\\n        re.I,\\\\n    ),\\\\n    \\\\\"Fracture\\\\\": re.compile(\\\\n        r\\\\\"\\\\\\\\bfracture\\\\\\\\b|\\\\\\\\bfractura\\\\\\\\b|\\\\\\\\bfraktur\\\\\\\\b|\\\\\\\\bkirik\\\\\\\\b|\\\\\\\\bfractuur\\\\\\\\b|\\\\\"\\\\n        r\\\\\"\\\\\\\\b \\\\u03ba\\\\u03ac\\\\u03c4\\\\u03b1\\\\u03b3\\\\u03bc\\\\u03b1\\\\\\\\b|\\\\\\\\b\\\\u0444\\\\u0440\\\\u0430\\\\u043a\\\\u0442\\\\u0443\\\\u0440\\\\u0430\\\\\\\\b\\\\\",\\\\n        re.I,\\\\n    ),\\\\n}\\\\n\\\\nPOSITIVE: dict[str, re.Pattern[str]] = {\\\\n    \\\\\"ACL\\\\\": re.compile(r\\\\\"tear|torn|rupture|rotura|riss|yirtik|puknuce|\\\\u03c1\\\\u03b7\\\\u03be\\\\u03b7\\\\\", re.I),\\\\n    \\\\\"MCL\\\\\": re.compile(r\\\\\"tear|torn|sprain|rupture|rotura|riss|yirtik|strain\\\\\", re.I),\\\\n    \\\\\"Medial Meniscus\\\\\": re.compile(\\\\n        r\\\\\"tear|torn|rupture|rotura|riss|yirtik|bucket.?handle|macerat\\\\\", re.I\\\\n    ),\\\\n    \\\\\"Lateral Meniscus\\\\\": re.compile(\\\\n        r\\\\\"tear|torn|rupture|rotura|riss|yirtik|bucket.?handle|macerat\\\\\", re.I\\\\n    ),\\\\n    \\\\\"Medial OA\\\\\": re.compile(\\\\n        r\\\\\"osteoarth|chondromalacia|cartilage loss|grade\\\\\\\\s*[2-4]|outerbridge\\\\\", re.I\\\\n    ),\\\\n    \\\\\"Lateral OA\\\\\": re.compile(\\\\n        r\\\\\"osteoarth|chondromalacia|cartilage loss|grade\\\\\\\\s*[2-4]|outerbridge\\\\\", re.I\\\\n    ),\\\\n    \\\\\"PF OA\\\\\": re.compile(\\\\n        r\\\\\"osteoarth|chondromalacia|cartilage loss|grade\\\\\\\\s*[2-4]|outerbridge\\\\\", re.I\\\\n    ),\\\\n    \\\\\"Effusion\\\\\": re.compile(\\\\n        r\\\\\"effusion|derrame|versamento|epanchement|erguss|efuzyon|present|moderate|large\\\\\",\\\\n        re.I,\\\\n    ),\\\\n    \\\\\"Synovitis\\\\\": re.compile(r\\\\\"synovitis|sinovit|synovite|inflammation\\\\\", re.I),\\\\n    \\\\\"Baker\\'s\\\\\": re.compile(r\\\\\"cyst|baker|kist|zyste\\\\\", re.I),\\\\n    \\\\\"Contusion\\\\\": re.compile(r\\\\\"contusion|bruise|edema|oedema|odem\\\\\", re.I),\\\\n    \\\\\"Fracture\\\\\": re.compile(r\\\\\"fracture|fractura|fraktur|kirik|fractuur\\\\\", re.I),\\\\n}\\\\n\\\\n\\\\ndef normalize_report(text: str) -> str:\\\\n    if not isinstance(text, str):\\\\n        return \\\\\"\\\\\"\\\\n    folded = text.translate(_PRE).lower()\\\\n    decomposed = unicodedata.normalize(\\\\\"NFKD\\\\\", folded)\\\\n    stripped = \\\\\"\\\\\".join(ch for ch in decomposed if not unicodedata.combining(ch))\\\\n    return re.sub(r\\\\\"[\\\\\\\\s_/\\\\\\\\\\\\\\\\-]+\\\\\", \\\\\" \\\\\", stripped)\\\\n\\\\n\\\\ndef _clause_hit(\\\\n    clause: str, anatomy: re.Pattern[str], positive: re.Pattern[str]\\\\n) -> bool:\\\\n    for match in anatomy.finditer(clause):\\\\n        start, end = match.span()\\\\n        window = clause[max(0, start - 70) : min(len(clause), end + 70)]\\\\n        if not positive.search(window):\\\\n            continue\\\\n        prefix = clause[max(0, start - 48) : start]\\\\n        suffix = clause[end : min(len(clause), end + 28)]\\\\n        if NEG_BEFORE.search(prefix) or NEG_AFTER.search(suffix):\\\\n            continue\\\\n        return True\\\\n    return False\\\\n\\\\n\\\\ndef extract_report_labels(report: str) -> dict[str, float]:\\\\n    \\\\\"\\\\\"\\\\\"Return 0/1/NaN per finding. NaN means the report did not mention it.\\\\\"\\\\\"\\\\\"\\\\n    text = normalize_report(report)\\\\n    labels: dict[str, float] = {name: float(\\\\\"nan\\\\\") for name in TARGETS}\\\\n    if not text.strip():\\\\n        return labels\\\\n    clauses = re.split(r\\\\\"[.\\\\\\\\n;!?]+\\\\\", text)\\\\n    for name in TARGETS:\\\\n        anatomy = ANATOMY[name]\\\\n        positive = POSITIVE[name]\\\\n        mentioned = False\\\\n        hit = False\\\\n        for clause in clauses:\\\\n            if not clause.strip():\\\\n                continue\\\\n            if anatomy.search(clause):\\\\n                mentioned = True\\\\n            if _clause_hit(clause, anatomy, positive):\\\\n                hit = True\\\\n        if hit:\\\\n            labels[name] = 1.0\\\\n        elif mentioned:\\\\n            labels[name] = 0.0\\\\n    return labels\\\\n\\\\n\\\\ndef weak_label_table(train: pd.DataFrame) -> pd.DataFrame:\\\\n    \\\\\"\\\\\"\\\\\"Gold labels override report extraction. Unmentioned findings stay NaN.\\\\\"\\\\\"\\\\\"\\\\n    rows: list[dict[str, float | str]] = []\\\\n    if REPORT_COL in train.columns:\\\\n        reports = train[REPORT_COL]\\\\n    else:\\\\n        reports = pd.Series(\\\\\"\\\\\", index=train.index)\\\\n    for uid, report in zip(train[ID_COL].astype(str), reports, strict=True):\\\\n        extracted = extract_report_labels(\\\\\"\\\\\" if pd.isna(report) else str(report))\\\\n        extracted[ID_COL] = uid\\\\n        rows.append(extracted)\\\\n    weak = pd.DataFrame(rows).set_index(ID_COL)\\\\n    gold = gold_mask(train)\\\\n    gold_block = train.loc[gold, [ID_COL, *TARGETS]].copy()\\\\n    gold_block[ID_COL] = gold_block[ID_COL].astype(str)\\\\n    gold_block = gold_block.set_index(ID_COL)\\\\n    for col in TARGETS:\\\\n        weak.loc[gold_block.index, col] = gold_block[col].astype(float)\\\\n    return weak.reset_index()\\\\n\\\\n\\\\ndef supervision_matrix(\\\\n    studies: pd.Series,\\\\n    labels: pd.DataFrame,\\\\n) -> np.ndarray:\\\\n    indexed = labels.set_index(ID_COL)\\\\n    aligned = indexed.reindex(studies.astype(str))\\\\n    return aligned[list(TARGETS)].to_numpy(dtype=float)\\\\n\", \"src/metric.py\": \"\\\\\"\\\\\"\\\\\"Macro ROC-AUC over the twelve knee findings.\\\\\"\\\\\"\\\\\"\\\\n\\\\nfrom __future__ import annotations\\\\n\\\\nimport numpy as np\\\\nfrom sklearn.metrics import roc_auc_score\\\\n\\\\nfrom constants import TARGETS\\\\n\\\\n\\\\ndef per_label_auc(\\\\n    y_true: np.ndarray,\\\\n    y_prob: np.ndarray,\\\\n) -> np.ndarray:\\\\n    \\\\\"\\\\\"\\\\\"Return one AUC per column. Undefined columns are NaN.\\\\\"\\\\\"\\\\\"\\\\n    y_true = np.asarray(y_true, dtype=float)\\\\n    y_prob = np.asarray(y_prob, dtype=float)\\\\n    if y_true.shape != y_prob.shape:\\\\n        raise ValueError(\\\\\"y_true and y_prob must have the same shape\\\\\")\\\\n    if y_true.ndim != 2 or y_true.shape[1] != len(TARGETS):\\\\n        raise ValueError(f\\\\\"Expected (n, {len(TARGETS)}) arrays\\\\\")\\\\n    if not np.isfinite(y_prob).all():\\\\n        raise ValueError(\\\\\"Predictions must be finite\\\\\")\\\\n\\\\n    scores = np.full(len(TARGETS), np.nan, dtype=float)\\\\n    for idx in range(len(TARGETS)):\\\\n        truth = y_true[:, idx]\\\\n        mask = np.isfinite(truth)\\\\n        if mask.sum() < 2:\\\\n            continue\\\\n        labels = np.unique(truth[mask])\\\\n        if labels.size < 2:\\\\n            continue\\\\n        scores[idx] = float(roc_auc_score(truth[mask], y_prob[mask, idx]))\\\\n    return scores\\\\n\\\\n\\\\ndef competition_metric(y_true: np.ndarray, y_prob: np.ndarray) -> float:\\\\n    \\\\\"\\\\\"\\\\\"Macro-average AUC. Higher is better. Constant ranking scores 0.5.\\\\\"\\\\\"\\\\\"\\\\n    scores = per_label_auc(y_true, y_prob)\\\\n    if np.isnan(scores).all():\\\\n        raise ValueError(\\\\\"AUC is undefined for every finding\\\\\")\\\\n    return float(np.nanmean(scores))\\\\n\\\\n\\\\ndef metric_breakdown(y_true: np.ndarray, y_prob: np.ndarray) -> dict[str, float]:\\\\n    scores = per_label_auc(y_true, y_prob)\\\\n    breakdown = {\\\\n        name: float(score) for name, score in zip(TARGETS, scores, strict=True)\\\\n    }\\\\n    breakdown[\\\\\"macro\\\\\"] = float(np.nanmean(scores))\\\\n    return breakdown\\\\n\", \"src/models.py\": \"\\\\\"\\\\\"\\\\\"Per-finding classifiers for the metadata baseline.\\\\\"\\\\\"\\\\\"\\\\n\\\\nfrom __future__ import annotations\\\\n\\\\nimport numpy as np\\\\nimport pandas as pd\\\\nfrom sklearn.ensemble import HistGradientBoostingClassifier\\\\nfrom sklearn.linear_model import LogisticRegression\\\\n\\\\nfrom constants import TARGETS\\\\n\\\\nHGB_PARAMS = {\\\\n    \\\\\"max_depth\\\\\": 3,\\\\n    \\\\\"learning_rate\\\\\": 0.08,\\\\n    \\\\\"max_iter\\\\\": 120,\\\\n    \\\\\"l2_regularization\\\\\": 1.0,\\\\n    \\\\\"min_samples_leaf\\\\\": 12,\\\\n    \\\\\"early_stopping\\\\\": True,\\\\n    \\\\\"validation_fraction\\\\\": 0.15,\\\\n    \\\\\"random_state\\\\\": 42,\\\\n}\\\\n\\\\n\\\\ndef _finite_mask(y: np.ndarray) -> np.ndarray:\\\\n    return np.isfinite(y)\\\\n\\\\n\\\\ndef fit_finding_model(x: pd.DataFrame, y: np.ndarray):\\\\n    \\\\\"\\\\\"\\\\\"Fit one binary model. Constant prevalence if a class is missing.\\\\\"\\\\\"\\\\\"\\\\n    mask = _finite_mask(y)\\\\n    if mask.sum() < 8:\\\\n        return (\\\\\"constant\\\\\", float(np.nanmean(y[mask])) if mask.any() else 0.5)\\\\n    labels = y[mask]\\\\n    unique = np.unique(labels)\\\\n    if unique.size < 2:\\\\n        return (\\\\\"constant\\\\\", float(unique[0]))\\\\n    frame = x.loc[mask]\\\\n    try:\\\\n        model = HistGradientBoostingClassifier(**HGB_PARAMS)\\\\n        model.fit(frame, labels.astype(int))\\\\n        return (\\\\\"hgb\\\\\", model)\\\\n    except ValueError:\\\\n        linear = LogisticRegression(max_iter=200, class_weight=\\\\\"balanced\\\\\")\\\\n        linear.fit(frame, labels.astype(int))\\\\n        return (\\\\\"linear\\\\\", linear)\\\\n\\\\n\\\\ndef predict_finding(bundle: tuple[str, object], x: pd.DataFrame) -> np.ndarray:\\\\n    kind, model = bundle\\\\n    n_rows = len(x)\\\\n    if kind == \\\\\"constant\\\\\":\\\\n        return np.full(n_rows, float(model), dtype=float)\\\\n    if kind == \\\\\"hgb\\\\\":\\\\n        return model.predict_proba(x)[:, 1]\\\\n    if kind == \\\\\"linear\\\\\":\\\\n        return model.predict_proba(x)[:, 1]\\\\n    raise TypeError(f\\\\\"Unknown model bundle {kind}\\\\\")\\\\n\\\\n\\\\ndef fit_all_findings(\\\\n    x: pd.DataFrame,\\\\n    y: np.ndarray,\\\\n) -> dict[str, tuple[str, object]]:\\\\n    if y.shape != (len(x), len(TARGETS)):\\\\n        raise ValueError(\\\\\"Label matrix does not match the feature table\\\\\")\\\\n    return {name: fit_finding_model(x, y[:, idx]) for idx, name in enumerate(TARGETS)}\\\\n\\\\n\\\\ndef predict_all_findings(\\\\n    models: dict[str, tuple[str, object]],\\\\n    x: pd.DataFrame,\\\\n) -> pd.DataFrame:\\\\n    columns = {}\\\\n    for name in TARGETS:\\\\n        if name not in models:\\\\n            raise KeyError(f\\\\\"Missing model for {name}\\\\\")\\\\n        columns[name] = predict_finding(models[name], x)\\\\n    frame = pd.DataFrame(columns, index=x.index)\\\\n    clipped = frame.clip(0.0, 1.0)\\\\n    if not np.isfinite(clipped.to_numpy()).all():\\\\n        raise ValueError(\\\\\"Non-finite finding probabilities\\\\\")\\\\n    return clipped\\\\n\", \"src/pipeline.py\": \"\\\\\"\\\\\"\\\\\"Portable experiment entrypoint for Kaggle Notebooks and Google Colab.\\\\\"\\\\\"\\\\\"\\\\n\\\\nfrom __future__ import annotations\\\\n\\\\nimport json\\\\nimport os\\\\nimport subprocess\\\\nimport time\\\\nfrom datetime import UTC, datetime\\\\nfrom pathlib import Path\\\\nfrom typing import Any\\\\n\\\\nimport numpy as np\\\\nimport pandas as pd\\\\n\\\\nfrom config import load_competition_config\\\\nfrom constants import FOLD_SEED, ID_COL, IMAGE_BATCH, IMAGE_EPOCHS, N_SPLITS, TARGETS\\\\nfrom data import (\\\\n    assert_series_schema,\\\\n    assert_test_schema,\\\\n    assert_train_schema,\\\\n    gold_mask,\\\\n    load_table,\\\\n    write_submission,\\\\n)\\\\nfrom features import align_features, series_study_features\\\\nfrom folds import load_or_create_folds\\\\nfrom labels import supervision_matrix, weak_label_table\\\\nfrom metric import competition_metric, metric_breakdown\\\\nfrom models import fit_all_findings, predict_all_findings\\\\nfrom report import file_sha256, write_report\\\\nfrom runtime import RuntimePaths, detect_runtime, find_series_root, resolve_paths\\\\n\\\\nPARENT_CV = {\\\\n    \\\\\"exp000\\\\\": None,\\\\n    \\\\\"exp001\\\\\": 0.5,\\\\n    \\\\\"exp002\\\\\": 0.493721,\\\\n}\\\\n\\\\n\\\\ndef _git_revision(root: Path) -> str:\\\\n    try:\\\\n        return subprocess.check_output(\\\\n            [\\\\\"git\\\\\", \\\\\"rev-parse\\\\\", \\\\\"HEAD\\\\\"],\\\\n            cwd=root,\\\\n            text=True,\\\\n            stderr=subprocess.DEVNULL,\\\\n        ).strip()\\\\n    except (OSError, subprocess.CalledProcessError):\\\\n        return \\\\\"unknown\\\\\"\\\\n\\\\n\\\\ndef run(experiment_id: str = \\\\\"exp002\\\\\") -> dict[str, Any]:\\\\n    \\\\\"\\\\\"\\\\\"Run one experiment and write submission.csv plus REPORT.md.\\\\\"\\\\\"\\\\\"\\\\n    cfg = load_competition_config()\\\\n    slug = cfg[\\\\\"competition\\\\\"][\\\\\"slug\\\\\"]\\\\n    paths = resolve_paths(slug)\\\\n    experiment_dir = paths.work_dir / experiment_id\\\\n    experiment_dir.mkdir(parents=True, exist_ok=True)\\\\n    started = time.perf_counter()\\\\n\\\\n    if experiment_id == \\\\\"exp000\\\\\":\\\\n        payload = run_constant(cfg, paths, experiment_dir)\\\\n    elif experiment_id == \\\\\"exp001\\\\\":\\\\n        payload = run_metadata_baseline(cfg, paths, experiment_dir)\\\\n    elif experiment_id == \\\\\"exp002\\\\\":\\\\n        payload = run_image_baseline(cfg, paths, experiment_dir)\\\\n    else:\\\\n        raise ValueError(f\\\\\"Unknown experiment_id {experiment_id!r}\\\\\")\\\\n\\\\n    payload.update(\\\\n        {\\\\n            \\\\\"experiment_id\\\\\": experiment_id,\\\\n            \\\\\"runtime\\\\\": paths.name,\\\\n            \\\\\"slug\\\\\": slug,\\\\n            \\\\\"work_dir\\\\\": str(paths.work_dir),\\\\n            \\\\\"data_dir\\\\\": str(paths.data_dir),\\\\n            \\\\\"duration_sec\\\\\": round(time.perf_counter() - started, 3),\\\\n            \\\\\"finished_at\\\\\": datetime.now(UTC).isoformat(),\\\\n            \\\\\"git_revision\\\\\": _git_revision(paths.competition_root),\\\\n            \\\\\"detected_runtime\\\\\": detect_runtime(),\\\\n        }\\\\n    )\\\\n    write_report(experiment_dir / \\\\\"REPORT.md\\\\\", payload)\\\\n    write_report(paths.work_dir / \\\\\"REPORT.md\\\\\", payload)\\\\n    return payload\\\\n\\\\n\\\\ndef run_constant(\\\\n    cfg: dict[str, Any],\\\\n    paths: RuntimePaths,\\\\n    experiment_dir: Path,\\\\n) -> dict[str, Any]:\\\\n    \\\\\"\\\\\"\\\\\"Schema smoke test: emit the sample file and score a constant 0.5 on gold.\\\\\"\\\\\"\\\\\"\\\\n    experiment_dir.mkdir(parents=True, exist_ok=True)\\\\n    sample = load_table(cfg, paths, \\\\\"sample_submission_file\\\\\")\\\\n    test = load_table(cfg, paths, \\\\\"test_file\\\\\")\\\\n    assert_test_schema(test, sample)\\\\n    submission_path = paths.work_dir / \\\\\"submission.csv\\\\\"\\\\n    submission = write_submission(\\\\n        sample,\\\\n        pd.DataFrame(0.5, index=sample[ID_COL].astype(str), columns=list(TARGETS)),\\\\n        submission_path,\\\\n    )\\\\n    submission.to_csv(experiment_dir / \\\\\"submission.csv\\\\\", index=False)\\\\n\\\\n    gold_cv = None\\\\n    n_gold = 0\\\\n    per_label: dict[str, float] = {}\\\\n    try:\\\\n        train = load_table(cfg, paths, \\\\\"train_file\\\\\")\\\\n        assert_train_schema(train)\\\\n        labeled = gold_mask(train)\\\\n        n_gold = int(labeled.sum())\\\\n        if n_gold:\\\\n            truth = train.loc[labeled, list(TARGETS)].to_numpy(dtype=float)\\\\n            pred = np.full_like(truth, 0.5)\\\\n            gold_cv = competition_metric(truth, pred)\\\\n            per_label = {\\\\n                name: score\\\\n                for name, score in metric_breakdown(truth, pred).items()\\\\n                if name != \\\\\"macro\\\\\"\\\\n            }\\\\n    except FileNotFoundError:\\\\n        train = None\\\\n\\\\n    return {\\\\n        \\\\\"status\\\\\": \\\\\"smoke\\\\\",\\\\n        \\\\\"rows\\\\\": len(submission),\\\\n        \\\\\"columns\\\\\": list(submission.columns),\\\\n        \\\\\"submission_path\\\\\": str(submission_path),\\\\n        \\\\\"submission_sha256\\\\\": file_sha256(submission_path),\\\\n        \\\\\"metric\\\\\": cfg[\\\\\"evaluation\\\\\"][\\\\\"metric\\\\\"],\\\\n        \\\\\"direction\\\\\": cfg[\\\\\"evaluation\\\\\"][\\\\\"direction\\\\\"],\\\\n        \\\\\"fold_version\\\\\": cfg[\\\\\"validation\\\\\"][\\\\\"fold_version\\\\\"],\\\\n        \\\\\"parent\\\\\": \\\\\"none\\\\\",\\\\n        \\\\\"hypothesis\\\\\": (\\\\n            \\\\\"A constant 0.5 ranking scores 0.5 macro AUC and is schema-valid.\\\\\"\\\\n        ),\\\\n        \\\\\"material_change\\\\\": (\\\\n            \\\\\"Copy the sample submission contract; no pixels, no reports.\\\\\"\\\\n        ),\\\\n        \\\\\"cv\\\\\": gold_cv,\\\\n        \\\\\"cv_std\\\\\": None,\\\\n        \\\\\"n_gold_scored\\\\\": n_gold,\\\\n        \\\\\"per_fold\\\\\": [],\\\\n        \\\\\"per_label\\\\\": per_label,\\\\n        \\\\\"interpretation\\\\\": (\\\\n            \\\\\"This is the official sample ranking. Use it only to prove the \\\\\"\\\\n            \\\\\"submission file and the local metric. It is not a model.\\\\\"\\\\n        ),\\\\n        \\\\\"next_actions\\\\\": [\\\\n            \\\\\"Run exp001: series-metadata HGB trained on report-weak labels.\\\\\",\\\\n            \\\\\"If metadata CV stays near 0.5, skip tuning and move to images.\\\\\",\\\\n        ],\\\\n    }\\\\n\\\\n\\\\ndef run_metadata_baseline(\\\\n    cfg: dict[str, Any],\\\\n    paths: RuntimePaths,\\\\n    experiment_dir: Path,\\\\n) -> dict[str, Any]:\\\\n    \\\\\"\\\\\"\\\\\"HGB on series metadata. Train on weak/gold labels; score gold OOF only.\\\\\"\\\\\"\\\\\"\\\\n    experiment_dir.mkdir(parents=True, exist_ok=True)\\\\n    train = load_table(cfg, paths, \\\\\"train_file\\\\\")\\\\n    test = load_table(cfg, paths, \\\\\"test_file\\\\\")\\\\n    sample = load_table(cfg, paths, \\\\\"sample_submission_file\\\\\")\\\\n    train_series = load_table(cfg, paths, \\\\\"train_series_file\\\\\")\\\\n    test_series = load_table(cfg, paths, \\\\\"test_series_file\\\\\")\\\\n    assert_train_schema(train)\\\\n    assert_test_schema(test, sample)\\\\n    assert_series_schema(train_series, split=\\\\\"train\\\\\")\\\\n    assert_series_schema(test_series, split=\\\\\"test\\\\\")\\\\n\\\\n    fold_path = paths.fold_dir / Path(cfg[\\\\\"validation\\\\\"][\\\\\"fold_file\\\\\"]).name\\\\n    folds = load_or_create_folds(\\\\n        train,\\\\n        fold_path,\\\\n        n_splits=int(cfg[\\\\\"validation\\\\\"][\\\\\"n_splits\\\\\"]),\\\\n        seed=int(cfg[\\\\\"validation\\\\\"][\\\\\"seed\\\\\"]),\\\\n    )\\\\n    train_ids = train[ID_COL].astype(str)\\\\n    fold_lookup = folds.set_index(ID_COL)[\\\\\"fold\\\\\"].reindex(train_ids)\\\\n    if fold_lookup.isna().any():\\\\n        raise ValueError(\\\\\"Fold lookup missed training studies\\\\\")\\\\n\\\\n    train_features = series_study_features(train_series)\\\\n    test_features = series_study_features(test_series)\\\\n    x_train = align_features(train_ids, train_features)\\\\n    x_test = align_features(test[ID_COL].astype(str), test_features)\\\\n    labels = weak_label_table(train)\\\\n    y_all = supervision_matrix(train_ids, labels)\\\\n    labeled = gold_mask(train).to_numpy()\\\\n    y_gold = train[list(TARGETS)].to_numpy(dtype=float)\\\\n\\\\n    oof = np.full((len(train), len(TARGETS)), np.nan, dtype=float)\\\\n    per_fold: list[float] = []\\\\n    for fold_id in range(int(cfg[\\\\\"validation\\\\\"][\\\\\"n_splits\\\\\"])):\\\\n        val_mask = fold_lookup.to_numpy() == fold_id\\\\n        train_mask = ~val_mask\\\\n        models = fit_all_findings(x_train.iloc[train_mask], y_all[train_mask])\\\\n        pred = predict_all_findings(models, x_train.iloc[val_mask])\\\\n        oof[val_mask] = pred.to_numpy(dtype=float)\\\\n        gold_val = val_mask & labeled\\\\n        if gold_val.sum() == 0:\\\\n            per_fold.append(float(\\\\\"nan\\\\\"))\\\\n            continue\\\\n        per_fold.append(competition_metric(y_gold[gold_val], oof[gold_val]))\\\\n\\\\n    gold_oof_mask = labeled & np.isfinite(oof).all(axis=1)\\\\n    if gold_oof_mask.sum() == 0:\\\\n        raise ValueError(\\\\\"No gold OOF predictions were produced\\\\\")\\\\n    cv = competition_metric(y_gold[gold_oof_mask], oof[gold_oof_mask])\\\\n    breakdown = metric_breakdown(y_gold[gold_oof_mask], oof[gold_oof_mask])\\\\n    fold_scores = [score for score in per_fold if score == score]\\\\n    cv_std = float(np.std(fold_scores)) if fold_scores else None\\\\n\\\\n    models = fit_all_findings(x_train, y_all)\\\\n    test_pred = predict_all_findings(models, x_test)\\\\n    test_pred.index = test[ID_COL].astype(str)\\\\n\\\\n    submission_path = paths.work_dir / \\\\\"submission.csv\\\\\"\\\\n    write_submission(sample, test_pred, submission_path)\\\\n    pd.DataFrame(test_pred).to_csv(experiment_dir / \\\\\"submission.csv\\\\\", index=True)\\\\n\\\\n    oof_frame = pd.DataFrame(oof, columns=list(TARGETS))\\\\n    oof_frame.insert(0, ID_COL, train_ids.to_numpy())\\\\n    oof_frame.insert(1, \\\\\"fold\\\\\", fold_lookup.to_numpy())\\\\n    oof_frame.insert(2, \\\\\"has_labels\\\\\", labeled.astype(int))\\\\n    for name in TARGETS:\\\\n        oof_frame[f\\\\\"{name}__true\\\\\"] = y_gold[:, TARGETS.index(name)]\\\\n    oof_path = experiment_dir / \\\\\"oof.parquet\\\\\"\\\\n    test_path = experiment_dir / \\\\\"test_preds.parquet\\\\\"\\\\n    oof_frame.to_parquet(oof_path, index=False)\\\\n    test_pred.reset_index().rename(columns={\\\\\"index\\\\\": ID_COL}).to_parquet(\\\\n        test_path, index=False\\\\n    )\\\\n\\\\n    parent_cv = PARENT_CV.get(\\\\\"exp001\\\\\")\\\\n    delta = None if parent_cv is None else cv - parent_cv\\\\n    keep = cv >= 0.52\\\\n    interpretation = (\\\\n        \\\\\"Series metadata has ranking signal above a constant 0.5 on gold OOF.\\\\\"\\\\n        if keep\\\\n        else (\\\\n            \\\\\"Series metadata did not beat a constant ranking on the gold studies. \\\\\"\\\\n            \\\\\"Do not tune this model. The next unit of work is images plus better \\\\\"\\\\n            \\\\\"report-derived labels.\\\\\"\\\\n        )\\\\n    )\\\\n    return {\\\\n        \\\\\"status\\\\\": \\\\\"keep\\\\\" if keep else \\\\\"reject\\\\\",\\\\n        \\\\\"rows\\\\\": len(test_pred),\\\\n        \\\\\"submission_path\\\\\": str(submission_path),\\\\n        \\\\\"submission_sha256\\\\\": file_sha256(submission_path),\\\\n        \\\\\"metric\\\\\": cfg[\\\\\"evaluation\\\\\"][\\\\\"metric\\\\\"],\\\\n        \\\\\"direction\\\\\": cfg[\\\\\"evaluation\\\\\"][\\\\\"direction\\\\\"],\\\\n        \\\\\"fold_version\\\\\": cfg[\\\\\"validation\\\\\"][\\\\\"fold_version\\\\\"],\\\\n        \\\\\"parent\\\\\": \\\\\"exp000\\\\\",\\\\n        \\\\\"hypothesis\\\\\": (\\\\n            \\\\\"Study-level MRI series counts contain ranking signal for the twelve \\\\\"\\\\n            \\\\\"findings when the model is trained on report-weak labels.\\\\\"\\\\n        ),\\\\n        \\\\\"material_change\\\\\": (\\\\n            \\\\\"HistGradientBoosting on plane/fat-sat slot counts; gold OOF only.\\\\\"\\\\n        ),\\\\n        \\\\\"cv\\\\\": cv,\\\\n        \\\\\"cv_std\\\\\": cv_std,\\\\n        \\\\\"delta_vs_parent\\\\\": delta,\\\\n        \\\\\"n_gold_scored\\\\\": int(gold_oof_mask.sum()),\\\\n        \\\\\"per_fold\\\\\": per_fold,\\\\n        \\\\\"per_label\\\\\": {k: v for k, v in breakdown.items() if k != \\\\\"macro\\\\\"},\\\\n        \\\\\"oof_path\\\\\": str(oof_path),\\\\n        \\\\\"test_pred_path\\\\\": str(test_path),\\\\n        \\\\\"seed\\\\\": FOLD_SEED,\\\\n        \\\\\"n_splits\\\\\": N_SPLITS,\\\\n        \\\\\"interpretation\\\\\": interpretation,\\\\n        \\\\\"next_actions\\\\\": [\\\\n            \\\\\"If CV \\\\u2248 0.5, implement a 2.5D CNN on the six anatomical slots.\\\\\",\\\\n            \\\\\"Replace the keyword matcher with a stronger multilingual report teacher.\\\\\",\\\\n            \\\\\"Audit gold OOF by finding; rare columns dominate macro AUC.\\\\\",\\\\n        ],\\\\n    }\\\\n\\\\n\\\\ndef _write_prediction_tables(\\\\n    *,\\\\n    train_ids: pd.Series,\\\\n    fold_lookup: pd.Series,\\\\n    labeled: np.ndarray,\\\\n    y_gold: np.ndarray,\\\\n    oof: np.ndarray,\\\\n    test_pred: pd.DataFrame,\\\\n    experiment_dir: Path,\\\\n) -> tuple[Path, Path]:\\\\n    oof_frame = pd.DataFrame(oof, columns=list(TARGETS))\\\\n    oof_frame.insert(0, ID_COL, train_ids.to_numpy())\\\\n    oof_frame.insert(1, \\\\\"fold\\\\\", fold_lookup.to_numpy())\\\\n    oof_frame.insert(2, \\\\\"has_labels\\\\\", labeled.astype(int))\\\\n    for name in TARGETS:\\\\n        oof_frame[f\\\\\"{name}__true\\\\\"] = y_gold[:, TARGETS.index(name)]\\\\n    oof_path = experiment_dir / \\\\\"oof.parquet\\\\\"\\\\n    test_path = experiment_dir / \\\\\"test_preds.parquet\\\\\"\\\\n    oof_frame.to_parquet(oof_path, index=False)\\\\n    test_pred.reset_index().rename(columns={\\\\\"index\\\\\": ID_COL}).to_parquet(\\\\n        test_path, index=False\\\\n    )\\\\n    return oof_path, test_path\\\\n\\\\n\\\\ndef run_image_baseline(\\\\n    cfg: dict[str, Any],\\\\n    paths: RuntimePaths,\\\\n    experiment_dir: Path,\\\\n) -> dict[str, Any]:\\\\n    \\\\\"\\\\\"\\\\\"2.5D CNN on six slots. Train on weak/gold labels; score gold OOF only.\\\\\"\\\\\"\\\\\"\\\\n    from dicom_io import load_split_tensors\\\\n    from image_train import get_device, predict_slot_cnn, train_slot_cnn\\\\n\\\\n    experiment_dir.mkdir(parents=True, exist_ok=True)\\\\n    train = load_table(cfg, paths, \\\\\"train_file\\\\\")\\\\n    test = load_table(cfg, paths, \\\\\"test_file\\\\\")\\\\n    sample = load_table(cfg, paths, \\\\\"sample_submission_file\\\\\")\\\\n    train_series = load_table(cfg, paths, \\\\\"train_series_file\\\\\")\\\\n    test_series = load_table(cfg, paths, \\\\\"test_series_file\\\\\")\\\\n    assert_train_schema(train)\\\\n    assert_test_schema(test, sample)\\\\n    assert_series_schema(train_series, split=\\\\\"train\\\\\")\\\\n    assert_series_schema(test_series, split=\\\\\"test\\\\\")\\\\n\\\\n    fold_path = paths.fold_dir / Path(cfg[\\\\\"validation\\\\\"][\\\\\"fold_file\\\\\"]).name\\\\n    folds = load_or_create_folds(\\\\n        train,\\\\n        fold_path,\\\\n        n_splits=int(cfg[\\\\\"validation\\\\\"][\\\\\"n_splits\\\\\"]),\\\\n        seed=int(cfg[\\\\\"validation\\\\\"][\\\\\"seed\\\\\"]),\\\\n    )\\\\n    train_ids = train[ID_COL].astype(str)\\\\n    test_ids = test[ID_COL].astype(str)\\\\n    fold_lookup = folds.set_index(ID_COL)[\\\\\"fold\\\\\"].reindex(train_ids)\\\\n    if fold_lookup.isna().any():\\\\n        raise ValueError(\\\\\"Fold lookup missed training studies\\\\\")\\\\n\\\\n    labels = weak_label_table(train)\\\\n    y_all = supervision_matrix(train_ids, labels)\\\\n    labeled = gold_mask(train).to_numpy()\\\\n    y_gold = train[list(TARGETS)].to_numpy(dtype=float)\\\\n\\\\n    train_root = find_series_root(paths, \\\\\"train\\\\\")\\\\n    test_root = find_series_root(paths, \\\\\"test\\\\\")\\\\n    smoke_env = os.environ.get(\\\\\"RSNA_SMOKE\\\\\")\\\\n    smoke_limit = int(smoke_env) if smoke_env else None\\\\n    if smoke_limit is not None:\\\\n        print(f\\\\\"[exp002] smoke limit={smoke_limit}\\\\\", flush=True)\\\\n\\\\n    print(f\\\\\"[exp002] decoding train DICOMs from {train_root}\\\\\", flush=True)\\\\n    train_tensors = load_split_tensors(\\\\n        train_ids,\\\\n        train_series,\\\\n        train_root,\\\\n        workers=8,\\\\n        limit=smoke_limit,\\\\n    )\\\\n    print(f\\\\\"[exp002] decoding test DICOMs from {test_root}\\\\\", flush=True)\\\\n    test_tensors = load_split_tensors(\\\\n        test_ids,\\\\n        test_series,\\\\n        test_root,\\\\n        workers=8,\\\\n        limit=smoke_limit,\\\\n    )\\\\n    if smoke_limit is not None:\\\\n        keep_train = np.array([uid in train_tensors for uid in train_ids], dtype=bool)\\\\n        train_ids = pd.Series(np.asarray(train_ids)[keep_train], dtype=str)\\\\n        fold_lookup = pd.Series(np.asarray(fold_lookup)[keep_train])\\\\n        y_all = y_all[keep_train]\\\\n        labeled = labeled[keep_train]\\\\n        y_gold = y_gold[keep_train]\\\\n        keep_test = np.array([uid in test_tensors for uid in test_ids], dtype=bool)\\\\n        test_ids = pd.Series(np.asarray(test_ids)[keep_test], dtype=str)\\\\n\\\\n    device = get_device()\\\\n    print(f\\\\\"[exp002] device={device} studies={len(train_ids)}\\\\\", flush=True)\\\\n    oof = np.full((len(train_ids), len(TARGETS)), np.nan, dtype=float)\\\\n    per_fold: list[float] = []\\\\n    n_splits = int(cfg[\\\\\"validation\\\\\"][\\\\\"n_splits\\\\\"])\\\\n    fold_values = fold_lookup.to_numpy()\\\\n    uid_list = train_ids.tolist()\\\\n    for fold_id in range(n_splits):\\\\n        val_mask = fold_values == fold_id\\\\n        train_mask = ~val_mask\\\\n        train_fold_ids = [\\\\n            uid for uid, keep in zip(uid_list, train_mask, strict=True) if keep\\\\n        ]\\\\n        val_fold_ids = [\\\\n            uid for uid, keep in zip(uid_list, val_mask, strict=True) if keep\\\\n        ]\\\\n        print(\\\\n            f\\\\\"[exp002] fold {fold_id} \\\\\"\\\\n            f\\\\\"train={len(train_fold_ids)} val={len(val_fold_ids)}\\\\\",\\\\n            flush=True,\\\\n        )\\\\n        if not val_fold_ids:\\\\n            per_fold.append(float(\\\\\"nan\\\\\"))\\\\n            continue\\\\n        model = train_slot_cnn(\\\\n            train_fold_ids,\\\\n            train_tensors,\\\\n            y_all[train_mask],\\\\n            seed=FOLD_SEED + fold_id,\\\\n            epochs=IMAGE_EPOCHS,\\\\n            batch_size=IMAGE_BATCH,\\\\n            device=device,\\\\n        )\\\\n        pred = predict_slot_cnn(\\\\n            model,\\\\n            val_fold_ids,\\\\n            train_tensors,\\\\n            batch_size=IMAGE_BATCH,\\\\n            device=device,\\\\n        )\\\\n        oof[val_mask] = pred.to_numpy(dtype=float)\\\\n        gold_val = val_mask & labeled\\\\n        if gold_val.sum() == 0:\\\\n            per_fold.append(float(\\\\\"nan\\\\\"))\\\\n            continue\\\\n        per_fold.append(competition_metric(y_gold[gold_val], oof[gold_val]))\\\\n        print(f\\\\\"[exp002] fold {fold_id} gold_auc={per_fold[-1]:.6f}\\\\\", flush=True)\\\\n\\\\n    gold_oof_mask = labeled & np.isfinite(oof).all(axis=1)\\\\n    if gold_oof_mask.sum() == 0:\\\\n        raise ValueError(\\\\\"No gold OOF predictions were produced\\\\\")\\\\n    cv = competition_metric(y_gold[gold_oof_mask], oof[gold_oof_mask])\\\\n    breakdown = metric_breakdown(y_gold[gold_oof_mask], oof[gold_oof_mask])\\\\n    fold_scores = [score for score in per_fold if score == score]\\\\n    cv_std = float(np.std(fold_scores)) if fold_scores else None\\\\n\\\\n    print(\\\\\"[exp002] fitting full-data model for test\\\\\", flush=True)\\\\n    full_model = train_slot_cnn(\\\\n        uid_list,\\\\n        train_tensors,\\\\n        y_all,\\\\n        seed=FOLD_SEED,\\\\n        epochs=IMAGE_EPOCHS,\\\\n        batch_size=IMAGE_BATCH,\\\\n        device=device,\\\\n    )\\\\n    test_pred = predict_slot_cnn(\\\\n        full_model,\\\\n        test_ids.tolist(),\\\\n        test_tensors,\\\\n        batch_size=IMAGE_BATCH,\\\\n        device=device,\\\\n    )\\\\n    test_pred.index = test_ids.astype(str)\\\\n\\\\n    submission_path = paths.work_dir / \\\\\"submission.csv\\\\\"\\\\n    write_submission(sample, test_pred, submission_path)\\\\n    pd.DataFrame(test_pred).to_csv(experiment_dir / \\\\\"submission.csv\\\\\", index=True)\\\\n    oof_path, test_path = _write_prediction_tables(\\\\n        train_ids=train_ids,\\\\n        fold_lookup=fold_lookup,\\\\n        labeled=labeled,\\\\n        y_gold=y_gold,\\\\n        oof=oof,\\\\n        test_pred=test_pred,\\\\n        experiment_dir=experiment_dir,\\\\n    )\\\\n\\\\n    parent_cv = PARENT_CV.get(\\\\\"exp002\\\\\")\\\\n    delta = None if parent_cv is None else cv - parent_cv\\\\n    keep = cv >= 0.55\\\\n    interpretation = (\\\\n        \\\\\"Six-slot 2.5D CNN beats the metadata reject on gold OOF.\\\\\"\\\\n        if keep\\\\n        else (\\\\n            \\\\\"The from-scratch CNN did not clear 0.55 gold OOF. Keep the submission \\\\\"\\\\n            \\\\\"only as a pixel-pipeline smoke test; next is a stronger encoder or \\\\\"\\\\n            \\\\\"report teacher, not more epochs on this tiny net.\\\\\"\\\\n        )\\\\n    )\\\\n    return {\\\\n        \\\\\"status\\\\\": \\\\\"keep\\\\\" if keep else \\\\\"reject\\\\\",\\\\n        \\\\\"rows\\\\\": len(test_pred),\\\\n        \\\\\"submission_path\\\\\": str(submission_path),\\\\n        \\\\\"submission_sha256\\\\\": file_sha256(submission_path),\\\\n        \\\\\"metric\\\\\": cfg[\\\\\"evaluation\\\\\"][\\\\\"metric\\\\\"],\\\\n        \\\\\"direction\\\\\": cfg[\\\\\"evaluation\\\\\"][\\\\\"direction\\\\\"],\\\\n        \\\\\"fold_version\\\\\": cfg[\\\\\"validation\\\\\"][\\\\\"fold_version\\\\\"],\\\\n        \\\\\"parent\\\\\": \\\\\"exp001\\\\\",\\\\n        \\\\\"hypothesis\\\\\": (\\\\n            \\\\\"A from-scratch 2.5D CNN on six plane x fat-sat slots, trained on \\\\\"\\\\n            \\\\\"report-weak labels, ranks the twelve findings above the metadata reject.\\\\\"\\\\n        ),\\\\n        \\\\\"material_change\\\\\": (\\\\n            \\\\\"SlotCNN, 18 x 128 x 128 (3 slices x 6 slots), 4 epochs, same v1 folds.\\\\\"\\\\n        ),\\\\n        \\\\\"cv\\\\\": cv,\\\\n        \\\\\"cv_std\\\\\": cv_std,\\\\n        \\\\\"delta_vs_parent\\\\\": delta,\\\\n        \\\\\"n_gold_scored\\\\\": int(gold_oof_mask.sum()),\\\\n        \\\\\"per_fold\\\\\": per_fold,\\\\n        \\\\\"per_label\\\\\": {k: v for k, v in breakdown.items() if k != \\\\\"macro\\\\\"},\\\\n        \\\\\"oof_path\\\\\": str(oof_path),\\\\n        \\\\\"test_pred_path\\\\\": str(test_path),\\\\n        \\\\\"seed\\\\\": FOLD_SEED,\\\\n        \\\\\"n_splits\\\\\": N_SPLITS,\\\\n        \\\\\"device\\\\\": str(device),\\\\n        \\\\\"epochs\\\\\": IMAGE_EPOCHS,\\\\n        \\\\\"interpretation\\\\\": interpretation,\\\\n        \\\\\"next_actions\\\\\": [\\\\n            \\\\\"If CV >= 0.55, keep this as the pixel floor and improve the teacher.\\\\\",\\\\n            \\\\\"If CV stays near 0.5, inspect empty-slot rate and DICOM decode failures.\\\\\",\\\\n            \\\\\"Do not attach the 0.941 stack until we own a working image pipeline.\\\\\",\\\\n        ],\\\\n    }\\\\n\\\\n\\\\nif __name__ == \\\\\"__main__\\\\\":\\\\n    print(json.dumps(run(), indent=2))\\\\n\", \"src/report.py\": \"\\\\\"\\\\\"\\\\\"Write an experiment report the next modeling pass can use.\\\\\"\\\\\"\\\\\"\\\\n\\\\nfrom __future__ import annotations\\\\n\\\\nimport hashlib\\\\nimport json\\\\nfrom pathlib import Path\\\\nfrom typing import Any\\\\n\\\\n\\\\ndef file_sha256(path: Path) -> str:\\\\n    digest = hashlib.sha256()\\\\n    with path.open(\\\\\"rb\\\\\") as handle:\\\\n        for chunk in iter(lambda: handle.read(1 << 20), b\\\\\"\\\\\"):\\\\n            digest.update(chunk)\\\\n    return digest.hexdigest()\\\\n\\\\n\\\\ndef format_auc(value: Any) -> str:\\\\n    if isinstance(value, bool) or value is None:\\\\n        return \\\\\"n/a\\\\\"\\\\n    if isinstance(value, int | float):\\\\n        if value != value:  # NaN\\\\n            return \\\\\"n/a\\\\\"\\\\n        return f\\\\\"{float(value):.6f}\\\\\"\\\\n    return str(value)\\\\n\\\\n\\\\ndef write_report(path: Path, payload: dict[str, Any]) -> None:\\\\n    \\\\\"\\\\\"\\\\\"Write REPORT.md plus metrics.json next to it.\\\\\"\\\\\"\\\\\"\\\\n    path.parent.mkdir(parents=True, exist_ok=True)\\\\n    (path.parent / \\\\\"metrics.json\\\\\").write_text(\\\\n        json.dumps(payload, indent=2, default=str),\\\\n        encoding=\\\\\"utf-8\\\\\",\\\\n    )\\\\n    path.write_text(render_report(payload), encoding=\\\\\"utf-8\\\\\")\\\\n\\\\n\\\\ndef render_report(payload: dict[str, Any]) -> str:\\\\n    per_fold = payload.get(\\\\\"per_fold\\\\\") or []\\\\n    fold_lines = (\\\\n        \\\\\"\\\\\\\\n\\\\\".join(\\\\n            f\\\\\"- fold {idx}: {format_auc(score)}\\\\\" for idx, score in enumerate(per_fold)\\\\n        )\\\\n        or \\\\\"- n/a\\\\\"\\\\n    )\\\\n    per_label = payload.get(\\\\\"per_label\\\\\") or {}\\\\n    if per_label:\\\\n        label_lines = \\\\\"\\\\\\\\n\\\\\".join(\\\\n            f\\\\\"- {name}: {format_auc(score)}\\\\\" for name, score in per_label.items()\\\\n        )\\\\n    else:\\\\n        label_lines = \\\\\"- n/a\\\\\"\\\\n    next_actions = payload.get(\\\\\"next_actions\\\\\") or []\\\\n    next_block = (\\\\n        \\\\\"\\\\\\\\n\\\\\".join(f\\\\\"{idx}. {item}\\\\\" for idx, item in enumerate(next_actions, start=1))\\\\n        or \\\\\"1. Inspect gold-label OOF before adding image complexity.\\\\\"\\\\n    )\\\\n    delta = payload.get(\\\\\"delta_vs_parent\\\\\")\\\\n    delta_text = (\\\\n        f\\\\\"{float(delta):+.6f}\\\\\"\\\\n        if isinstance(delta, int | float) and not isinstance(delta, bool)\\\\n        else \\\\\"n/a\\\\\"\\\\n    )\\\\n    return f\\\\\"\\\\\"\\\\\"# Experiment report: {payload.get(\\\\\"experiment_id\\\\\")}\\\\n\\\\nPaste this whole file back into chat after a Kaggle run.\\\\nSubmit `submission.csv` from the notebook output.\\\\n\\\\n## Contract\\\\n\\\\n- Competition: `{payload.get(\\\\\"slug\\\\\")}`\\\\n- Metric: `{payload.get(\\\\\"metric\\\\\")}` ({payload.get(\\\\\"direction\\\\\")})\\\\n- Fold version: `{payload.get(\\\\\"fold_version\\\\\", \\\\\"v1\\\\\")}`\\\\n- Parent: `{payload.get(\\\\\"parent\\\\\", \\\\\"none\\\\\")}`\\\\n- Hypothesis: {payload.get(\\\\\"hypothesis\\\\\", \\\\\"\\\\\")}\\\\n- Material change: {payload.get(\\\\\"material_change\\\\\", \\\\\"\\\\\")}\\\\n\\\\n## Result\\\\n\\\\n- Status: `{payload.get(\\\\\"status\\\\\")}`\\\\n- Gold OOF CV: **{format_auc(payload.get(\\\\\"cv\\\\\"))}**\\\\n- CV std: {payload.get(\\\\\"cv_std\\\\\", \\\\\"n/a\\\\\")}\\\\n- Gold studies scored: {payload.get(\\\\\"n_gold_scored\\\\\", \\\\\"n/a\\\\\")}\\\\n- Delta vs parent: {delta_text}\\\\n- Runtime: `{payload.get(\\\\\"runtime\\\\\")}`\\\\n- Duration sec: `{payload.get(\\\\\"duration_sec\\\\\", \\\\\"n/a\\\\\")}`\\\\n- Submission: `{payload.get(\\\\\"submission_path\\\\\")}`\\\\n- SHA-256: `{payload.get(\\\\\"submission_sha256\\\\\", \\\\\"\\\\\")}`\\\\n\\\\n### Per-fold CV (gold studies only)\\\\n\\\\n{fold_lines}\\\\n\\\\n### Per-finding OOF AUC\\\\n\\\\n{label_lines}\\\\n\\\\n## Interpretation\\\\n\\\\n{payload.get(\\\\\"interpretation\\\\\", \\\\\"\\\\\")}\\\\n\\\\n## Next experiments\\\\n\\\\n{next_block}\\\\n\\\\n## Artifacts\\\\n\\\\n- metrics: `metrics.json`\\\\n- OOF: `{payload.get(\\\\\"oof_path\\\\\", \\\\\"\\\\\")}`\\\\n- test preds: `{payload.get(\\\\\"test_pred_path\\\\\", \\\\\"\\\\\")}`\\\\n\\\\\"\\\\\"\\\\\"\\\\n\", \"src/runtime.py\": \"\\\\\"\\\\\"\\\\\"Detect Kaggle / Colab / local runtimes and resolve portable paths.\\\\\"\\\\\"\\\\\"\\\\n\\\\nfrom __future__ import annotations\\\\n\\\\nimport os\\\\nimport shutil\\\\nimport sys\\\\nfrom collections.abc import Mapping\\\\nfrom dataclasses import dataclass\\\\nfrom pathlib import Path\\\\n\\\\nRuntimeName = str\\\\n\\\\nKAGGLE_INPUT = Path(\\\\\"/kaggle/input\\\\\")\\\\n_DATA_FILENAMES = (\\\\n    \\\\\"train.csv\\\\\",\\\\n    \\\\\"test.csv\\\\\",\\\\n    \\\\\"sample_submission.csv\\\\\",\\\\n    \\\\\"train_series.csv\\\\\",\\\\n)\\\\n_SKIP_WALK_DIRS = frozenset(\\\\n    {\\\\\"train_series\\\\\", \\\\\"test_series\\\\\", \\\\\"train_images\\\\\", \\\\\"test_images\\\\\"}\\\\n)\\\\n\\\\n\\\\n@dataclass(frozen=True)\\\\nclass RuntimePaths:\\\\n    name: RuntimeName\\\\n    slug: str\\\\n    competition_root: Path\\\\n    src_dir: Path\\\\n    data_dir: Path\\\\n    work_dir: Path\\\\n    fold_dir: Path\\\\n\\\\n\\\\ndef detect_runtime(env: Mapping[str, str] | None = None) -> RuntimeName:\\\\n    \\\\\"\\\\\"\\\\\"Return \\'kaggle\\', \\'colab\\', or \\'local\\' from filesystem and environment.\\\\\"\\\\\"\\\\\"\\\\n    environ = os.environ if env is None else env\\\\n    if environ.get(\\\\\"KAGGLE_KERNEL_RUN_TYPE\\\\\") or KAGGLE_INPUT.exists():\\\\n        return \\\\\"kaggle\\\\\"\\\\n    if \\\\\"COLAB_RELEASE_TAG\\\\\" in environ:\\\\n        return \\\\\"colab\\\\\"\\\\n    try:\\\\n        import google.colab  # noqa: F401\\\\n    except ImportError:\\\\n        return \\\\\"local\\\\\"\\\\n    return \\\\\"colab\\\\\"\\\\n\\\\n\\\\ndef find_src_dir(start: Path | None = None) -> Path:\\\\n    \\\\\"\\\\\"\\\\\"Locate the competition `src/` directory that contains pipeline.py.\\\\\"\\\\\"\\\\\"\\\\n    cwd = Path.cwd()\\\\n    candidates = [\\\\n        Path(__file__).resolve().parent,\\\\n        cwd / \\\\\"src\\\\\",\\\\n        cwd.parent / \\\\\"src\\\\\",\\\\n        Path(\\\\\"/kaggle/working/src\\\\\"),\\\\n        Path(\\\\\"/content/src\\\\\"),\\\\n    ]\\\\n    if start is not None:\\\\n        candidates.insert(0, start)\\\\n    if _exists(KAGGLE_INPUT):\\\\n        for child in _iterdir(KAGGLE_INPUT):\\\\n            candidates.append(child / \\\\\"src\\\\\")\\\\n\\\\n    seen: set[Path] = set()\\\\n    for src in candidates:\\\\n        resolved = src.resolve()\\\\n        if resolved in seen:\\\\n            continue\\\\n        seen.add(resolved)\\\\n        if (resolved / \\\\\"pipeline.py\\\\\").is_file():\\\\n            return resolved\\\\n\\\\n    raise FileNotFoundError(\\\\n        \\\\\"Cannot find src/pipeline.py. Clone the repo, or attach the packed \\\\\"\\\\n        \\\\\"Kaggle dataset created by `make pack`.\\\\\"\\\\n    )\\\\n\\\\n\\\\ndef clear_working_dir(work_dir: Path) -> list[str]:\\\\n    \\\\\"\\\\\"\\\\\"Delete previous artifacts so a reused Kaggle notebook starts clean.\\\\\"\\\\\"\\\\\"\\\\n    if not work_dir.exists():\\\\n        return []\\\\n    removed: list[str] = []\\\\n    for child in sorted(work_dir.iterdir()):\\\\n        if child.name.startswith(\\\\\".\\\\\"):\\\\n            continue\\\\n        if child.is_dir():\\\\n            shutil.rmtree(child)\\\\n        else:\\\\n            child.unlink()\\\\n        removed.append(child.name)\\\\n    return removed\\\\n\\\\n\\\\ndef bootstrap_src(start: Path | None = None) -> Path:\\\\n    \\\\\"\\\\\"\\\\\"Put competition src/ on sys.path and return it.\\\\\"\\\\\"\\\\\"\\\\n    src_dir = find_src_dir(start)\\\\n    src_str = str(src_dir)\\\\n    if src_str not in sys.path:\\\\n        sys.path.insert(0, src_str)\\\\n    return src_dir\\\\n\\\\n\\\\ndef _exists(path: Path) -> bool:\\\\n    try:\\\\n        return path.exists()\\\\n    except OSError:\\\\n        return False\\\\n\\\\n\\\\ndef _is_file(path: Path) -> bool:\\\\n    try:\\\\n        return path.is_file()\\\\n    except OSError:\\\\n        return False\\\\n\\\\n\\\\ndef _is_dir(path: Path) -> bool:\\\\n    try:\\\\n        return path.is_dir()\\\\n    except OSError:\\\\n        return False\\\\n\\\\n\\\\ndef _iterdir(path: Path) -> list[Path]:\\\\n    try:\\\\n        return sorted(path.iterdir())\\\\n    except OSError:\\\\n        return []\\\\n\\\\n\\\\ndef _first_existing(paths: list[Path]) -> Path | None:\\\\n    for path in paths:\\\\n        if _exists(path):\\\\n            return path\\\\n    return None\\\\n\\\\n\\\\ndef _first_existing_file(paths: list[Path]) -> Path | None:\\\\n    for path in paths:\\\\n        if _is_file(path):\\\\n            return path\\\\n    return None\\\\n\\\\n\\\\ndef _unique(paths: list[Path]) -> list[Path]:\\\\n    seen: set[Path] = set()\\\\n    out: list[Path] = []\\\\n    for path in paths:\\\\n        if path in seen:\\\\n            continue\\\\n        seen.add(path)\\\\n        out.append(path)\\\\n    return out\\\\n\\\\n\\\\ndef _walk_named_files(root: Path, name: str, max_depth: int = 4) -> list[Path]:\\\\n    \\\\\"\\\\\"\\\\\"Find a basename under root. Avoid Path.glob(\\'**\\') on Kaggle mounts.\\\\\"\\\\\"\\\\\"\\\\n    if not _exists(root):\\\\n        return []\\\\n    found: list[Path] = []\\\\n    stack: list[tuple[Path, int]] = [(root, 0)]\\\\n    while stack:\\\\n        current, depth = stack.pop()\\\\n        for child in _iterdir(current):\\\\n            if child.name == name and _is_file(child):\\\\n                found.append(child)\\\\n            elif (\\\\n                depth < max_depth\\\\n                and _is_dir(child)\\\\n                and child.name not in _SKIP_WALK_DIRS\\\\n            ):\\\\n                stack.append((child, depth + 1))\\\\n    return sorted(found)\\\\n\\\\n\\\\ndef _kaggle_file_candidates(name: str, slug: str, input_root: Path) -> list[Path]:\\\\n    explicit = _unique(\\\\n        [\\\\n            input_root / \\\\\"competitions\\\\\" / slug / name,\\\\n            input_root / slug / name,\\\\n            input_root / name,\\\\n            input_root / \\\\\"competitions\\\\\" / slug / \\\\\"raw\\\\\" / name,\\\\n            input_root / slug / \\\\\"raw\\\\\" / name,\\\\n        ]\\\\n    )\\\\n    found = _first_existing_file(explicit)\\\\n    if found is not None:\\\\n        return explicit\\\\n    return _unique([*explicit, *_walk_named_files(input_root, name)])\\\\n\\\\n\\\\ndef _format_tree(root: Path, max_depth: int = 3, limit: int = 80) -> str:\\\\n    lines: list[str] = []\\\\n\\\\n    def walk(path: Path, depth: int) -> None:\\\\n        if depth > max_depth or len(lines) >= limit:\\\\n            return\\\\n        for child in _iterdir(path):\\\\n            lines.append(f\\\\\"  {\\'  \\' * depth}{child}\\\\\")\\\\n            if _is_dir(child) and child.name not in _SKIP_WALK_DIRS:\\\\n                walk(child, depth + 1)\\\\n\\\\n    walk(root, 0)\\\\n    return \\\\\"\\\\\\\\n\\\\\".join(lines) or \\\\\"  (empty or unreadable)\\\\\"\\\\n\\\\n\\\\ndef _kaggle_data_dir(slug: str, competition_root: Path, input_root: Path) -> Path:\\\\n    for name in _DATA_FILENAMES:\\\\n        found = _first_existing_file(_kaggle_file_candidates(name, slug, input_root))\\\\n        if found is not None:\\\\n            return found.parent\\\\n    return _first_existing(\\\\n        [\\\\n            input_root / \\\\\"competitions\\\\\" / slug,\\\\n            input_root / slug,\\\\n            competition_root / \\\\\"data\\\\\" / \\\\\"raw\\\\\",\\\\n            competition_root / \\\\\"data\\\\\",\\\\n        ]\\\\n    ) or (input_root / \\\\\"competitions\\\\\" / slug)\\\\n\\\\n\\\\ndef resolve_paths(\\\\n    slug: str,\\\\n    *,\\\\n    runtime: RuntimeName | None = None,\\\\n    src_dir: Path | None = None,\\\\n    input_root: Path | None = None,\\\\n) -> RuntimePaths:\\\\n    \\\\\"\\\\\"\\\\\"Resolve data and output directories for the active runtime.\\\\\"\\\\\"\\\\\"\\\\n    runtime = runtime or detect_runtime()\\\\n    src_dir = src_dir or find_src_dir()\\\\n    competition_root = src_dir.parent\\\\n    kaggle_input = input_root if input_root is not None else KAGGLE_INPUT\\\\n\\\\n    if runtime == \\\\\"kaggle\\\\\":\\\\n        data_dir = _kaggle_data_dir(slug, competition_root, kaggle_input)\\\\n        work_dir = Path(\\\\\"/kaggle/working\\\\\")\\\\n        fold_dir = _first_existing(\\\\n            [\\\\n                competition_root / \\\\\"folds\\\\\",\\\\n                Path(\\\\\"/kaggle/working/folds\\\\\"),\\\\n            ]\\\\n        ) or Path(\\\\\"/kaggle/working/folds\\\\\")\\\\n    elif runtime == \\\\\"colab\\\\\":\\\\n        data_dir = _first_existing(\\\\n            [\\\\n                competition_root / \\\\\"data\\\\\" / \\\\\"raw\\\\\",\\\\n                Path(f\\\\\"/content/data/{slug}\\\\\"),\\\\n                Path(\\\\\"/content/data\\\\\"),\\\\n            ]\\\\n        ) or Path(f\\\\\"/content/data/{slug}\\\\\")\\\\n        work_dir = Path(f\\\\\"/content/work/{slug}\\\\\")\\\\n        fold_dir = _first_existing(\\\\n            [competition_root / \\\\\"folds\\\\\", work_dir / \\\\\"folds\\\\\"]\\\\n        ) or (work_dir / \\\\\"folds\\\\\")\\\\n    else:\\\\n        data_dir = competition_root / \\\\\"data\\\\\" / \\\\\"raw\\\\\"\\\\n        work_dir = competition_root / \\\\\"artifacts\\\\\"\\\\n        fold_dir = competition_root / \\\\\"folds\\\\\"\\\\n\\\\n    work_dir.mkdir(parents=True, exist_ok=True)\\\\n    return RuntimePaths(\\\\n        name=runtime,\\\\n        slug=slug,\\\\n        competition_root=competition_root,\\\\n        src_dir=src_dir,\\\\n        data_dir=data_dir,\\\\n        work_dir=work_dir,\\\\n        fold_dir=fold_dir,\\\\n    )\\\\n\\\\n\\\\ndef find_data_file(\\\\n    configured: str,\\\\n    paths: RuntimePaths,\\\\n    *,\\\\n    input_root: Path | None = None,\\\\n) -> Path:\\\\n    \\\\\"\\\\\"\\\\\"Find a competition file across local, Kaggle, and Colab layouts.\\\\\"\\\\\"\\\\\"\\\\n    name = Path(configured).name\\\\n    slug = paths.slug\\\\n    root = input_root if input_root is not None else KAGGLE_INPUT\\\\n    candidates = _unique(\\\\n        [\\\\n            paths.competition_root / configured,\\\\n            paths.data_dir / configured,\\\\n            paths.data_dir / name,\\\\n            paths.data_dir / \\\\\"raw\\\\\" / name,\\\\n            paths.competition_root / \\\\\"data\\\\\" / \\\\\"raw\\\\\" / name,\\\\n            paths.competition_root / \\\\\"data\\\\\" / name,\\\\n            *_kaggle_file_candidates(name, slug, root),\\\\n        ]\\\\n    )\\\\n    found = _first_existing_file(candidates)\\\\n    if found is None:\\\\n        checked = \\\\\"\\\\\\\\n\\\\\".join(f\\\\\"  - {path}\\\\\" for path in candidates)\\\\n        extra = \\\\\"\\\\\"\\\\n        if _exists(root):\\\\n            extra = f\\\\\"\\\\\\\\nUnder {root}:\\\\\\\\n{_format_tree(root)}\\\\\"\\\\n        raise FileNotFoundError(\\\\n            f\\\\\"Could not find \\'{configured}\\' ({name}) for runtime \\'{paths.name}\\'. \\\\\"\\\\n            f\\\\\"Checked:\\\\\\\\n{checked}{extra}\\\\\"\\\\n        )\\\\n    return found\\\\n\\\\n\\\\ndef find_series_root(\\\\n    paths: RuntimePaths,\\\\n    split: str,\\\\n    *,\\\\n    input_root: Path | None = None,\\\\n) -> Path:\\\\n    \\\\\"\\\\\"\\\\\"Locate train_series/ or test_series/ without walking DICOM files.\\\\\"\\\\\"\\\\\"\\\\n    name = f\\\\\"{split}_series\\\\\"\\\\n    root = input_root if input_root is not None else KAGGLE_INPUT\\\\n    candidates = _unique(\\\\n        [\\\\n            paths.data_dir / name,\\\\n            paths.data_dir / \\\\\"raw\\\\\" / name,\\\\n            paths.competition_root / \\\\\"data\\\\\" / \\\\\"raw\\\\\" / name,\\\\n            paths.competition_root / \\\\\"data\\\\\" / name,\\\\n            root / \\\\\"competitions\\\\\" / paths.slug / name,\\\\n            root / paths.slug / name,\\\\n            root / name,\\\\n        ]\\\\n    )\\\\n    found = _first_existing([path for path in candidates if _is_dir(path)])\\\\n    if found is not None:\\\\n        return found\\\\n    if _exists(root):\\\\n        for child in _iterdir(root):\\\\n            if not _is_dir(child) or child.name in _SKIP_WALK_DIRS:\\\\n                continue\\\\n            nested = child / name\\\\n            if _is_dir(nested):\\\\n                return nested\\\\n    checked = \\\\\"\\\\\\\\n\\\\\".join(f\\\\\"  - {path}\\\\\" for path in candidates)\\\\n    raise FileNotFoundError(f\\\\\"Could not find directory \\'{name}\\'. Checked:\\\\\\\\n{checked}\\\\\")\\\\n\"}')\nROOT = Path('/kaggle/working/rsna-knee-abnormality-detection')\nif not Path('/kaggle/working').exists():\n    ROOT = Path.cwd() / '_kaggle_bundle'\nfor relative, content in BUNDLE.items():\n    path = ROOT / relative\n    path.parent.mkdir(parents=True, exist_ok=True)\n    path.write_text(content, encoding='utf-8')\nprint('unpacked', len(BUNDLE), 'files into', ROOT)\n","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\nfrom pathlib import Path\n\nroot = Path('/kaggle/working/rsna-knee-abnormality-detection')\nif not (root / 'src' / 'pipeline.py').is_file():\n    root = Path.cwd() / '_kaggle_bundle'\n    if not (root / 'src' / 'pipeline.py').is_file():\n        root = Path.cwd()\nsrc = str(root / 'src')\nif src in sys.path:\n    sys.path.remove(src)\nsys.path.insert(0, src)\nfor name in (\n    '__init__',\n    'config',\n    'constants',\n    'data',\n    'dicom_io',\n    'features',\n    'folds',\n    'image_train',\n    'labels',\n    'metric',\n    'models',\n    'pipeline',\n    'report',\n    'runtime',\n):\n    sys.modules.pop(name, None)\nprint('src', src)\nkaggle_input = Path('/kaggle/input')\nprint('kaggle_input exists', kaggle_input.exists())\nif kaggle_input.exists():\n    for child in sorted(kaggle_input.iterdir()):\n        print(' ', child.name)\nfrom pipeline import run\n\nresult = run(EXPERIMENT_ID)\nprint('CV', result.get('cv'))\nprint('gold scored', result.get('n_gold_scored'))\nprint('submission', result.get('submission_path'))\n","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\nreport = Path('/kaggle/working/REPORT.md')\nif not report.is_file():\n    report = Path(result['work_dir']) / 'REPORT.md'\ntext = report.read_text(encoding='utf-8')\nprint(text)\nprint('\\n---\\nPaste the report above back into chat.')\nprint('Then submit submission.csv on Kaggle and send the public LB.')\n","metadata":{},"outputs":[],"execution_count":null}]}