| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- """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)
- @router.get("/bc-fields")
- def list_bc_fields():
- """Return the boundary-condition field catalog (single source of truth).
- Used by the frontend to render the project boundary form and the plan
- detail boundary display from one canonical catalog, so BC keys/labels
- cannot drift across pages (P1-1).
- """
- from ..services.bc_fields import get_bc_field_catalog
- fields = get_bc_field_catalog()
- return {"total": len(fields), "fields": fields}
- # ---------------------------------------------------------------------------
- # 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)
|