File indexing completed on 2026-08-12 08:24:55
0001
0002 """
0003 Example: run dRICH optimization with AID2E optimizer and scheduler.
0004 Each Ax trial is submitted as one scheduler job calling run_trial(),
0005 which then uses DAGExecutor to run drich_eval.py stages and return
0006 metrics. After each batch of trials is completed, the objectives are returned
0007 to Ax and next batch is suggested.
0008 """
0009
0010 import argparse
0011 import json
0012 import shlex
0013
0014 from ax.service.utils.report_utils import exp_to_df
0015
0016 from aid2e.utilities import (
0017 build_optimizer_from_config,
0018 build_scheduler_from_config,
0019 build_workflow_executor_from_config,
0020 )
0021 from aid2e.utilities.configurations import (
0022 BranchDefinition,
0023 JobDefinition,
0024 StageDefinition,
0025 WorkflowDefinition,
0026 )
0027 from drich_utils import load_drich_config, make_paths
0028
0029
0030
0031
0032 def load_trial_config(config_path, output_dir):
0033 """Load config and prepare DAGExecutor paths."""
0034
0035 config_path, cfg, _ = load_drich_config(config_path)
0036 paths = make_paths(output_dir)
0037 return config_path, cfg, paths
0038
0039
0040 def build_trial_workflow(cfg, config_path, paths, trial_index):
0041 """Create the one-trial DAG in the stage order from workflow.yml."""
0042
0043 source_workflow = cfg.workflows.workflows[0]
0044 source_branch = source_workflow.branches[0]
0045 trial_id = str(trial_index)
0046 result_json = paths.results_dir / f"out-{trial_index}.json"
0047 base_payload = {
0048 "trial_index": trial_index,
0049 "output_dir": shlex.quote(str(paths.output_root)),
0050 "config_path": shlex.quote(str(config_path)),
0051 "result_json": str(result_json),
0052 }
0053
0054 def make_stage(source):
0055 source_job = source.jobs[0]
0056 stage_resources = dict(source.scheduler.parameters) if source.scheduler else {}
0057 jobs = [
0058 JobDefinition(
0059 name=source_job.name,
0060 command=source_job.command,
0061 rule=source_job.rule,
0062 payload={**source_job.payload, **base_payload},
0063 resources={**stage_resources, **source_job.resources},
0064 outputs=source_job.outputs,
0065 )
0066 ]
0067 return StageDefinition(
0068 name=source.name,
0069 jobs=jobs,
0070 job_factory=source.job_factory,
0071 scheduler=source.scheduler,
0072 parallelism=source.parallelism,
0073 outputs=source.outputs,
0074 )
0075
0076 return WorkflowDefinition(
0077 name=f"drich_trial_{trial_id}",
0078 stack_type=source_workflow.stack_type,
0079 branches=[
0080 BranchDefinition(
0081 name=source_branch.name,
0082 stages=[make_stage(stage) for stage in source_branch.stages],
0083 scheduler=source_branch.scheduler,
0084 )
0085 ],
0086 scheduler=source_workflow.scheduler,
0087 )
0088
0089
0090 def run_trial(config_path, output_dir, trial_index, design_point):
0091 """Execute one trial workflow and return its objective metrics."""
0092
0093 config_path, cfg, paths = load_trial_config(config_path, output_dir)
0094
0095 workflow = build_trial_workflow(cfg, config_path, paths, trial_index)
0096 executor = build_workflow_executor_from_config(
0097 workflow,
0098 problem_cfg=cfg.problem,
0099 scheduler_cfg=cfg.scheduler,
0100 base_output_dir=str(paths.output_root),
0101 log_level="WARNING",
0102 )
0103 executor.execute(design_point)
0104
0105 result_path = paths.results_dir / f"out-{trial_index}.json"
0106 if not result_path.exists():
0107 raise RuntimeError(f"DAGExecutor did not write objective result file: {result_path}")
0108 return json.loads(result_path.read_text())
0109
0110
0111
0112
0113 def configure_optimizer(run_config, max_trials=None):
0114 """Build the AID2E optimizer."""
0115
0116 cfg = run_config["cfg"]
0117
0118 if max_trials is not None:
0119 cfg.optimizer.parameters = {**cfg.optimizer.parameters, "n_iterations": max_trials}
0120
0121 optimizer = build_optimizer_from_config(cfg.problem, cfg.optimizer)
0122 ax_config = optimizer.config
0123 sobol_trials = min(ax_config.n_initial_samples, ax_config.n_iterations)
0124
0125 optimizer_state = {
0126 "objectives": optimizer.objective_names,
0127 "ax_config": ax_config,
0128 "optimizer": optimizer,
0129 "sobol_trials": sobol_trials,
0130 }
0131 return optimizer_state
0132
0133
0134 def run_trial_batch(assignments, phase_name, batch_id, run_config, optimizer_state, trial_state):
0135 """Submit trial workflow jobs and collect completed metrics."""
0136
0137 output_dir = run_config["output_dir"]
0138 active_trials = {trial_index: design_point for trial_index, design_point in assignments}
0139
0140 job_definitions = []
0141 for trial_index, design_point in assignments:
0142 job_definitions.append(
0143 {
0144 "job_id": str(trial_index),
0145 "name": f"trial_{trial_index}",
0146 "function": run_trial,
0147 "params": {
0148 "config_path": str(run_config["config_path"]),
0149 "output_dir": str(output_dir),
0150 "trial_index": trial_index,
0151 "design_point": design_point,
0152 },
0153 }
0154 )
0155
0156 stage_name = f"{phase_name.lower()}_batch_{batch_id}_trials"
0157
0158
0159 scheduler = build_scheduler_from_config(run_config["cfg"].scheduler)
0160 parallelism_policy = {"max_concurrent": optimizer_state["ax_config"].batch_size}
0161 try:
0162 result = scheduler.run_stage(
0163 stage_name,
0164 job_definitions,
0165 parallelism_policy=parallelism_policy,
0166 working_dir=str(output_dir),
0167 )
0168 finally:
0169 scheduler.shutdown()
0170
0171
0172 if not result.success:
0173 failed_trials = {
0174 int(status.job_id)
0175 for status in result.job_statuses
0176 if status.status != "completed" and str(status.job_id).isdigit()
0177 }
0178 if not failed_trials:
0179 raise RuntimeError(result.error_message or f"stage failed: {stage_name}")
0180 for trial_index in sorted(failed_trials & set(active_trials)):
0181 optimizer_state["optimizer"].set_trial_status(
0182 trial_index=trial_index,
0183 status="failed",
0184 parameters=active_trials[trial_index],
0185 metadata={"reason": result.error_message},
0186 )
0187 trial_state["failed_by_trial"].add(trial_index)
0188 del active_trials[trial_index]
0189
0190 completed_statuses = {
0191 int(status.job_id): status
0192 for status in result.job_statuses
0193 if status.status == "completed" and str(status.job_id).isdigit()
0194 }
0195
0196
0197 batch_results = {}
0198 for trial_index, design_point in active_trials.items():
0199 outputs = completed_statuses[trial_index].outputs or {}
0200 batch_results[trial_index] = (design_point, outputs["result"])
0201 return batch_results
0202
0203
0204
0205
0206 def run_optimization(run_config, optimizer_state):
0207 trial_state = {"errors_by_trial": {}, "failed_by_trial": set()}
0208 trial_index = 0
0209
0210
0211 phases = [
0212 ("Sobol", optimizer_state["sobol_trials"]),
0213 ("Bayes", optimizer_state["ax_config"].n_iterations - optimizer_state["sobol_trials"]),
0214 ]
0215
0216 for phase_name, n_trials in phases:
0217 if n_trials <= 0:
0218 continue
0219 batch_id, completed = 0, 0
0220
0221 while completed < n_trials:
0222 batch_id += 1
0223 n_new = min(optimizer_state["ax_config"].batch_size, n_trials - completed)
0224
0225
0226 design_points = optimizer_state["optimizer"].suggest_candidates(n_candidates=n_new)
0227 assignments = list(zip(range(trial_index, trial_index + n_new), design_points))
0228 trial_index += len(assignments)
0229
0230
0231 batch_results = run_trial_batch(assignments, phase_name, batch_id, run_config, optimizer_state, trial_state)
0232 completed += len(assignments) - len(batch_results)
0233
0234
0235 failed = len(trial_state["failed_by_trial"])
0236 if failed > run_config["max_failed_trials"]:
0237 raise RuntimeError(f"Too many failed trials: {failed} failed, max_failed_trials={run_config['max_failed_trials']}")
0238
0239
0240 for idx, (design_point, raw_metrics) in batch_results.items():
0241 metrics = {name: float(raw_metrics[name]) for name in optimizer_state["objectives"]}
0242 trial_state["errors_by_trial"][idx] = {
0243 name: float(raw_metrics[f"{name}_sem"]) for name in optimizer_state["objectives"]
0244 }
0245 optimizer_state["optimizer"].update_with_results(
0246 idx, design_point, metrics
0247 )
0248 completed += 1
0249
0250
0251 if batch_results:
0252 df = exp_to_df(optimizer_state["optimizer"].experiment)
0253 for name in optimizer_state["objectives"]:
0254 df[f"{name}_sem"] = df["trial_index"].map(
0255 lambda idx: trial_state["errors_by_trial"].get(int(idx), {}).get(name)
0256 )
0257 df.sort_values("trial_index").to_csv(run_config["results_csv"], index=False)
0258
0259 pareto_trials = []
0260 for trial in optimizer_state["optimizer"].get_pareto_front():
0261 pareto_trials.append(
0262 {
0263 "trial_index": trial.index,
0264 "parameters": trial.parameters,
0265 "metrics": trial.metrics,
0266 }
0267 )
0268 run_config["pareto_front_json"].write_text(json.dumps(pareto_trials, indent=2))
0269 optimizer_state["optimizer"].save_optimization_results(run_config["optimization_results_json"])
0270 elif trial_state["failed_by_trial"]:
0271 optimizer_state["optimizer"].save_optimization_results(run_config["optimization_results_json"])
0272
0273
0274
0275
0276 def main(argv=None) -> int:
0277 parser = argparse.ArgumentParser(description="Run example optimization from workflow.yml")
0278 parser.add_argument("--config", default="examples/drich/workflow.yml", help="Path to workflow YAML")
0279 parser.add_argument("--max-trials", type=int, default=None, help="Optional cap on total trials")
0280 args = parser.parse_args(argv)
0281
0282 config_path, cfg, eval_config = load_drich_config(args.config)
0283 paths = make_paths(cfg.problem.output_location)
0284 paths.output_root.mkdir(parents=True, exist_ok=True)
0285 run_config = {
0286 "config_path": config_path,
0287 "cfg": cfg,
0288 "output_dir": paths.output_root,
0289 "max_failed_trials": int(eval_config.get("max_failed_trials", 0)),
0290 "optimization_results_json": paths.output_root / "drich_optimization_results.json",
0291 "pareto_front_json": paths.output_root / f"{eval_config['output_name']}_pareto_front.json",
0292 "results_csv": paths.output_root / f"{eval_config['output_name']}.csv",
0293 }
0294 run_optimization(run_config, configure_optimizer(run_config, max_trials=args.max_trials))
0295 return 0
0296
0297
0298 if __name__ == "__main__":
0299 raise SystemExit(main())