#!/usr/bin/env python3 """scan_experiments.py - inventory of the machine-learning experiments in a project (stdlib only). python scan_experiments.py [ROOT] [--out paper/inventory] Walks ROOT (default '.'), skipping paper/, .git, venvs, node_modules, caches, and classifies: code .py / .ipynb files that import an ML framework; entry points (__main__, argparse, hydra, click), CLI arguments with defaults, seeds, datasets, models, optimizers, metrics configs .yaml/.yml/.json/.toml files that look like hyperparameter configs results .csv/.tsv/.json/.jsonl result tables, *.log files with metric lines, TensorBoard event files, wandb/ and mlruns/ folders, checkpoints (only counted) docs README*, *.md, requirements.txt, environment.yml, pyproject.toml (dependencies) Writes .json (full detail) and .md (human-readable), prints a compact summary. The script only reads; nothing is executed. """ import ast, json, re, sys from collections import Counter from pathlib import Path SKIP = {"paper", ".git", "__pycache__", ".venv", "venv", "env", "node_modules", ".ipynb_checkpoints", ".pytest_cache", ".mypy_cache", ".ruff_cache", "dist", "build", "site-packages", ".idea", ".vscode"} ML_LIBS = {"torch": "PyTorch", "tensorflow": "TensorFlow", "keras": "Keras", "jax": "JAX", "flax": "Flax", "sklearn": "scikit-learn", "xgboost": "XGBoost", "lightgbm": "LightGBM", "catboost": "CatBoost", "transformers": "Hugging Face Transformers", "datasets": "Hugging Face Datasets", "lightning": "PyTorch Lightning", "pytorch_lightning": "PyTorch Lightning", "torchvision": "torchvision", "timm": "timm", "peft": "PEFT", "trl": "TRL", "accelerate": "Accelerate", "optuna": "Optuna", "statsmodels": "statsmodels", "gymnasium": "Gymnasium", "gym": "Gym", "stable_baselines3": "Stable-Baselines3", "torch_geometric": "PyG", "dgl": "DGL", "sentence_transformers": "Sentence-Transformers", "wandb": "Weights & Biases", "mlflow": "MLflow", "numpy": "NumPy", "pandas": "pandas", "scipy": "SciPy"} CORE_ML = set(ML_LIBS) - {"numpy", "pandas", "scipy", "wandb", "mlflow", "optuna"} DATASETS = r"""MNIST|Fashion-?MNIST|KMNIST|EMNIST|CIFAR-?10|CIFAR-?100|SVHN|STL-?10|ImageNet(?:-1k|-21k)?|Tiny-?ImageNet| CelebA|LSUN|COCO|MS-?COCO|Pascal\s?VOC|Cityscapes|ADE20K|Places365|Caltech-?101|Caltech-?256|Oxford-?Pets|Flowers-?102|Food-?101| IMDB|SST-?2|GLUE|SuperGLUE|SQuAD|MNLI|QNLI|QQP|RTE|CoLA|AG-?News|Yelp|Amazon|WikiText-?\d*|Penn\s?Treebank|PTB|OpenWebText| C4|The\s?Pile|LAMBADA|HellaSwag|MMLU|GSM8K|HumanEval|MBPP|ARC|TruthfulQA|WMT\d*|Multi30k|LibriSpeech|Common\s?Voice| UCI|Adult|Iris|Wine|Breast\s?Cancer|California\s?Housing|Boston|Covertype|Higgs|Titanic|OpenML| Cora|CiteSeer|PubMed|OGB[\w-]*|QM9|ZINC|MovieLens|Criteo|ETTh\d|ETTm\d|Electricity|Traffic|Weather|M4| CartPole|MountainCar|Atari|MuJoCo|HalfCheetah|Hopper|Walker2d|Ant|Humanoid|Procgen|D4RL""" DATASET_RE = re.compile(r"(? lines[0].count(",") else "," return {"kind": "table", "columns": [c.strip() for c in lines[0].split(sep)][:40], "rows": len(lines) - 1, "head": lines[1:4]} if p.suffix == ".jsonl": lines = text.splitlines() try: first = json.loads(lines[0]) except (json.JSONDecodeError, IndexError): return None return {"kind": "jsonl", "keys": list(first)[:40] if isinstance(first, dict) else None, "rows": len(lines), "head": lines[:2]} if p.suffix == ".json": try: js = json.loads(text) except json.JSONDecodeError: return None def shape(x, d=0): if d > 2: return "..." if isinstance(x, dict): return {k: shape(v, d + 1) for k, v in list(x.items())[:15]} if isinstance(x, list): return [f"list[{len(x)}]", shape(x[0], d + 1) if x else None] return type(x).__name__ if not isinstance(x, (int, float)) else x return {"kind": "json", "shape": shape(js)} return None def is_config(text, suffix): keys = set(re.findall(r"(?m)^\s*[\"']?([A-Za-z_][\w.-]*)[\"']?\s*[:=]", text)) return len({k.lower().split(".")[-1] for k in keys} & HP_KEYS) >= 2 def scan(root): inv = {"root": str(root.resolve()), "code": [], "notebooks": [], "configs": [], "results": [], "logs": [], "tensorboard": [], "trackers": [], "checkpoints": Counter(), "docs": [], "figures": [], "libraries": Counter(), "datasets": Counter(), "models": Counter(), "optimizers": Counter(), "metrics": Counter()} for d in ("wandb", "mlruns", "lightning_logs", "runs", "outputs", "multirun"): for p in root.rglob(d): if p.is_dir() and not (set(p.relative_to(root).parts) & SKIP): inv["trackers"].append(str(p.relative_to(root))) for p in walk(root): rel, suf, name = str(p.relative_to(root)), p.suffix.lower(), p.name.lower() if suf in (".pt", ".pth", ".ckpt", ".safetensors", ".h5", ".keras", ".pkl", ".joblib", ".onnx", ".bin", ".npz", ".npy"): inv["checkpoints"][suf] += 1; continue if "tfevents" in name: inv["tensorboard"].append(rel); continue if suf in (".png", ".jpg", ".jpeg", ".pdf", ".svg"): inv["figures"].append(rel); continue if name.startswith("readme") or name in ("requirements.txt", "environment.yml", "pyproject.toml", "setup.py", "agents.md") or suf == ".md": text = read(p, 20_000) inv["docs"].append({"path": rel, "first_lines": [l for l in text.splitlines() if l.strip()][:6]}) inv["datasets"].update(m.group(1) for m in DATASET_RE.finditer(text)) inv["models"].update(MODEL_RE.findall(text)) continue if suf in (".py", ".ipynb"): text = read(p) outs = [] if suf == ".ipynb": text, outs = nb_source(text) info = py_info(text, rel) libs = sorted(i for i in info["imports"] if i in ML_LIBS) inv["libraries"].update(ML_LIBS[l] for l in libs) ds = Counter(m.group(1) for m in DATASET_RE.finditer(text)) ds.update(":".join(x for x in m.groups() if x) for m in HF_DS_RE.finditer(text)) models = Counter(MODEL_RE.findall(text)) optims = Counter(OPTIM_RE.findall(text)) metrics = Counter(m.lower() for m in METRIC_RE.findall(text)) inv["datasets"].update(ds); inv["models"].update(models); inv["optimizers"].update(optims) if not (set(libs) & CORE_ML) and not ds and not info["entry_point"]: continue # plain helper without ML content inv["metrics"].update(metrics) entry = {"path": rel, "lines": text.count("\n") + 1, "ml_libraries": [ML_LIBS[l] for l in libs], "entry_point": info["entry_point"], "cli_args": info["cli_args"][:40], "seeds": info["seeds"], "datasets": sorted(ds), "models": sorted(models), "optimizers": sorted(optims), "metrics": sorted(metrics), "functions": info["functions"][:30], "classes": info["classes"][:20]} if outs: entry["metric_outputs"] = outs inv["notebooks" if suf == ".ipynb" else "code"].append(entry) continue if suf in (".yaml", ".yml", ".toml", ".cfg", ".ini") or (suf == ".json" and not RESULT_HINT.search(rel)): text = read(p, 200_000) if is_config(text, suf): inv["configs"].append({"path": rel, "preview": [l for l in text.splitlines() if l.strip()][:25]}) inv["datasets"].update(m.group(1) for m in DATASET_RE.finditer(text)) continue if suf != ".json": continue if suf in (".csv", ".tsv", ".json", ".jsonl"): text = read(p) ti = table_info(p, text) if ti: cols = " ".join(ti.get("columns") or ti.get("keys") or [str(ti.get("shape"))[:500]]) inv["metrics"].update(m.lower() for m in METRIC_RE.findall(cols)) inv["results"].append({"path": rel, "bytes": p.stat().st_size, **ti}) continue if suf in (".log", ".txt", ".out", ".err"): text = read(p) hits = LOG_METRIC_RE.findall(text) if hits: inv["logs"].append({"path": rel, "metric_lines": len(hits), "metrics": sorted({h[0].lower() for h in hits})[:20], "last": [l for l in text.splitlines() if LOG_METRIC_RE.search(l)][-3:]}) for k in ("libraries", "datasets", "models", "optimizers", "metrics", "checkpoints"): inv[k] = dict(inv[k].most_common(40)) return inv def to_md(inv): L = [f"# Experiment inventory\n\nRoot: `{inv['root']}`\n"] for k in ("libraries", "datasets", "models", "optimizers", "metrics"): L.append(f"- **{k}**: " + (", ".join(f"{a} ({n})" for a, n in inv[k].items()) or "-")) L.append(f"- **checkpoints**: {inv['checkpoints'] or '-'} | **tensorboard files**: {len(inv['tensorboard'])}" f" | **trackers**: {', '.join(inv['trackers']) or '-'}\n") L.append("## Code (entry points first)\n") for c in sorted(inv["code"] + inv["notebooks"], key=lambda c: (not c["entry_point"], c["path"])): L.append(f"### `{c['path']}` ({c['lines']} lines{', ENTRY POINT' if c['entry_point'] else ''})") L.append(f"libs: {', '.join(c['ml_libraries']) or '-'}; datasets: {', '.join(c['datasets']) or '-'}; " f"models: {', '.join(c['models']) or '-'}; optimizers: {', '.join(c['optimizers']) or '-'}; " f"metrics: {', '.join(c['metrics']) or '-'}; seeds: {', '.join(c['seeds']) or '-'}") if c["cli_args"]: L.append("args: " + "; ".join(f"{a['flag']}={a.get('default', '?')}" for a in c["cli_args"])) if c["classes"]: L.append("classes: " + ", ".join(c["classes"])) if c["functions"]: L.append("functions: " + ", ".join(c["functions"])) for o in c.get("metric_outputs", []): L.append(f" output> {o[:200]}") L.append("") L.append("## Configs\n") for c in inv["configs"]: L.append(f"### `{c['path']}`\n```\n" + "\n".join(c["preview"]) + "\n```") L.append("\n## Result files\n") for r in inv["results"]: desc = (f"columns={r['columns']}, rows={r['rows']}" if r["kind"] == "table" else f"keys={r['keys']}, rows={r['rows']}" if r["kind"] == "jsonl" else f"shape={json.dumps(r['shape'])[:400]}") L.append(f"- `{r['path']}` ({r['kind']}, {r['bytes']} B): {desc}") L.append("\n## Logs with metrics\n") for g in inv["logs"]: L.append(f"- `{g['path']}`: {g['metric_lines']} metric lines, {g['metrics']}; last: {g['last'][-1][:150] if g['last'] else ''}") if inv["tensorboard"]: L.append("\n## TensorBoard event files\n" + "\n".join(f"- `{t}`" for t in inv["tensorboard"][:50])) if inv["figures"]: L.append("\n## Existing figures (not reused blindly - regenerate from data)\n" + "\n".join(f"- `{f}`" for f in inv["figures"][:50])) L.append("\n## Docs\n") for d in inv["docs"]: L.append(f"- `{d['path']}`: " + " / ".join(d["first_lines"])[:300]) return "\n".join(L) + "\n" def main(argv): if "-h" in argv or "--help" in argv: print(__doc__); return 0 out = Path(argv[argv.index("--out") + 1]) if "--out" in argv else Path("paper/inventory") pos = [a for i, a in enumerate(argv) if not a.startswith("--") and (i == 0 or argv[i - 1] != "--out")] root = Path(pos[0] if pos else ".") inv = scan(root) out.parent.mkdir(parents=True, exist_ok=True) out.with_suffix(".json").write_text(json.dumps(inv, indent=1, ensure_ascii=False, default=list), encoding="utf-8") out.with_suffix(".md").write_text(to_md(inv), encoding="utf-8") entries = [c["path"] for c in inv["code"] + inv["notebooks"] if c["entry_point"]] print(f"wrote {out}.md and {out}.json") print(f"code files with ML content: {len(inv['code'])} (+{len(inv['notebooks'])} notebooks); entry points: {', '.join(entries[:12]) or '-'}") print(f"configs: {len(inv['configs'])}; result files: {len(inv['results'])}; logs with metrics: {len(inv['logs'])}; " f"tensorboard: {len(inv['tensorboard'])}; trackers: {', '.join(inv['trackers']) or '-'}") for k in ("libraries", "datasets", "models", "metrics"): print(f"{k}: " + (", ".join(list(inv[k])[:12]) or "-")) if not inv["results"] and not inv["logs"] and not inv["tensorboard"]: print("NOTE: no result files found - the experiments may not have been run yet.") return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))