""" Experiment result tracking and comparison table generation. Logs results from all tokenizer × domain × model × mode × split combinations and generates CSV / LaTeX tables for the paper. """ import csv import json import os from collections import defaultdict class ExperimentTracker: """ Track and store experimental results across all configurations. Results are stored in a nested dict: results[domain][tokenizer][model][mode][split] = metrics_dict """ def __init__(self, output_dir: str = "results"): self.output_dir = output_dir os.makedirs(output_dir, exist_ok=True) self.results: dict = defaultdict( lambda: defaultdict( lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(dict))) ) ) def log_result( self, domain: str, tokenizer: str, model: str, mode: str, split: str, metrics: dict, ) -> None: """ Log a single experimental result. Args: domain: e.g. 'blocks', 'gripper' tokenizer: e.g. 'wl', 'simhash', 'shortest_path', 'graphbpe', 'random' model: e.g. 'lstm', 'xgboost' mode: 'state' or 'delta' split: 'validation', 'test-interpolation', 'test-extrapolation' metrics: dict with keys like 'solved_rate', 'exec_rate', 'vocab_size' """ self.results[domain][tokenizer][model][mode][split] = metrics print( f" Logged: {domain}/{tokenizer}/{model}/{mode}/{split} → " f"{metrics.get('solved_rate', 'N/A')}" ) def generate_comparison_table(self) -> str: """ Generate a comparison table across all methods. Returns: CSV string """ rows = [] header = [ "Domain", "Tokenizer", "Model", "Mode", "Val Solved", "Interp Solved", "Extrap Solved", "Val Exec", "Interp Exec", "Extrap Exec", "Vocab Size", ] rows.append(header) for domain in sorted(self.results.keys()): for tokenizer in sorted(self.results[domain].keys()): for model in sorted(self.results[domain][tokenizer].keys()): for mode in sorted( self.results[domain][tokenizer][model].keys() ): splits = self.results[domain][tokenizer][model][mode] row = [domain, tokenizer, model, mode] for split in [ "validation", "test-interpolation", "test-extrapolation", ]: m = splits.get(split, {}) row.append(f"{m.get('solved_rate', 0):.2%}") for split in [ "validation", "test-interpolation", "test-extrapolation", ]: m = splits.get(split, {}) row.append(f"{m.get('exec_rate', 0):.2%}") # Vocab size (same for all splits) any_m = next(iter(splits.values()), {}) row.append(str(any_m.get("vocab_size", "N/A"))) rows.append(row) # Write CSV csv_path = os.path.join(self.output_dir, "tokenization_comparison.csv") with open(csv_path, "w", newline="") as f: writer = csv.writer(f) writer.writerows(rows) print(f"Saved comparison table to {csv_path}") # Generate LaTeX self._generate_latex_table(rows) return csv_path def _generate_latex_table(self, rows: list[list]) -> None: """Generate LaTeX table from rows.""" header = rows[0] data = rows[1:] n_cols = len(header) col_spec = "l" * n_cols lines = [ r"\begin{table}[ht]", r"\centering", r"\caption{Comparison of tokenization strategies}", r"\label{tab:tokenization_comparison}", f"\\begin{{tabular}}{{{col_spec}}}", r"\toprule", " & ".join(header) + r" \\", r"\midrule", ] for row in data: lines.append(" & ".join(str(x) for x in row) + r" \\") lines.extend( [ r"\bottomrule", r"\end{tabular}", r"\end{table}", ] ) tex_path = os.path.join(self.output_dir, "tokenization_comparison.tex") with open(tex_path, "w") as f: f.write("\n".join(lines)) print(f"Saved LaTeX table to {tex_path}") def save_results(self, filepath: str | None = None) -> None: """Save all results to JSON.""" if filepath is None: filepath = os.path.join(self.output_dir, "full_results.json") # Convert defaultdicts to regular dicts for JSON serialization def to_dict(d): if isinstance(d, defaultdict): return {k: to_dict(v) for k, v in d.items()} return d with open(filepath, "w") as f: json.dump(to_dict(self.results), f, indent=2) print(f"Saved full results to {filepath}") def load_results(self, filepath: str) -> None: """Load results from JSON.""" with open(filepath, "r") as f: data = json.load(f) # Populate results for domain, tok_data in data.items(): for tok, model_data in tok_data.items(): for model, mode_data in model_data.items(): for mode, split_data in mode_data.items(): for split, metrics in split_data.items(): self.results[domain][tok][model][mode][split] = metrics def generate_vocabulary_stats(self) -> None: """Generate vocabulary size stats per tokenizer per domain.""" stats = {} for domain in self.results: for tok in self.results[domain]: for model in self.results[domain][tok]: for mode in self.results[domain][tok][model]: for split, m in self.results[domain][tok][model][ mode ].items(): if "vocab_size" in m: stats.setdefault(tok, {})[domain] = m["vocab_size"] stats_path = os.path.join(self.output_dir, "vocabulary_stats.json") with open(stats_path, "w") as f: json.dump(stats, f, indent=2) print(f"Saved vocabulary stats to {stats_path}")