bteodoru commited on
Commit
b139faa
·
verified ·
1 Parent(s): 8bcce55

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +719 -147
app.py CHANGED
@@ -1,241 +1,813 @@
1
- from fastapi import FastAPI, HTTPException, Request, Depends
2
- from fastapi.responses import JSONResponse
 
 
 
 
 
 
 
3
  from fastapi.middleware.cors import CORSMiddleware
 
4
  import pandas as pd
5
  import joblib
6
  import numpy as np
7
  import os
8
  import time
9
- from sklearn.ensemble import RandomForestRegressor # Important for model deserialization
 
 
10
  from pydantic import BaseModel, ValidationError, Field, field_validator, model_validator
11
- from typing import Any, Dict, List, Optional
 
 
 
 
 
 
12
 
13
- # Create FastAPI application
14
  app = FastAPI(
15
- title="Soil UCS Prediction API",
16
- description="API for predicting the unconfined compressive strength (UCS) of cement-stabilized soils",
17
- version="1.0.0"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  )
19
 
20
- # Add CORS middleware to allow requests only from your interface server
21
  app.add_middleware(
22
  CORSMiddleware,
23
  allow_origins=[
24
- "http://www.bi4e-at.tuiasi.ro/ucs-prediction/",
25
- "https://www.bi4e-at.tuiasi.ro/ucs-prediction/"
 
 
26
  ],
27
  allow_credentials=True,
28
- allow_methods=["GET", "POST"], # Restrict to only necessary methods
29
  allow_headers=["*"],
30
  )
31
 
32
- # Global variables
33
- MODEL_PATH = 'rf_model_optim.joblib' # Path to the model
34
- DEFAULT_FEATURE_ORDER = ['cement_perecent', 'curing_period', 'compaction_rate'] # Default feature order
35
 
36
- # Load the model
37
- try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  start_time = time.time()
39
- model = joblib.load(MODEL_PATH)
40
 
41
- # Determine feature order from model or use default
42
- if hasattr(model, 'feature_names_in_'):
43
- FEATURE_ORDER = model.feature_names_in_
44
- else:
45
- FEATURE_ORDER = np.array(DEFAULT_FEATURE_ORDER)
46
-
47
- load_time = time.time() - start_time
48
- print(f"Model loaded successfully in {load_time:.2f} seconds! Feature Order: {FEATURE_ORDER}")
49
- except Exception as e:
50
- import traceback
51
- print(f"Error loading model: {str(e)}")
52
- print(traceback.format_exc())
53
- model = None
54
- FEATURE_ORDER = np.array(DEFAULT_FEATURE_ORDER)
55
-
56
- # Input data model definition
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  class SoilInput(BaseModel):
 
 
 
 
 
 
58
  cement_perecent: float = Field(
59
  ...,
60
- description="Cement percentage in the mixture (0-15%)"
 
 
61
  )
62
  curing_period: float = Field(
63
  ...,
64
- description="Curing period in days (1-90 days)"
 
 
65
  )
66
  compaction_rate: float = Field(
67
  ...,
68
- description="Compaction rate in mm/min (0.5-2.0 mm/min)"
 
 
69
  )
70
 
71
  @model_validator(mode="after")
72
- def check_cement_and_curing(self):
 
 
 
 
 
 
73
  if self.cement_perecent == 0:
74
  self.curing_period = 0
75
- else:
76
- if not (1 <= self.curing_period <= 90):
77
- raise ValueError("Curing period must be between 1 and 90 days")
78
  return self
79
 
80
- @field_validator('cement_perecent')
81
- @classmethod
82
- def validate_cement(cls, v: float) -> float:
83
- if not 0 <= v <= 15:
84
- raise ValueError("Cement percentage must be between 0% and 15%")
85
- return v
86
-
87
- @field_validator('compaction_rate')
88
- @classmethod
89
- def validate_compaction(cls, v: float) -> float:
90
- if not 0.5 <= v <= 2:
91
- raise ValueError("Compaction rate must be between 0.5 and 2 mm/min")
92
- return v
93
-
94
  class Config:
95
- schema_extra = {
96
  "example": {
97
- "cement_perecent": 5,
98
- "curing_period": 28,
99
  "compaction_rate": 1.0
100
  }
101
  }
102
 
103
- # Prediction response model
104
- class PredictionResponse(BaseModel):
105
- success: bool
106
- prediction: float
107
- units: str
108
- input_parameters: Dict[str, float]
109
- prediction_time_ms: Optional[float] = None
110
-
111
- # Function to check if model is loaded
112
- async def get_model():
113
- if model is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  raise HTTPException(
115
  status_code=503,
116
- detail="Model was not loaded correctly. Please try again later."
117
  )
118
- return model
 
 
 
 
119
 
120
- # Prediction endpoint
121
- @app.post("/predict", response_model=PredictionResponse, summary="UCS Prediction")
122
- async def predict(soil_data: SoilInput, model=Depends(get_model)):
123
  """
124
- Predicts the UCS (unconfined compressive strength) of soil.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- - **cement_perecent**: Cement percentage in the mixture (0-15%)
127
- - **curing_period**: Curing period in days (1-90 days)
128
- - **compaction_rate**: Compaction rate in mm/min (0.5-2 mm/min)
 
 
129
 
130
- Returns the UCS strength in kPa.
 
 
 
 
 
 
 
131
  """
 
132
  try:
133
  start_time = time.time()
134
 
135
- # Build DataFrame for prediction
136
  input_data = soil_data.dict()
137
  input_df = pd.DataFrame([input_data])
138
 
139
- # Ensure correct feature order
140
  prediction_df = pd.DataFrame()
141
  for feature in FEATURE_ORDER:
142
  if feature in input_df.columns:
143
  prediction_df[feature] = input_df[feature]
144
  else:
145
- raise ValueError(f"Feature '{feature}' is missing from input data")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
- # Make prediction
148
- prediction = model.predict(prediction_df)
149
 
150
- # Calculate prediction time
151
- prediction_time_ms = (time.time() - start_time) * 1000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
  return {
154
  "success": True,
155
- "prediction": float(prediction[0]),
156
- "units": "kPa",
157
- "input_parameters": input_data,
158
- "prediction_time_ms": prediction_time_ms
 
 
 
 
 
 
 
159
  }
 
160
  except Exception as e:
161
- raise HTTPException(status_code=400, detail=str(e))
162
 
163
- # Status endpoint
164
- @app.get("/status", summary="API Status")
165
- async def status():
166
  """
167
- Returns the API status and model availability.
 
 
168
  """
169
- return {
170
- "status": "API is running",
171
- "model_loaded": model is not None,
172
- "model_type": "Random Forest Regressor" if model is not None else None,
173
- "feature_order": FEATURE_ORDER.tolist() if model is not None else [],
174
- "server_time": time.time()
 
 
 
 
 
 
 
 
175
  }
 
 
 
 
 
 
 
 
 
 
176
 
177
- # Model info endpoint
178
- @app.get("/model-info", summary="Model Information")
179
- async def model_info(model=Depends(get_model)):
180
  """
181
- Returns detailed information about the model being used.
 
 
182
  """
 
183
  try:
184
- # Extract model parameters
185
- model_params = model.get_params()
186
-
187
- return {
188
- "model_type": type(model).__name__,
189
- "features": FEATURE_ORDER.tolist(),
190
- "target": "UCS (kPa)",
 
 
 
 
 
 
 
 
 
 
 
191
  "valid_ranges": {
192
- "cement_perecent": {"min": 0, "max": 15, "units": "%"},
193
- "curing_period": {"min": 0, "max": 90, "units": "days"},
194
- "compaction_rate": {"min": 0.5, "max": 2, "units": "mm/min"}
195
  },
196
- "model_parameters": {
197
- "n_estimators": model_params.get("n_estimators", None),
198
- "max_depth": model_params.get("max_depth", None),
199
- "min_samples_split": model_params.get("min_samples_split", None),
200
- "min_samples_leaf": model_params.get("min_samples_leaf", None)
 
201
  }
202
  }
 
 
 
 
 
 
 
 
 
 
 
203
  except Exception as e:
204
- raise HTTPException(status_code=500, detail=f"Error retrieving model information: {str(e)}")
205
 
206
- # Documentation endpoint
207
- @app.get("/", summary="API Documentation")
208
- async def root():
209
- """
210
- Returns information about the API.
211
- """
212
- return {
213
- "message": "Welcome to the UCS prediction API for cement-stabilized soils",
214
- "endpoints": {
215
- "predict": "/predict - Make UCS predictions",
216
- "status": "/status - Check API status",
217
- "model-info": "/model-info - Get model information"
218
- },
219
- "docs": "/docs - Swagger documentation",
220
- "redoc": "/redoc - ReDoc documentation"
221
- }
222
 
223
- # ValidationError exception handler
224
  @app.exception_handler(ValidationError)
225
  async def validation_exception_handler(request: Request, exc: ValidationError):
226
- errors = []
 
 
 
 
 
227
  for error in exc.errors():
 
228
  message = error.get('msg', '')
229
- if message.startswith("Value error, "):
230
- message = message[12:]
231
 
232
- errors.append({
233
- "loc": error.get('loc', []),
234
- "msg": message,
235
- "type": error.get('type', '')
 
 
 
 
 
 
 
 
236
  })
237
 
238
  return JSONResponse(
239
  status_code=422,
240
- content={"detail": errors}
241
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
10
  from fastapi.middleware.cors import CORSMiddleware
11
+ from fastapi.staticfiles import StaticFiles
12
  import pandas as pd
13
  import joblib
14
  import numpy as np
15
  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"],
63
  allow_headers=["*"],
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
83
+ 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 să fie robust și să 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:
127
+ FEATURE_ORDER = np.array(system_metadata['feature_names'])
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ă că 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 că 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 că 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
  )
211
 
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(..., regex="^(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 să 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(
325
+ lower=float(central_prediction - margin),
326
+ upper=float(central_prediction + margin),
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 {
334
+ 'central_prediction': float(central_prediction),
335
+ 'uncertainty_estimate': float(uncertainty_estimate),
336
+ 'relative_uncertainty': float(relative_uncertainty),
337
+ 'confidence_intervals': confidence_intervals
338
+ }
339
+
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 că 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>
405
+ <html>
406
+ <head>
407
+ <title>UCS Prediction API</title>
408
+ <style>
409
+ body { font-family: Arial, sans-serif; margin: 40px; }
410
+ .header { color: #2c3e50; }
411
+ .endpoint { background: #f8f9fa; padding: 15px; margin: 10px 0; border-left: 4px solid #007bff; }
412
+ </style>
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
+ }
474
+ ```
475
  """
476
+
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 = {
509
+ "primary_model": type(primary_model).__name__,
510
+ "uncertainty_model": type(uncertainty_model).__name__,
511
+ "feature_order": FEATURE_ORDER.tolist(),
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'],
522
+ units="kPa",
523
+ uncertainty_estimate=prediction_result['uncertainty_estimate'],
524
+ relative_uncertainty=prediction_result['relative_uncertainty'],
525
+ confidence_intervals=prediction_result['confidence_intervals'],
526
+ interpretation=interpretation,
527
+ input_parameters=input_data,
528
+ prediction_time_ms=processing_time,
529
+ model_info=model_info
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:
592
+ prediction_df[feature] = input_df[feature]
593
+
594
+ input_array = prediction_df.values
595
+ prediction_result = predict_with_uncertainty(input_array)
596
+
597
+ results.append({
598
+ param_to_vary: float(value),
599
+ "central_prediction": prediction_result['central_prediction'],
600
+ "uncertainty_estimate": prediction_result['uncertainty_estimate'],
601
+ "relative_uncertainty": prediction_result['relative_uncertainty'],
602
+ "confidence_95_lower": prediction_result['confidence_intervals']['95%'].lower,
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
+
610
+ sensitivity_stats = {
611
+ "parameter_range": {
612
+ "min": float(min_variation),
613
+ "max": float(max_variation),
614
+ "base_value": float(base_value)
615
+ },
616
+ "prediction_sensitivity": {
617
+ "min_prediction": float(min(predictions)),
618
+ "max_prediction": float(max(predictions)),
619
+ "range": float(max(predictions) - min(predictions)),
620
+ "relative_change": float((max(predictions) - min(predictions)) / base_params.get("central_prediction", predictions[num_points//2]) * 100)
621
+ },
622
+ "uncertainty_sensitivity": {
623
+ "min_uncertainty": float(min(uncertainties)),
624
+ "max_uncertainty": float(max(uncertainties)),
625
+ "range": float(max(uncertainties) - min(uncertainties))
626
+ }
627
+ }
628
 
629
  return {
630
  "success": True,
631
+ "parameter_analyzed": param_to_vary,
632
+ "base_parameters": base_params,
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 = {
656
+ "api_status": "running",
657
+ "timestamp": datetime.now().isoformat(),
658
+ "system_loaded": system_loaded,
659
+ "models_status": {
660
+ "primary_model": primary_model is not None,
661
+ "uncertainty_model": uncertainty_model is not None,
662
+ "metadata_available": system_metadata is not None
663
+ },
664
+ "feature_configuration": {
665
+ "feature_order": FEATURE_ORDER.tolist() if FEATURE_ORDER is not None else [],
666
+ "num_features": len(FEATURE_ORDER) if FEATURE_ORDER is not None else 0
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()
674
+ status_info["functionality_test"] = "passed" if test_result else "failed"
675
+ except Exception as e:
676
+ status_info["functionality_test"] = f"error: {str(e)}"
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:
689
+ model_info = {
690
+ "system_type": "Two-stage Random Forest Uncertainty Quantification",
691
+ "models": {
692
+ "primary_model": {
693
+ "type": type(primary_model).__name__,
694
+ "parameters": primary_model.get_params(),
695
+ "purpose": "Central UCS prediction"
696
+ },
697
+ "uncertainty_model": {
698
+ "type": type(uncertainty_model).__name__,
699
+ "parameters": uncertainty_model.get_params(),
700
+ "purpose": "Prediction error magnitude estimation"
701
+ }
702
+ },
703
+ "features": {
704
+ "input_features": FEATURE_ORDER.tolist(),
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
  },
712
+ "confidence_levels": ["68%", "80%", "90%", "95%"],
713
+ "target_variable": {
714
+ "name": "UCS",
715
+ "description": "Unconfined Compressive Strength",
716
+ "units": "kPa",
717
+ "typical_range": "150-5500 kPa based on experimental data"
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"),
725
+ "training_timestamp": system_metadata.get("training_timestamp", "Unknown"),
726
+ "model_version": "2.0.0"
727
+ }
728
+
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 = []
746
  for error in exc.errors():
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,
760
+ "message": message,
761
+ "error_type": error.get('type', '')
762
  })
763
 
764
  return JSONResponse(
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 că 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 că 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)