Skip to content

Commit 2ea564b

Browse files
committed
Prediction/simulation updates
1 parent 6cda87d commit 2ea564b

File tree

3 files changed

+187
-5
lines changed

3 files changed

+187
-5
lines changed

apps/apigw/app/main.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,21 @@ async def lifespan(app: FastAPI):
7070
# Include routers
7171
app.include_router(telemetry.router, prefix="/api/v1/telemetry", tags=["telemetry"])
7272
app.include_router(missions_router.router, prefix="/api/v1/missions", tags=["missions"])
73-
app.include_router(detections.router, prefix="/api/v1/detections", tags=["detections"])
74-
app.include_router(alerts.router, prefix="/api/v1/alerts", tags=["alerts"])
75-
app.include_router(triangulation.router, prefix="/api/v1/triangulation", tags=["triangulation"])
76-
app.include_router(prediction.router, prefix="/api/v1/prediction", tags=["prediction"])
77-
app.include_router(integrations.router, prefix="/api/v1/integrations", tags=["integrations"])
73+
74+
# Optional routers (load if available)
75+
for _name, _prefix, _tags in [
76+
("app.routers.detections", "/api/v1/detections", ["detections"]),
77+
("app.routers.alerts", "/api/v1/alerts", ["alerts"]),
78+
("app.routers.triangulation", "/api/v1/triangulation", ["triangulation"]),
79+
("app.routers.prediction", "/api/v1/prediction", ["prediction"]),
80+
("app.routers.integrations", "/api/v1/integrations", ["integrations"]),
81+
]:
82+
try:
83+
mod = __import__(_name, fromlist=["router"]) # type: ignore
84+
app.include_router(mod.router, prefix=_prefix, tags=_tags) # type: ignore
85+
logger.info(f"Loaded router: {_name}")
86+
except Exception as e:
87+
logger.warning(f"Router {_name} not loaded: {e}")
7888

7989

8090
@app.get("/")
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""
2+
Prediction API endpoints for fire spread simulation.
3+
"""
4+
5+
from datetime import datetime, timezone
6+
from typing import List
7+
8+
from fastapi import APIRouter, HTTPException
9+
10+
# Local API schemas
11+
from app.schemas.prediction import (
12+
SpreadParameters as ApiSpreadParameters,
13+
SpreadResult as ApiSpreadResult,
14+
Isochrone as ApiIsochrone,
15+
SpreadConfidence as ApiSpreadConfidence,
16+
Point as ApiPoint,
17+
)
18+
19+
# Algorithms engine from shared package
20+
try:
21+
from packages.algorithms.src.spread_modeling import (
22+
FireSpreadEngine,
23+
SpreadParameters as AlgoSpreadParameters,
24+
SpreadResult as AlgoSpreadResult,
25+
)
26+
except Exception as e:
27+
# Fallback: make import error visible when endpoint is hit
28+
FireSpreadEngine = None # type: ignore
29+
AlgoSpreadParameters = None # type: ignore
30+
AlgoSpreadResult = None # type: ignore
31+
32+
router = APIRouter()
33+
34+
35+
def _map_api_to_algo(params: ApiSpreadParameters) -> AlgoSpreadParameters: # type: ignore
36+
ignition_points = [(p.latitude, p.longitude) for p in params.ignition_points]
37+
cond = params.conditions
38+
return AlgoSpreadParameters(
39+
ignition_points=ignition_points,
40+
wind_speed=cond.wind_speed_mps,
41+
wind_direction=cond.wind_direction_deg,
42+
temperature=cond.temperature_c,
43+
humidity=cond.relative_humidity,
44+
fuel_moisture=cond.fuel_moisture,
45+
fuel_model=cond.fuel_model,
46+
simulation_hours=params.simulation_hours,
47+
time_step_minutes=int(params.time_step_minutes),
48+
monte_carlo_runs=params.monte_carlo_runs,
49+
)
50+
51+
52+
def _map_algo_to_api(result: AlgoSpreadResult, duration_hours: float) -> ApiSpreadResult: # type: ignore
53+
# Convert perimeter points
54+
perimeter_points: List[ApiPoint] = [
55+
ApiPoint(latitude=lat, longitude=lon, altitude=0.0) for (lat, lon) in result.perimeter
56+
]
57+
58+
# Convert isochrones (engine returns tuples list in geometry)
59+
api_isochrones: List[ApiIsochrone] = []
60+
for iso in result.isochrones:
61+
geom = [ApiPoint(latitude=lat, longitude=lon, altitude=0.0) for (lat, lon) in iso.get("geometry", [])]
62+
api_isochrones.append(
63+
ApiIsochrone(
64+
hours_from_start=int(iso.get("hours_from_start", 0)),
65+
geometry=geom,
66+
area_hectares=float(iso.get("area_hectares", 0.0)),
67+
perimeter_km=float(iso.get("perimeter_km", 0.0)),
68+
)
69+
)
70+
71+
conf_value = float(result.confidence)
72+
api_conf = ApiSpreadConfidence(
73+
overall_confidence=conf_value,
74+
weather_confidence=conf_value,
75+
fuel_confidence=conf_value,
76+
terrain_confidence=conf_value,
77+
confidence_factors="heuristic",
78+
)
79+
80+
return ApiSpreadResult(
81+
simulation_id=result.simulation_id,
82+
created_at=datetime.now(tz=timezone.utc),
83+
isochrones=api_isochrones,
84+
perimeter=perimeter_points,
85+
total_area_hectares=float(result.total_area_hectares),
86+
max_spread_rate_mph=float(result.max_spread_rate_mph),
87+
simulation_duration_hours=float(duration_hours),
88+
statistics={k: float(v) for k, v in (result.statistics or {}).items()},
89+
confidence=api_conf,
90+
)
91+
92+
93+
@router.post("/simulate", response_model=ApiSpreadResult)
94+
async def simulate_spread(body: ApiSpreadParameters) -> ApiSpreadResult:
95+
"""Run a fire spread simulation using the shared algorithms engine."""
96+
if FireSpreadEngine is None or AlgoSpreadParameters is None:
97+
raise HTTPException(status_code=500, detail="Algorithms package not available on PYTHONPATH")
98+
99+
try:
100+
engine = FireSpreadEngine()
101+
algo_params = _map_api_to_algo(body)
102+
algo_result = engine.simulate_spread(algo_params)
103+
return _map_algo_to_api(algo_result, duration_hours=body.simulation_hours)
104+
except HTTPException:
105+
raise
106+
except Exception as e:
107+
raise HTTPException(status_code=500, detail=f"Simulation failed: {e}")
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
Prediction and modeling schemas for API requests and responses.
3+
"""
4+
from datetime import datetime
5+
from typing import Dict, List
6+
from pydantic import BaseModel, Field
7+
8+
9+
class Point(BaseModel):
10+
latitude: float
11+
longitude: float
12+
altitude: float = 0.0
13+
14+
15+
class EnvironmentalConditions(BaseModel):
16+
timestamp: datetime
17+
latitude: float
18+
longitude: float
19+
temperature_c: float
20+
relative_humidity: float = Field(ge=0, le=100)
21+
wind_speed_mps: float = Field(ge=0)
22+
wind_direction_deg: float = Field(ge=0, le=360)
23+
fuel_moisture: float = Field(ge=0, le=1)
24+
soil_moisture: float = Field(ge=0, le=1)
25+
fuel_model: int = Field(ge=1, le=13)
26+
slope_deg: float = Field(ge=0, le=90)
27+
aspect_deg: float = Field(ge=0, le=360)
28+
canopy_cover: float = Field(ge=0, le=1)
29+
elevation_m: float
30+
31+
32+
class SpreadParameters(BaseModel):
33+
ignition_points: List[Point]
34+
conditions: EnvironmentalConditions
35+
simulation_hours: int = Field(gt=0, le=168)
36+
time_step_minutes: float = Field(gt=0, le=60)
37+
monte_carlo_runs: int = Field(gt=0, le=1000)
38+
custom_parameters: Dict[str, float] = {}
39+
40+
41+
class Isochrone(BaseModel):
42+
hours_from_start: int = Field(ge=0)
43+
geometry: List[Point]
44+
area_hectares: float = Field(ge=0)
45+
perimeter_km: float = Field(ge=0)
46+
47+
48+
class SpreadConfidence(BaseModel):
49+
overall_confidence: float = Field(ge=0, le=1)
50+
weather_confidence: float = Field(ge=0, le=1)
51+
fuel_confidence: float = Field(ge=0, le=1)
52+
terrain_confidence: float = Field(ge=0, le=1)
53+
confidence_factors: str = ""
54+
55+
56+
class SpreadResult(BaseModel):
57+
simulation_id: str
58+
created_at: datetime
59+
isochrones: List[Isochrone]
60+
perimeter: List[Point]
61+
total_area_hectares: float = Field(ge=0)
62+
max_spread_rate_mph: float = Field(ge=0)
63+
simulation_duration_hours: float = Field(ge=0)
64+
statistics: Dict[str, float] = {}
65+
confidence: SpreadConfidence

0 commit comments

Comments
 (0)