Update app.py
Browse files
app.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
# ==============================================================================
|
| 2 |
-
#
|
| 3 |
# ==============================================================================
|
| 4 |
-
#
|
| 5 |
-
#
|
| 6 |
-
#
|
| 7 |
|
| 8 |
from fastapi import FastAPI, HTTPException, Request, Depends, Query
|
| 9 |
from fastapi.responses import JSONResponse, HTMLResponse
|
|
@@ -16,47 +16,47 @@ import os
|
|
| 16 |
import time
|
| 17 |
import pickle
|
| 18 |
from datetime import datetime
|
| 19 |
-
from sklearn.ensemble import RandomForestRegressor #
|
| 20 |
from pydantic import BaseModel, ValidationError, Field, field_validator, model_validator
|
| 21 |
from typing import Any, Dict, List, Optional, Union
|
| 22 |
from scipy import stats
|
| 23 |
import json
|
| 24 |
|
| 25 |
# ==============================================================================
|
| 26 |
-
#
|
| 27 |
# ==============================================================================
|
| 28 |
|
| 29 |
app = FastAPI(
|
| 30 |
title="UCS Prediction API with Uncertainty Quantification",
|
| 31 |
description="""
|
| 32 |
-
**API
|
| 33 |
|
| 34 |
-
|
| 35 |
"Prediction of Unconfined Compressive Strength in Cement-Treated Soil: A Machine Learning Approach".
|
| 36 |
|
| 37 |
-
**
|
| 38 |
-
-
|
| 39 |
-
-
|
| 40 |
-
-
|
| 41 |
-
-
|
| 42 |
|
| 43 |
-
**
|
| 44 |
""",
|
| 45 |
version="2.0.0",
|
| 46 |
contact={
|
| 47 |
-
"name": "
|
| 48 |
"email": "iancu-bogdan.teodoru@academic.tuiasi.ro",
|
| 49 |
}
|
| 50 |
)
|
| 51 |
|
| 52 |
-
#
|
| 53 |
app.add_middleware(
|
| 54 |
CORSMiddleware,
|
| 55 |
allow_origins=[
|
| 56 |
"http://www.bi4e-at.tuiasi.ro",
|
| 57 |
"https://www.bi4e-at.tuiasi.ro",
|
| 58 |
-
"http://localhost:3000", #
|
| 59 |
-
"http://localhost:8000" #
|
| 60 |
],
|
| 61 |
allow_credentials=True,
|
| 62 |
allow_methods=["GET", "POST", "OPTIONS"],
|
|
@@ -64,19 +64,19 @@ app.add_middleware(
|
|
| 64 |
)
|
| 65 |
|
| 66 |
# ==============================================================================
|
| 67 |
-
#
|
| 68 |
# ==============================================================================
|
| 69 |
|
| 70 |
-
#
|
| 71 |
MODELS_DIR = "./models_for_deployment"
|
| 72 |
PRIMARY_MODEL_PATH = os.path.join(MODELS_DIR, "rf_primary_model.joblib")
|
| 73 |
UNCERTAINTY_MODEL_PATH = os.path.join(MODELS_DIR, "rf_uncertainty_model.joblib")
|
| 74 |
METADATA_PATH = os.path.join(MODELS_DIR, "system_metadata.pkl")
|
| 75 |
|
| 76 |
-
#
|
| 77 |
-
DEFAULT_FEATURE_ORDER = ['
|
| 78 |
|
| 79 |
-
#
|
| 80 |
primary_model = None
|
| 81 |
uncertainty_model = None
|
| 82 |
system_metadata = None
|
|
@@ -84,43 +84,43 @@ FEATURE_ORDER = None
|
|
| 84 |
|
| 85 |
def load_uncertainty_system():
|
| 86 |
"""
|
| 87 |
-
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
"""
|
| 94 |
global primary_model, uncertainty_model, system_metadata, FEATURE_ORDER
|
| 95 |
|
| 96 |
-
print("🚀
|
| 97 |
start_time = time.time()
|
| 98 |
|
| 99 |
try:
|
| 100 |
-
#
|
| 101 |
if os.path.exists(PRIMARY_MODEL_PATH):
|
| 102 |
primary_model = joblib.load(PRIMARY_MODEL_PATH)
|
| 103 |
-
print(f"✅
|
| 104 |
else:
|
| 105 |
-
raise FileNotFoundError(f"
|
| 106 |
|
| 107 |
-
#
|
| 108 |
if os.path.exists(UNCERTAINTY_MODEL_PATH):
|
| 109 |
uncertainty_model = joblib.load(UNCERTAINTY_MODEL_PATH)
|
| 110 |
-
print(f"✅
|
| 111 |
else:
|
| 112 |
-
raise FileNotFoundError(f"
|
| 113 |
|
| 114 |
-
#
|
| 115 |
if os.path.exists(METADATA_PATH):
|
| 116 |
with open(METADATA_PATH, 'rb') as f:
|
| 117 |
system_metadata = pickle.load(f)
|
| 118 |
-
print(f"✅
|
| 119 |
else:
|
| 120 |
-
print("⚠️
|
| 121 |
system_metadata = {"feature_names": DEFAULT_FEATURE_ORDER}
|
| 122 |
|
| 123 |
-
#
|
| 124 |
if hasattr(primary_model, 'feature_names_in_'):
|
| 125 |
FEATURE_ORDER = primary_model.feature_names_in_
|
| 126 |
elif system_metadata and 'feature_names' in system_metadata:
|
|
@@ -128,83 +128,83 @@ def load_uncertainty_system():
|
|
| 128 |
else:
|
| 129 |
FEATURE_ORDER = np.array(DEFAULT_FEATURE_ORDER)
|
| 130 |
|
| 131 |
-
#
|
| 132 |
validation_result = validate_models_compatibility()
|
| 133 |
if not validation_result:
|
| 134 |
-
raise ValueError("
|
| 135 |
|
| 136 |
load_time = time.time() - start_time
|
| 137 |
-
print(f"🎉
|
| 138 |
print(f"📊 Features: {FEATURE_ORDER.tolist()}")
|
| 139 |
|
| 140 |
return True
|
| 141 |
|
| 142 |
except Exception as e:
|
| 143 |
-
print(f"❌
|
| 144 |
import traceback
|
| 145 |
print(traceback.format_exc())
|
| 146 |
return False
|
| 147 |
|
| 148 |
def validate_models_compatibility():
|
| 149 |
"""
|
| 150 |
-
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
| 154 |
"""
|
| 155 |
try:
|
| 156 |
-
# Test
|
| 157 |
test_input = np.array([[5.0, 14.0, 1.0]]) # cement, curing, compaction
|
| 158 |
|
| 159 |
-
#
|
| 160 |
primary_pred = primary_model.predict(test_input)[0]
|
| 161 |
|
| 162 |
-
#
|
| 163 |
uncertainty_input = np.column_stack([test_input, [[primary_pred]]])
|
| 164 |
uncertainty_pred = uncertainty_model.predict(uncertainty_input)[0]
|
| 165 |
|
| 166 |
-
#
|
| 167 |
assert isinstance(primary_pred, (int, float, np.number))
|
| 168 |
assert isinstance(uncertainty_pred, (int, float, np.number))
|
| 169 |
assert primary_pred > 0
|
| 170 |
assert uncertainty_pred > 0
|
| 171 |
|
| 172 |
-
print(f"✅
|
| 173 |
return True
|
| 174 |
|
| 175 |
except Exception as e:
|
| 176 |
-
print(f"❌
|
| 177 |
return False
|
| 178 |
|
| 179 |
-
#
|
| 180 |
system_loaded = load_uncertainty_system()
|
| 181 |
|
| 182 |
# ==============================================================================
|
| 183 |
-
#
|
| 184 |
# ==============================================================================
|
| 185 |
|
| 186 |
class SoilInput(BaseModel):
|
| 187 |
"""
|
| 188 |
-
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
"""
|
| 193 |
-
|
| 194 |
...,
|
| 195 |
-
description="
|
| 196 |
ge=0, le=15,
|
| 197 |
example=5.0
|
| 198 |
)
|
| 199 |
curing_period: float = Field(
|
| 200 |
...,
|
| 201 |
-
description="
|
| 202 |
ge=0, le=90,
|
| 203 |
example=28.0
|
| 204 |
)
|
| 205 |
compaction_rate: float = Field(
|
| 206 |
...,
|
| 207 |
-
description="
|
| 208 |
ge=0.5, le=2.0,
|
| 209 |
example=1.0
|
| 210 |
)
|
|
@@ -212,113 +212,113 @@ class SoilInput(BaseModel):
|
|
| 212 |
@model_validator(mode="after")
|
| 213 |
def validate_cement_curing_relationship(self):
|
| 214 |
"""
|
| 215 |
-
|
| 216 |
|
| 217 |
-
|
| 218 |
-
|
| 219 |
"""
|
| 220 |
-
if self.
|
| 221 |
self.curing_period = 0
|
| 222 |
-
elif self.
|
| 223 |
-
raise ValueError("
|
| 224 |
return self
|
| 225 |
|
| 226 |
class Config:
|
| 227 |
json_schema_extra = {
|
| 228 |
"example": {
|
| 229 |
-
"
|
| 230 |
"curing_period": 28.0,
|
| 231 |
"compaction_rate": 1.0
|
| 232 |
}
|
| 233 |
}
|
| 234 |
|
| 235 |
class ConfidenceInterval(BaseModel):
|
| 236 |
-
"""
|
| 237 |
-
lower: float = Field(..., description="
|
| 238 |
-
upper: float = Field(..., description="
|
| 239 |
-
width: float = Field(..., description="
|
| 240 |
|
| 241 |
class UncertaintyPredictionResponse(BaseModel):
|
| 242 |
"""
|
| 243 |
-
|
| 244 |
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
"""
|
| 249 |
-
success: bool = Field(..., description="
|
| 250 |
|
| 251 |
-
#
|
| 252 |
-
central_prediction: float = Field(..., description="
|
| 253 |
-
units: str = Field(default="kPa", description="
|
| 254 |
|
| 255 |
-
#
|
| 256 |
-
uncertainty_estimate: float = Field(..., description="
|
| 257 |
-
relative_uncertainty: float = Field(..., description="
|
| 258 |
|
| 259 |
-
#
|
| 260 |
confidence_intervals: Dict[str, ConfidenceInterval] = Field(
|
| 261 |
...,
|
| 262 |
-
description="
|
| 263 |
)
|
| 264 |
|
| 265 |
-
#
|
| 266 |
-
interpretation: Dict[str, str] = Field(..., description="
|
| 267 |
|
| 268 |
# Metadata
|
| 269 |
-
input_parameters: Dict[str, float] = Field(..., description="
|
| 270 |
-
prediction_time_ms: Optional[float] = Field(None, description="
|
| 271 |
-
model_info: Optional[Dict[str, Any]] = Field(None, description="
|
| 272 |
|
| 273 |
class SensitivityAnalysisRequest(BaseModel):
|
| 274 |
-
"""
|
| 275 |
base_parameters: SoilInput
|
| 276 |
-
parameter_to_vary: str = Field(..., pattern="^(
|
| 277 |
-
variation_range: float = Field(default=10.0, ge=1.0, le=50.0, description="
|
| 278 |
-
num_points: int = Field(default=11, ge=5, le=21, description="
|
| 279 |
|
| 280 |
# ==============================================================================
|
| 281 |
-
#
|
| 282 |
# ==============================================================================
|
| 283 |
|
| 284 |
def predict_with_uncertainty(input_data: np.ndarray,
|
| 285 |
confidence_levels: List[float] = [0.68, 0.80, 0.90, 0.95]) -> Dict[str, Any]:
|
| 286 |
"""
|
| 287 |
-
|
| 288 |
|
| 289 |
-
|
| 290 |
-
1.
|
| 291 |
-
2.
|
| 292 |
-
3.
|
| 293 |
|
| 294 |
Args:
|
| 295 |
-
input_data:
|
| 296 |
-
confidence_levels:
|
| 297 |
|
| 298 |
Returns:
|
| 299 |
-
|
| 300 |
"""
|
| 301 |
|
| 302 |
-
#
|
| 303 |
central_prediction = primary_model.predict(input_data)[0]
|
| 304 |
|
| 305 |
-
#
|
| 306 |
-
#
|
| 307 |
-
# features
|
| 308 |
uncertainty_input = np.column_stack([input_data, [[central_prediction]]])
|
| 309 |
|
| 310 |
-
#
|
| 311 |
uncertainty_estimate = uncertainty_model.predict(uncertainty_input)[0]
|
| 312 |
|
| 313 |
-
#
|
| 314 |
confidence_intervals = {}
|
| 315 |
|
| 316 |
for conf_level in confidence_levels:
|
| 317 |
-
# Z-score
|
| 318 |
-
#
|
| 319 |
z_score = stats.norm.ppf((1 + conf_level) / 2)
|
| 320 |
|
| 321 |
-
#
|
| 322 |
margin = z_score * uncertainty_estimate
|
| 323 |
|
| 324 |
confidence_intervals[f'{conf_level:.0%}'] = ConfidenceInterval(
|
|
@@ -327,7 +327,7 @@ def predict_with_uncertainty(input_data: np.ndarray,
|
|
| 327 |
width=float(2 * margin)
|
| 328 |
)
|
| 329 |
|
| 330 |
-
#
|
| 331 |
relative_uncertainty = (uncertainty_estimate / central_prediction) * 100 if central_prediction != 0 else 0
|
| 332 |
|
| 333 |
return {
|
|
@@ -340,65 +340,65 @@ def predict_with_uncertainty(input_data: np.ndarray,
|
|
| 340 |
def generate_interpretation_guide(central_prediction: float, uncertainty_estimate: float,
|
| 341 |
confidence_intervals: Dict[str, ConfidenceInterval]) -> Dict[str, str]:
|
| 342 |
"""
|
| 343 |
-
|
| 344 |
|
| 345 |
-
|
| 346 |
-
|
| 347 |
"""
|
| 348 |
|
| 349 |
-
#
|
| 350 |
interval_95 = confidence_intervals.get('95%')
|
| 351 |
|
| 352 |
-
#
|
| 353 |
relative_unc = (uncertainty_estimate / central_prediction) * 100
|
| 354 |
|
| 355 |
if relative_unc <= 10:
|
| 356 |
-
confidence_level = "
|
| 357 |
-
reliability_desc = "
|
| 358 |
elif relative_unc <= 20:
|
| 359 |
-
confidence_level = "
|
| 360 |
-
reliability_desc = "
|
| 361 |
elif relative_unc <= 30:
|
| 362 |
-
confidence_level = "
|
| 363 |
-
reliability_desc = "
|
| 364 |
else:
|
| 365 |
-
confidence_level = "
|
| 366 |
-
reliability_desc = "
|
| 367 |
|
| 368 |
interpretation = {
|
| 369 |
-
"central_prediction": f"
|
| 370 |
|
| 371 |
-
"uncertainty": f"
|
| 372 |
-
f"
|
| 373 |
|
| 374 |
-
"confidence_95": f"
|
| 375 |
-
f"{interval_95.lower:.0f}
|
| 376 |
|
| 377 |
"reliability": reliability_desc,
|
| 378 |
|
| 379 |
-
"practical_guidance": f"
|
| 380 |
-
f"
|
| 381 |
}
|
| 382 |
|
| 383 |
return interpretation
|
| 384 |
|
| 385 |
async def validate_models_loaded():
|
| 386 |
-
"""Dependency function
|
| 387 |
if not system_loaded or primary_model is None or uncertainty_model is None:
|
| 388 |
raise HTTPException(
|
| 389 |
status_code=503,
|
| 390 |
-
detail="
|
| 391 |
)
|
| 392 |
return True
|
| 393 |
|
| 394 |
# ==============================================================================
|
| 395 |
-
#
|
| 396 |
# ==============================================================================
|
| 397 |
|
| 398 |
-
@app.get("/", response_class=HTMLResponse, summary="
|
| 399 |
async def root():
|
| 400 |
"""
|
| 401 |
-
|
| 402 |
"""
|
| 403 |
return """
|
| 404 |
<!DOCTYPE html>
|
|
@@ -413,61 +413,61 @@ async def root():
|
|
| 413 |
</head>
|
| 414 |
<body>
|
| 415 |
<h1 class="header">🏗️ UCS Prediction API with Uncertainty Quantification</h1>
|
| 416 |
-
<p>API
|
| 417 |
|
| 418 |
-
<h2>📋
|
| 419 |
<div class="endpoint">
|
| 420 |
-
<strong>POST /predict</strong> -
|
| 421 |
</div>
|
| 422 |
<div class="endpoint">
|
| 423 |
-
<strong>POST /sensitivity-analysis</strong> -
|
| 424 |
</div>
|
| 425 |
<div class="endpoint">
|
| 426 |
-
<strong>GET /status</strong> -
|
| 427 |
</div>
|
| 428 |
<div class="endpoint">
|
| 429 |
-
<strong>GET /model-info</strong> -
|
| 430 |
</div>
|
| 431 |
|
| 432 |
-
<h2>📖
|
| 433 |
-
<p><a href="/docs">Swagger UI -
|
| 434 |
-
<p><a href="/redoc">ReDoc -
|
| 435 |
|
| 436 |
<footer style="margin-top: 40px; color: #666;">
|
| 437 |
-
<p>
|
| 438 |
</footer>
|
| 439 |
</body>
|
| 440 |
</html>
|
| 441 |
"""
|
| 442 |
|
| 443 |
@app.post("/predict", response_model=UncertaintyPredictionResponse,
|
| 444 |
-
summary="
|
| 445 |
async def predict_ucs_with_uncertainty(
|
| 446 |
soil_data: SoilInput,
|
| 447 |
-
include_model_info: bool = Query(False, description="Include
|
| 448 |
_: bool = Depends(validate_models_loaded)
|
| 449 |
):
|
| 450 |
"""
|
| 451 |
-
**
|
| 452 |
|
| 453 |
-
|
| 454 |
-
|
| 455 |
|
| 456 |
-
**
|
| 457 |
-
- **
|
| 458 |
-
- **curing_period**:
|
| 459 |
-
- **compaction_rate**:
|
| 460 |
|
| 461 |
-
**
|
| 462 |
-
-
|
| 463 |
-
-
|
| 464 |
-
-
|
| 465 |
-
-
|
| 466 |
|
| 467 |
-
**
|
| 468 |
```json
|
| 469 |
{
|
| 470 |
-
"
|
| 471 |
"curing_period": 28,
|
| 472 |
"compaction_rate": 1.0
|
| 473 |
}
|
|
@@ -477,32 +477,32 @@ async def predict_ucs_with_uncertainty(
|
|
| 477 |
try:
|
| 478 |
start_time = time.time()
|
| 479 |
|
| 480 |
-
#
|
| 481 |
input_data = soil_data.dict()
|
| 482 |
input_df = pd.DataFrame([input_data])
|
| 483 |
|
| 484 |
-
#
|
| 485 |
prediction_df = pd.DataFrame()
|
| 486 |
for feature in FEATURE_ORDER:
|
| 487 |
if feature in input_df.columns:
|
| 488 |
prediction_df[feature] = input_df[feature]
|
| 489 |
else:
|
| 490 |
-
raise ValueError(f"Feature
|
| 491 |
|
| 492 |
-
#
|
| 493 |
input_array = prediction_df.values
|
| 494 |
|
| 495 |
-
#
|
| 496 |
prediction_result = predict_with_uncertainty(input_array)
|
| 497 |
|
| 498 |
-
#
|
| 499 |
interpretation = generate_interpretation_guide(
|
| 500 |
prediction_result['central_prediction'],
|
| 501 |
prediction_result['uncertainty_estimate'],
|
| 502 |
prediction_result['confidence_intervals']
|
| 503 |
)
|
| 504 |
|
| 505 |
-
#
|
| 506 |
model_info = None
|
| 507 |
if include_model_info:
|
| 508 |
model_info = {
|
|
@@ -512,10 +512,10 @@ async def predict_ucs_with_uncertainty(
|
|
| 512 |
"system_metadata": system_metadata if system_metadata else "Not available"
|
| 513 |
}
|
| 514 |
|
| 515 |
-
#
|
| 516 |
processing_time = (time.time() - start_time) * 1000
|
| 517 |
|
| 518 |
-
#
|
| 519 |
return UncertaintyPredictionResponse(
|
| 520 |
success=True,
|
| 521 |
central_prediction=prediction_result['central_prediction'],
|
|
@@ -530,62 +530,62 @@ async def predict_ucs_with_uncertainty(
|
|
| 530 |
)
|
| 531 |
|
| 532 |
except ValueError as ve:
|
| 533 |
-
raise HTTPException(status_code=400, detail=f"
|
| 534 |
except Exception as e:
|
| 535 |
-
raise HTTPException(status_code=500, detail=f"
|
| 536 |
|
| 537 |
-
@app.post("/sensitivity-analysis", summary="
|
| 538 |
async def perform_sensitivity_analysis(
|
| 539 |
request: SensitivityAnalysisRequest,
|
| 540 |
_: bool = Depends(validate_models_loaded)
|
| 541 |
):
|
| 542 |
"""
|
| 543 |
-
**
|
| 544 |
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
"""
|
| 549 |
|
| 550 |
try:
|
| 551 |
base_params = request.base_parameters.dict()
|
| 552 |
param_to_vary = request.parameter_to_vary
|
| 553 |
-
variation_range = request.variation_range / 100 #
|
| 554 |
num_points = request.num_points
|
| 555 |
|
| 556 |
-
#
|
| 557 |
base_value = base_params[param_to_vary]
|
| 558 |
|
| 559 |
-
#
|
| 560 |
min_variation = base_value * (1 - variation_range)
|
| 561 |
max_variation = base_value * (1 + variation_range)
|
| 562 |
|
| 563 |
-
#
|
| 564 |
-
if param_to_vary == "
|
| 565 |
min_variation = max(0, min_variation)
|
| 566 |
max_variation = min(15, max_variation)
|
| 567 |
elif param_to_vary == "curing_period":
|
| 568 |
-
min_variation = max(0 if base_params["
|
| 569 |
max_variation = min(90, max_variation)
|
| 570 |
elif param_to_vary == "compaction_rate":
|
| 571 |
min_variation = max(0.5, min_variation)
|
| 572 |
max_variation = min(2.0, max_variation)
|
| 573 |
|
| 574 |
-
#
|
| 575 |
variation_values = np.linspace(min_variation, max_variation, num_points)
|
| 576 |
|
| 577 |
results = []
|
| 578 |
|
| 579 |
for value in variation_values:
|
| 580 |
-
#
|
| 581 |
modified_params = base_params.copy()
|
| 582 |
modified_params[param_to_vary] = float(value)
|
| 583 |
|
| 584 |
-
#
|
| 585 |
-
if modified_params["
|
| 586 |
modified_params["curing_period"] = 0
|
| 587 |
|
| 588 |
-
#
|
| 589 |
input_df = pd.DataFrame([modified_params])
|
| 590 |
prediction_df = pd.DataFrame()
|
| 591 |
for feature in FEATURE_ORDER:
|
|
@@ -603,7 +603,7 @@ async def perform_sensitivity_analysis(
|
|
| 603 |
"confidence_95_upper": prediction_result['confidence_intervals']['95%'].upper
|
| 604 |
})
|
| 605 |
|
| 606 |
-
#
|
| 607 |
predictions = [r["central_prediction"] for r in results]
|
| 608 |
uncertainties = [r["uncertainty_estimate"] for r in results]
|
| 609 |
|
|
@@ -633,23 +633,23 @@ async def perform_sensitivity_analysis(
|
|
| 633 |
"sensitivity_data": results,
|
| 634 |
"sensitivity_statistics": sensitivity_stats,
|
| 635 |
"interpretation": {
|
| 636 |
-
"parameter_impact": f"
|
| 637 |
-
f"
|
| 638 |
-
"recommendation": "
|
| 639 |
if sensitivity_stats['prediction_sensitivity']['relative_change'] > 10
|
| 640 |
-
else "
|
| 641 |
}
|
| 642 |
}
|
| 643 |
|
| 644 |
except Exception as e:
|
| 645 |
-
raise HTTPException(status_code=500, detail=f"
|
| 646 |
|
| 647 |
-
@app.get("/status", summary="
|
| 648 |
async def get_system_status():
|
| 649 |
"""
|
| 650 |
-
**
|
| 651 |
|
| 652 |
-
Useful
|
| 653 |
"""
|
| 654 |
|
| 655 |
status_info = {
|
|
@@ -667,7 +667,7 @@ async def get_system_status():
|
|
| 667 |
}
|
| 668 |
}
|
| 669 |
|
| 670 |
-
#
|
| 671 |
if system_loaded:
|
| 672 |
try:
|
| 673 |
test_result = validate_models_compatibility()
|
|
@@ -677,12 +677,12 @@ async def get_system_status():
|
|
| 677 |
|
| 678 |
return status_info
|
| 679 |
|
| 680 |
-
@app.get("/model-info", summary="
|
| 681 |
async def get_model_information(_: bool = Depends(validate_models_loaded)):
|
| 682 |
"""
|
| 683 |
-
**
|
| 684 |
|
| 685 |
-
|
| 686 |
"""
|
| 687 |
|
| 688 |
try:
|
|
@@ -705,7 +705,7 @@ async def get_model_information(_: bool = Depends(validate_models_loaded)):
|
|
| 705 |
"feature_engineering": "Feature augmentation for uncertainty model (original features + central prediction)"
|
| 706 |
},
|
| 707 |
"valid_ranges": {
|
| 708 |
-
"
|
| 709 |
"curing_period": {"min": 0, "max": 90, "units": "days", "note": "0 only valid for 0% cement"},
|
| 710 |
"compaction_rate": {"min": 0.5, "max": 2.0, "units": "mm/min", "note": "Within experimental range"}
|
| 711 |
},
|
|
@@ -718,7 +718,7 @@ async def get_model_information(_: bool = Depends(validate_models_loaded)):
|
|
| 718 |
}
|
| 719 |
}
|
| 720 |
|
| 721 |
-
#
|
| 722 |
if system_metadata:
|
| 723 |
model_info["training_metadata"] = {
|
| 724 |
"training_samples": system_metadata.get("n_training_samples", "Unknown"),
|
|
@@ -729,17 +729,17 @@ async def get_model_information(_: bool = Depends(validate_models_loaded)):
|
|
| 729 |
return model_info
|
| 730 |
|
| 731 |
except Exception as e:
|
| 732 |
-
raise HTTPException(status_code=500, detail=f"
|
| 733 |
|
| 734 |
# ==============================================================================
|
| 735 |
-
#
|
| 736 |
# ==============================================================================
|
| 737 |
|
| 738 |
@app.exception_handler(ValidationError)
|
| 739 |
async def validation_exception_handler(request: Request, exc: ValidationError):
|
| 740 |
"""
|
| 741 |
-
|
| 742 |
-
|
| 743 |
"""
|
| 744 |
|
| 745 |
friendly_errors = []
|
|
@@ -747,13 +747,13 @@ async def validation_exception_handler(request: Request, exc: ValidationError):
|
|
| 747 |
field = " -> ".join(str(loc) for loc in error.get('loc', []))
|
| 748 |
message = error.get('msg', '')
|
| 749 |
|
| 750 |
-
#
|
| 751 |
if "greater than or equal" in message:
|
| 752 |
-
message = f"
|
| 753 |
elif "less than or equal" in message:
|
| 754 |
-
message = f"
|
| 755 |
elif "string does not match regex" in message:
|
| 756 |
-
message = f"
|
| 757 |
|
| 758 |
friendly_errors.append({
|
| 759 |
"field": field,
|
|
@@ -765,49 +765,49 @@ async def validation_exception_handler(request: Request, exc: ValidationError):
|
|
| 765 |
status_code=422,
|
| 766 |
content={
|
| 767 |
"success": False,
|
| 768 |
-
"error": "
|
| 769 |
"details": friendly_errors,
|
| 770 |
-
"help": "
|
| 771 |
}
|
| 772 |
)
|
| 773 |
|
| 774 |
@app.exception_handler(Exception)
|
| 775 |
async def general_exception_handler(request: Request, exc: Exception):
|
| 776 |
"""
|
| 777 |
-
|
| 778 |
"""
|
| 779 |
return JSONResponse(
|
| 780 |
status_code=500,
|
| 781 |
content={
|
| 782 |
"success": False,
|
| 783 |
-
"error": "
|
| 784 |
-
"message": "
|
| 785 |
-
"request_id": str(time.time()) #
|
| 786 |
}
|
| 787 |
)
|
| 788 |
|
| 789 |
# ==============================================================================
|
| 790 |
-
#
|
| 791 |
# ==============================================================================
|
| 792 |
|
| 793 |
@app.on_event("startup")
|
| 794 |
async def startup_event():
|
| 795 |
"""
|
| 796 |
-
|
| 797 |
-
|
| 798 |
"""
|
| 799 |
-
print("🚀
|
| 800 |
|
| 801 |
if system_loaded:
|
| 802 |
-
print("✅
|
| 803 |
-
print(f"📊 Features
|
| 804 |
else:
|
| 805 |
-
print("❌
|
| 806 |
-
print("
|
| 807 |
|
| 808 |
-
print("🌐 API
|
| 809 |
|
| 810 |
if __name__ == "__main__":
|
| 811 |
-
#
|
| 812 |
import uvicorn
|
| 813 |
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)
|
|
|
|
| 1 |
# ==============================================================================
|
| 2 |
+
# FASTAPI APPLICATION WITH INTEGRATED UNCERTAINTY SYSTEM
|
| 3 |
# ==============================================================================
|
| 4 |
+
# This application replaces the simple prediction system with an advanced
|
| 5 |
+
# uncertainty quantification framework, providing engineers not only predictions
|
| 6 |
+
# but also calibrated confidence intervals for informed decision making
|
| 7 |
|
| 8 |
from fastapi import FastAPI, HTTPException, Request, Depends, Query
|
| 9 |
from fastapi.responses import JSONResponse, HTMLResponse
|
|
|
|
| 16 |
import time
|
| 17 |
import pickle
|
| 18 |
from datetime import datetime
|
| 19 |
+
from sklearn.ensemble import RandomForestRegressor # Required for deserialization
|
| 20 |
from pydantic import BaseModel, ValidationError, Field, field_validator, model_validator
|
| 21 |
from typing import Any, Dict, List, Optional, Union
|
| 22 |
from scipy import stats
|
| 23 |
import json
|
| 24 |
|
| 25 |
# ==============================================================================
|
| 26 |
+
# FASTAPI APPLICATION CONFIGURATION
|
| 27 |
# ==============================================================================
|
| 28 |
|
| 29 |
app = FastAPI(
|
| 30 |
title="UCS Prediction API with Uncertainty Quantification",
|
| 31 |
description="""
|
| 32 |
+
**Advanced API for predicting Unconfined Compressive Strength (UCS) of cement-stabilized soils**
|
| 33 |
|
| 34 |
+
This application implements the uncertainty quantification system developed in the research
|
| 35 |
"Prediction of Unconfined Compressive Strength in Cement-Treated Soil: A Machine Learning Approach".
|
| 36 |
|
| 37 |
+
**Main features:**
|
| 38 |
+
- Accurate UCS predictions using optimized Random Forest
|
| 39 |
+
- Complete uncertainty quantification with calibrated confidence intervals
|
| 40 |
+
- Sensitivity analysis for parameter optimization
|
| 41 |
+
- Interpretability through feature importance analysis
|
| 42 |
|
| 43 |
+
**Developed by:** Research Team - Technical University Gheorghe Asachi of Iași
|
| 44 |
""",
|
| 45 |
version="2.0.0",
|
| 46 |
contact={
|
| 47 |
+
"name": "UCS Development Team",
|
| 48 |
"email": "iancu-bogdan.teodoru@academic.tuiasi.ro",
|
| 49 |
}
|
| 50 |
)
|
| 51 |
|
| 52 |
+
# CORS configuration for web interface
|
| 53 |
app.add_middleware(
|
| 54 |
CORSMiddleware,
|
| 55 |
allow_origins=[
|
| 56 |
"http://www.bi4e-at.tuiasi.ro",
|
| 57 |
"https://www.bi4e-at.tuiasi.ro",
|
| 58 |
+
"http://localhost:3000", # For local development
|
| 59 |
+
"http://localhost:8000" # For local testing
|
| 60 |
],
|
| 61 |
allow_credentials=True,
|
| 62 |
allow_methods=["GET", "POST", "OPTIONS"],
|
|
|
|
| 64 |
)
|
| 65 |
|
| 66 |
# ==============================================================================
|
| 67 |
+
# MODEL CONFIGURATION AND SYSTEM LOADING
|
| 68 |
# ==============================================================================
|
| 69 |
|
| 70 |
+
# Paths to serialized models
|
| 71 |
MODELS_DIR = "./models_for_deployment"
|
| 72 |
PRIMARY_MODEL_PATH = os.path.join(MODELS_DIR, "rf_primary_model.joblib")
|
| 73 |
UNCERTAINTY_MODEL_PATH = os.path.join(MODELS_DIR, "rf_uncertainty_model.joblib")
|
| 74 |
METADATA_PATH = os.path.join(MODELS_DIR, "system_metadata.pkl")
|
| 75 |
|
| 76 |
+
# Feature order (critical for compatibility)
|
| 77 |
+
DEFAULT_FEATURE_ORDER = ['cement_percent', 'curing_period', 'compaction_rate']
|
| 78 |
|
| 79 |
+
# Global variables for system
|
| 80 |
primary_model = None
|
| 81 |
uncertainty_model = None
|
| 82 |
system_metadata = None
|
|
|
|
| 84 |
|
| 85 |
def load_uncertainty_system():
|
| 86 |
"""
|
| 87 |
+
Loads and validates the entire uncertainty system.
|
| 88 |
|
| 89 |
+
This function orchestrates the loading of all system components
|
| 90 |
+
and performs basic validations to ensure proper operation.
|
| 91 |
+
The process is designed to be robust and provide detailed information
|
| 92 |
+
about any issues encountered during loading.
|
| 93 |
"""
|
| 94 |
global primary_model, uncertainty_model, system_metadata, FEATURE_ORDER
|
| 95 |
|
| 96 |
+
print("🚀 Loading uncertainty system...")
|
| 97 |
start_time = time.time()
|
| 98 |
|
| 99 |
try:
|
| 100 |
+
# Load primary model
|
| 101 |
if os.path.exists(PRIMARY_MODEL_PATH):
|
| 102 |
primary_model = joblib.load(PRIMARY_MODEL_PATH)
|
| 103 |
+
print(f"✅ Primary model loaded: {type(primary_model).__name__}")
|
| 104 |
else:
|
| 105 |
+
raise FileNotFoundError(f"Primary model not found at: {PRIMARY_MODEL_PATH}")
|
| 106 |
|
| 107 |
+
# Load uncertainty model
|
| 108 |
if os.path.exists(UNCERTAINTY_MODEL_PATH):
|
| 109 |
uncertainty_model = joblib.load(UNCERTAINTY_MODEL_PATH)
|
| 110 |
+
print(f"✅ Uncertainty model loaded: {type(uncertainty_model).__name__}")
|
| 111 |
else:
|
| 112 |
+
raise FileNotFoundError(f"Uncertainty model not found at: {UNCERTAINTY_MODEL_PATH}")
|
| 113 |
|
| 114 |
+
# Load system metadata
|
| 115 |
if os.path.exists(METADATA_PATH):
|
| 116 |
with open(METADATA_PATH, 'rb') as f:
|
| 117 |
system_metadata = pickle.load(f)
|
| 118 |
+
print(f"✅ System metadata loaded: {len(system_metadata)} keys")
|
| 119 |
else:
|
| 120 |
+
print("⚠️ System metadata not found, using default values")
|
| 121 |
system_metadata = {"feature_names": DEFAULT_FEATURE_ORDER}
|
| 122 |
|
| 123 |
+
# Determine feature order
|
| 124 |
if hasattr(primary_model, 'feature_names_in_'):
|
| 125 |
FEATURE_ORDER = primary_model.feature_names_in_
|
| 126 |
elif system_metadata and 'feature_names' in system_metadata:
|
|
|
|
| 128 |
else:
|
| 129 |
FEATURE_ORDER = np.array(DEFAULT_FEATURE_ORDER)
|
| 130 |
|
| 131 |
+
# Validate model compatibility
|
| 132 |
validation_result = validate_models_compatibility()
|
| 133 |
if not validation_result:
|
| 134 |
+
raise ValueError("Models are not compatible with each other")
|
| 135 |
|
| 136 |
load_time = time.time() - start_time
|
| 137 |
+
print(f"🎉 Uncertainty system loaded successfully in {load_time:.2f} seconds!")
|
| 138 |
print(f"📊 Features: {FEATURE_ORDER.tolist()}")
|
| 139 |
|
| 140 |
return True
|
| 141 |
|
| 142 |
except Exception as e:
|
| 143 |
+
print(f"❌ Error loading system: {str(e)}")
|
| 144 |
import traceback
|
| 145 |
print(traceback.format_exc())
|
| 146 |
return False
|
| 147 |
|
| 148 |
def validate_models_compatibility():
|
| 149 |
"""
|
| 150 |
+
Validates that models are compatible and work together.
|
| 151 |
|
| 152 |
+
This validation includes dimensional compatibility tests,
|
| 153 |
+
data type checks and a complete functional test.
|
| 154 |
"""
|
| 155 |
try:
|
| 156 |
+
# Test with synthetic data
|
| 157 |
test_input = np.array([[5.0, 14.0, 1.0]]) # cement, curing, compaction
|
| 158 |
|
| 159 |
+
# Test primary model
|
| 160 |
primary_pred = primary_model.predict(test_input)[0]
|
| 161 |
|
| 162 |
+
# Test uncertainty model with feature augmentation
|
| 163 |
uncertainty_input = np.column_stack([test_input, [[primary_pred]]])
|
| 164 |
uncertainty_pred = uncertainty_model.predict(uncertainty_input)[0]
|
| 165 |
|
| 166 |
+
# Check that results are numeric and reasonable
|
| 167 |
assert isinstance(primary_pred, (int, float, np.number))
|
| 168 |
assert isinstance(uncertainty_pred, (int, float, np.number))
|
| 169 |
assert primary_pred > 0
|
| 170 |
assert uncertainty_pred > 0
|
| 171 |
|
| 172 |
+
print(f"✅ Compatibility test: UCS={primary_pred:.1f} kPa, σ={uncertainty_pred:.1f} kPa")
|
| 173 |
return True
|
| 174 |
|
| 175 |
except Exception as e:
|
| 176 |
+
print(f"❌ Compatibility test failed: {str(e)}")
|
| 177 |
return False
|
| 178 |
|
| 179 |
+
# Load system at application startup
|
| 180 |
system_loaded = load_uncertainty_system()
|
| 181 |
|
| 182 |
# ==============================================================================
|
| 183 |
+
# PYDANTIC MODELS FOR INPUT AND OUTPUT
|
| 184 |
# ==============================================================================
|
| 185 |
|
| 186 |
class SoilInput(BaseModel):
|
| 187 |
"""
|
| 188 |
+
Model for soil input data.
|
| 189 |
|
| 190 |
+
This class defines and validates input parameters,
|
| 191 |
+
ensuring values are within validated experimental ranges.
|
| 192 |
"""
|
| 193 |
+
cement_percent: float = Field(
|
| 194 |
...,
|
| 195 |
+
description="Cement percentage in mixture",
|
| 196 |
ge=0, le=15,
|
| 197 |
example=5.0
|
| 198 |
)
|
| 199 |
curing_period: float = Field(
|
| 200 |
...,
|
| 201 |
+
description="Curing period in days",
|
| 202 |
ge=0, le=90,
|
| 203 |
example=28.0
|
| 204 |
)
|
| 205 |
compaction_rate: float = Field(
|
| 206 |
...,
|
| 207 |
+
description="Compaction rate in mm/min",
|
| 208 |
ge=0.5, le=2.0,
|
| 209 |
example=1.0
|
| 210 |
)
|
|
|
|
| 212 |
@model_validator(mode="after")
|
| 213 |
def validate_cement_curing_relationship(self):
|
| 214 |
"""
|
| 215 |
+
Validates the relationship between cement content and curing period.
|
| 216 |
|
| 217 |
+
For untreated soil (0% cement), curing period is forced to 0
|
| 218 |
+
because there is no cement hydration process.
|
| 219 |
"""
|
| 220 |
+
if self.cement_percent == 0:
|
| 221 |
self.curing_period = 0
|
| 222 |
+
elif self.cement_percent > 0 and self.curing_period < 1:
|
| 223 |
+
raise ValueError("For cement-treated soil, curing period must be ≥ 1 day")
|
| 224 |
return self
|
| 225 |
|
| 226 |
class Config:
|
| 227 |
json_schema_extra = {
|
| 228 |
"example": {
|
| 229 |
+
"cement_percent": 5.0,
|
| 230 |
"curing_period": 28.0,
|
| 231 |
"compaction_rate": 1.0
|
| 232 |
}
|
| 233 |
}
|
| 234 |
|
| 235 |
class ConfidenceInterval(BaseModel):
|
| 236 |
+
"""Model for a confidence interval."""
|
| 237 |
+
lower: float = Field(..., description="Lower bound of the interval")
|
| 238 |
+
upper: float = Field(..., description="Upper bound of the interval")
|
| 239 |
+
width: float = Field(..., description="Width of the interval")
|
| 240 |
|
| 241 |
class UncertaintyPredictionResponse(BaseModel):
|
| 242 |
"""
|
| 243 |
+
Complete response with uncertainty quantification.
|
| 244 |
|
| 245 |
+
This extended structure provides the engineer with a complete picture
|
| 246 |
+
of the prediction, including not only the estimated value but also confidence
|
| 247 |
+
in that estimate through calibrated intervals.
|
| 248 |
"""
|
| 249 |
+
success: bool = Field(..., description="Request processing status")
|
| 250 |
|
| 251 |
+
# Central prediction
|
| 252 |
+
central_prediction: float = Field(..., description="Most probable UCS prediction")
|
| 253 |
+
units: str = Field(default="kPa", description="Units of measurement")
|
| 254 |
|
| 255 |
+
# Uncertainty information
|
| 256 |
+
uncertainty_estimate: float = Field(..., description="Absolute uncertainty estimate (1-sigma)")
|
| 257 |
+
relative_uncertainty: float = Field(..., description="Relative uncertainty as percentage")
|
| 258 |
|
| 259 |
+
# Confidence intervals
|
| 260 |
confidence_intervals: Dict[str, ConfidenceInterval] = Field(
|
| 261 |
...,
|
| 262 |
+
description="Confidence intervals for multiple probability levels"
|
| 263 |
)
|
| 264 |
|
| 265 |
+
# User interpretation
|
| 266 |
+
interpretation: Dict[str, str] = Field(..., description="Interpretation guide for results")
|
| 267 |
|
| 268 |
# Metadata
|
| 269 |
+
input_parameters: Dict[str, float] = Field(..., description="Input parameters used")
|
| 270 |
+
prediction_time_ms: Optional[float] = Field(None, description="Processing time in milliseconds")
|
| 271 |
+
model_info: Optional[Dict[str, Any]] = Field(None, description="Information about models used")
|
| 272 |
|
| 273 |
class SensitivityAnalysisRequest(BaseModel):
|
| 274 |
+
"""Request for sensitivity analysis."""
|
| 275 |
base_parameters: SoilInput
|
| 276 |
+
parameter_to_vary: str = Field(..., pattern="^(cement_percent|curing_period|compaction_rate)$")
|
| 277 |
+
variation_range: float = Field(default=10.0, ge=1.0, le=50.0, description="Variation range in percentage")
|
| 278 |
+
num_points: int = Field(default=11, ge=5, le=21, description="Number of points for analysis")
|
| 279 |
|
| 280 |
# ==============================================================================
|
| 281 |
+
# CORE FUNCTIONS FOR UNCERTAINTY PREDICTION
|
| 282 |
# ==============================================================================
|
| 283 |
|
| 284 |
def predict_with_uncertainty(input_data: np.ndarray,
|
| 285 |
confidence_levels: List[float] = [0.68, 0.80, 0.90, 0.95]) -> Dict[str, Any]:
|
| 286 |
"""
|
| 287 |
+
Performs complete prediction with uncertainty quantification.
|
| 288 |
|
| 289 |
+
This function implements the two-stage algorithm developed in research:
|
| 290 |
+
1. Primary model generates central UCS prediction
|
| 291 |
+
2. Uncertainty model estimates magnitude of probable error
|
| 292 |
+
3. Confidence intervals are constructed assuming normal distribution
|
| 293 |
|
| 294 |
Args:
|
| 295 |
+
input_data: Numpy array with features [cement%, curing_days, compaction_rate]
|
| 296 |
+
confidence_levels: List of confidence levels for which to calculate intervals
|
| 297 |
|
| 298 |
Returns:
|
| 299 |
+
Dictionary with central prediction, uncertainty estimation and confidence intervals
|
| 300 |
"""
|
| 301 |
|
| 302 |
+
# Stage 1: Central prediction with primary model
|
| 303 |
central_prediction = primary_model.predict(input_data)[0]
|
| 304 |
|
| 305 |
+
# Stage 2: Preparing input for uncertainty model
|
| 306 |
+
# Uncertainty model uses feature augmentation:
|
| 307 |
+
# original features + central prediction
|
| 308 |
uncertainty_input = np.column_stack([input_data, [[central_prediction]]])
|
| 309 |
|
| 310 |
+
# Stage 3: Uncertainty prediction (magnitude of expected error)
|
| 311 |
uncertainty_estimate = uncertainty_model.predict(uncertainty_input)[0]
|
| 312 |
|
| 313 |
+
# Stage 4: Calculating confidence intervals
|
| 314 |
confidence_intervals = {}
|
| 315 |
|
| 316 |
for conf_level in confidence_levels:
|
| 317 |
+
# Z-score corresponding to confidence level
|
| 318 |
+
# For normal distribution: 68% → z≈1.0, 90% → z≈1.645, 95% → z≈1.96
|
| 319 |
z_score = stats.norm.ppf((1 + conf_level) / 2)
|
| 320 |
|
| 321 |
+
# Margin of error = z-score × uncertainty estimate
|
| 322 |
margin = z_score * uncertainty_estimate
|
| 323 |
|
| 324 |
confidence_intervals[f'{conf_level:.0%}'] = ConfidenceInterval(
|
|
|
|
| 327 |
width=float(2 * margin)
|
| 328 |
)
|
| 329 |
|
| 330 |
+
# Calculating relative uncertainty
|
| 331 |
relative_uncertainty = (uncertainty_estimate / central_prediction) * 100 if central_prediction != 0 else 0
|
| 332 |
|
| 333 |
return {
|
|
|
|
| 340 |
def generate_interpretation_guide(central_prediction: float, uncertainty_estimate: float,
|
| 341 |
confidence_intervals: Dict[str, ConfidenceInterval]) -> Dict[str, str]:
|
| 342 |
"""
|
| 343 |
+
Generates a personalized interpretation guide for prediction results.
|
| 344 |
|
| 345 |
+
This function translates statistical results into practical language for engineers,
|
| 346 |
+
providing the necessary context for informed decision making in projects.
|
| 347 |
"""
|
| 348 |
|
| 349 |
+
# Calculate 95% interval for interpretation
|
| 350 |
interval_95 = confidence_intervals.get('95%')
|
| 351 |
|
| 352 |
+
# Confidence classification based on relative uncertainty
|
| 353 |
relative_unc = (uncertainty_estimate / central_prediction) * 100
|
| 354 |
|
| 355 |
if relative_unc <= 10:
|
| 356 |
+
confidence_level = "very high"
|
| 357 |
+
reliability_desc = "The prediction is very reliable for design decision making."
|
| 358 |
elif relative_unc <= 20:
|
| 359 |
+
confidence_level = "high"
|
| 360 |
+
reliability_desc = "The prediction is reliable, we recommend validation through limited testing."
|
| 361 |
elif relative_unc <= 30:
|
| 362 |
+
confidence_level = "moderate"
|
| 363 |
+
reliability_desc = "The prediction provides a useful estimate, but additional testing is recommended."
|
| 364 |
else:
|
| 365 |
+
confidence_level = "limited"
|
| 366 |
+
reliability_desc = "The prediction is indicative, extensive testing is recommended for validation."
|
| 367 |
|
| 368 |
interpretation = {
|
| 369 |
+
"central_prediction": f"The most probable UCS value is {central_prediction:.0f} kPa, based on the input parameters.",
|
| 370 |
|
| 371 |
+
"uncertainty": f"The estimated uncertainty is ±{uncertainty_estimate:.0f} kPa ({relative_unc:.1f}%), "
|
| 372 |
+
f"indicating {confidence_level} confidence in the prediction.",
|
| 373 |
|
| 374 |
+
"confidence_95": f"We have 95% confidence that the actual UCS value is between "
|
| 375 |
+
f"{interval_95.lower:.0f} and {interval_95.upper:.0f} kPa." if interval_95 else "",
|
| 376 |
|
| 377 |
"reliability": reliability_desc,
|
| 378 |
|
| 379 |
+
"practical_guidance": f"For applications with UCS requirements > {central_prediction + uncertainty_estimate:.0f} kPa, "
|
| 380 |
+
f"consider increasing cement content or extending the curing period."
|
| 381 |
}
|
| 382 |
|
| 383 |
return interpretation
|
| 384 |
|
| 385 |
async def validate_models_loaded():
|
| 386 |
+
"""Dependency function for validating model loading."""
|
| 387 |
if not system_loaded or primary_model is None or uncertainty_model is None:
|
| 388 |
raise HTTPException(
|
| 389 |
status_code=503,
|
| 390 |
+
detail="Model system is not loaded correctly. Contact administrator."
|
| 391 |
)
|
| 392 |
return True
|
| 393 |
|
| 394 |
# ==============================================================================
|
| 395 |
+
# API ENDPOINTS
|
| 396 |
# ==============================================================================
|
| 397 |
|
| 398 |
+
@app.get("/", response_class=HTMLResponse, summary="Main page")
|
| 399 |
async def root():
|
| 400 |
"""
|
| 401 |
+
Returns the main page with API information.
|
| 402 |
"""
|
| 403 |
return """
|
| 404 |
<!DOCTYPE html>
|
|
|
|
| 413 |
</head>
|
| 414 |
<body>
|
| 415 |
<h1 class="header">🏗️ UCS Prediction API with Uncertainty Quantification</h1>
|
| 416 |
+
<p>Advanced API for predicting unconfined compressive strength of cement-stabilized soils.</p>
|
| 417 |
|
| 418 |
+
<h2>📋 Available endpoints:</h2>
|
| 419 |
<div class="endpoint">
|
| 420 |
+
<strong>POST /predict</strong> - UCS prediction with uncertainty quantification
|
| 421 |
</div>
|
| 422 |
<div class="endpoint">
|
| 423 |
+
<strong>POST /sensitivity-analysis</strong> - Parameter sensitivity analysis
|
| 424 |
</div>
|
| 425 |
<div class="endpoint">
|
| 426 |
+
<strong>GET /status</strong> - System status
|
| 427 |
</div>
|
| 428 |
<div class="endpoint">
|
| 429 |
+
<strong>GET /model-info</strong> - Detailed model information
|
| 430 |
</div>
|
| 431 |
|
| 432 |
+
<h2>📖 Documentation:</h2>
|
| 433 |
+
<p><a href="/docs">Swagger UI - Interactive documentation</a></p>
|
| 434 |
+
<p><a href="/redoc">ReDoc - Alternative documentation</a></p>
|
| 435 |
|
| 436 |
<footer style="margin-top: 40px; color: #666;">
|
| 437 |
+
<p>Developed by the research team - Technical University Gheorghe Asachi of Iași</p>
|
| 438 |
</footer>
|
| 439 |
</body>
|
| 440 |
</html>
|
| 441 |
"""
|
| 442 |
|
| 443 |
@app.post("/predict", response_model=UncertaintyPredictionResponse,
|
| 444 |
+
summary="UCS Prediction with Uncertainty Quantification")
|
| 445 |
async def predict_ucs_with_uncertainty(
|
| 446 |
soil_data: SoilInput,
|
| 447 |
+
include_model_info: bool = Query(False, description="Include detailed model information"),
|
| 448 |
_: bool = Depends(validate_models_loaded)
|
| 449 |
):
|
| 450 |
"""
|
| 451 |
+
**Performs UCS prediction with complete uncertainty quantification.**
|
| 452 |
|
| 453 |
+
This endpoint implements the advanced uncertainty system developed in our research,
|
| 454 |
+
providing not only the central prediction but also calibrated confidence intervals at multiple levels.
|
| 455 |
|
| 456 |
+
**Input parameters:**
|
| 457 |
+
- **cement_percent**: Cement content (0-15%)
|
| 458 |
+
- **curing_period**: Curing period (0-90 days)
|
| 459 |
+
- **compaction_rate**: Compaction rate (0.5-2.0 mm/min)
|
| 460 |
|
| 461 |
+
**Results include:**
|
| 462 |
+
- Central UCS prediction in kPa
|
| 463 |
+
- Absolute and relative uncertainty estimation
|
| 464 |
+
- Confidence intervals at 68%, 80%, 90% and 95%
|
| 465 |
+
- Personalized interpretation guide for results
|
| 466 |
|
| 467 |
+
**Typical usage:**
|
| 468 |
```json
|
| 469 |
{
|
| 470 |
+
"cement_percent": 7.5,
|
| 471 |
"curing_period": 28,
|
| 472 |
"compaction_rate": 1.0
|
| 473 |
}
|
|
|
|
| 477 |
try:
|
| 478 |
start_time = time.time()
|
| 479 |
|
| 480 |
+
# Preparing input data in model-expected format
|
| 481 |
input_data = soil_data.dict()
|
| 482 |
input_df = pd.DataFrame([input_data])
|
| 483 |
|
| 484 |
+
# Ensuring correct feature order
|
| 485 |
prediction_df = pd.DataFrame()
|
| 486 |
for feature in FEATURE_ORDER:
|
| 487 |
if feature in input_df.columns:
|
| 488 |
prediction_df[feature] = input_df[feature]
|
| 489 |
else:
|
| 490 |
+
raise ValueError(f"Feature '{feature}' missing from input data")
|
| 491 |
|
| 492 |
+
# Converting to numpy array for scikit-learn models
|
| 493 |
input_array = prediction_df.values
|
| 494 |
|
| 495 |
+
# Performing prediction with uncertainty
|
| 496 |
prediction_result = predict_with_uncertainty(input_array)
|
| 497 |
|
| 498 |
+
# Generating interpretation guide
|
| 499 |
interpretation = generate_interpretation_guide(
|
| 500 |
prediction_result['central_prediction'],
|
| 501 |
prediction_result['uncertainty_estimate'],
|
| 502 |
prediction_result['confidence_intervals']
|
| 503 |
)
|
| 504 |
|
| 505 |
+
# Optional model information
|
| 506 |
model_info = None
|
| 507 |
if include_model_info:
|
| 508 |
model_info = {
|
|
|
|
| 512 |
"system_metadata": system_metadata if system_metadata else "Not available"
|
| 513 |
}
|
| 514 |
|
| 515 |
+
# Calculating processing time
|
| 516 |
processing_time = (time.time() - start_time) * 1000
|
| 517 |
|
| 518 |
+
# Building complete response
|
| 519 |
return UncertaintyPredictionResponse(
|
| 520 |
success=True,
|
| 521 |
central_prediction=prediction_result['central_prediction'],
|
|
|
|
| 530 |
)
|
| 531 |
|
| 532 |
except ValueError as ve:
|
| 533 |
+
raise HTTPException(status_code=400, detail=f"Validation error: {str(ve)}")
|
| 534 |
except Exception as e:
|
| 535 |
+
raise HTTPException(status_code=500, detail=f"Processing error: {str(e)}")
|
| 536 |
|
| 537 |
+
@app.post("/sensitivity-analysis", summary="Parameter Sensitivity Analysis")
|
| 538 |
async def perform_sensitivity_analysis(
|
| 539 |
request: SensitivityAnalysisRequest,
|
| 540 |
_: bool = Depends(validate_models_loaded)
|
| 541 |
):
|
| 542 |
"""
|
| 543 |
+
**Performs sensitivity analysis for a specific parameter.**
|
| 544 |
|
| 545 |
+
This analysis shows how variation of an input parameter affects
|
| 546 |
+
both the central prediction and associated uncertainty, providing valuable
|
| 547 |
+
insights for mix design optimization.
|
| 548 |
"""
|
| 549 |
|
| 550 |
try:
|
| 551 |
base_params = request.base_parameters.dict()
|
| 552 |
param_to_vary = request.parameter_to_vary
|
| 553 |
+
variation_range = request.variation_range / 100 # Convert from percentage
|
| 554 |
num_points = request.num_points
|
| 555 |
|
| 556 |
+
# Base values
|
| 557 |
base_value = base_params[param_to_vary]
|
| 558 |
|
| 559 |
+
# Calculate variation range
|
| 560 |
min_variation = base_value * (1 - variation_range)
|
| 561 |
max_variation = base_value * (1 + variation_range)
|
| 562 |
|
| 563 |
+
# Respect physical parameter limits
|
| 564 |
+
if param_to_vary == "cement_percent":
|
| 565 |
min_variation = max(0, min_variation)
|
| 566 |
max_variation = min(15, max_variation)
|
| 567 |
elif param_to_vary == "curing_period":
|
| 568 |
+
min_variation = max(0 if base_params["cement_percent"] == 0 else 1, min_variation)
|
| 569 |
max_variation = min(90, max_variation)
|
| 570 |
elif param_to_vary == "compaction_rate":
|
| 571 |
min_variation = max(0.5, min_variation)
|
| 572 |
max_variation = min(2.0, max_variation)
|
| 573 |
|
| 574 |
+
# Generate analysis points
|
| 575 |
variation_values = np.linspace(min_variation, max_variation, num_points)
|
| 576 |
|
| 577 |
results = []
|
| 578 |
|
| 579 |
for value in variation_values:
|
| 580 |
+
# Create modified parameters
|
| 581 |
modified_params = base_params.copy()
|
| 582 |
modified_params[param_to_vary] = float(value)
|
| 583 |
|
| 584 |
+
# Validate cement-curing relationship for each point
|
| 585 |
+
if modified_params["cement_percent"] == 0:
|
| 586 |
modified_params["curing_period"] = 0
|
| 587 |
|
| 588 |
+
# Perform prediction
|
| 589 |
input_df = pd.DataFrame([modified_params])
|
| 590 |
prediction_df = pd.DataFrame()
|
| 591 |
for feature in FEATURE_ORDER:
|
|
|
|
| 603 |
"confidence_95_upper": prediction_result['confidence_intervals']['95%'].upper
|
| 604 |
})
|
| 605 |
|
| 606 |
+
# Calculate sensitivity statistics
|
| 607 |
predictions = [r["central_prediction"] for r in results]
|
| 608 |
uncertainties = [r["uncertainty_estimate"] for r in results]
|
| 609 |
|
|
|
|
| 633 |
"sensitivity_data": results,
|
| 634 |
"sensitivity_statistics": sensitivity_stats,
|
| 635 |
"interpretation": {
|
| 636 |
+
"parameter_impact": f"A {variation_range*100:.1f}% variation in {param_to_vary} "
|
| 637 |
+
f"produces a change of {sensitivity_stats['prediction_sensitivity']['range']:.1f} kPa in UCS",
|
| 638 |
+
"recommendation": "The parameter with the greatest impact should be carefully controlled in the field"
|
| 639 |
if sensitivity_stats['prediction_sensitivity']['relative_change'] > 10
|
| 640 |
+
else "The parameter has moderate impact, small variations are acceptable"
|
| 641 |
}
|
| 642 |
}
|
| 643 |
|
| 644 |
except Exception as e:
|
| 645 |
+
raise HTTPException(status_code=500, detail=f"Error in sensitivity analysis: {str(e)}")
|
| 646 |
|
| 647 |
+
@app.get("/status", summary="System Status")
|
| 648 |
async def get_system_status():
|
| 649 |
"""
|
| 650 |
+
**Returns complete system status for uncertainty quantification.**
|
| 651 |
|
| 652 |
+
Useful for monitoring application health and diagnosing problems.
|
| 653 |
"""
|
| 654 |
|
| 655 |
status_info = {
|
|
|
|
| 667 |
}
|
| 668 |
}
|
| 669 |
|
| 670 |
+
# Quick functionality test if models are loaded
|
| 671 |
if system_loaded:
|
| 672 |
try:
|
| 673 |
test_result = validate_models_compatibility()
|
|
|
|
| 677 |
|
| 678 |
return status_info
|
| 679 |
|
| 680 |
+
@app.get("/model-info", summary="Model Information")
|
| 681 |
async def get_model_information(_: bool = Depends(validate_models_loaded)):
|
| 682 |
"""
|
| 683 |
+
**Returns detailed information about the models used.**
|
| 684 |
|
| 685 |
+
Includes model parameters, historical performance and applicability limits.
|
| 686 |
"""
|
| 687 |
|
| 688 |
try:
|
|
|
|
| 705 |
"feature_engineering": "Feature augmentation for uncertainty model (original features + central prediction)"
|
| 706 |
},
|
| 707 |
"valid_ranges": {
|
| 708 |
+
"cement_percent": {"min": 0, "max": 15, "units": "%", "note": "Based on experimental data"},
|
| 709 |
"curing_period": {"min": 0, "max": 90, "units": "days", "note": "0 only valid for 0% cement"},
|
| 710 |
"compaction_rate": {"min": 0.5, "max": 2.0, "units": "mm/min", "note": "Within experimental range"}
|
| 711 |
},
|
|
|
|
| 718 |
}
|
| 719 |
}
|
| 720 |
|
| 721 |
+
# Add metadata if available
|
| 722 |
if system_metadata:
|
| 723 |
model_info["training_metadata"] = {
|
| 724 |
"training_samples": system_metadata.get("n_training_samples", "Unknown"),
|
|
|
|
| 729 |
return model_info
|
| 730 |
|
| 731 |
except Exception as e:
|
| 732 |
+
raise HTTPException(status_code=500, detail=f"Error obtaining information: {str(e)}")
|
| 733 |
|
| 734 |
# ==============================================================================
|
| 735 |
+
# EXCEPTION HANDLERS
|
| 736 |
# ==============================================================================
|
| 737 |
|
| 738 |
@app.exception_handler(ValidationError)
|
| 739 |
async def validation_exception_handler(request: Request, exc: ValidationError):
|
| 740 |
"""
|
| 741 |
+
Custom handler for Pydantic validation errors.
|
| 742 |
+
Provides more user-friendly error messages.
|
| 743 |
"""
|
| 744 |
|
| 745 |
friendly_errors = []
|
|
|
|
| 747 |
field = " -> ".join(str(loc) for loc in error.get('loc', []))
|
| 748 |
message = error.get('msg', '')
|
| 749 |
|
| 750 |
+
# Customize messages for common cases
|
| 751 |
if "greater than or equal" in message:
|
| 752 |
+
message = f"Value for {field} is too small"
|
| 753 |
elif "less than or equal" in message:
|
| 754 |
+
message = f"Value for {field} is too large"
|
| 755 |
elif "string does not match regex" in message:
|
| 756 |
+
message = f"Value for {field} is not valid"
|
| 757 |
|
| 758 |
friendly_errors.append({
|
| 759 |
"field": field,
|
|
|
|
| 765 |
status_code=422,
|
| 766 |
content={
|
| 767 |
"success": False,
|
| 768 |
+
"error": "Input data validation error",
|
| 769 |
"details": friendly_errors,
|
| 770 |
+
"help": "Check that all values are within specified ranges and try again"
|
| 771 |
}
|
| 772 |
)
|
| 773 |
|
| 774 |
@app.exception_handler(Exception)
|
| 775 |
async def general_exception_handler(request: Request, exc: Exception):
|
| 776 |
"""
|
| 777 |
+
General handler for unexpected exceptions.
|
| 778 |
"""
|
| 779 |
return JSONResponse(
|
| 780 |
status_code=500,
|
| 781 |
content={
|
| 782 |
"success": False,
|
| 783 |
+
"error": "Internal server error",
|
| 784 |
+
"message": "An unexpected error occurred. Contact administrator if problem persists.",
|
| 785 |
+
"request_id": str(time.time()) # For tracking in logs
|
| 786 |
}
|
| 787 |
)
|
| 788 |
|
| 789 |
# ==============================================================================
|
| 790 |
+
# FINAL CONFIGURATION AND STARTUP
|
| 791 |
# ==============================================================================
|
| 792 |
|
| 793 |
@app.on_event("startup")
|
| 794 |
async def startup_event():
|
| 795 |
"""
|
| 796 |
+
Event executed at application startup.
|
| 797 |
+
Performs final checks and prepares system for production.
|
| 798 |
"""
|
| 799 |
+
print("🚀 Starting UCS Prediction API v2.0...")
|
| 800 |
|
| 801 |
if system_loaded:
|
| 802 |
+
print("✅ Uncertainty system loaded and functional")
|
| 803 |
+
print(f"📊 Features configured: {FEATURE_ORDER.tolist()}")
|
| 804 |
else:
|
| 805 |
+
print("❌ WARNING: System was not loaded correctly!")
|
| 806 |
+
print(" Check that model files are present in the models_for_deployment/ directory")
|
| 807 |
|
| 808 |
+
print("🌐 API available for requests")
|
| 809 |
|
| 810 |
if __name__ == "__main__":
|
| 811 |
+
# For development running
|
| 812 |
import uvicorn
|
| 813 |
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)
|