File size: 6,690 Bytes
90a51c6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
"""
rest_api.py

REST / OpenAPI surface for Hydraulic Solver Teaching Mode.

Run:
    CLIENT_API_KEY=your-secret uvicorn rest_api:app --host 0.0.0.0 --port 8000

OpenAPI:
    /openapi.json
Docs:
    /docs
"""

from __future__ import annotations

import os
from typing import List, Optional

from fastapi import Depends, FastAPI, Header, HTTPException
from pydantic import BaseModel, Field

import hydraulic_core as hc


API_KEY = os.getenv("CLIENT_API_KEY", "")

app = FastAPI(
    title="Hydraulic Solver Teaching Mode API",
    version="1.0.0",
    description="REST/OpenAPI surface exposing the same deterministic tools as the MCP server.",
)


def require_auth(authorization: Optional[str] = Header(default=None)) -> None:
    if not API_KEY:
        return
    expected = f"Bearer {API_KEY}"
    if authorization != expected:
        raise HTTPException(status_code=401, detail="Missing or invalid bearer token.")


class LoadNetworkRequest(BaseModel):
    inp_text: str
    title: str = "uploaded_network"


class SinglePipeRequest(BaseModel):
    unit_system: str = "MKS / L/s"
    length: float
    diameter: float
    c_hw: float = 120.0
    target_headloss: float
    initial_flow: float
    max_iter: int = 25
    tolerance: float = 0.0001


class HardyLoopRequest(BaseModel):
    unit_system: str = "MKS / L/s"
    flows: List[float]
    lengths: List[float]
    diameters: List[float]
    c_values: List[float]
    max_iter: int = 25
    tolerance: float = 0.00001


class TwoLoopRequest(BaseModel):
    unit_system: str = "MKS / L/s"
    flows: List[float] = Field(..., description="Seven initial pipe flows P1..P7.")
    common_length: float
    common_diameter: float
    c_hw: float = 120.0
    max_iter: int = 25
    tolerance: float = 0.00001


class ThreeReservoirRequest(BaseModel):
    unit_system: str = "MKS / L/s"
    reservoir_heads: List[float]
    demand: float
    initial_head: float
    lengths: List[float]
    diameters: List[float]
    c_values: List[float]
    max_iter: int = 25
    tolerance: float = 0.0001


class PDDRequest(BaseModel):
    unit_system: str = "MKS / L/s"
    required_demand: float
    available_pressure: float
    minimum_pressure: float
    required_pressure: float
    exponent: float = 0.5


class TankEPSRequest(BaseModel):
    unit_system: str = "MKS / L/s"
    diameter: float
    initial_level: float
    min_level: float
    max_level: float
    timestep_hr: float = 1.0
    inflows: List[float]
    outflows: List[float]


class ValveRequest(BaseModel):
    unit_system: str = "MKS / L/s"
    valve_type: str = "PRV"
    upstream_head: float
    setting: float
    flow: float
    diameter: float
    minor_loss_k: float = 0.0


class PumpRequest(BaseModel):
    unit_system: str = "MKS / L/s"
    shutoff_head: float
    design_flow: float
    static_head: float
    pump_curve_k: float
    system_curve_k: float


class PressureZoneRequest(BaseModel):
    unit_system: str = "MKS / L/s"
    source_head: float
    prv_setting: float
    node_elevations: List[float]
    demand_multiplier: float = 1.0
    min_pressure: float = 14.0
    max_pressure: float = 56.0


class LeakageRequest(BaseModel):
    unit_system: str = "MKS / L/s"
    average_pressure: float
    authorized_demand: float
    leakage_coefficient: float
    pressure_exponent: float = 1.0


class WaterAgeRequest(BaseModel):
    unit_system: str = "MKS / L/s"
    pipe_volume: float
    tank_volume: float
    demand: float
    dead_end_factor: float = 2.0


class ChlorineRequest(BaseModel):
    initial_chlorine_mg_l: float
    bulk_decay_per_day: float
    travel_time_hours: float
    wall_decay_factor: float = 0.0


@app.get("/health")
def health():
    return {"ok": True, "service": "hydraulic-solver-teaching-mode-api"}


@app.post("/sessions", dependencies=[Depends(require_auth)])
def create_session(req: LoadNetworkRequest):
    return hc.load_network(req.inp_text, req.title)


@app.get("/sessions/{session_id}", dependencies=[Depends(require_auth)])
def get_session_summary(session_id: str):
    return hc.network_summary(session_id)


@app.delete("/sessions/{session_id}", dependencies=[Depends(require_auth)])
def delete_session(session_id: str):
    return hc.close_session(session_id)


@app.post("/tools/solve_single_pipe", dependencies=[Depends(require_auth)])
def solve_single_pipe(req: SinglePipeRequest):
    return hc.solve_single_pipe(**req.model_dump())


@app.post("/tools/solve_hardy_cross_loop", dependencies=[Depends(require_auth)])
def solve_hardy_cross_loop(req: HardyLoopRequest):
    return hc.solve_hardy_cross_loop(**req.model_dump())


@app.post("/tools/solve_two_loop_hardy_cross", dependencies=[Depends(require_auth)])
def solve_two_loop_hardy_cross(req: TwoLoopRequest):
    return hc.solve_two_loop_hardy_cross(**req.model_dump())


@app.post("/tools/solve_three_reservoir", dependencies=[Depends(require_auth)])
def solve_three_reservoir(req: ThreeReservoirRequest):
    return hc.solve_three_reservoir(**req.model_dump())


@app.post("/tools/solve_pdd_demand", dependencies=[Depends(require_auth)])
def solve_pdd_demand(req: PDDRequest):
    return hc.solve_pdd_demand(**req.model_dump())


@app.post("/tools/simulate_tank_eps", dependencies=[Depends(require_auth)])
def simulate_tank_eps(req: TankEPSRequest):
    return hc.simulate_tank_eps(**req.model_dump())


@app.post("/tools/evaluate_valve_behavior", dependencies=[Depends(require_auth)])
def evaluate_valve_behavior(req: ValveRequest):
    return hc.evaluate_valve_behavior(**req.model_dump())


@app.post("/tools/solve_pump_operating_point", dependencies=[Depends(require_auth)])
def solve_pump_operating_point(req: PumpRequest):
    return hc.solve_pump_operating_point(**req.model_dump())


@app.post("/tools/pressure_zone_analysis", dependencies=[Depends(require_auth)])
def pressure_zone_analysis(req: PressureZoneRequest):
    return hc.pressure_zone_analysis(**req.model_dump())


@app.post("/tools/leakage_nrw_analysis", dependencies=[Depends(require_auth)])
def leakage_nrw_analysis(req: LeakageRequest):
    return hc.leakage_nrw_analysis(**req.model_dump())


@app.post("/tools/water_age_analysis", dependencies=[Depends(require_auth)])
def water_age_analysis(req: WaterAgeRequest):
    return hc.water_age_analysis(**req.model_dump())


@app.post("/tools/chlorine_decay_analysis", dependencies=[Depends(require_auth)])
def chlorine_decay_analysis(req: ChlorineRequest):
    return hc.chlorine_decay_analysis(**req.model_dump())


@app.get("/tools/generate_epanet_validation_inp", dependencies=[Depends(require_auth)])
def generate_epanet_validation_inp(case: str = "three_reservoir_mks"):
    return hc.generate_epanet_validation_inp(case)