From ceff8ebd02ce789053e457321a3474ff1b0ab42b Mon Sep 17 00:00:00 2001 From: Kauna <16511995+klei22@users.noreply.github.com> Date: Fri, 29 May 2026 18:59:52 -0700 Subject: [PATCH 1/4] feat(csv-mc): add integer csv multicontext demo --- data/csv_mc_int/README.md | 60 ++++ data/csv_mc_int/get_dataset.sh | 19 ++ data/csv_mc_int/input.csv | 13 + data/csv_mc_int/prepare.py | 1 + .../prepare_csv_integer_multicontext.py | 295 ++++++++++++++++++ data/csv_mc_int/utils | 1 + demos/csv_mc_cone_monitor.py | 199 ++++++++++++ demos/csv_mc_int_demo.sh | 67 ++++ sample.py | 192 +++++++++++- 9 files changed, 842 insertions(+), 5 deletions(-) create mode 100644 data/csv_mc_int/README.md create mode 100755 data/csv_mc_int/get_dataset.sh create mode 100644 data/csv_mc_int/input.csv create mode 120000 data/csv_mc_int/prepare.py create mode 100755 data/csv_mc_int/prepare_csv_integer_multicontext.py create mode 120000 data/csv_mc_int/utils create mode 100755 demos/csv_mc_cone_monitor.py create mode 100755 demos/csv_mc_int_demo.sh diff --git a/data/csv_mc_int/README.md b/data/csv_mc_int/README.md new file mode 100644 index 0000000000..f0cac0daf5 --- /dev/null +++ b/data/csv_mc_int/README.md @@ -0,0 +1,60 @@ +# CSV Integer Multicontext + +`data/csv_mc_int` converts a generic integer CSV into one regular nanoGPT +multicontext dataset per CSV column. It is intended for integer-valued time +series or tabular streams where every column has a known inclusive range. + +Each input value is range checked and encoded as: + +```text +token_id = raw_integer_value - int_min +vocab_size = int_max - int_min + 1 +``` + +That gives every column its own vocabulary while regular `--training_mode +multicontext` learns the columns together. Sampled token IDs are decoded back to +raw integer CSV values by `sample.py` using the metadata saved in each column +folder. + +## Quick start + +```bash +data/csv_mc_int/get_dataset.sh data/csv_mc_int/input.csv \ + --output_root csv_mc_int \ + --range time:0:100 \ + --range temp:0:100 \ + --range pressure:900:1100 +``` + +Outputs are written under `data///`: + +- `train.bin` +- `val.bin` +- `meta.pkl` +- `values.csv` (optional with `--save_values_csv`) + +A `manifest.json` is also written to `data//` with the ordered +`multicontext_datasets` list for training and sampling scripts. + +## Headerless CSV + +Use zero-based generic columns with `--no_header`: + +```bash +data/csv_mc_int/get_dataset.sh readings.csv --no_header \ + --output_root csv_mc_int_readings \ + --range 0:0:1023 \ + --range 1:-100:100 +``` + +Headerless output folders are named `col_0`, `col_1`, and so on. + +## Default ranges + +If every column shares a range, use: + +```bash +data/csv_mc_int/get_dataset.sh readings.csv --default_range 0:65535 +``` + +Per-column `--range` values override `--default_range`. diff --git a/data/csv_mc_int/get_dataset.sh b/data/csv_mc_int/get_dataset.sh new file mode 100755 index 0000000000..7dc3123581 --- /dev/null +++ b/data/csv_mc_int/get_dataset.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Convert a generic integer CSV into per-column multicontext datasets. +# Usage: +# ./get_dataset.sh [input.csv] --range col:int_min:int_max ... +# ./get_dataset.sh [input.csv] --default_range int_min:int_max + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INPUT_CSV="${SCRIPT_DIR}/input.csv" + +if [[ $# -gt 0 && "$1" != -* ]]; then + INPUT_CSV="$1" + shift +fi + +python3 "${SCRIPT_DIR}/prepare_csv_integer_multicontext.py" \ + --input_csv "${INPUT_CSV}" \ + "$@" diff --git a/data/csv_mc_int/input.csv b/data/csv_mc_int/input.csv new file mode 100644 index 0000000000..5da66d3281 --- /dev/null +++ b/data/csv_mc_int/input.csv @@ -0,0 +1,13 @@ +time,temp,pressure +0,20,1000 +1,21,1001 +2,22,1002 +3,21,1003 +4,23,1004 +5,24,1005 +6,23,1006 +7,25,1007 +8,26,1008 +9,25,1009 +10,27,1010 +11,28,1011 diff --git a/data/csv_mc_int/prepare.py b/data/csv_mc_int/prepare.py new file mode 120000 index 0000000000..713f6b0012 --- /dev/null +++ b/data/csv_mc_int/prepare.py @@ -0,0 +1 @@ +../template/prepare.py \ No newline at end of file diff --git a/data/csv_mc_int/prepare_csv_integer_multicontext.py b/data/csv_mc_int/prepare_csv_integer_multicontext.py new file mode 100755 index 0000000000..b0cafb2bfa --- /dev/null +++ b/data/csv_mc_int/prepare_csv_integer_multicontext.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +"""Build regular multicontext integer datasets from CSV columns. + +Every column is range-checked against an inclusive [int_min, int_max] and then +stored as token ids shifted to zero by subtracting int_min. Each column folder +gets an independent vocab_size of int_max - int_min + 1. +""" + +from __future__ import annotations + +import argparse +import csv +import json +import pickle +import re +from array import array +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, Iterable, List, Sequence + + + +@dataclass(frozen=True) +class IntRange: + int_min: int + int_max: int + + @property + def vocab_size(self) -> int: + return self.int_max - self.int_min + 1 + + +def parse_int_range(text: str) -> IntRange: + parts = text.split(":") + if len(parts) != 2: + raise argparse.ArgumentTypeError("Expected :") + int_min, int_max = int(parts[0]), int(parts[1]) + if int_min > int_max: + raise argparse.ArgumentTypeError("int_min must be <= int_max") + return IntRange(int_min, int_max) + + +def parse_column_range(text: str) -> tuple[str, IntRange]: + parts = text.split(":") + if len(parts) != 3: + raise argparse.ArgumentTypeError("Expected ::") + column = parts[0].strip() + if not column: + raise argparse.ArgumentTypeError("Column name/index cannot be empty") + int_min, int_max = int(parts[1]), int(parts[2]) + if int_min > int_max: + raise argparse.ArgumentTypeError("int_min must be <= int_max") + return column, IntRange(int_min, int_max) + + +def clean_context_name(name: str, fallback_idx: int) -> str: + cleaned = re.sub(r"[^A-Za-z0-9_-]+", "_", name.strip()).strip("_") + return cleaned or f"col_{fallback_idx}" + + +def ensure_unique(values: Sequence[str], label: str) -> None: + seen: set[str] = set() + dupes: set[str] = set() + for value in values: + if value in seen: + dupes.add(value) + seen.add(value) + if dupes: + raise ValueError(f"Duplicate {label}: {sorted(dupes)}") + + +def column_aliases(raw_name: str, context_name: str, idx: int) -> set[str]: + return {raw_name, context_name, str(idx), f"col_{idx}", f"column_{idx}", f"column_{idx + 1}"} + + +def read_rows(input_csv: Path, has_header: bool) -> tuple[list[str], list[list[int]]]: + rows: list[list[int]] = [] + with input_csv.open("r", newline="", encoding="utf-8") as f: + reader = csv.reader(f) + try: + first = next(reader) + except StopIteration as exc: + raise ValueError("CSV is empty") from exc + + if has_header: + headers = [cell.strip() for cell in first] + if any(not h for h in headers): + raise ValueError("Header cells must be non-empty") + ensure_unique(headers, "CSV headers") + else: + headers = [f"col_{i}" for i in range(len(first))] + rows.append(parse_int_row(first, 1, len(headers))) + + for row_num, row in enumerate(reader, start=2 if has_header else 2): + if not row or all(cell.strip() == "" for cell in row): + continue + rows.append(parse_int_row(row, row_num, len(headers))) + + if len(headers) == 0: + raise ValueError("CSV must contain at least one column") + if len(rows) < 2: + raise ValueError("CSV must contain at least two data rows") + return headers, rows + + +def parse_int_row(row: Sequence[str], row_num: int, expected_cols: int) -> list[int]: + if len(row) != expected_cols: + raise ValueError(f"Row {row_num} has {len(row)} columns; expected {expected_cols}") + values: list[int] = [] + for col_idx, cell in enumerate(row): + stripped = cell.strip() + if stripped == "": + raise ValueError(f"Empty cell at row {row_num}, column {col_idx}") + try: + values.append(int(stripped, 10)) + except ValueError as exc: + raise ValueError(f"Non-integer cell at row {row_num}, column {col_idx}: {cell!r}") from exc + return values + + +def resolve_ranges( + *, + headers: Sequence[str], + context_names: Sequence[str], + range_specs: Iterable[tuple[str, IntRange]], + default_range: IntRange | None, +) -> list[IntRange]: + ranges: list[IntRange | None] = [None] * len(headers) + alias_to_idx: dict[str, int] = {} + for idx, (header, context_name) in enumerate(zip(headers, context_names)): + for alias in column_aliases(header, context_name, idx): + alias_to_idx[alias] = idx + + for column_key, int_range in range_specs: + if column_key not in alias_to_idx: + raise ValueError( + f"Range specified for unknown column {column_key!r}. " + f"Known columns/aliases include: {sorted(alias_to_idx)[:20]}" + ) + ranges[alias_to_idx[column_key]] = int_range + + resolved: list[IntRange] = [] + missing: list[str] = [] + for idx, maybe_range in enumerate(ranges): + if maybe_range is None: + if default_range is None: + missing.append(headers[idx]) + else: + resolved.append(default_range) + else: + resolved.append(maybe_range) + + if missing: + raise ValueError( + "Missing integer ranges for columns: " + + ", ".join(missing) + + ". Provide --range :: or --default_range :." + ) + return resolved + + +def storage_type_for_vocab(vocab_size: int) -> tuple[str, str]: + return ("I", "uint32") if vocab_size > 65536 else ("H", "uint16") + + +def write_token_file(path: Path, token_ids: Sequence[int], typecode: str) -> None: + values = array(typecode, token_ids) + with path.open("wb") as f: + values.tofile(f) + + +def write_values_csv(path: Path, raw_values: Sequence[int], token_ids: Sequence[int]) -> None: + with path.open("w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(["row_index", "raw_value", "token_id"]) + for idx, (raw, token) in enumerate(zip(raw_values, token_ids)): + writer.writerow([idx, int(raw), int(token)]) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Convert integer CSV columns into regular multicontext datasets.") + parser.add_argument("--input_csv", default="input.csv", help="Input CSV path.") + parser.add_argument("--output_root", default="csv_mc_int", help="Output folder under data/.") + parser.add_argument("--train_ratio", type=float, default=0.9, help="Train split ratio in (0, 1).") + parser.add_argument("--has_header", dest="has_header", default=True, action=argparse.BooleanOptionalAction) + parser.add_argument("--no_header", dest="has_header", action="store_false", help="Treat first row as data and name folders col_0, col_1, ...") + parser.add_argument("--range", dest="ranges", action="append", default=[], type=parse_column_range, help="Repeatable :: range. Columns can be names, col_N, or N.") + parser.add_argument("--default_range", type=parse_int_range, default=None, help="Default : for columns without --range.") + parser.add_argument("--allow_out_of_range", action="store_true", help="Clip out-of-range values instead of failing after printing the fit report.") + parser.add_argument("--save_values_csv", action="store_true", help="Save raw/token pairs in each column folder.") + args = parser.parse_args() + + if not (0.0 < args.train_ratio < 1.0): + raise ValueError("--train_ratio must be in (0, 1)") + + input_csv = Path(args.input_csv).resolve() + if not input_csv.exists(): + raise FileNotFoundError(f"Input CSV not found: {input_csv}") + + headers, rows = read_rows(input_csv, args.has_header) + context_names = [clean_context_name(h, i) for i, h in enumerate(headers)] + ensure_unique(context_names, "sanitized output folder names") + ranges = resolve_ranges(headers=headers, context_names=context_names, range_specs=args.ranges, default_range=args.default_range) + + total_rows = len(rows) + total_cols = len(headers) + columns = [[row[idx] for row in rows] for idx in range(total_cols)] + actual_mins = [min(column) for column in columns] + actual_maxs = [max(column) for column in columns] + fits = [(actual_mins[i] >= r.int_min and actual_maxs[i] <= r.int_max) for i, r in enumerate(ranges)] + + print(f"Input: {input_csv}") + print(f"Header row: {args.has_header}") + print(f"Rows: {total_rows}; columns: {total_cols}") + print("Column range fit report:") + for idx, (header, context_name, int_range, fits_range) in enumerate(zip(headers, context_names, ranges, fits)): + print( + f" [{idx}] folder={context_name!r} column={header!r} " + f"declared=[{int_range.int_min},{int_range.int_max}] " + f"actual=[{int(actual_mins[idx])},{int(actual_maxs[idx])}] " + f"vocab_size={int_range.vocab_size} fits={fits_range}" + ) + + if not all(fits) and not args.allow_out_of_range: + raise ValueError("At least one column has values outside its declared range. Re-run with corrected ranges or --allow_out_of_range.") + + repo_root = Path(__file__).resolve().parents[2] + output_root = repo_root / "data" / args.output_root + output_root.mkdir(parents=True, exist_ok=True) + + train_n = int(total_rows * args.train_ratio) + if train_n <= 0 or train_n >= total_rows: + raise ValueError(f"Invalid split: train={train_n}, total={total_rows}") + + manifest = { + "tokenizer": "csv_integer_range_multicontext_manifest", + "source_csv": str(input_csv), + "has_header": bool(args.has_header), + "rows": int(total_rows), + "train_rows": int(train_n), + "val_rows": int(total_rows - train_n), + "output_root": args.output_root, + "multicontext_datasets": [], + "columns": [], + } + + for idx, (header, context_name, int_range) in enumerate(zip(headers, context_names, ranges)): + context_dir = output_root / context_name + context_dir.mkdir(parents=True, exist_ok=True) + raw_values = columns[idx] + clipped = [min(max(value, int_range.int_min), int_range.int_max) for value in raw_values] + token_ids = [value - int_range.int_min for value in clipped] + typecode, dtype_name = storage_type_for_vocab(int_range.vocab_size) + write_token_file(context_dir / "train.bin", token_ids[:train_n], typecode) + write_token_file(context_dir / "val.bin", token_ids[train_n:], typecode) + if args.save_values_csv: + write_values_csv(context_dir / "values.csv", raw_values, token_ids) + + dataset_name = f"{args.output_root}/{context_name}" + meta = { + "tokenizer": "csv_integer_range", + "vocab_size": int(int_range.vocab_size), + "source_csv": str(input_csv), + "source_column": header, + "context_name": context_name, + "column_index": int(idx), + "has_header": bool(args.has_header), + "int_min": int(int_range.int_min), + "int_max": int(int_range.int_max), + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": int(actual_mins[idx]), + "actual_int_max": int(actual_maxs[idx]), + "fits_declared_range": bool(fits[idx]), + "dtype": dtype_name, + "samples": int(total_rows), + "train_ratio": float(args.train_ratio), + } + with (context_dir / "meta.pkl").open("wb") as f: + pickle.dump(meta, f) + + manifest["multicontext_datasets"].append(dataset_name) + manifest["columns"].append(meta) + + with (output_root / "manifest.json").open("w", encoding="utf-8") as f: + json.dump(manifest, f, indent=2) + + print(f"Output root: {output_root}") + print("Multicontext datasets:") + for dataset in manifest["multicontext_datasets"]: + print(f" {dataset}") + + +if __name__ == "__main__": + main() diff --git a/data/csv_mc_int/utils b/data/csv_mc_int/utils new file mode 120000 index 0000000000..ea6a0ddd72 --- /dev/null +++ b/data/csv_mc_int/utils @@ -0,0 +1 @@ +../template/utils \ No newline at end of file diff --git a/demos/csv_mc_cone_monitor.py b/demos/csv_mc_cone_monitor.py new file mode 100755 index 0000000000..e77d041a09 --- /dev/null +++ b/demos/csv_mc_cone_monitor.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +"""Monitor a CSV stream, sample a cone of multicontext continuations, and plot it. + +The script waits until complete rows are appended to a target CSV file. On each +change it writes a stable prompt snapshot, runs sample.py with +--multicontext_csv_input and --num_samples , then updates a Plotly +HTML graph that overlays observed rows and all sampled futures per column. +""" + +from __future__ import annotations + +import argparse +import csv +import json +import subprocess +import sys +import time +from datetime import datetime +from pathlib import Path +from typing import Iterable, List + + +def read_complete_text(path: Path) -> str: + data = path.read_bytes() + if not data: + return "" + text = data.decode("utf-8") + if not text.endswith(("\n", "\r")): + last_newline = max(text.rfind("\n"), text.rfind("\r")) + text = "" if last_newline < 0 else text[: last_newline + 1] + return text + + +def count_data_rows(csv_text: str, has_header: bool) -> int: + rows = [row for row in csv.reader(csv_text.splitlines()) if row] + if has_header and rows: + rows = rows[1:] + return len(rows) + + +def load_manifest_datasets(manifest_path: Path) -> List[str]: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + datasets = manifest.get("multicontext_datasets") + if not isinstance(datasets, list) or not datasets: + raise ValueError(f"manifest has no multicontext_datasets: {manifest_path}") + return [str(dataset) for dataset in datasets] + + +def read_numeric_csv(path: Path) -> tuple[list[str], list[list[float]]]: + with path.open("r", newline="", encoding="utf-8") as f: + reader = csv.reader(f) + header = next(reader) + rows = [[float(cell) for cell in row] for row in reader if row] + return header, rows + + +def write_cone_plot(prompt_csv: Path, sample_csvs: Iterable[Path], output_html: Path) -> None: + try: + import plotly.graph_objects as go + from plotly.subplots import make_subplots + except ImportError as exc: + raise RuntimeError("plotly is required for cone plotting; install with `pip install plotly`.") from exc + + headers, observed = read_numeric_csv(prompt_csv) + sample_paths = list(sample_csvs) + fig = make_subplots( + rows=len(headers), + cols=1, + shared_xaxes=True, + vertical_spacing=0.03, + subplot_titles=headers, + ) + + observed_x = list(range(len(observed))) + for col_idx, header in enumerate(headers, start=1): + observed_y = [row[col_idx - 1] for row in observed] + fig.add_trace( + go.Scatter( + x=observed_x, + y=observed_y, + mode="lines+markers", + name=f"observed:{header}", + legendgroup="observed", + line=dict(width=3), + showlegend=(col_idx == 1), + ), + row=col_idx, + col=1, + ) + + for sample_idx, sample_path in enumerate(sample_paths, start=1): + _, sample_rows = read_numeric_csv(sample_path) + y_vals = [row[col_idx - 1] for row in sample_rows] + x_vals = list(range(len(y_vals))) + fig.add_trace( + go.Scatter( + x=x_vals, + y=y_vals, + mode="lines", + name=f"cone_{sample_idx}", + legendgroup=f"cone_{sample_idx}", + opacity=0.45, + showlegend=(col_idx == 1), + ), + row=col_idx, + col=1, + ) + fig.update_yaxes(title_text=header, row=col_idx, col=1) + + fig.update_xaxes(title_text="row index", row=len(headers), col=1) + fig.update_layout( + title=f"CSV multicontext cone updated {datetime.now().isoformat(timespec='seconds')}", + template="plotly_white", + height=max(350 * len(headers), 500), + ) + output_html.parent.mkdir(parents=True, exist_ok=True) + fig.write_html(str(output_html), include_plotlyjs="cdn") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Monitor CSV rows and update multicontext prediction cone plot.") + parser.add_argument("--target_csv", required=True, help="CSV file to monitor for complete appended rows.") + parser.add_argument("--manifest", default="data/csv_mc_int/manifest.json", help="Prepared dataset manifest.json.") + parser.add_argument("--out_dir", default="out/csv_mc_int", help="Checkpoint directory passed to sample.py.") + parser.add_argument("--work_dir", default="out/csv_mc_int/cone_monitor", help="Snapshots, samples, and plot output directory.") + parser.add_argument("--cone_width", type=int, default=5, help="Number of sampled futures per update.") + parser.add_argument("--max_new_tokens", type=int, default=32, help="Rows to generate beyond the prompt.") + parser.add_argument("--poll_seconds", type=float, default=2.0, help="Polling interval.") + parser.add_argument("--iterations", type=int, default=0, help="Maximum updates; 0 means run forever.") + parser.add_argument("--device", default="cpu") + parser.add_argument("--dtype", default="float32", choices=["float32", "float16", "bfloat16"]) + parser.add_argument("--no_header", dest="has_header", action="store_false", help="Target CSV has no header row.") + args = parser.parse_args() + + target_csv = Path(args.target_csv) + manifest_path = Path(args.manifest) + work_dir = Path(args.work_dir) + snapshots_dir = work_dir / "snapshots" + samples_dir = work_dir / "samples" + plot_path = work_dir / "cone.html" + datasets = load_manifest_datasets(manifest_path) + + last_rows = -1 + updates = 0 + print(f"Monitoring {target_csv} for complete rows. Plot: {plot_path}") + while True: + complete_text = read_complete_text(target_csv) + row_count = count_data_rows(complete_text, args.has_header) + if row_count > last_rows and row_count > 0: + stamp = datetime.now().strftime("%Y%m%d_%H%M%S") + snapshot = snapshots_dir / f"prompt_{stamp}.csv" + snapshot.parent.mkdir(parents=True, exist_ok=True) + snapshot.write_text(complete_text, encoding="utf-8") + + run_samples_dir = samples_dir / stamp + cmd = [ + sys.executable, + "sample.py", + "--out_dir", + args.out_dir, + "--device", + args.device, + "--dtype", + args.dtype, + "--no-compile", + "--multicontext", + "--multicontext_datasets", + *datasets, + "--multicontext_csv_input", + str(snapshot), + "--multicontext_csv_output_dir", + str(run_samples_dir), + "--max_new_tokens", + str(args.max_new_tokens), + "--top_k", + "1", + "--num_samples", + str(args.cone_width), + "--no-print_model_info", + ] + if not args.has_header: + cmd.append("--no-multicontext_csv_has_header") + + print("Running:", " ".join(cmd)) + subprocess.run(cmd, check=True) + sample_csvs = sorted(run_samples_dir.glob("*.csv")) + write_cone_plot(snapshot, sample_csvs, plot_path) + print(f"Updated {plot_path} with {len(sample_csvs)} cone samples from {row_count} observed rows.") + + last_rows = row_count + updates += 1 + if args.iterations and updates >= args.iterations: + break + + time.sleep(args.poll_seconds) + + +if __name__ == "__main__": + main() diff --git a/demos/csv_mc_int_demo.sh b/demos/csv_mc_int_demo.sh new file mode 100755 index 0000000000..2619d1c70a --- /dev/null +++ b/demos/csv_mc_int_demo.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Generic integer CSV regular multicontext demo: +# 1) split a CSV into one integer-range dataset per column +# 2) train regular multicontext with a separate vocabulary per column +# 3) sample from a CSV prompt and write timestamped CSV continuations + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +CSV_INPUT="${1:-data/csv_mc_int/input.csv}" +OUTPUT_ROOT="${CSV_MC_OUTPUT_ROOT:-csv_mc_int}" +OUT_DIR="${CSV_MC_OUT_DIR:-out/csv_mc_int}" +MAX_ITERS="${CSV_MC_MAX_ITERS:-200}" + +# Override or extend these ranges for your own CSV. Every value is checked +# before any binary dataset files are written. +data/csv_mc_int/get_dataset.sh "$CSV_INPUT" \ + --output_root "$OUTPUT_ROOT" \ + --train_ratio 0.8 \ + --range time:0:10000 \ + --range temp:-100:200 \ + --range pressure:800:1200 \ + --save_values_csv + +mapfile -t DATASETS < <(python3 - < None: processor.encode_with_reserved_tokens(text, encode_bytes, emit_reserved) return ids +def _read_multicontext_csv_prompt(csv_path: str, has_header: bool) -> tuple[List[str], List[List[str]]]: + """Read a complete CSV prompt and return headers plus column-oriented values.""" + path = Path(csv_path) + if not path.exists(): + raise FileNotFoundError(f"CSV prompt not found: {path}") + + with path.open("r", newline="", encoding="utf-8") as f: + reader = csv.reader(f) + try: + first = next(reader) + except StopIteration as exc: + raise ValueError(f"CSV prompt is empty: {path}") from exc + + rows: List[List[str]] = [] + if has_header: + headers = [cell.strip() for cell in first] + if any(not header for header in headers): + raise ValueError("CSV prompt header cells must be non-empty") + else: + headers = [f"col_{idx}" for idx in range(len(first))] + rows.append([cell.strip() for cell in first]) + + for row_num, row in enumerate(reader, start=2): + if not row or all(cell.strip() == "" for cell in row): + continue + if len(row) != len(headers): + raise ValueError(f"CSV prompt row {row_num} has {len(row)} columns; expected {len(headers)}") + rows.append([cell.strip() for cell in row]) + + if not rows: + raise ValueError(f"CSV prompt has no data rows: {path}") + + columns = [[row[col_idx] for row in rows] for col_idx in range(len(headers))] + return headers, columns + + +def _csv_column_for_dataset( + *, + dataset_name: str, + dataset_index: int, + meta: Dict[str, object], + headers: Sequence[str], + columns: Sequence[Sequence[str]], +) -> List[str]: + aliases = [ + str(meta.get('source_column', '')), + str(meta.get('context_name', '')), + Path(dataset_name).name, + str(meta.get('column_index', '')), + f"col_{meta.get('column_index', dataset_index)}", + ] + header_to_idx = {header: idx for idx, header in enumerate(headers)} + for alias in aliases: + if alias in header_to_idx: + return list(columns[header_to_idx[alias]]) + if dataset_index < len(columns): + return list(columns[dataset_index]) + raise ValueError(f"CSV prompt has no column for dataset {dataset_name!r}") + + +def _decoded_csv_values(token_ids: Sequence[int], decode_fn: Callable[[Sequence[int]], str]) -> List[str]: + decoded = decode_fn(list(token_ids)) + if decoded.strip() == "": + return [] + return [piece.strip() for piece in decoded.split(',')] + + +def _write_multicontext_csv_sample( + *, + output_path: Union[str, Path], + dataset_names: Sequence[str], + dataset_meta: Dict[str, Dict[str, object]], + decode_lookup: Dict[str, Callable[[Sequence[int]], str]], + token_state: Dict[str, torch.Tensor], + prompt_lengths: Dict[str, int], + include_prompt: bool, +) -> Path: + path = Path(output_path) + path.parent.mkdir(parents=True, exist_ok=True) + + headers = [ + str(dataset_meta[name].get('source_column') or dataset_meta[name].get('context_name') or Path(name).name) + for name in dataset_names + ] + decoded_columns: List[List[str]] = [] + for name in dataset_names: + token_ids = token_state[name][0].detach().cpu().tolist() + if not include_prompt: + token_ids = token_ids[prompt_lengths.get(name, 0):] + decoded_columns.append(_decoded_csv_values(token_ids, decode_lookup[name])) + + lengths = {len(col) for col in decoded_columns} + if len(lengths) != 1: + raise ValueError(f"Decoded multicontext columns have mismatched lengths: {sorted(lengths)}") + + with path.open("w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(headers) + for row in zip(*decoded_columns): + writer.writerow(row) + return path + + def get_tokenizer_functions(meta): """Get encode/decode functions based on tokenizer metadata""" if 'tokenizer' not in meta: @@ -1304,6 +1418,30 @@ def decode(token_ids): return encode, decode + if meta['tokenizer'] == 'csv_integer_range': + int_min = int(meta.get('int_min', 0)) + int_max = int(meta.get('int_max', int_min + int(meta.get('vocab_size', 1)) - 1)) + + def encode(text): + text = text.strip() + if not text: + return [] + values = [] + for piece in text.replace(',', ' ').split(): + raw_value = int(piece) + if raw_value < int_min or raw_value > int_max: + raise ValueError( + f"CSV integer value {raw_value} outside [{int_min}, {int_max}] " + f"for column {meta.get('source_column', meta.get('context_name', 'unknown'))}" + ) + values.append(raw_value - int_min) + return values + + def decode(token_ids): + return ','.join(str(int(token_id) + int_min) for token_id in token_ids) + + return encode, decode + if meta['tokenizer'] in {'sinewave', 'sinewave_fp16_bits', 'csv_fp16_bits', 'csv_quantized_int'}: def encode(text): text = text.strip() @@ -1455,11 +1593,12 @@ def main(): args.multicontext and args.multicontext_start is None and args.multicontext_start_files is None + and args.multicontext_csv_input is None and args.multicontext_datasets ): args.multicontext_start = [args.start] * len(args.multicontext_datasets) - if args.multicontext and args.multicontext_start_files is not None: + if args.multicontext and (args.multicontext_start_files is not None or args.multicontext_csv_input is not None): # Avoid single-context encode path when multicontext starts come from raw token files. start_ids = [0] else: @@ -1563,11 +1702,24 @@ def main(): elif args.multicontext: if not args.multicontext_datasets: raise ValueError("Must specify --multicontext_datasets when using --multicontext") - if args.multicontext_start is None and args.multicontext_start_files is None: - raise ValueError("Must specify --multicontext_start or --multicontext_start_files when using --multicontext") + if args.multicontext_start is None and args.multicontext_start_files is None and args.multicontext_csv_input is None: + raise ValueError("Must specify --multicontext_start, --multicontext_start_files, or --multicontext_csv_input when using --multicontext") dataset_names = list(args.multicontext_datasets) - if args.multicontext_start_files is not None: + csv_headers = None + csv_columns = None + if args.multicontext_csv_input is not None: + if args.multicontext_start_files is not None or args.multicontext_start is not None: + raise ValueError( + "Use only one of --multicontext_csv_input, --multicontext_start_files, or --multicontext_start." + ) + csv_headers, csv_columns = _read_multicontext_csv_prompt( + args.multicontext_csv_input, + args.multicontext_csv_has_header, + ) + start_strings = None + start_files = None + elif args.multicontext_start_files is not None: if len(args.multicontext_datasets) != len(args.multicontext_start_files): raise ValueError( "Number of --multicontext_datasets must match number of --multicontext_start_files." @@ -1595,7 +1747,16 @@ def main(): encode_i, decode_i = get_tokenizer_functions(dataset_meta[dataset_name]) - if start_files is not None: + if csv_columns is not None and csv_headers is not None: + csv_values = _csv_column_for_dataset( + dataset_name=dataset_name, + dataset_index=i, + meta=dataset_meta[dataset_name], + headers=csv_headers, + columns=csv_columns, + ) + token_ids = encode_i(",".join(csv_values)) + elif start_files is not None: start_file = start_files[i] if not os.path.exists(start_file): raise FileNotFoundError(f"start file not found: {start_file}") @@ -1720,6 +1881,27 @@ def main(): for name, text in output_dict.items(): file.write(f"\n{name}: \n{text}\n") + if args.multicontext_csv_input is not None: + if args.multicontext_csv_output_file is not None: + if args.num_samples != 1: + raise ValueError("--multicontext_csv_output_file requires --num_samples 1") + csv_output_path = Path(args.multicontext_csv_output_file) + else: + csv_output_dir = Path(args.multicontext_csv_output_dir or Path(args.out_dir) / "csv_samples") + input_stem = Path(args.multicontext_csv_input).stem + csv_output_path = csv_output_dir / f"{input_stem}_sample{sample_idx + 1}_{timestamp}.csv" + + written_csv = _write_multicontext_csv_sample( + output_path=csv_output_path, + dataset_names=dataset_names, + dataset_meta=dataset_meta, + decode_lookup=decode_lookup, + token_state=token_state, + prompt_lengths=prompt_lengths, + include_prompt=args.multicontext_csv_output_include_prompt, + ) + print(f"Saved multicontext CSV sample to: {written_csv}") + if args.numerical_multicontext_plotly: plot_output = write_plotly_report( output_path=args.numerical_multicontext_plotly_file, From 014b8d0f6bc82a6b1e70ccc1cdbefdf5854e6f2a Mon Sep 17 00:00:00 2001 From: klei22 Date: Sat, 30 May 2026 00:33:19 -0700 Subject: [PATCH 2/4] Add roomba viewer --- data/roomba/roomba_grayscale_viewer.html | 794 +++++++++++++++++++++++ 1 file changed, 794 insertions(+) create mode 100644 data/roomba/roomba_grayscale_viewer.html diff --git a/data/roomba/roomba_grayscale_viewer.html b/data/roomba/roomba_grayscale_viewer.html new file mode 100644 index 0000000000..87f559e66c --- /dev/null +++ b/data/roomba/roomba_grayscale_viewer.html @@ -0,0 +1,794 @@ + + + + + + Roomba Pose + Grayscale Viewer + + + +

Roomba Top-Down Pose + Grayscale Viewer

+ +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ Default p* layout is normal image order: p0 at top-left, then p1 to the right, wrapping by width to the next row. + For your p0..p8 sample, use width 3 and height 3. Timestamp and distance deltas use forward modulo wrapping. +
+
+ +
+ + + + +
+ +
+
+

top-down robot pose

+ +
+
+ +
+

p* grayscale video

+ +
+
+
+ + + + From 7ce6673e3a4eefe49280b8b45c67c4155600044e Mon Sep 17 00:00:00 2001 From: klei22 Date: Sat, 30 May 2026 00:36:16 -0700 Subject: [PATCH 3/4] Add template --- data/csv_mc_int/input.csv | 19 +- data/csv_mc_int/manifest.json | 373 ++++++++++++++++++++ data/csv_mc_int/roomba_data_conditioning.py | 48 +++ data/csv_mc_int/run_roomba_dataset.sh | 47 +++ demos/csv_mc_int_demo.sh | 50 +-- 5 files changed, 501 insertions(+), 36 deletions(-) create mode 100644 data/csv_mc_int/manifest.json create mode 100644 data/csv_mc_int/roomba_data_conditioning.py create mode 100644 data/csv_mc_int/run_roomba_dataset.sh diff --git a/data/csv_mc_int/input.csv b/data/csv_mc_int/input.csv index 5da66d3281..9c1c45e086 100644 --- a/data/csv_mc_int/input.csv +++ b/data/csv_mc_int/input.csv @@ -1,13 +1,6 @@ -time,temp,pressure -0,20,1000 -1,21,1001 -2,22,1002 -3,21,1003 -4,23,1004 -5,24,1005 -6,23,1006 -7,25,1007 -8,26,1008 -9,25,1009 -10,27,1010 -11,28,1011 +timestamp,action,bump_left,bump_right,wheel_dropped,total_distance_mm,heading_deg,speed_mm_s,battery_percent,p0,p1,p2,p3,p4,p5,p6,p7,p8 +1780004702,stop,0,0,0,3272,156,0,67.4,66,79,71,68,74,65,55,63,51 +1780004702,stop,0,0,0,3272,156,0,67.4,66,79,71,68,74,65,55,63,51 +1780004702,stop,0,0,0,3272,156,0,67.4,66,79,72,68,74,65,55,63,51 +1780004702,stop,0,0,0,3272,156,0,67.4,70,87,75,72,82,69,57,68,52 +1780004702,stop,0,0,0,3272,156,0,67.4,78,97,84,80,91,77,63,76,58 diff --git a/data/csv_mc_int/manifest.json b/data/csv_mc_int/manifest.json new file mode 100644 index 0000000000..b573ea562c --- /dev/null +++ b/data/csv_mc_int/manifest.json @@ -0,0 +1,373 @@ +{ + "tokenizer": "csv_integer_range_multicontext_manifest", + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "has_header": true, + "rows": 88226, + "train_rows": 79403, + "val_rows": 8823, + "output_root": "csv_mc_int", + "multicontext_datasets": [ + "csv_mc_int/timestamp", + "csv_mc_int/action", + "csv_mc_int/bump_left", + "csv_mc_int/bump_right", + "csv_mc_int/wheel_dropped", + "csv_mc_int/total_distance_mm", + "csv_mc_int/heading_deg", + "csv_mc_int/speed_mm_s", + "csv_mc_int/battery_percent", + "csv_mc_int/p0", + "csv_mc_int/p1", + "csv_mc_int/p2", + "csv_mc_int/p3", + "csv_mc_int/p4", + "csv_mc_int/p5", + "csv_mc_int/p6", + "csv_mc_int/p7", + "csv_mc_int/p8" + ], + "columns": [ + { + "tokenizer": "csv_integer_range", + "vocab_size": 10, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "timestamp", + "context_name": "timestamp", + "column_index": 0, + "has_header": true, + "int_min": 0, + "int_max": 9, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 0, + "actual_int_max": 9, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 6, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "action", + "context_name": "action", + "column_index": 1, + "has_header": true, + "int_min": 0, + "int_max": 5, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 0, + "actual_int_max": 5, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 2, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "bump_left", + "context_name": "bump_left", + "column_index": 2, + "has_header": true, + "int_min": 0, + "int_max": 1, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 0, + "actual_int_max": 1, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 2, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "bump_right", + "context_name": "bump_right", + "column_index": 3, + "has_header": true, + "int_min": 0, + "int_max": 1, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 0, + "actual_int_max": 1, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 2, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "wheel_dropped", + "context_name": "wheel_dropped", + "column_index": 4, + "has_header": true, + "int_min": 0, + "int_max": 1, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 0, + "actual_int_max": 1, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 1000, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "total_distance_mm", + "context_name": "total_distance_mm", + "column_index": 5, + "has_header": true, + "int_min": 0, + "int_max": 999, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 0, + "actual_int_max": 999, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 359, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "heading_deg", + "context_name": "heading_deg", + "column_index": 6, + "has_header": true, + "int_min": 0, + "int_max": 358, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 0, + "actual_int_max": 358, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 637, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "speed_mm_s", + "context_name": "speed_mm_s", + "column_index": 7, + "has_header": true, + "int_min": -254, + "int_max": 382, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": -254, + "actual_int_max": 382, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 1001, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "battery_percent", + "context_name": "battery_percent", + "column_index": 8, + "has_header": true, + "int_min": 0, + "int_max": 1000, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 626, + "actual_int_max": 674, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 256, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "p0", + "context_name": "p0", + "column_index": 9, + "has_header": true, + "int_min": 0, + "int_max": 255, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 32, + "actual_int_max": 204, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 256, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "p1", + "context_name": "p1", + "column_index": 10, + "has_header": true, + "int_min": 0, + "int_max": 255, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 38, + "actual_int_max": 196, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 256, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "p2", + "context_name": "p2", + "column_index": 11, + "has_header": true, + "int_min": 0, + "int_max": 255, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 22, + "actual_int_max": 167, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 256, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "p3", + "context_name": "p3", + "column_index": 12, + "has_header": true, + "int_min": 0, + "int_max": 255, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 40, + "actual_int_max": 252, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 256, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "p4", + "context_name": "p4", + "column_index": 13, + "has_header": true, + "int_min": 0, + "int_max": 255, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 62, + "actual_int_max": 244, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 256, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "p5", + "context_name": "p5", + "column_index": 14, + "has_header": true, + "int_min": 0, + "int_max": 255, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 32, + "actual_int_max": 197, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 256, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "p6", + "context_name": "p6", + "column_index": 15, + "has_header": true, + "int_min": 0, + "int_max": 255, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 32, + "actual_int_max": 255, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 256, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "p7", + "context_name": "p7", + "column_index": 16, + "has_header": true, + "int_min": 0, + "int_max": 255, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 51, + "actual_int_max": 252, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + }, + { + "tokenizer": "csv_integer_range", + "vocab_size": 256, + "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", + "source_column": "p8", + "context_name": "p8", + "column_index": 17, + "has_header": true, + "int_min": 0, + "int_max": 255, + "value_encoding": "token_id = raw_integer_value - int_min", + "value_decoding": "raw_integer_value = token_id + int_min", + "actual_int_min": 51, + "actual_int_max": 225, + "fits_declared_range": true, + "dtype": "uint16", + "samples": 88226, + "train_ratio": 0.9 + } + ] +} \ No newline at end of file diff --git a/data/csv_mc_int/roomba_data_conditioning.py b/data/csv_mc_int/roomba_data_conditioning.py new file mode 100644 index 0000000000..08d85280a7 --- /dev/null +++ b/data/csv_mc_int/roomba_data_conditioning.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +import argparse +import pandas as pd +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--input_csv", default="input.csv") + parser.add_argument("--output_csv", default="roomba_integer.csv") + parser.add_argument("--mapping_csv", default="action_mapping.csv") + args = parser.parse_args() + + df = pd.read_csv(args.input_csv) + + # 1. Encode action column as integers + df["action"], action_categories = pd.factorize(df["action"]) + + mapping = pd.DataFrame({ + "id": range(len(action_categories)), + "action": action_categories, + }) + mapping.to_csv(args.mapping_csv, index=False) + + # 2. Battery percent: multiply by 10 + df["battery_percent"] = (df["battery_percent"] * 10).round().astype(int) + + # 3. total_distance_mm modulo 1000 + df["total_distance_mm"] = df["total_distance_mm"].astype(int) % 1000 + + # 4. timestamp modulo 10 + df["timestamp"] = df["timestamp"].astype(int) % 10 + + # 5. Make all remaining numeric columns integer-safe + for col in df.columns: + if col != "action": + df[col] = pd.to_numeric(df[col], errors="raise") + df[col] = df[col].round().astype(int) + + df.to_csv(args.output_csv, index=False) + + print(f"Wrote conditioned CSV: {args.output_csv}") + print(f"Wrote action mapping: {args.mapping_csv}") + + +if __name__ == "__main__": + main() diff --git a/data/csv_mc_int/run_roomba_dataset.sh b/data/csv_mc_int/run_roomba_dataset.sh new file mode 100644 index 0000000000..75362de455 --- /dev/null +++ b/data/csv_mc_int/run_roomba_dataset.sh @@ -0,0 +1,47 @@ + +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +INPUT_RAW="${1:-${SCRIPT_DIR}/input.csv}" +CONDITIONED_CSV="${SCRIPT_DIR}/roomba_integer.csv" + +python3 "${SCRIPT_DIR}/roomba_data_conditioning.py" \ + --input_csv "${INPUT_RAW}" \ + --output_csv "${CONDITIONED_CSV}" \ + --mapping_csv "${SCRIPT_DIR}/action_mapping.csv" + +RANGE_ARGS=$(python3 - < Date: Sat, 30 May 2026 19:39:24 +0100 Subject: [PATCH 4/4] Delete data/csv_mc_int/manifest.json --- data/csv_mc_int/manifest.json | 373 ---------------------------------- 1 file changed, 373 deletions(-) delete mode 100644 data/csv_mc_int/manifest.json diff --git a/data/csv_mc_int/manifest.json b/data/csv_mc_int/manifest.json deleted file mode 100644 index b573ea562c..0000000000 --- a/data/csv_mc_int/manifest.json +++ /dev/null @@ -1,373 +0,0 @@ -{ - "tokenizer": "csv_integer_range_multicontext_manifest", - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "has_header": true, - "rows": 88226, - "train_rows": 79403, - "val_rows": 8823, - "output_root": "csv_mc_int", - "multicontext_datasets": [ - "csv_mc_int/timestamp", - "csv_mc_int/action", - "csv_mc_int/bump_left", - "csv_mc_int/bump_right", - "csv_mc_int/wheel_dropped", - "csv_mc_int/total_distance_mm", - "csv_mc_int/heading_deg", - "csv_mc_int/speed_mm_s", - "csv_mc_int/battery_percent", - "csv_mc_int/p0", - "csv_mc_int/p1", - "csv_mc_int/p2", - "csv_mc_int/p3", - "csv_mc_int/p4", - "csv_mc_int/p5", - "csv_mc_int/p6", - "csv_mc_int/p7", - "csv_mc_int/p8" - ], - "columns": [ - { - "tokenizer": "csv_integer_range", - "vocab_size": 10, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "timestamp", - "context_name": "timestamp", - "column_index": 0, - "has_header": true, - "int_min": 0, - "int_max": 9, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 0, - "actual_int_max": 9, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 6, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "action", - "context_name": "action", - "column_index": 1, - "has_header": true, - "int_min": 0, - "int_max": 5, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 0, - "actual_int_max": 5, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 2, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "bump_left", - "context_name": "bump_left", - "column_index": 2, - "has_header": true, - "int_min": 0, - "int_max": 1, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 0, - "actual_int_max": 1, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 2, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "bump_right", - "context_name": "bump_right", - "column_index": 3, - "has_header": true, - "int_min": 0, - "int_max": 1, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 0, - "actual_int_max": 1, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 2, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "wheel_dropped", - "context_name": "wheel_dropped", - "column_index": 4, - "has_header": true, - "int_min": 0, - "int_max": 1, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 0, - "actual_int_max": 1, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 1000, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "total_distance_mm", - "context_name": "total_distance_mm", - "column_index": 5, - "has_header": true, - "int_min": 0, - "int_max": 999, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 0, - "actual_int_max": 999, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 359, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "heading_deg", - "context_name": "heading_deg", - "column_index": 6, - "has_header": true, - "int_min": 0, - "int_max": 358, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 0, - "actual_int_max": 358, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 637, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "speed_mm_s", - "context_name": "speed_mm_s", - "column_index": 7, - "has_header": true, - "int_min": -254, - "int_max": 382, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": -254, - "actual_int_max": 382, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 1001, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "battery_percent", - "context_name": "battery_percent", - "column_index": 8, - "has_header": true, - "int_min": 0, - "int_max": 1000, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 626, - "actual_int_max": 674, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 256, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "p0", - "context_name": "p0", - "column_index": 9, - "has_header": true, - "int_min": 0, - "int_max": 255, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 32, - "actual_int_max": 204, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 256, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "p1", - "context_name": "p1", - "column_index": 10, - "has_header": true, - "int_min": 0, - "int_max": 255, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 38, - "actual_int_max": 196, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 256, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "p2", - "context_name": "p2", - "column_index": 11, - "has_header": true, - "int_min": 0, - "int_max": 255, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 22, - "actual_int_max": 167, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 256, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "p3", - "context_name": "p3", - "column_index": 12, - "has_header": true, - "int_min": 0, - "int_max": 255, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 40, - "actual_int_max": 252, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 256, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "p4", - "context_name": "p4", - "column_index": 13, - "has_header": true, - "int_min": 0, - "int_max": 255, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 62, - "actual_int_max": 244, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 256, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "p5", - "context_name": "p5", - "column_index": 14, - "has_header": true, - "int_min": 0, - "int_max": 255, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 32, - "actual_int_max": 197, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 256, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "p6", - "context_name": "p6", - "column_index": 15, - "has_header": true, - "int_min": 0, - "int_max": 255, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 32, - "actual_int_max": 255, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 256, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "p7", - "context_name": "p7", - "column_index": 16, - "has_header": true, - "int_min": 0, - "int_max": 255, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 51, - "actual_int_max": 252, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - }, - { - "tokenizer": "csv_integer_range", - "vocab_size": 256, - "source_csv": "/home/kauna/nanogpt_csv_generic_mc_forecast/data/csv_mc_int/roomba_integer.csv", - "source_column": "p8", - "context_name": "p8", - "column_index": 17, - "has_header": true, - "int_min": 0, - "int_max": 255, - "value_encoding": "token_id = raw_integer_value - int_min", - "value_decoding": "raw_integer_value = token_id + int_min", - "actual_int_min": 51, - "actual_int_max": 225, - "fits_declared_range": true, - "dtype": "uint16", - "samples": 88226, - "train_ratio": 0.9 - } - ] -} \ No newline at end of file