"""mixed design: full factorial on primary factors, latin hypercube on secondary""" import sys sys.path.insert(0, "..") import logging from itertools import product import json import hashlib from pathlib import Path from concurrent.futures import ProcessPoolExecutor import numpy as np from scipy.stats.qmc import LatinHypercube from factors import FACTORS, DEMAND_FUNCTIONS, SEEDS_PER_CONFIG logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger(__name__) LH_SAMPLES = 10 def generate_configs(lh_samples: int = LH_SAMPLES): primary = [f for f in FACTORS if f.primary] secondary = [f for f in FACTORS if not f.primary] primary_grid = list(product(*[f.levels for f in primary])) lhs = LatinHypercube(d=len(secondary), seed=42) configs = [] for p_combo in primary_grid: samples = lhs.random(n=lh_samples) for s in samples: sec_vals = { secondary[i].name: secondary[i].levels[int(s[i] * len(secondary[i].levels))] for i in range(len(secondary)) } base = {primary[i].name: p_combo[i] for i in range(len(primary))} base.update(sec_vals) for seed in range(SEEDS_PER_CONFIG): cfg = {**base, "seed": seed} cfg["id"] = hashlib.md5(json.dumps(cfg, sort_keys=True).encode()).hexdigest()[:8] configs.append(cfg) return configs def run_single(cfg: dict) -> dict: from engine.wrapper import PHANTOM import numpy as np np.random.seed(cfg["seed"]) demand_fn = DEMAND_FUNCTIONS[cfg["demand_fn"]] env = PHANTOM( n_products=cfg["n_products"], alpha=cfg["alpha"], N=cfg["N"], ) env.market.demand = (demand_fn, (cfg["demand_mu"], cfg["demand_sigma"])) obs, _ = env.reset() total_reward, steps = 0.0, 0 for _ in range(100): action = env.action_space.sample() obs, reward, term, trunc, _ = env.step(action) total_reward += reward steps += 1 if term: break env.close() return { "id": cfg["id"], "config": cfg, "total_reward": total_reward, "avg_reward": total_reward / steps, "steps": steps, } def run_study(max_workers: int = None, output: str = "results_mixed.jsonl", lh_samples: int = LH_SAMPLES): configs = generate_configs(lh_samples) n_primary_cells = int(np.prod([len(f.levels) for f in FACTORS if f.primary])) log.info(f"mixed LH: {len(configs)} configs ({n_primary_cells} primary × {lh_samples} LH × {SEEDS_PER_CONFIG} seeds)") results = [] with ProcessPoolExecutor(max_workers=max_workers) as ex: for i, result in enumerate(ex.map(run_single, configs)): results.append(result) if (i+1) % 100 == 0: log.info(f"progress: {i+1}/{len(configs)}") Path(output).write_text("\n".join(json.dumps(r) for r in results)) log.info(f"wrote {len(results)} results to {output}") return results if __name__ == "__main__": import argparse p = argparse.ArgumentParser() p.add_argument("--workers", type=int, default=None) p.add_argument("--output", default="results_mixed.jsonl") p.add_argument("--lh-samples", type=int, default=10) p.add_argument("--dry-run", action="store_true", help="only show design size") args = p.parse_args() primary = [f for f in FACTORS if f.primary] secondary = [f for f in FACTORS if not f.primary] configs = generate_configs(args.lh_samples) log.info(f"design: {len(configs)} runs | primary: {[f.name for f in primary]} | secondary (LH): {[f.name for f in secondary]}") if not args.dry_run: run_study(args.workers, args.output, args.lh_samples)