Download launch.py from haodongcui/HaM_World: direct link, hf CLI and curl.
- Browser
- Download file 15.4 kB
-
https://huggingface.co/haodongcui/HaM_World/resolve/main/launch.py
- Command line
-
hf download hf://haodongcui/HaM_World/launch.py
-
curl -L -o launch.py https://huggingface.co/haodongcui/HaM_World/resolve/main/launch.py
15.4 kB
| from __future__ import annotations | |
| import argparse | |
| import copy | |
| import importlib | |
| import json | |
| import sys | |
| from pathlib import Path | |
| from typing import Any | |
| import yaml | |
| FINAL_ROOT = Path(__file__).resolve().parent | |
| CODE_ROOTS = [ | |
| FINAL_ROOT, | |
| FINAL_ROOT / "baselines", | |
| ] | |
| for root in reversed(CODE_ROOTS): | |
| root_str = str(root) | |
| if root_str not in sys.path: | |
| sys.path.insert(0, root_str) | |
| ALGORITHM_SPECS = { | |
| "hamworld": { | |
| "label": "HaM-World", | |
| "package": "hamworld", | |
| "default_config": FINAL_ROOT / "hamworld" / "configs" / "compare_dmcontrol.yaml", | |
| "preset_configs": { | |
| "compare_dmcontrol": FINAL_ROOT / "hamworld" / "configs" / "compare_dmcontrol.yaml", | |
| "finger_reacher": FINAL_ROOT / "hamworld" / "configs" / "finger_reacher.yaml", | |
| }, | |
| }, | |
| "dreamerv3": { | |
| "label": "DreamerV3", | |
| "package": "dreamerv3", | |
| "default_config": FINAL_ROOT / "baselines" / "dreamerv3" / "configs" / "compare_dmcontrol.yaml", | |
| "preset_configs": { | |
| "compare_dmcontrol": FINAL_ROOT / "baselines" / "dreamerv3" / "configs" / "compare_dmcontrol.yaml", | |
| "finger_reacher": FINAL_ROOT / "baselines" / "dreamerv3" / "configs" / "finger_reacher.yaml", | |
| }, | |
| }, | |
| "tdmpc2": { | |
| "label": "TD-MPC2", | |
| "package": "tdmpc2", | |
| "default_config": FINAL_ROOT / "baselines" / "tdmpc2" / "configs" / "compare_dmcontrol.yaml", | |
| "preset_configs": { | |
| "compare_dmcontrol": FINAL_ROOT / "baselines" / "tdmpc2" / "configs" / "compare_dmcontrol.yaml", | |
| "finger_reacher": FINAL_ROOT / "baselines" / "tdmpc2" / "configs" / "finger_reacher.yaml", | |
| }, | |
| }, | |
| "ppo": { | |
| "label": "PPO", | |
| "package": "ppo", | |
| "default_config": FINAL_ROOT / "baselines" / "ppo" / "configs" / "low_budget_compare_dmcontrol.yaml", | |
| "preset_configs": { | |
| "compare_dmcontrol": FINAL_ROOT / "baselines" / "ppo" / "configs" / "low_budget_compare_dmcontrol.yaml", | |
| "finger_reacher": FINAL_ROOT / "baselines" / "ppo" / "configs" / "low_budget_finger_reacher.yaml", | |
| }, | |
| }, | |
| "sac": { | |
| "label": "SAC", | |
| "package": "sac", | |
| "default_config": FINAL_ROOT / "baselines" / "sac" / "configs" / "low_budget_compare_dmcontrol.yaml", | |
| "preset_configs": { | |
| "compare_dmcontrol": FINAL_ROOT / "baselines" / "sac" / "configs" / "low_budget_compare_dmcontrol.yaml", | |
| "finger_reacher": FINAL_ROOT / "baselines" / "sac" / "configs" / "low_budget_finger_reacher.yaml", | |
| }, | |
| }, | |
| } | |
| DEFAULT_ALL_ALGORITHMS = ["hamworld", "dreamerv3", "tdmpc2", "ppo", "sac"] | |
| PRESETS = { | |
| "compare_dmcontrol": ["cartpole_swingup", "cheetah_run"], | |
| "finger_reacher": ["finger_spin", "reacher_easy"], | |
| } | |
| def resolve_repo_path(raw_path: str) -> Path: | |
| path = Path(raw_path).expanduser() | |
| if not path.is_absolute(): | |
| path = (FINAL_ROOT / path).resolve() | |
| return path | |
| def load_config(path: Path) -> dict[str, Any]: | |
| with path.open("r", encoding="utf-8") as handle: | |
| data = yaml.safe_load(handle) | |
| if not isinstance(data, dict): | |
| raise ValueError(f"Config at {path} must decode to a dictionary.") | |
| data.setdefault("_config_path", str(path)) | |
| return data | |
| def _coerce_value(raw: str) -> Any: | |
| lowered = raw.lower() | |
| if lowered == "true": | |
| return True | |
| if lowered == "false": | |
| return False | |
| if lowered == "none": | |
| return None | |
| try: | |
| return json.loads(raw) | |
| except json.JSONDecodeError: | |
| pass | |
| try: | |
| if "." in raw: | |
| return float(raw) | |
| return int(raw) | |
| except ValueError: | |
| return raw | |
| def _set_by_dotted_path(config: dict[str, Any], dotted_key: str, value: Any) -> None: | |
| parts = dotted_key.split(".") | |
| node = config | |
| for part in parts[:-1]: | |
| if part not in node or not isinstance(node[part], dict): | |
| node[part] = {} | |
| node = node[part] | |
| node[parts[-1]] = value | |
| def apply_overrides(config: dict[str, Any], overrides: list[str]) -> dict[str, Any]: | |
| updated = copy.deepcopy(config) | |
| for override in overrides: | |
| if "=" not in override: | |
| raise ValueError(f"Invalid override '{override}'. Expected key=value.") | |
| dotted_key, raw_value = override.split("=", 1) | |
| _set_by_dotted_path(updated, dotted_key, _coerce_value(raw_value)) | |
| return updated | |
| def expand_task_runs(config: dict[str, Any], tasks: list[str]) -> list[dict[str, Any]]: | |
| configured_tasks = config.get("benchmark", {}).get("tasks", []) | |
| task_map = {task.get("id"): task for task in configured_tasks} | |
| missing = [task_id for task_id in tasks if task_id not in task_map] | |
| if missing: | |
| available = ", ".join(sorted(task_map)) if task_map else "<none>" | |
| raise ValueError(f"Task(s) not found in {config['_config_path']}: {', '.join(missing)}. Available: {available}.") | |
| runs: list[dict[str, Any]] = [] | |
| for task_id in tasks: | |
| run_config = copy.deepcopy(config) | |
| run_config["task"] = copy.deepcopy(task_map[task_id]) | |
| run_config["experiment"] = copy.deepcopy(config.get("experiment", {})) | |
| base_name = run_config["experiment"].get("name", "experiment") | |
| run_config["experiment"]["resolved_name"] = f"{base_name}_{task_id}" | |
| runs.append(run_config) | |
| return runs | |
| def normalize_tasks(tasks: list[str]) -> list[str]: | |
| deduped: list[str] = [] | |
| seen: set[str] = set() | |
| for task in tasks: | |
| if task not in seen: | |
| seen.add(task) | |
| deduped.append(task) | |
| return deduped | |
| def select_tasks(args: argparse.Namespace) -> list[str]: | |
| if getattr(args, "tasks", None): | |
| return normalize_tasks(args.tasks) | |
| return PRESETS[args.preset] | |
| def config_path_for_algorithm(algorithm: str, args: argparse.Namespace) -> Path: | |
| override_key = f"{algorithm}_config".replace("-", "_") | |
| override = getattr(args, override_key, None) | |
| if override: | |
| return resolve_repo_path(override) | |
| preset_name = getattr(args, "preset", None) | |
| if preset_name is not None: | |
| preset_configs = ALGORITHM_SPECS[algorithm].get("preset_configs", {}) | |
| if preset_name in preset_configs: | |
| return preset_configs[preset_name] | |
| return ALGORITHM_SPECS[algorithm]["default_config"] | |
| def build_overrides(args: argparse.Namespace) -> list[str]: | |
| overrides = list(getattr(args, "override", []) or []) | |
| output_root = resolve_repo_path(args.output_root) | |
| overrides.append(f"experiment.output_dir={output_root}") | |
| if getattr(args, "seed", None) is not None: | |
| overrides.append(f"experiment.seed={args.seed}") | |
| return overrides | |
| def describe_task(task: dict[str, Any]) -> str: | |
| if task["suite"] == "dmcontrol": | |
| target = f"{task['domain']}/{task['task']}" | |
| else: | |
| target = task.get("env_name", "<unknown>") | |
| return ( | |
| f"{task['id']}: suite={task['suite']}, target={target}, " | |
| f"action_repeat={task.get('action_repeat', 1)}, episode_length={task.get('episode_length', 'n/a')}" | |
| ) | |
| def list_command(args: argparse.Namespace) -> int: | |
| print("Available presets:") | |
| for preset_name, tasks in PRESETS.items(): | |
| print(f" {preset_name}: {', '.join(tasks)}") | |
| print("\nAlgorithms:") | |
| for algorithm in (args.algo if args.algo != "all" else DEFAULT_ALL_ALGORITHMS): | |
| spec = ALGORITHM_SPECS[algorithm] | |
| config_path = config_path_for_algorithm(algorithm, args) | |
| config = load_config(config_path) | |
| print(f" {spec['label']} ({algorithm})") | |
| print(f" config: {config_path}") | |
| for task in config.get("benchmark", {}).get("tasks", []): | |
| print(f" - {describe_task(task)}") | |
| return 0 | |
| def build_run_configs(algorithm: str, args: argparse.Namespace, tasks: list[str]) -> tuple[Path, list[dict[str, Any]]]: | |
| if getattr(args, "resume", None): | |
| import torch | |
| checkpoint_path = resolve_repo_path(args.resume) | |
| checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False) | |
| checkpoint_config = checkpoint["config"] | |
| checkpoint_algorithm = checkpoint_config["experiment"]["algorithm"] | |
| if algorithm != checkpoint_algorithm: | |
| raise ValueError( | |
| f"Resume checkpoint {checkpoint_path} is for algorithm '{checkpoint_algorithm}', not '{algorithm}'." | |
| ) | |
| resolved = apply_overrides(checkpoint_config, build_overrides(args)) | |
| resolved["_resume_checkpoint"] = str(checkpoint_path) | |
| return checkpoint_path, [resolved] | |
| config_path = config_path_for_algorithm(algorithm, args) | |
| config = load_config(config_path) | |
| resolved = apply_overrides(config, build_overrides(args)) | |
| return config_path, expand_task_runs(resolved, tasks) | |
| def print_run_plan(algorithm: str, config_path: Path, runs: list[dict[str, Any]], output_root: Path) -> None: | |
| for run_config in runs: | |
| task = run_config["task"] | |
| seed = run_config["experiment"]["seed"] | |
| print( | |
| f"[plan] algorithm={algorithm} task={task['id']} seed={seed} " | |
| f"config={config_path} outputs={output_root}" | |
| ) | |
| def train_runs(algorithm: str, runs: list[dict[str, Any]]) -> None: | |
| package_name = ALGORITHM_SPECS[algorithm]["package"] | |
| train_module = importlib.import_module(f"{package_name}.train") | |
| label = ALGORITHM_SPECS[algorithm]["label"] | |
| for run_config in runs: | |
| run_config_copy = copy.deepcopy(run_config) | |
| resume_checkpoint = run_config_copy.pop("_resume_checkpoint", None) | |
| task_id = run_config_copy["task"]["id"] | |
| print(f"[train] {label} -> {task_id}") | |
| train_module.main(config=run_config_copy, resume_checkpoint=resume_checkpoint) | |
| def train_command(args: argparse.Namespace) -> int: | |
| if getattr(args, "resume", None): | |
| import torch | |
| checkpoint_path = resolve_repo_path(args.resume) | |
| checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False) | |
| checkpoint_config = checkpoint["config"] | |
| tasks = [checkpoint_config["task"]["id"]] | |
| algorithms = [checkpoint_config["experiment"]["algorithm"]] | |
| else: | |
| tasks = select_tasks(args) | |
| algorithms = DEFAULT_ALL_ALGORITHMS if args.algo == "all" else [args.algo] | |
| output_root = resolve_repo_path(args.output_root) | |
| prepared: list[tuple[str, Path, list[dict[str, Any]]]] = [] | |
| for algorithm in algorithms: | |
| config_path, runs = build_run_configs(algorithm, args, tasks) | |
| prepared.append((algorithm, config_path, runs)) | |
| print_run_plan(algorithm, config_path, runs, output_root) | |
| if args.dry_run: | |
| return 0 | |
| for algorithm, _, runs in prepared: | |
| train_runs(algorithm, runs) | |
| return 0 | |
| def add_task_selector_arguments(parser: argparse.ArgumentParser) -> None: | |
| task_group = parser.add_mutually_exclusive_group() | |
| task_group.add_argument( | |
| "--tasks", | |
| nargs="+", | |
| default=None, | |
| help="Task ids to run. Example: --tasks cartpole_swingup cheetah_run", | |
| ) | |
| task_group.add_argument( | |
| "--preset", | |
| choices=sorted(PRESETS), | |
| default="compare_dmcontrol", | |
| help="Named task preset. Defaults to compare_dmcontrol.", | |
| ) | |
| def add_shared_run_arguments(parser: argparse.ArgumentParser, include_algo: bool) -> None: | |
| if include_algo: | |
| parser.add_argument( | |
| "--algo", | |
| choices=["hamworld", "dreamerv3", "tdmpc2", "ppo", "sac", "all"], | |
| default="all", | |
| help="Algorithm selector.", | |
| ) | |
| add_task_selector_arguments(parser) | |
| parser.add_argument("--output-root", default="outputs", help="Root directory used for all experiment outputs.") | |
| parser.add_argument("--seed", default=None, type=int, help="Optional seed override for all selected algorithms.") | |
| parser.add_argument("--override", action="append", default=[], help="Extra dotted config override, e.g. training.total_steps=10000") | |
| parser.add_argument("--hamworld-config", dest="hamworld_config", default=None, help="Optional config path override for HaM-World.") | |
| parser.add_argument("--dreamerv3-config", dest="dreamerv3_config", default=None, help="Optional config path override for DreamerV3.") | |
| parser.add_argument("--tdmpc2-config", dest="tdmpc2_config", default=None, help="Optional config path override for TD-MPC2.") | |
| parser.add_argument("--ppo-config", dest="ppo_config", default=None, help="Optional config path override for PPO.") | |
| parser.add_argument("--sac-config", dest="sac_config", default=None, help="Optional config path override for SAC.") | |
| parser.add_argument("--resume", default=None, help="Optional checkpoint path to resume a single run from.") | |
| parser.add_argument("--dry-run", action="store_true", help="Print the resolved run plan without starting training.") | |
| def build_parser() -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser(description="Unified launcher for the final 5-algorithm paper repository.") | |
| subparsers = parser.add_subparsers(dest="command", required=True) | |
| list_parser = subparsers.add_parser("list", help="Show available presets, tasks, and default configs.") | |
| list_parser.add_argument( | |
| "--algo", | |
| nargs="+", | |
| choices=["hamworld", "dreamerv3", "tdmpc2", "ppo", "sac", "all"], | |
| default=["all"], | |
| help="Algorithm selector.", | |
| ) | |
| list_parser.add_argument("--hamworld-config", dest="hamworld_config", default=None, help="Optional config path override for HaM-World.") | |
| list_parser.add_argument("--dreamerv3-config", dest="dreamerv3_config", default=None, help="Optional config path override for DreamerV3.") | |
| list_parser.add_argument("--tdmpc2-config", dest="tdmpc2_config", default=None, help="Optional config path override for TD-MPC2.") | |
| list_parser.add_argument("--ppo-config", dest="ppo_config", default=None, help="Optional config path override for PPO.") | |
| list_parser.add_argument("--sac-config", dest="sac_config", default=None, help="Optional config path override for SAC.") | |
| list_parser.set_defaults(handler=list_command) | |
| train_parser = subparsers.add_parser("train", help="Train one or more supported algorithms.") | |
| add_shared_run_arguments(train_parser, include_algo=True) | |
| train_parser.set_defaults(handler=train_command) | |
| return parser | |
| def main(argv: list[str] | None = None) -> int: | |
| parser = build_parser() | |
| args = parser.parse_args(argv) | |
| try: | |
| if getattr(args, "command", None) == "list" and args.algo == ["all"]: | |
| args.algo = DEFAULT_ALL_ALGORITHMS | |
| return args.handler(args) | |
| except KeyboardInterrupt: | |
| print("Interrupted by user.", file=sys.stderr) | |
| return 130 | |
| except Exception as exc: # noqa: BLE001 | |
| print(f"Error: {exc}", file=sys.stderr) | |
| return 1 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |