adaptive_loop.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. """Adaptive Simulation Loop (P3-M5).
  2. Integrates all P3 components into a complete end-to-end adaptive
  3. simulation closed loop:
  4. 1. AI plan generation (natural language -> structured plan)
  5. 2. L0 pre-screening (exclude infeasible regions)
  6. 3. Feasibility-first search (active learning + trust region)
  7. 4. Simulation execution (external Motor-CAD/Maxwell)
  8. 5. AI result analysis (trends, anomalies, optimization suggestions)
  9. 6. Multi-fidelity calibration + confidence grading
  10. 7. Experience library enhancement (extract design knowledge)
  11. 8. Adaptive next-batch selection (loop back to step 3)
  12. """
  13. import json
  14. from typing import Dict, List, Optional, Any, Callable
  15. from datetime import datetime
  16. from enum import Enum
  17. from ..services.l0_prescreening import L0PreScreeningEngine
  18. from ..services.feasibility_search import FeasibilityFirstSearch, ParameterRange
  19. from ..services.plan_generator import AIPlanGenerator
  20. from ..services.result_analyst import AIResultAnalyst
  21. from ..services.experience_enhancer import ExperienceEnhancer
  22. class LoopPhase(str, Enum):
  23. """Phases of the adaptive simulation loop."""
  24. INIT = "init"
  25. PLAN_GENERATED = "plan_generated"
  26. L0_SCREENED = "l0_screened"
  27. SEARCH_INITIALIZED = "search_initialized"
  28. BATCH_SELECTED = "batch_selected"
  29. SIMULATION_RUNNING = "simulation_running"
  30. RESULTS_ANALYZED = "results_analyzed"
  31. EXPERIENCE_UPDATED = "experience_updated"
  32. CONVERGED = "converged"
  33. BUDGET_EXHAUSTED = "budget_exhausted"
  34. COMPLETED = "completed"
  35. class AdaptiveLoop:
  36. """Complete adaptive simulation closed loop.
  37. This orchestrator ties together all P3-M1 through P3-M5 components
  38. into a single, stateful simulation optimization loop.
  39. """
  40. def __init__(
  41. self,
  42. loop_id: Optional[str] = None,
  43. user_requirement: Optional[str] = None,
  44. total_budget: int = 80,
  45. batch_size: int = 4,
  46. ):
  47. self.loop_id = loop_id or f"loop_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
  48. self.user_requirement = user_requirement
  49. self.total_budget = total_budget
  50. self.batch_size = batch_size
  51. self.phase = LoopPhase.INIT
  52. # Components
  53. self.l0_engine = L0PreScreeningEngine()
  54. self.plan_generator = AIPlanGenerator(l0_engine=self.l0_engine)
  55. self.result_analyst = AIResultAnalyst()
  56. self.experience_enhancer = ExperienceEnhancer()
  57. self.search: Optional[FeasibilityFirstSearch] = None
  58. # State
  59. self.plan: Optional[Dict[str, Any]] = None
  60. self.plan_validation: Optional[Dict[str, Any]] = None
  61. self.all_results: List[Dict[str, Any]] = []
  62. self.latest_analysis: Optional[Dict[str, Any]] = None
  63. self.latest_insights: Optional[Dict[str, Any]] = None
  64. self.history: List[Dict[str, Any]] = []
  65. self.created_at = datetime.now().isoformat()
  66. self.updated_at = datetime.now().isoformat()
  67. def generate_plan(self, user_requirement: Optional[str] = None) -> Dict[str, Any]:
  68. """Step 1: Generate simulation plan from natural language.
  69. Args:
  70. user_requirement: Natural language requirement (uses stored if None)
  71. Returns:
  72. Generated plan with validation
  73. """
  74. if user_requirement:
  75. self.user_requirement = user_requirement
  76. if not self.user_requirement:
  77. raise ValueError("No user requirement provided")
  78. result = self.plan_generator.generate(self.user_requirement)
  79. self.plan = result.get("plan", {})
  80. self.plan_validation = result.get("validation", {})
  81. self.phase = LoopPhase.PLAN_GENERATED
  82. self._record_history("plan_generated", {"plan_name": self.plan.get("plan_name", "")})
  83. return {
  84. "loop_id": self.loop_id,
  85. "phase": self.phase.value,
  86. "plan": self.plan,
  87. "validation": self.plan_validation,
  88. }
  89. def initialize_search(self) -> Dict[str, Any]:
  90. """Step 2-3: Initialize feasibility-first search from plan.
  91. Converts plan scan variables to search parameters and
  92. generates initial LHS batch with L0 pre-screening.
  93. """
  94. if not self.plan:
  95. raise RuntimeError("Plan not generated. Call generate_plan() first.")
  96. # Convert scan variables to ParameterRange
  97. parameters = []
  98. for var in self.plan.get("scan_variables", []):
  99. if isinstance(var, dict) and "name" in var and "min_value" in var and "max_value" in var:
  100. parameters.append(ParameterRange(
  101. name=var["name"],
  102. min_value=float(var["min_value"]),
  103. max_value=float(var["max_value"]),
  104. step=var.get("step"),
  105. unit=var.get("unit", ""),
  106. description=var.get("description", ""),
  107. ))
  108. if not parameters:
  109. raise RuntimeError("No valid scan variables in plan")
  110. # Get search strategy from plan
  111. search_strategy = self.plan.get("search_strategy", {})
  112. acceptance = self.plan.get("acceptance_criteria", {})
  113. self.search = FeasibilityFirstSearch(
  114. parameters=parameters,
  115. l0_engine=self.l0_engine,
  116. total_budget=search_strategy.get("max_solver_calls", self.total_budget),
  117. batch_size=search_strategy.get("batch_size", self.batch_size),
  118. initial_samples=search_strategy.get("initial_samples", 16),
  119. objective_metric=acceptance.get("objective_metric", "tavg_nm"),
  120. objective_direction=acceptance.get("objective_direction", "maximize"),
  121. )
  122. # Generate initial batch
  123. initial_batch = self.search.generate_initial_batch()
  124. self.phase = LoopPhase.SEARCH_INITIALIZED
  125. self._record_history("search_initialized", {
  126. "n_parameters": len(parameters),
  127. "initial_batch_size": len(initial_batch),
  128. })
  129. return {
  130. "loop_id": self.loop_id,
  131. "phase": self.phase.value,
  132. "search_id": self.search.state.run_id,
  133. "initial_batch": [
  134. {"id": p.id, "params": p.params, "status": p.status}
  135. for p in initial_batch
  136. ],
  137. "state": self.search.get_state_summary(),
  138. }
  139. def get_next_batch(self) -> Dict[str, Any]:
  140. """Step 3 (loop): Select next batch of points to simulate.
  141. Uses active learning with trust region refinement.
  142. """
  143. if not self.search:
  144. raise RuntimeError("Search not initialized. Call initialize_search() first.")
  145. batch = self.search.select_next_batch()
  146. if not batch:
  147. if self.search.state.convergence_status == "converged":
  148. self.phase = LoopPhase.CONVERGED
  149. else:
  150. self.phase = LoopPhase.BUDGET_EXHAUSTED
  151. return {
  152. "loop_id": self.loop_id,
  153. "phase": self.phase.value,
  154. "batch": [],
  155. "message": f"Search {self.search.state.convergence_status}",
  156. }
  157. self.phase = LoopPhase.BATCH_SELECTED
  158. self._record_history("batch_selected", {
  159. "batch_id": self.search.state.current_batch - 1,
  160. "batch_size": len(batch),
  161. })
  162. return {
  163. "loop_id": self.loop_id,
  164. "phase": self.phase.value,
  165. "batch_id": self.search.state.current_batch - 1,
  166. "points": [
  167. {"id": p.id, "params": p.params, "status": p.status}
  168. for p in batch
  169. ],
  170. "state": self.search.get_state_summary(),
  171. }
  172. def report_results(self, point_results: List[Dict[str, Any]]) -> Dict[str, Any]:
  173. """Step 4-5: Report simulation results and trigger analysis.
  174. Args:
  175. point_results: List of {point_id, metrics, status} dicts
  176. Returns:
  177. Analysis results and next-step recommendations
  178. """
  179. if not self.search:
  180. raise RuntimeError("Search not initialized")
  181. # Report each result to search
  182. for pr in point_results:
  183. point_id = pr.get("point_id")
  184. metrics = pr.get("metrics", {})
  185. status = pr.get("status", "ok")
  186. if point_id is not None:
  187. self.search.report_result(point_id, metrics, status)
  188. # Add to all results
  189. result_entry = {"point_id": point_id, **metrics}
  190. self.all_results.append(result_entry)
  191. # Run AI analysis on accumulated results
  192. targets = self.plan.get("acceptance_criteria", {}).get("hard_constraints", {})
  193. target_dict = {}
  194. if isinstance(targets, list):
  195. for constraint in targets:
  196. # Parse simple constraints like "efficiency_pct >= 92"
  197. if ">=" in constraint:
  198. parts = constraint.split(">=")
  199. target_dict[parts[0].strip()] = float(parts[1].strip())
  200. elif "<=" in constraint:
  201. parts = constraint.split("<=")
  202. target_dict[parts[0].strip()] = float(parts[1].strip())
  203. self.latest_analysis = self.result_analyst.analyze(
  204. results=self.all_results,
  205. targets=target_dict if target_dict else None,
  206. fidelity="L3",
  207. scan_parameters=[p.name for p in self.search.parameters] if self.search else None,
  208. )
  209. self.phase = LoopPhase.RESULTS_ANALYZED
  210. self._record_history("results_analyzed", {
  211. "n_results": len(self.all_results),
  212. "confidence": self.latest_analysis.get("confidence", {}).get("grade", "?"),
  213. })
  214. return {
  215. "loop_id": self.loop_id,
  216. "phase": self.phase.value,
  217. "analysis": self.latest_analysis,
  218. "search_state": self.search.get_state_summary(),
  219. "recommendation": self._get_recommendation(),
  220. }
  221. def update_experience(self) -> Dict[str, Any]:
  222. """Step 6: Extract insights and update experience library.
  223. Returns:
  224. Extracted insights and experience entry
  225. """
  226. if len(self.all_results) < 5:
  227. return {"message": "Insufficient results for experience extraction (need >= 5)"}
  228. self.latest_insights = self.experience_enhancer.extract_insights(
  229. results=self.all_results,
  230. project_context={
  231. "topology": self.plan.get("topology", ""),
  232. "plan_name": self.plan.get("plan_name", ""),
  233. },
  234. )
  235. # Generate experience entry
  236. experience_entry = self.experience_enhancer.generate_experience_entry(
  237. insights=self.latest_insights,
  238. project_name=self.plan.get("plan_name", "unknown"),
  239. topology=self.plan.get("topology", "unknown"),
  240. )
  241. self.phase = LoopPhase.EXPERIENCE_UPDATED
  242. self._record_history("experience_updated", {
  243. "n_rules": len(experience_entry.get("design_rules", [])),
  244. })
  245. return {
  246. "loop_id": self.loop_id,
  247. "phase": self.phase.value,
  248. "insights": self.latest_insights,
  249. "experience_entry": experience_entry,
  250. }
  251. def check_completion(self) -> Dict[str, Any]:
  252. """Check if loop should terminate.
  253. Returns:
  254. Completion status and reason
  255. """
  256. if not self.search:
  257. return {"completed": False, "reason": "search_not_initialized"}
  258. state = self.search.get_state_summary()
  259. if state["convergence_status"] == "converged":
  260. self.phase = LoopPhase.CONVERGED
  261. return {"completed": True, "reason": "converged", "state": state}
  262. if state["remaining_budget"] <= 0:
  263. self.phase = LoopPhase.BUDGET_EXHAUSTED
  264. return {"completed": True, "reason": "budget_exhausted", "state": state}
  265. return {"completed": False, "reason": "continue", "state": state}
  266. def _get_recommendation(self) -> str:
  267. """Generate next-step recommendation based on current state."""
  268. if not self.search:
  269. return "Initialize search first"
  270. state = self.search.get_state_summary()
  271. analysis = self.latest_analysis or {}
  272. confidence = analysis.get("confidence", {}).get("grade", "?")
  273. if state["convergence_status"] == "converged":
  274. return f"Search converged. Best: {state['best_objective_value']}. Confidence: {confidence}."
  275. if state["remaining_budget"] <= 0:
  276. return "Budget exhausted."
  277. if confidence in ("D",):
  278. return f"Low confidence ({confidence}). Consider increasing sample count or fidelity level."
  279. if state["trust_region_active"]:
  280. return f"Trust region active (radius={state['trust_region_radius']:.3f}). Continue exploitation. Best: {state['best_objective_value']}"
  281. return f"Exploration phase. {state['feasible_points']} feasible points found. Continue sampling."
  282. def _record_history(self, event: str, data: Dict[str, Any]):
  283. """Record a history event."""
  284. self.history.append({
  285. "event": event,
  286. "timestamp": datetime.now().isoformat(),
  287. "phase": self.phase.value,
  288. "data": data,
  289. })
  290. self.updated_at = datetime.now().isoformat()
  291. def get_state(self) -> Dict[str, Any]:
  292. """Get complete loop state."""
  293. return {
  294. "loop_id": self.loop_id,
  295. "phase": self.phase.value,
  296. "user_requirement": self.user_requirement,
  297. "total_budget": self.total_budget,
  298. "batch_size": self.batch_size,
  299. "plan_name": self.plan.get("plan_name", "") if self.plan else None,
  300. "n_results": len(self.all_results),
  301. "search_state": self.search.get_state_summary() if self.search else None,
  302. "latest_confidence": self.latest_analysis.get("confidence", {}).get("grade") if self.latest_analysis else None,
  303. "history": self.history,
  304. "created_at": self.created_at,
  305. "updated_at": self.updated_at,
  306. }
  307. # Global loop registry (in-memory, P3-M5 prototype)
  308. _loops: Dict[str, AdaptiveLoop] = {}
  309. def get_loop(loop_id: str) -> Optional[AdaptiveLoop]:
  310. """Get an existing adaptive loop by ID."""
  311. return _loops.get(loop_id)
  312. def create_loop(user_requirement: str, total_budget: int = 80, batch_size: int = 4) -> AdaptiveLoop:
  313. """Create a new adaptive loop."""
  314. loop = AdaptiveLoop(
  315. user_requirement=user_requirement,
  316. total_budget=total_budget,
  317. batch_size=batch_size,
  318. )
  319. _loops[loop.loop_id] = loop
  320. return loop
  321. def list_loops() -> List[Dict[str, Any]]:
  322. """List all active loops."""
  323. return [
  324. {"loop_id": lid, "phase": loop.phase.value, "n_results": len(loop.all_results)}
  325. for lid, loop in _loops.items()
  326. ]