test_p3_orchestrator.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """P3-M2 regression: AdaptiveOrchestrator full closed loop with fake executor.
  2. Run: python scripts/test_p3_orchestrator.py (exit 0 = PASS)
  3. Uses an isolated temp SQLite DB and temp loop-state dir, so it never touches
  4. the real web DB or real loop state. No real Motor-CAD is involved.
  5. """
  6. import json
  7. import os
  8. import sys
  9. import tempfile
  10. _TMP = os.path.join(tempfile.mkdtemp(), "test_afm.db")
  11. _TMP_STATE = os.path.join(tempfile.mkdtemp(), "loops")
  12. os.environ["AFM_DB_PATH"] = _TMP
  13. sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "web", "backend"))
  14. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  15. from app.database import init_db # noqa: E402
  16. init_db()
  17. from app.services.strategy_orchestrator import AdaptiveOrchestrator # noqa: E402
  18. from app.services.task_manager import get_task_manager # noqa: E402
  19. tm = get_task_manager()
  20. orch = AdaptiveOrchestrator(state_dir=_TMP_STATE)
  21. # ------------------------------------------------------------------ 1. start
  22. res = orch.start_loop(
  23. loop_id="loop-p3-test",
  24. parameters=[
  25. {"name": "airgap_mm", "min_value": 0.8, "max_value": 2.0, "step": 0.1, "unit": "mm"},
  26. {"name": "current_a", "min_value": 5.0, "max_value": 20.0, "step": 0.5},
  27. ],
  28. total_budget=8,
  29. batch_size=4,
  30. initial_samples=4,
  31. objective_metric="tavg_nm",
  32. objective_direction="maximize",
  33. )
  34. assert res["phase"] == "running", res
  35. assert res["current_task_id"], res
  36. assert res["batch_task"]["task_type"] == "adaptive_batch", res
  37. first_batch_ids = res["batch_task"]["point_ids"]
  38. assert isinstance(first_batch_ids, list) and len(first_batch_ids) > 0, first_batch_ids
  39. print("[1] start_loop OK: task=%s first_batch=%d points"
  40. % (res["current_task_id"], len(first_batch_ids)))
  41. def run_batch(tid):
  42. """Fake executor: read task.json parameters, fabricate results, report."""
  43. task = tm.get_task(tid)
  44. with open(task["task_file"], "r", encoding="utf-8") as f:
  45. payload = json.load(f)
  46. results = []
  47. for i, params in enumerate(payload["parameters"]):
  48. pid = params.get("point_id")
  49. results.append({
  50. "point_id": pid,
  51. "point_index": i,
  52. "params": params,
  53. "metrics": {"tavg_nm": round(8.0 + 0.5 * pid, 3), "efficiency_pct": 90.0 + (pid % 5)},
  54. "status": "OK",
  55. })
  56. tm.report_results(tid, results, status="completed")
  57. return results
  58. # ---------------------------------------------------------------- 2. drive
  59. steps = 0
  60. max_steps = 12
  61. batches_seen = set()
  62. while steps < max_steps:
  63. steps += 1
  64. view = orch.get_loop_status("loop-p3-test")
  65. if view["phase"] in ("converged", "budget_exhausted", "failed"):
  66. break
  67. tid = view["current_task_id"]
  68. if tid is None:
  69. view = orch.advance_loop("loop-p3-test")
  70. continue
  71. run_batch(tid)
  72. view = orch.advance_loop("loop-p3-test")
  73. batches_seen.add(view.get("current_batch"))
  74. print("[2] step %d -> batch=%s task=%s phase=%s n_results=%s"
  75. % (steps, view.get("current_batch"), view.get("current_task_id"),
  76. view["phase"], view["n_results"]))
  77. final = orch.get_loop_status("loop-p3-test")
  78. print("[3] FINAL phase=%s batches=%s n_results=%s"
  79. % (final["phase"], sorted(batches_seen), final["n_results"]))
  80. assert final["phase"] in ("converged", "budget_exhausted"), final
  81. assert final["n_results"] >= 4, final
  82. assert len(batches_seen) >= 1, batches_seen
  83. # all reported points must be reflected in search state
  84. search_state = final.get("search_state") or {}
  85. assert search_state.get("completed_points", 0) >= 4, search_state
  86. assert search_state.get("used_budget", 0) > 0, search_state
  87. # ------------------------------------------------------------- 3. misc
  88. loops = orch.list_loops()
  89. assert len(loops) == 1 and loops[0]["loop_id"] == "loop-p3-test", loops
  90. try:
  91. orch.start_loop(loop_id="loop-p3-test",
  92. parameters=[{"name": "airgap_mm", "min_value": 1, "max_value": 2}])
  93. raise SystemExit("duplicate loop should fail")
  94. except ValueError:
  95. print("[4] duplicate loop rejected OK")
  96. state_path = os.path.join(_TMP_STATE, "loop-p3-test_loop.json")
  97. assert os.path.exists(state_path), state_path
  98. print("[5] loop state persisted OK")
  99. # ------------------------------------------------------------ 4. task model
  100. # create_task with explicit adaptive-batch fields
  101. t2 = tm.create_task(
  102. plan_id=None,
  103. plan_data={"topology": "SSSR"},
  104. parameters=[{"airgap_mm": 1.0, "point_id": 0}],
  105. task_name="meta-batch",
  106. task_type="adaptive_batch",
  107. loop_id="loop-x",
  108. batch_id=3,
  109. point_ids=[0, 1],
  110. dynamic=True,
  111. )
  112. assert t2["task_type"] == "adaptive_batch", t2
  113. assert t2["loop_id"] == "loop-x" and t2["batch_id"] == 3, t2
  114. assert t2["point_ids"] == [0, 1] and t2["dynamic"] is True, t2
  115. print("[6] task model adaptive-batch fields OK")
  116. print("\nALL P3-M2 ORCHESTRATOR TESTS PASSED")