| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- """Adaptive Simulation Loop (P3-M5).
- Integrates all P3 components into a complete end-to-end adaptive
- simulation closed loop:
- 1. AI plan generation (natural language -> structured plan)
- 2. L0 pre-screening (exclude infeasible regions)
- 3. Feasibility-first search (active learning + trust region)
- 4. Simulation execution (external Motor-CAD/Maxwell)
- 5. AI result analysis (trends, anomalies, optimization suggestions)
- 6. Multi-fidelity calibration + confidence grading
- 7. Experience library enhancement (extract design knowledge)
- 8. Adaptive next-batch selection (loop back to step 3)
- """
- import json
- from typing import Dict, List, Optional, Any, Callable
- from datetime import datetime
- from enum import Enum
- from ..services.l0_prescreening import L0PreScreeningEngine
- from ..services.feasibility_search import FeasibilityFirstSearch, ParameterRange
- from ..services.plan_generator import AIPlanGenerator
- from ..services.result_analyst import AIResultAnalyst
- from ..services.experience_enhancer import ExperienceEnhancer
- class LoopPhase(str, Enum):
- """Phases of the adaptive simulation loop."""
- INIT = "init"
- PLAN_GENERATED = "plan_generated"
- L0_SCREENED = "l0_screened"
- SEARCH_INITIALIZED = "search_initialized"
- BATCH_SELECTED = "batch_selected"
- SIMULATION_RUNNING = "simulation_running"
- RESULTS_ANALYZED = "results_analyzed"
- EXPERIENCE_UPDATED = "experience_updated"
- CONVERGED = "converged"
- BUDGET_EXHAUSTED = "budget_exhausted"
- COMPLETED = "completed"
- class AdaptiveLoop:
- """Complete adaptive simulation closed loop.
- This orchestrator ties together all P3-M1 through P3-M5 components
- into a single, stateful simulation optimization loop.
- """
- def __init__(
- self,
- loop_id: Optional[str] = None,
- user_requirement: Optional[str] = None,
- total_budget: int = 80,
- batch_size: int = 4,
- ):
- self.loop_id = loop_id or f"loop_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
- self.user_requirement = user_requirement
- self.total_budget = total_budget
- self.batch_size = batch_size
- self.phase = LoopPhase.INIT
- # Components
- self.l0_engine = L0PreScreeningEngine()
- self.plan_generator = AIPlanGenerator(l0_engine=self.l0_engine)
- self.result_analyst = AIResultAnalyst()
- self.experience_enhancer = ExperienceEnhancer()
- self.search: Optional[FeasibilityFirstSearch] = None
- # State
- self.plan: Optional[Dict[str, Any]] = None
- self.plan_validation: Optional[Dict[str, Any]] = None
- self.all_results: List[Dict[str, Any]] = []
- self.latest_analysis: Optional[Dict[str, Any]] = None
- self.latest_insights: Optional[Dict[str, Any]] = None
- self.history: List[Dict[str, Any]] = []
- self.created_at = datetime.now().isoformat()
- self.updated_at = datetime.now().isoformat()
- def generate_plan(self, user_requirement: Optional[str] = None) -> Dict[str, Any]:
- """Step 1: Generate simulation plan from natural language.
- Args:
- user_requirement: Natural language requirement (uses stored if None)
- Returns:
- Generated plan with validation
- """
- if user_requirement:
- self.user_requirement = user_requirement
- if not self.user_requirement:
- raise ValueError("No user requirement provided")
- result = self.plan_generator.generate(self.user_requirement)
- self.plan = result.get("plan", {})
- self.plan_validation = result.get("validation", {})
- self.phase = LoopPhase.PLAN_GENERATED
- self._record_history("plan_generated", {"plan_name": self.plan.get("plan_name", "")})
- return {
- "loop_id": self.loop_id,
- "phase": self.phase.value,
- "plan": self.plan,
- "validation": self.plan_validation,
- }
- def initialize_search(self) -> Dict[str, Any]:
- """Step 2-3: Initialize feasibility-first search from plan.
- Converts plan scan variables to search parameters and
- generates initial LHS batch with L0 pre-screening.
- """
- if not self.plan:
- raise RuntimeError("Plan not generated. Call generate_plan() first.")
- # Convert scan variables to ParameterRange
- parameters = []
- for var in self.plan.get("scan_variables", []):
- if isinstance(var, dict) and "name" in var and "min_value" in var and "max_value" in var:
- parameters.append(ParameterRange(
- name=var["name"],
- min_value=float(var["min_value"]),
- max_value=float(var["max_value"]),
- step=var.get("step"),
- unit=var.get("unit", ""),
- description=var.get("description", ""),
- ))
- if not parameters:
- raise RuntimeError("No valid scan variables in plan")
- # Get search strategy from plan
- search_strategy = self.plan.get("search_strategy", {})
- acceptance = self.plan.get("acceptance_criteria", {})
- self.search = FeasibilityFirstSearch(
- parameters=parameters,
- l0_engine=self.l0_engine,
- total_budget=search_strategy.get("max_solver_calls", self.total_budget),
- batch_size=search_strategy.get("batch_size", self.batch_size),
- initial_samples=search_strategy.get("initial_samples", 16),
- objective_metric=acceptance.get("objective_metric", "tavg_nm"),
- objective_direction=acceptance.get("objective_direction", "maximize"),
- )
- # Generate initial batch
- initial_batch = self.search.generate_initial_batch()
- self.phase = LoopPhase.SEARCH_INITIALIZED
- self._record_history("search_initialized", {
- "n_parameters": len(parameters),
- "initial_batch_size": len(initial_batch),
- })
- return {
- "loop_id": self.loop_id,
- "phase": self.phase.value,
- "search_id": self.search.state.run_id,
- "initial_batch": [
- {"id": p.id, "params": p.params, "status": p.status}
- for p in initial_batch
- ],
- "state": self.search.get_state_summary(),
- }
- def get_next_batch(self) -> Dict[str, Any]:
- """Step 3 (loop): Select next batch of points to simulate.
- Uses active learning with trust region refinement.
- """
- if not self.search:
- raise RuntimeError("Search not initialized. Call initialize_search() first.")
- batch = self.search.select_next_batch()
- if not batch:
- if self.search.state.convergence_status == "converged":
- self.phase = LoopPhase.CONVERGED
- else:
- self.phase = LoopPhase.BUDGET_EXHAUSTED
- return {
- "loop_id": self.loop_id,
- "phase": self.phase.value,
- "batch": [],
- "message": f"Search {self.search.state.convergence_status}",
- }
- self.phase = LoopPhase.BATCH_SELECTED
- self._record_history("batch_selected", {
- "batch_id": self.search.state.current_batch - 1,
- "batch_size": len(batch),
- })
- return {
- "loop_id": self.loop_id,
- "phase": self.phase.value,
- "batch_id": self.search.state.current_batch - 1,
- "points": [
- {"id": p.id, "params": p.params, "status": p.status}
- for p in batch
- ],
- "state": self.search.get_state_summary(),
- }
- def report_results(self, point_results: List[Dict[str, Any]]) -> Dict[str, Any]:
- """Step 4-5: Report simulation results and trigger analysis.
- Args:
- point_results: List of {point_id, metrics, status} dicts
- Returns:
- Analysis results and next-step recommendations
- """
- if not self.search:
- raise RuntimeError("Search not initialized")
- # Report each result to search
- for pr in point_results:
- point_id = pr.get("point_id")
- metrics = pr.get("metrics", {})
- status = pr.get("status", "ok")
- if point_id is not None:
- self.search.report_result(point_id, metrics, status)
- # Add to all results
- result_entry = {"point_id": point_id, **metrics}
- self.all_results.append(result_entry)
- # Run AI analysis on accumulated results
- targets = self.plan.get("acceptance_criteria", {}).get("hard_constraints", {})
- target_dict = {}
- if isinstance(targets, list):
- for constraint in targets:
- # Parse simple constraints like "efficiency_pct >= 92"
- if ">=" in constraint:
- parts = constraint.split(">=")
- target_dict[parts[0].strip()] = float(parts[1].strip())
- elif "<=" in constraint:
- parts = constraint.split("<=")
- target_dict[parts[0].strip()] = float(parts[1].strip())
- self.latest_analysis = self.result_analyst.analyze(
- results=self.all_results,
- targets=target_dict if target_dict else None,
- fidelity="L3",
- scan_parameters=[p.name for p in self.search.parameters] if self.search else None,
- )
- self.phase = LoopPhase.RESULTS_ANALYZED
- self._record_history("results_analyzed", {
- "n_results": len(self.all_results),
- "confidence": self.latest_analysis.get("confidence", {}).get("grade", "?"),
- })
- return {
- "loop_id": self.loop_id,
- "phase": self.phase.value,
- "analysis": self.latest_analysis,
- "search_state": self.search.get_state_summary(),
- "recommendation": self._get_recommendation(),
- }
- def update_experience(self) -> Dict[str, Any]:
- """Step 6: Extract insights and update experience library.
- Returns:
- Extracted insights and experience entry
- """
- if len(self.all_results) < 5:
- return {"message": "Insufficient results for experience extraction (need >= 5)"}
- self.latest_insights = self.experience_enhancer.extract_insights(
- results=self.all_results,
- project_context={
- "topology": self.plan.get("topology", ""),
- "plan_name": self.plan.get("plan_name", ""),
- },
- )
- # Generate experience entry
- experience_entry = self.experience_enhancer.generate_experience_entry(
- insights=self.latest_insights,
- project_name=self.plan.get("plan_name", "unknown"),
- topology=self.plan.get("topology", "unknown"),
- )
- self.phase = LoopPhase.EXPERIENCE_UPDATED
- self._record_history("experience_updated", {
- "n_rules": len(experience_entry.get("design_rules", [])),
- })
- return {
- "loop_id": self.loop_id,
- "phase": self.phase.value,
- "insights": self.latest_insights,
- "experience_entry": experience_entry,
- }
- def check_completion(self) -> Dict[str, Any]:
- """Check if loop should terminate.
- Returns:
- Completion status and reason
- """
- if not self.search:
- return {"completed": False, "reason": "search_not_initialized"}
- state = self.search.get_state_summary()
- if state["convergence_status"] == "converged":
- self.phase = LoopPhase.CONVERGED
- return {"completed": True, "reason": "converged", "state": state}
- if state["remaining_budget"] <= 0:
- self.phase = LoopPhase.BUDGET_EXHAUSTED
- return {"completed": True, "reason": "budget_exhausted", "state": state}
- return {"completed": False, "reason": "continue", "state": state}
- def _get_recommendation(self) -> str:
- """Generate next-step recommendation based on current state."""
- if not self.search:
- return "Initialize search first"
- state = self.search.get_state_summary()
- analysis = self.latest_analysis or {}
- confidence = analysis.get("confidence", {}).get("grade", "?")
- if state["convergence_status"] == "converged":
- return f"Search converged. Best: {state['best_objective_value']}. Confidence: {confidence}."
- if state["remaining_budget"] <= 0:
- return "Budget exhausted."
- if confidence in ("D",):
- return f"Low confidence ({confidence}). Consider increasing sample count or fidelity level."
- if state["trust_region_active"]:
- return f"Trust region active (radius={state['trust_region_radius']:.3f}). Continue exploitation. Best: {state['best_objective_value']}"
- return f"Exploration phase. {state['feasible_points']} feasible points found. Continue sampling."
- def _record_history(self, event: str, data: Dict[str, Any]):
- """Record a history event."""
- self.history.append({
- "event": event,
- "timestamp": datetime.now().isoformat(),
- "phase": self.phase.value,
- "data": data,
- })
- self.updated_at = datetime.now().isoformat()
- def get_state(self) -> Dict[str, Any]:
- """Get complete loop state."""
- return {
- "loop_id": self.loop_id,
- "phase": self.phase.value,
- "user_requirement": self.user_requirement,
- "total_budget": self.total_budget,
- "batch_size": self.batch_size,
- "plan_name": self.plan.get("plan_name", "") if self.plan else None,
- "n_results": len(self.all_results),
- "search_state": self.search.get_state_summary() if self.search else None,
- "latest_confidence": self.latest_analysis.get("confidence", {}).get("grade") if self.latest_analysis else None,
- "history": self.history,
- "created_at": self.created_at,
- "updated_at": self.updated_at,
- }
- # Global loop registry (in-memory, P3-M5 prototype)
- _loops: Dict[str, AdaptiveLoop] = {}
- def get_loop(loop_id: str) -> Optional[AdaptiveLoop]:
- """Get an existing adaptive loop by ID."""
- return _loops.get(loop_id)
- def create_loop(user_requirement: str, total_budget: int = 80, batch_size: int = 4) -> AdaptiveLoop:
- """Create a new adaptive loop."""
- loop = AdaptiveLoop(
- user_requirement=user_requirement,
- total_budget=total_budget,
- batch_size=batch_size,
- )
- _loops[loop.loop_id] = loop
- return loop
- def list_loops() -> List[Dict[str, Any]]:
- """List all active loops."""
- return [
- {"loop_id": lid, "phase": loop.phase.value, "n_results": len(loop.all_results)}
- for lid, loop in _loops.items()
- ]
|