bteodoru commited on
Commit
6d521c2
·
verified ·
1 Parent(s): 4ab0a7f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +230 -230
app.py CHANGED
@@ -1,9 +1,9 @@
1
  # ==============================================================================
2
- # APLICAȚIA FASTAPI CU SISTEM DE INCERTITUDINE INTEGRATĂ
3
  # ==============================================================================
4
- # Această aplicație înlocuiește sistemul simplu de predicție cu un framework
5
- # avansat de cuantificare a incertitudinii, oferind inginierilor nu doar predicții
6
- # ci și intervale de încredere calibrate pentru luarea deciziilor informate
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 # Necesar pentru deserializare
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
- # CONFIGURAȚIA APLICAȚIEI FASTAPI
27
  # ==============================================================================
28
 
29
  app = FastAPI(
30
  title="UCS Prediction API with Uncertainty Quantification",
31
  description="""
32
- **API avansat pentru predicția rezistenței la compresiune simplă (UCS) a solurilor stabilizate cu cement**
33
 
34
- Această aplicație implementează sistemul de cuantificare a incertitudinii dezvoltat în cercetarea
35
  "Prediction of Unconfined Compressive Strength in Cement-Treated Soil: A Machine Learning Approach".
36
 
37
- **Caracteristici principale:**
38
- - Predicții precise ale UCS folosind Random Forest optimizat
39
- - Cuantificare completă a incertitudinii cu intervale de încredere calibrate
40
- - Analiză de sensibilitate pentru optimizarea parametrilor
41
- - Interpretabilitate prin analiza importanței features-urilor
42
 
43
- **Dezvoltat de:** Echipa de cercetare - Universitatea Tehnică Gheorghe Asachi din Iași
44
  """,
45
  version="2.0.0",
46
  contact={
47
- "name": "Echipa de Dezvoltare UCS",
48
  "email": "iancu-bogdan.teodoru@academic.tuiasi.ro",
49
  }
50
  )
51
 
52
- # Configurarea CORS pentru interfața web
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", # Pentru dezvoltarea locală
59
- "http://localhost:8000" # Pentru testarea locală
60
  ],
61
  allow_credentials=True,
62
  allow_methods=["GET", "POST", "OPTIONS"],
@@ -64,19 +64,19 @@ app.add_middleware(
64
  )
65
 
66
  # ==============================================================================
67
- # CONFIGURAȚIA MODELELOR ȘI ÎNCĂRCAREA SISTEMULUI
68
  # ==============================================================================
69
 
70
- # Căile către modelele serializate
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
- # Ordinea features-urilor (critică pentru compatibilitate)
77
- DEFAULT_FEATURE_ORDER = ['cement_perecent', 'curing_period', 'compaction_rate']
78
 
79
- # Variabile globale pentru sistem
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
- Încarcă și validează întregul sistem de incertitudine.
88
 
89
- Această funcție orchestrează încărcarea tuturor componentelor sistemului
90
- și efectuează validări de bază pentru a asigura funcționarea corectă.
91
- Procesul este proiectat fie robust și ofere informații detaliate
92
- despre orice probleme întâlnite în timpul încărcării.
93
  """
94
  global primary_model, uncertainty_model, system_metadata, FEATURE_ORDER
95
 
96
- print("🚀 Încărcarea sistemului de incertitudine...")
97
  start_time = time.time()
98
 
99
  try:
100
- # Încărcăm modelul principal
101
  if os.path.exists(PRIMARY_MODEL_PATH):
102
  primary_model = joblib.load(PRIMARY_MODEL_PATH)
103
- print(f"✅ Model principal încărcat: {type(primary_model).__name__}")
104
  else:
105
- raise FileNotFoundError(f"Modelul principal nu a fost găsit la: {PRIMARY_MODEL_PATH}")
106
 
107
- # Încărcăm modelul de incertitudine
108
  if os.path.exists(UNCERTAINTY_MODEL_PATH):
109
  uncertainty_model = joblib.load(UNCERTAINTY_MODEL_PATH)
110
- print(f"✅ Model incertitudine încărcat: {type(uncertainty_model).__name__}")
111
  else:
112
- raise FileNotFoundError(f"Modelul de incertitudine nu a fost găsit la: {UNCERTAINTY_MODEL_PATH}")
113
 
114
- # Încărcăm metadata-ul sistemului
115
  if os.path.exists(METADATA_PATH):
116
  with open(METADATA_PATH, 'rb') as f:
117
  system_metadata = pickle.load(f)
118
- print(f"✅ Metadata sistem încărcată: {len(system_metadata)} chei")
119
  else:
120
- print("⚠️ Metadata-ul sistemului nu a fost găsit, se folosesc valorile default")
121
  system_metadata = {"feature_names": DEFAULT_FEATURE_ORDER}
122
 
123
- # Determinăm ordinea features-urilor
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
- # Validăm compatibilitatea modelelor
132
  validation_result = validate_models_compatibility()
133
  if not validation_result:
134
- raise ValueError("Modelele nu sunt compatibile între ele")
135
 
136
  load_time = time.time() - start_time
137
- print(f"🎉 Sistem de incertitudine încărcat cu succes în {load_time:.2f} secunde!")
138
  print(f"📊 Features: {FEATURE_ORDER.tolist()}")
139
 
140
  return True
141
 
142
  except Exception as e:
143
- print(f"❌ Eroare la încărcarea sistemului: {str(e)}")
144
  import traceback
145
  print(traceback.format_exc())
146
  return False
147
 
148
  def validate_models_compatibility():
149
  """
150
- Validează modelele sunt compatibile și funcționează împreună.
151
 
152
- Această validare include teste de compatibilitate dimensională,
153
- verificarea tipurilor de date și un test funcțional complet.
154
  """
155
  try:
156
- # Test cu date sintetice
157
  test_input = np.array([[5.0, 14.0, 1.0]]) # cement, curing, compaction
158
 
159
- # Testăm modelul principal
160
  primary_pred = primary_model.predict(test_input)[0]
161
 
162
- # Testăm modelul de incertitudine cu feature augmentation
163
  uncertainty_input = np.column_stack([test_input, [[primary_pred]]])
164
  uncertainty_pred = uncertainty_model.predict(uncertainty_input)[0]
165
 
166
- # Verificăm rezultatele sunt numerice și rezonabile
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"✅ Test compatibilitate: UCS={primary_pred:.1f} kPa, σ={uncertainty_pred:.1f} kPa")
173
  return True
174
 
175
  except Exception as e:
176
- print(f"❌ Test compatibilitate eșuat: {str(e)}")
177
  return False
178
 
179
- # Încărcăm sistemul la pornirea aplicației
180
  system_loaded = load_uncertainty_system()
181
 
182
  # ==============================================================================
183
- # MODELELE PYDANTIC PENTRU INPUT ȘI OUTPUT
184
  # ==============================================================================
185
 
186
  class SoilInput(BaseModel):
187
  """
188
- Modelul pentru datele de input ale solului.
189
 
190
- Această clasă definește și validează parametrii de intrare,
191
- asigurându-se valorile sunt în intervalele experimentale validate.
192
  """
193
- cement_perecent: float = Field(
194
  ...,
195
- description="Procentajul de cement în amestec",
196
  ge=0, le=15,
197
  example=5.0
198
  )
199
  curing_period: float = Field(
200
  ...,
201
- description="Perioada de maturare în zile",
202
  ge=0, le=90,
203
  example=28.0
204
  )
205
  compaction_rate: float = Field(
206
  ...,
207
- description="Rata de compactare în mm/min",
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
- Validează relația dintre conținutul de cement și perioada de maturare.
216
 
217
- Pentru solul netratat (0% cement), perioada de maturare este forțată la 0
218
- deoarece nu există proces de hidratare a cimentului.
219
  """
220
- if self.cement_perecent == 0:
221
  self.curing_period = 0
222
- elif self.cement_perecent > 0 and self.curing_period < 1:
223
- raise ValueError("Pentru sol tratat cu cement, perioada de maturare trebuie să fie ≥ 1 zi")
224
  return self
225
 
226
  class Config:
227
  json_schema_extra = {
228
  "example": {
229
- "cement_perecent": 5.0,
230
  "curing_period": 28.0,
231
  "compaction_rate": 1.0
232
  }
233
  }
234
 
235
  class ConfidenceInterval(BaseModel):
236
- """Modelul pentru un interval de încredere."""
237
- lower: float = Field(..., description="Limita inferioară a intervalului")
238
- upper: float = Field(..., description="Limita superioară a intervalului")
239
- width: float = Field(..., description="Lățimea intervalului")
240
 
241
  class UncertaintyPredictionResponse(BaseModel):
242
  """
243
- Răspunsul complet cu cuantificare de incertitudine.
244
 
245
- Această structură extinsă oferă inginerului o imagine completă
246
- a predicției, incluzând nu doar valoarea estimată ci și încrederea
247
- în acea estimare prin intervale calibrate.
248
  """
249
- success: bool = Field(..., description="Statusul procesării cererii")
250
 
251
- # Predicția centrală
252
- central_prediction: float = Field(..., description="Predicția UCS cea mai probabilă")
253
- units: str = Field(default="kPa", description="Unitățile de măsură")
254
 
255
- # Informații despre incertitudine
256
- uncertainty_estimate: float = Field(..., description="Estimarea incertitudinii absolute (1-sigma)")
257
- relative_uncertainty: float = Field(..., description="Incertitudinea relativă ca procent")
258
 
259
- # Intervale de încredere
260
  confidence_intervals: Dict[str, ConfidenceInterval] = Field(
261
  ...,
262
- description="Intervale de încredere pentru multiple nivele de probabilitate"
263
  )
264
 
265
- # Interpretarea pentru utilizator
266
- interpretation: Dict[str, str] = Field(..., description="Ghid de interpretare pentru rezultate")
267
 
268
  # Metadata
269
- input_parameters: Dict[str, float] = Field(..., description="Parametrii de intrare folosiți")
270
- prediction_time_ms: Optional[float] = Field(None, description="Timpul de procesare în milisecunde")
271
- model_info: Optional[Dict[str, Any]] = Field(None, description="Informații despre modelele folosite")
272
 
273
  class SensitivityAnalysisRequest(BaseModel):
274
- """Cererea pentru analiza de sensibilitate."""
275
  base_parameters: SoilInput
276
- parameter_to_vary: str = Field(..., pattern="^(cement_perecent|curing_period|compaction_rate)$")
277
- variation_range: float = Field(default=10.0, ge=1.0, le=50.0, description="Intervalul de variație în procente")
278
- num_points: int = Field(default=11, ge=5, le=21, description="Numărul de puncte pentru analiză")
279
 
280
  # ==============================================================================
281
- # FUNCȚIILE CORE PENTRU PREDICȚIA CU INCERTITUDINE
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
- Realizează predicția completă cu cuantificare de incertitudine.
288
 
289
- Această funcție implementează algoritmul în două etape dezvoltat în cercetare:
290
- 1. Modelul principal generează predicția centrală UCS
291
- 2. Modelul de incertitudine estimează magnitudinea erorii probabile
292
- 3. Se construiesc intervale de încredere presupunând distribuție normală
293
 
294
  Args:
295
- input_data: Array numpy cu features-urile [cement%, curing_days, compaction_rate]
296
- confidence_levels: Lista nivelelor de încredere pentru care calculăm intervale
297
 
298
  Returns:
299
- Dicționar cu predicția centrală, estimarea incertitudinii și intervalele de încredere
300
  """
301
 
302
- # Etapa 1: Predicția centrală cu modelul principal
303
  central_prediction = primary_model.predict(input_data)[0]
304
 
305
- # Etapa 2: Pregătirea input-ului pentru modelul de incertitudine
306
- # Modelul de incertitudine folosește feature augmentation:
307
- # features originale + predicția centrală
308
  uncertainty_input = np.column_stack([input_data, [[central_prediction]]])
309
 
310
- # Etapa 3: Predicția incertitudinii (magnitudinea erorii așteptate)
311
  uncertainty_estimate = uncertainty_model.predict(uncertainty_input)[0]
312
 
313
- # Etapa 4: Calcularea intervalelor de încredere
314
  confidence_intervals = {}
315
 
316
  for conf_level in confidence_levels:
317
- # Z-score corespunzător nivelului de încredere
318
- # Pentru distribuția normală: 68% → z≈1.0, 90% → z≈1.645, 95% → z≈1.96
319
  z_score = stats.norm.ppf((1 + conf_level) / 2)
320
 
321
- # Marginea de eroare = z-score × estimarea incertitudinii
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
- # Calcularea incertitudinii relative
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
- Generează un ghid de interpretare personalizat pentru rezultatele predicției.
344
 
345
- Această funcție traduce rezultatele statistice în limbaj practic pentru ingineri,
346
- oferind contextul necesar pentru luarea deciziilor informate în proiecte.
347
  """
348
 
349
- # Calculăm intervalul 95% pentru interpretare
350
  interval_95 = confidence_intervals.get('95%')
351
 
352
- # Clasificarea încrederii bazată pe incertitudinea relativă
353
  relative_unc = (uncertainty_estimate / central_prediction) * 100
354
 
355
  if relative_unc <= 10:
356
- confidence_level = "foarte mare"
357
- reliability_desc = "Predicția este foarte fiabilă pentru luarea deciziilor de proiectare."
358
  elif relative_unc <= 20:
359
- confidence_level = "mare"
360
- reliability_desc = "Predicția este fiabilă, recomandăm validare prin teste limitate."
361
  elif relative_unc <= 30:
362
- confidence_level = "moderată"
363
- reliability_desc = "Predicția oferă o estimare utilă, dar se recomandă testare suplimentară."
364
  else:
365
- confidence_level = "limitată"
366
- reliability_desc = "Predicția este indicativă, se recomandă testare extinsă pentru validare."
367
 
368
  interpretation = {
369
- "central_prediction": f"Valoarea UCS cea mai probabilă este {central_prediction:.0f} kPa, bazată pe parametrii introduși.",
370
 
371
- "uncertainty": f"Incertitudinea estimată este ±{uncertainty_estimate:.0f} kPa ({relative_unc:.1f}%), "
372
- f"indicând o încredere {confidence_level} în predicție.",
373
 
374
- "confidence_95": f"Avem 95% încredere valoarea reală UCS se află între "
375
- f"{interval_95.lower:.0f} și {interval_95.upper:.0f} kPa." if interval_95 else "",
376
 
377
  "reliability": reliability_desc,
378
 
379
- "practical_guidance": f"Pentru aplicații cu cerințe UCS > {central_prediction + uncertainty_estimate:.0f} kPa, "
380
- f"considerați mărirea conținutului de cement sau extinderea perioadei de maturare."
381
  }
382
 
383
  return interpretation
384
 
385
  async def validate_models_loaded():
386
- """Dependency function pentru validarea încărcării modelelor."""
387
  if not system_loaded or primary_model is None or uncertainty_model is None:
388
  raise HTTPException(
389
  status_code=503,
390
- detail="Sistemul de modele nu este încărcat corect. Contactați administratorul."
391
  )
392
  return True
393
 
394
  # ==============================================================================
395
- # ENDPOINT-URILE API
396
  # ==============================================================================
397
 
398
- @app.get("/", response_class=HTMLResponse, summary="Pagina principală")
399
  async def root():
400
  """
401
- Returnează pagina principală cu informații despre API.
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 avansat pentru predicția rezistenței la compresiune simplă a solurilor stabilizate cu cement.</p>
417
 
418
- <h2>📋 Endpoint-uri disponibile:</h2>
419
  <div class="endpoint">
420
- <strong>POST /predict</strong> - Predicție UCS cu cuantificare de incertitudine
421
  </div>
422
  <div class="endpoint">
423
- <strong>POST /sensitivity-analysis</strong> - Analiza sensibilității parametrilor
424
  </div>
425
  <div class="endpoint">
426
- <strong>GET /status</strong> - Statusul sistemului
427
  </div>
428
  <div class="endpoint">
429
- <strong>GET /model-info</strong> - Informații detaliate despre modele
430
  </div>
431
 
432
- <h2>📖 Documentație:</h2>
433
- <p><a href="/docs">Swagger UI - Documentație interactivă</a></p>
434
- <p><a href="/redoc">ReDoc - Documentație alternativă</a></p>
435
 
436
  <footer style="margin-top: 40px; color: #666;">
437
- <p>Dezvoltat de echipa de cercetare - Universitatea Tehnică Gheorghe Asachi din Iași</p>
438
  </footer>
439
  </body>
440
  </html>
441
  """
442
 
443
  @app.post("/predict", response_model=UncertaintyPredictionResponse,
444
- summary="Predicție UCS cu Cuantificare de Incertitudine")
445
  async def predict_ucs_with_uncertainty(
446
  soil_data: SoilInput,
447
- include_model_info: bool = Query(False, description="Include informații detaliate despre modele"),
448
  _: bool = Depends(validate_models_loaded)
449
  ):
450
  """
451
- **Realizează predicția UCS cu cuantificare completă a incertitudinii.**
452
 
453
- Acest endpoint implementează sistemul avansat de incertitudine dezvoltat în cercetarea noastră,
454
- oferind nu doar predicția centrală ci și intervale de încredere calibrate la multiple nivele.
455
 
456
- **Parametrii de intrare:**
457
- - **cement_perecent**: Conținutul de cement (0-15%)
458
- - **curing_period**: Perioada de maturare (0-90 zile)
459
- - **compaction_rate**: Rata de compactare (0.5-2.0 mm/min)
460
 
461
- **Rezultatele includ:**
462
- - Predicția centrală UCS în kPa
463
- - Estimarea incertitudinii absolute și relative
464
- - Intervale de încredere la 68%, 80%, 90% și 95%
465
- - Ghid de interpretare personalizat pentru rezultate
466
 
467
- **Utilizare tipică:**
468
  ```json
469
  {
470
- "cement_perecent": 7.5,
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
- # Pregătirea datelor de input în formatul așteptat de model
481
  input_data = soil_data.dict()
482
  input_df = pd.DataFrame([input_data])
483
 
484
- # Asigurarea ordinii corecte a features-urilor
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-ul '{feature}' lipsește din datele de intrare")
491
 
492
- # Convertirea la numpy array pentru modelele scikit-learn
493
  input_array = prediction_df.values
494
 
495
- # Realizarea predicției cu incertitudine
496
  prediction_result = predict_with_uncertainty(input_array)
497
 
498
- # Generarea ghidului de interpretare
499
  interpretation = generate_interpretation_guide(
500
  prediction_result['central_prediction'],
501
  prediction_result['uncertainty_estimate'],
502
  prediction_result['confidence_intervals']
503
  )
504
 
505
- # Informații opționale despre modele
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
- # Calcularea timpului de procesare
516
  processing_time = (time.time() - start_time) * 1000
517
 
518
- # Construirea răspunsului complet
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"Eroare de validare: {str(ve)}")
534
  except Exception as e:
535
- raise HTTPException(status_code=500, detail=f"Eroare de procesare: {str(e)}")
536
 
537
- @app.post("/sensitivity-analysis", summary="Analiza Sensibilității Parametrilor")
538
  async def perform_sensitivity_analysis(
539
  request: SensitivityAnalysisRequest,
540
  _: bool = Depends(validate_models_loaded)
541
  ):
542
  """
543
- **Efectuează analiza sensibilității pentru un parametru specific.**
544
 
545
- Această analiză arată cum variația unui parametru de intrare afectează
546
- atât predicția centrală cât și incertitudinea asociată, oferind insight-uri
547
- valoroase pentru optimizarea mix design-ului.
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 # Convertim din procente
554
  num_points = request.num_points
555
 
556
- # Valorile de bază
557
  base_value = base_params[param_to_vary]
558
 
559
- # Calculăm intervalul de variație
560
  min_variation = base_value * (1 - variation_range)
561
  max_variation = base_value * (1 + variation_range)
562
 
563
- # Respectăm limitele fizice ale parametrilor
564
- if param_to_vary == "cement_perecent":
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_perecent"] == 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
- # Generăm punctele pentru analiză
575
  variation_values = np.linspace(min_variation, max_variation, num_points)
576
 
577
  results = []
578
 
579
  for value in variation_values:
580
- # Creăm parametrii modificați
581
  modified_params = base_params.copy()
582
  modified_params[param_to_vary] = float(value)
583
 
584
- # Validăm relația cement-curing pentru fiecare punct
585
- if modified_params["cement_perecent"] == 0:
586
  modified_params["curing_period"] = 0
587
 
588
- # Realizăm predicția
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
- # Calculăm statisticile sensibilității
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"Variația de {variation_range*100:.1f}% în {param_to_vary} "
637
- f"produce o schimbare de {sensitivity_stats['prediction_sensitivity']['range']:.1f} kPa în UCS",
638
- "recommendation": "Parametrul cu cel mai mare impact ar trebui controlat cu atenție în teren"
639
  if sensitivity_stats['prediction_sensitivity']['relative_change'] > 10
640
- else "Parametrul are impact moderat, variațiile mici sunt acceptabile"
641
  }
642
  }
643
 
644
  except Exception as e:
645
- raise HTTPException(status_code=500, detail=f"Eroare în analiza sensibilității: {str(e)}")
646
 
647
- @app.get("/status", summary="Status Sistem")
648
  async def get_system_status():
649
  """
650
- **Returnează statusul complet al sistemului de incertitudine.**
651
 
652
- Useful pentru monitorizarea sănătății aplicației și diagnosticarea problemelor.
653
  """
654
 
655
  status_info = {
@@ -667,7 +667,7 @@ async def get_system_status():
667
  }
668
  }
669
 
670
- # Testul rapid de funcționalitate dacă modelele sunt încărcate
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="Informații Modele")
681
  async def get_model_information(_: bool = Depends(validate_models_loaded)):
682
  """
683
- **Returnează informații detaliate despre modelele utilizate.**
684
 
685
- Include parametrii modelelor, performanțele istorice și limitele de aplicabilitate.
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
- "cement_perecent": {"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,7 +718,7 @@ async def get_model_information(_: bool = Depends(validate_models_loaded)):
718
  }
719
  }
720
 
721
- # Adăugăm metadata-ul dacă este disponibil
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"Eroare la obținerea informațiilor: {str(e)}")
733
 
734
  # ==============================================================================
735
- # HANDLER-E PENTRU EXCEPȚII
736
  # ==============================================================================
737
 
738
  @app.exception_handler(ValidationError)
739
  async def validation_exception_handler(request: Request, exc: ValidationError):
740
  """
741
- Handler personalizat pentru erorile de validare Pydantic.
742
- Oferă mesaje de eroare mai prietenoase pentru utilizatori.
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
- # Personalizăm mesajele pentru cazuri comune
751
  if "greater than or equal" in message:
752
- message = f"Valoarea pentru {field} este prea mică"
753
  elif "less than or equal" in message:
754
- message = f"Valoarea pentru {field} este prea mare"
755
  elif "string does not match regex" in message:
756
- message = f"Valoarea pentru {field} nu este validă"
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": "Eroare de validare a datelor de intrare",
769
  "details": friendly_errors,
770
- "help": "Verificați toate valorile sunt în intervalele specificate și încercați din nou"
771
  }
772
  )
773
 
774
  @app.exception_handler(Exception)
775
  async def general_exception_handler(request: Request, exc: Exception):
776
  """
777
- Handler general pentru excepții neașteptate.
778
  """
779
  return JSONResponse(
780
  status_code=500,
781
  content={
782
  "success": False,
783
- "error": "Eroare internă a serverului",
784
- "message": "A apărut o eroare neașteptată. Contactați administratorul dacă problema persistă.",
785
- "request_id": str(time.time()) # Pentru tracking în loguri
786
  }
787
  )
788
 
789
  # ==============================================================================
790
- # CONFIGURAȚIE FINALĂ ȘI STARTUP
791
  # ==============================================================================
792
 
793
  @app.on_event("startup")
794
  async def startup_event():
795
  """
796
- Eveniment executat la pornirea aplicației.
797
- Efectuează verificări finale și pregătește sistemul pentru producție.
798
  """
799
- print("🚀 Pornirea aplicației UCS Prediction API v2.0...")
800
 
801
  if system_loaded:
802
- print("✅ Sistem de incertitudine încărcat și funcțional")
803
- print(f"📊 Features configurate: {FEATURE_ORDER.tolist()}")
804
  else:
805
- print("❌ ATENȚIE: Sistemul nu a fost încărcat corect!")
806
- print(" Verificați fișierele de modele sunt prezente în directorul models_for_deployment/")
807
 
808
- print("🌐 API disponibil pentru cereri")
809
 
810
  if __name__ == "__main__":
811
- # Pentru rularea în dezvoltare
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)