"""Plan generation API router (rule engine + parameter registry).""" from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from ..database import get_db from ..models.project import Project from ..schemas.generation import ( ParameterRegistryResponse, RangeRecommendRequest, RangeRecommendation, PlanGenerateRequest, PlanGenerateResponse, ) from ..services.rule_engine import ( get_parameter_registry, get_parameter, recommend_range, generate_plan, BoundaryConditions, ) router = APIRouter(prefix="/api", tags=["generation"]) # --------------------------------------------------------------------------- # Parameter registry # --------------------------------------------------------------------------- @router.get("/scan-parameters", response_model=ParameterRegistryResponse) def list_scan_parameters(category: str | None = None): """List all scannable Motor-CAD parameters with their metadata. Used by the frontend plan editor to populate variable dropdowns. """ params = get_parameter_registry() if category: params = [p for p in params if p["category"].lower() == category.lower()] return ParameterRegistryResponse(total=len(params), parameters=params) # --------------------------------------------------------------------------- # Range recommendation # --------------------------------------------------------------------------- @router.post("/recommend-range", response_model=RangeRecommendation) def recommend_scan_range(request: RangeRecommendRequest): """Recommend a scan range for a parameter based on boundary conditions.""" p = get_parameter(request.parameter_name) if p is None: raise HTTPException( status_code=404, detail=f"Unknown parameter: {request.parameter_name}", ) bc = BoundaryConditions.from_dict(request.boundary_conditions or {}) rng = recommend_range(request.parameter_name, bc) return RangeRecommendation(**rng) # --------------------------------------------------------------------------- # Plan generation # --------------------------------------------------------------------------- @router.post("/generate-plan", response_model=PlanGenerateResponse) def generate_plan_from_bc(request: PlanGenerateRequest): """Generate a recommended simulation plan from boundary conditions. Uses the rule engine to recommend scan ranges for each parameter and returns a plan draft that can be edited and then saved via the plans API. """ bc = BoundaryConditions.from_dict(request.boundary_conditions or {}) plan = generate_plan( bc=bc, param_names=request.parameter_names, model_path=request.model_path, ) return PlanGenerateResponse(**plan) @router.post("/projects/{project_id}/generate-plan", response_model=PlanGenerateResponse) def generate_plan_for_project( project_id: int, request: PlanGenerateRequest, db: Session = Depends(get_db), ): """Generate a plan for an existing project using its boundary conditions. The project's stored boundary conditions are used as the base; any boundary_conditions in the request override them. """ project = db.query(Project).filter(Project.id == project_id).first() if not project: raise HTTPException(status_code=404, detail="Project not found") # Merge: project BC as base, request BC overrides merged_bc = dict(project.get_boundary_conditions()) if request.boundary_conditions: merged_bc.update(request.boundary_conditions) # Ensure topology from project merged_bc.setdefault("topology", project.topology or "SSSR") bc = BoundaryConditions.from_dict(merged_bc) model_path = request.model_path or project.model_path or "" plan = generate_plan( bc=bc, param_names=request.parameter_names, model_path=model_path, ) return PlanGenerateResponse(**plan)