generation.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. """Plan generation API router (rule engine + parameter registry)."""
  2. from fastapi import APIRouter, Depends, HTTPException
  3. from sqlalchemy.orm import Session
  4. from ..database import get_db
  5. from ..models.project import Project
  6. from ..schemas.generation import (
  7. ParameterRegistryResponse,
  8. RangeRecommendRequest, RangeRecommendation,
  9. PlanGenerateRequest, PlanGenerateResponse,
  10. )
  11. from ..services.rule_engine import (
  12. get_parameter_registry, get_parameter,
  13. recommend_range, generate_plan, BoundaryConditions,
  14. )
  15. router = APIRouter(prefix="/api", tags=["generation"])
  16. # ---------------------------------------------------------------------------
  17. # Parameter registry
  18. # ---------------------------------------------------------------------------
  19. @router.get("/scan-parameters", response_model=ParameterRegistryResponse)
  20. def list_scan_parameters(category: str | None = None):
  21. """List all scannable Motor-CAD parameters with their metadata.
  22. Used by the frontend plan editor to populate variable dropdowns.
  23. """
  24. params = get_parameter_registry()
  25. if category:
  26. params = [p for p in params if p["category"].lower() == category.lower()]
  27. return ParameterRegistryResponse(total=len(params), parameters=params)
  28. @router.get("/bc-fields")
  29. def list_bc_fields():
  30. """Return the boundary-condition field catalog (single source of truth).
  31. Used by the frontend to render the project boundary form and the plan
  32. detail boundary display from one canonical catalog, so BC keys/labels
  33. cannot drift across pages (P1-1).
  34. """
  35. from ..services.bc_fields import get_bc_field_catalog
  36. fields = get_bc_field_catalog()
  37. return {"total": len(fields), "fields": fields}
  38. # ---------------------------------------------------------------------------
  39. # Range recommendation
  40. # ---------------------------------------------------------------------------
  41. @router.post("/recommend-range", response_model=RangeRecommendation)
  42. def recommend_scan_range(request: RangeRecommendRequest):
  43. """Recommend a scan range for a parameter based on boundary conditions."""
  44. p = get_parameter(request.parameter_name)
  45. if p is None:
  46. raise HTTPException(
  47. status_code=404,
  48. detail=f"Unknown parameter: {request.parameter_name}",
  49. )
  50. bc = BoundaryConditions.from_dict(request.boundary_conditions or {})
  51. rng = recommend_range(request.parameter_name, bc)
  52. return RangeRecommendation(**rng)
  53. # ---------------------------------------------------------------------------
  54. # Plan generation
  55. # ---------------------------------------------------------------------------
  56. @router.post("/generate-plan", response_model=PlanGenerateResponse)
  57. def generate_plan_from_bc(request: PlanGenerateRequest):
  58. """Generate a recommended simulation plan from boundary conditions.
  59. Uses the rule engine to recommend scan ranges for each parameter and
  60. returns a plan draft that can be edited and then saved via the plans API.
  61. """
  62. bc = BoundaryConditions.from_dict(request.boundary_conditions or {})
  63. plan = generate_plan(
  64. bc=bc,
  65. param_names=request.parameter_names,
  66. model_path=request.model_path,
  67. )
  68. return PlanGenerateResponse(**plan)
  69. @router.post("/projects/{project_id}/generate-plan", response_model=PlanGenerateResponse)
  70. def generate_plan_for_project(
  71. project_id: int,
  72. request: PlanGenerateRequest,
  73. db: Session = Depends(get_db),
  74. ):
  75. """Generate a plan for an existing project using its boundary conditions.
  76. The project's stored boundary conditions are used as the base; any
  77. boundary_conditions in the request override them.
  78. """
  79. project = db.query(Project).filter(Project.id == project_id).first()
  80. if not project:
  81. raise HTTPException(status_code=404, detail="Project not found")
  82. # Merge: project BC as base, request BC overrides
  83. merged_bc = dict(project.get_boundary_conditions())
  84. if request.boundary_conditions:
  85. merged_bc.update(request.boundary_conditions)
  86. # Ensure topology from project
  87. merged_bc.setdefault("topology", project.topology or "SSSR")
  88. bc = BoundaryConditions.from_dict(merged_bc)
  89. model_path = request.model_path or project.model_path or ""
  90. plan = generate_plan(
  91. bc=bc,
  92. param_names=request.parameter_names,
  93. model_path=model_path,
  94. )
  95. return PlanGenerateResponse(**plan)