|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import FastAPI, HTTPException, Request, Depends, Query |
|
|
from fastapi.responses import JSONResponse, HTMLResponse |
|
|
from fastapi.middleware.cors import CORSMiddleware |
|
|
from fastapi.staticfiles import StaticFiles |
|
|
import pandas as pd |
|
|
import joblib |
|
|
import numpy as np |
|
|
import os |
|
|
import time |
|
|
import pickle |
|
|
from datetime import datetime |
|
|
from sklearn.ensemble import RandomForestRegressor |
|
|
from pydantic import BaseModel, ValidationError, Field, field_validator, model_validator |
|
|
from typing import Any, Dict, List, Optional, Union |
|
|
from scipy import stats |
|
|
import json |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = FastAPI( |
|
|
title="UCS Prediction API with Uncertainty Quantification", |
|
|
description=""" |
|
|
**Advanced API for predicting Unconfined Compressive Strength (UCS) of cement-stabilized soils** |
|
|
|
|
|
This application implements the uncertainty quantification system developed in the research |
|
|
"Prediction of Unconfined Compressive Strength in Cement-Treated Soil: A Machine Learning Approach". |
|
|
|
|
|
**Main features:** |
|
|
- Accurate UCS predictions using optimized Random Forest |
|
|
- Complete uncertainty quantification with calibrated confidence intervals |
|
|
- Sensitivity analysis for parameter optimization |
|
|
- Interpretability through feature importance analysis |
|
|
|
|
|
**Developed by:** Research Team - Technical University Gheorghe Asachi of IaΘi |
|
|
""", |
|
|
version="2.0.0", |
|
|
contact={ |
|
|
"name": "UCS Development Team", |
|
|
"email": "iancu-bogdan.teodoru@academic.tuiasi.ro", |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
app.add_middleware( |
|
|
CORSMiddleware, |
|
|
allow_origins=[ |
|
|
"http://www.bi4e-at.tuiasi.ro", |
|
|
"https://www.bi4e-at.tuiasi.ro" |
|
|
|
|
|
|
|
|
], |
|
|
allow_credentials=True, |
|
|
allow_methods=["GET", "POST", "OPTIONS"], |
|
|
allow_headers=["*"], |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
MODELS_DIR = "./models_for_deployment" |
|
|
PRIMARY_MODEL_PATH = os.path.join(MODELS_DIR, "rf_primary_model.joblib") |
|
|
UNCERTAINTY_MODEL_PATH = os.path.join(MODELS_DIR, "rf_uncertainty_model.joblib") |
|
|
METADATA_PATH = os.path.join(MODELS_DIR, "system_metadata.pkl") |
|
|
|
|
|
|
|
|
DEFAULT_FEATURE_ORDER = ['cement_percent', 'curing_period', 'compaction_rate'] |
|
|
|
|
|
|
|
|
primary_model = None |
|
|
uncertainty_model = None |
|
|
system_metadata = None |
|
|
FEATURE_ORDER = None |
|
|
|
|
|
def load_uncertainty_system(): |
|
|
""" |
|
|
Loads and validates the entire uncertainty system. |
|
|
|
|
|
This function orchestrates the loading of all system components |
|
|
and performs basic validations to ensure proper operation. |
|
|
The process is designed to be robust and provide detailed information |
|
|
about any issues encountered during loading. |
|
|
""" |
|
|
global primary_model, uncertainty_model, system_metadata, FEATURE_ORDER |
|
|
|
|
|
print("π Loading uncertainty system...") |
|
|
start_time = time.time() |
|
|
|
|
|
try: |
|
|
|
|
|
if os.path.exists(PRIMARY_MODEL_PATH): |
|
|
primary_model = joblib.load(PRIMARY_MODEL_PATH) |
|
|
print(f"β
Primary model loaded: {type(primary_model).__name__}") |
|
|
else: |
|
|
raise FileNotFoundError(f"Primary model not found at: {PRIMARY_MODEL_PATH}") |
|
|
|
|
|
|
|
|
if os.path.exists(UNCERTAINTY_MODEL_PATH): |
|
|
uncertainty_model = joblib.load(UNCERTAINTY_MODEL_PATH) |
|
|
print(f"β
Uncertainty model loaded: {type(uncertainty_model).__name__}") |
|
|
else: |
|
|
raise FileNotFoundError(f"Uncertainty model not found at: {UNCERTAINTY_MODEL_PATH}") |
|
|
|
|
|
|
|
|
if os.path.exists(METADATA_PATH): |
|
|
with open(METADATA_PATH, 'rb') as f: |
|
|
system_metadata = pickle.load(f) |
|
|
print(f"β
System metadata loaded: {len(system_metadata)} keys") |
|
|
else: |
|
|
print("β οΈ System metadata not found, using default values") |
|
|
system_metadata = {"feature_names": DEFAULT_FEATURE_ORDER} |
|
|
|
|
|
|
|
|
if hasattr(primary_model, 'feature_names_in_'): |
|
|
FEATURE_ORDER = primary_model.feature_names_in_ |
|
|
elif system_metadata and 'feature_names' in system_metadata: |
|
|
FEATURE_ORDER = np.array(system_metadata['feature_names']) |
|
|
else: |
|
|
FEATURE_ORDER = np.array(DEFAULT_FEATURE_ORDER) |
|
|
|
|
|
|
|
|
validation_result = validate_models_compatibility() |
|
|
if not validation_result: |
|
|
raise ValueError("Models are not compatible with each other") |
|
|
|
|
|
load_time = time.time() - start_time |
|
|
print(f"π Uncertainty system loaded successfully in {load_time:.2f} seconds!") |
|
|
print(f"π Features: {FEATURE_ORDER.tolist()}") |
|
|
|
|
|
return True |
|
|
|
|
|
except Exception as e: |
|
|
print(f"β Error loading system: {str(e)}") |
|
|
import traceback |
|
|
print(traceback.format_exc()) |
|
|
return False |
|
|
|
|
|
def validate_models_compatibility(): |
|
|
""" |
|
|
Validates that models are compatible and work together. |
|
|
|
|
|
This validation includes dimensional compatibility tests, |
|
|
data type checks and a complete functional test. |
|
|
""" |
|
|
try: |
|
|
|
|
|
test_input = np.array([[5.0, 14.0, 1.0]]) |
|
|
|
|
|
|
|
|
primary_pred = primary_model.predict(test_input)[0] |
|
|
|
|
|
|
|
|
uncertainty_input = np.column_stack([test_input, [[primary_pred]]]) |
|
|
uncertainty_pred = uncertainty_model.predict(uncertainty_input)[0] |
|
|
|
|
|
|
|
|
assert isinstance(primary_pred, (int, float, np.number)) |
|
|
assert isinstance(uncertainty_pred, (int, float, np.number)) |
|
|
assert primary_pred > 0 |
|
|
assert uncertainty_pred > 0 |
|
|
|
|
|
print(f"β
Compatibility test: UCS={primary_pred:.1f} kPa, Ο={uncertainty_pred:.1f} kPa") |
|
|
return True |
|
|
|
|
|
except Exception as e: |
|
|
print(f"β Compatibility test failed: {str(e)}") |
|
|
return False |
|
|
|
|
|
|
|
|
system_loaded = load_uncertainty_system() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SoilInput(BaseModel): |
|
|
""" |
|
|
Model for soil input data. |
|
|
|
|
|
This class defines and validates input parameters, |
|
|
ensuring values are within validated experimental ranges. |
|
|
""" |
|
|
cement_perecent: float = Field( |
|
|
..., |
|
|
description="Cement percentage in mixture", |
|
|
ge=0, le=15, |
|
|
example=5.0 |
|
|
) |
|
|
curing_period: float = Field( |
|
|
..., |
|
|
description="Curing period in days", |
|
|
ge=0, le=90, |
|
|
example=28.0 |
|
|
) |
|
|
compaction_rate: float = Field( |
|
|
..., |
|
|
description="Compaction rate in mm/min", |
|
|
ge=0.5, le=2.0, |
|
|
example=1.0 |
|
|
) |
|
|
|
|
|
@model_validator(mode="after") |
|
|
def validate_cement_curing_relationship(self): |
|
|
""" |
|
|
Validates the relationship between cement content and curing period. |
|
|
|
|
|
For untreated soil (0% cement), curing period is forced to 0 |
|
|
because there is no cement hydration process. |
|
|
""" |
|
|
if self.cement_perecent == 0: |
|
|
self.curing_period = 0 |
|
|
elif self.cement_perecent > 0 and self.curing_period < 1: |
|
|
raise ValueError("For cement-treated soil, curing period must be β₯ 1 day") |
|
|
return self |
|
|
|
|
|
class Config: |
|
|
json_schema_extra = { |
|
|
"example": { |
|
|
"cement_perecent": 5.0, |
|
|
"curing_period": 28.0, |
|
|
"compaction_rate": 1.0 |
|
|
} |
|
|
} |
|
|
|
|
|
class ConfidenceInterval(BaseModel): |
|
|
"""Model for a confidence interval.""" |
|
|
lower: float = Field(..., description="Lower bound of the interval") |
|
|
upper: float = Field(..., description="Upper bound of the interval") |
|
|
width: float = Field(..., description="Width of the interval") |
|
|
|
|
|
class UncertaintyPredictionResponse(BaseModel): |
|
|
""" |
|
|
Complete response with uncertainty quantification. |
|
|
|
|
|
This extended structure provides the engineer with a complete picture |
|
|
of the prediction, including not only the estimated value but also confidence |
|
|
in that estimate through calibrated intervals. |
|
|
""" |
|
|
success: bool = Field(..., description="Request processing status") |
|
|
|
|
|
|
|
|
central_prediction: float = Field(..., description="Most probable UCS prediction") |
|
|
units: str = Field(default="kPa", description="Units of measurement") |
|
|
|
|
|
|
|
|
uncertainty_estimate: float = Field(..., description="Absolute uncertainty estimate (1-sigma)") |
|
|
relative_uncertainty: float = Field(..., description="Relative uncertainty as percentage") |
|
|
|
|
|
|
|
|
confidence_intervals: Dict[str, ConfidenceInterval] = Field( |
|
|
..., |
|
|
description="Confidence intervals for multiple probability levels" |
|
|
) |
|
|
|
|
|
|
|
|
interpretation: Dict[str, str] = Field(..., description="Interpretation guide for results") |
|
|
|
|
|
|
|
|
input_parameters: Dict[str, float] = Field(..., description="Input parameters used") |
|
|
prediction_time_ms: Optional[float] = Field(None, description="Processing time in milliseconds") |
|
|
model_info: Optional[Dict[str, Any]] = Field(None, description="Information about models used") |
|
|
|
|
|
class SensitivityAnalysisRequest(BaseModel): |
|
|
"""Request for sensitivity analysis.""" |
|
|
base_parameters: SoilInput |
|
|
parameter_to_vary: str = Field(..., pattern="^(cement_perecent|curing_period|compaction_rate)$") |
|
|
variation_range: float = Field(default=10.0, ge=1.0, le=50.0, description="Variation range in percentage") |
|
|
num_points: int = Field(default=11, ge=5, le=21, description="Number of points for analysis") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def predict_with_uncertainty(input_data: np.ndarray, |
|
|
confidence_levels: List[float] = [0.68, 0.80, 0.90, 0.95]) -> Dict[str, Any]: |
|
|
""" |
|
|
Performs complete prediction with uncertainty quantification. |
|
|
|
|
|
This function implements the two-stage algorithm developed in research: |
|
|
1. Primary model generates central UCS prediction |
|
|
2. Uncertainty model estimates magnitude of probable error |
|
|
3. Confidence intervals are constructed assuming normal distribution |
|
|
|
|
|
Args: |
|
|
input_data: Numpy array with features [cement%, curing_days, compaction_rate] |
|
|
confidence_levels: List of confidence levels for which to calculate intervals |
|
|
|
|
|
Returns: |
|
|
Dictionary with central prediction, uncertainty estimation and confidence intervals |
|
|
""" |
|
|
|
|
|
|
|
|
central_prediction = primary_model.predict(input_data)[0] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
uncertainty_input = np.column_stack([input_data, [[central_prediction]]]) |
|
|
|
|
|
|
|
|
uncertainty_estimate = uncertainty_model.predict(uncertainty_input)[0] |
|
|
|
|
|
|
|
|
confidence_intervals = {} |
|
|
|
|
|
for conf_level in confidence_levels: |
|
|
|
|
|
|
|
|
z_score = stats.norm.ppf((1 + conf_level) / 2) |
|
|
|
|
|
|
|
|
margin = z_score * uncertainty_estimate |
|
|
|
|
|
confidence_intervals[f'{conf_level:.0%}'] = ConfidenceInterval( |
|
|
lower=float(central_prediction - margin), |
|
|
upper=float(central_prediction + margin), |
|
|
width=float(2 * margin) |
|
|
) |
|
|
|
|
|
|
|
|
relative_uncertainty = (uncertainty_estimate / central_prediction) * 100 if central_prediction != 0 else 0 |
|
|
|
|
|
return { |
|
|
'central_prediction': float(central_prediction), |
|
|
'uncertainty_estimate': float(uncertainty_estimate), |
|
|
'relative_uncertainty': float(relative_uncertainty), |
|
|
'confidence_intervals': confidence_intervals |
|
|
} |
|
|
|
|
|
def generate_interpretation_guide(central_prediction: float, uncertainty_estimate: float, |
|
|
confidence_intervals: Dict[str, ConfidenceInterval]) -> Dict[str, str]: |
|
|
""" |
|
|
Generates a personalized interpretation guide for prediction results. |
|
|
|
|
|
This function translates statistical results into practical language for engineers, |
|
|
providing the necessary context for informed decision making in projects. |
|
|
""" |
|
|
|
|
|
|
|
|
interval_95 = confidence_intervals.get('95%') |
|
|
|
|
|
|
|
|
relative_unc = (uncertainty_estimate / central_prediction) * 100 |
|
|
|
|
|
if relative_unc <= 10: |
|
|
confidence_level = "very high" |
|
|
reliability_desc = "The prediction is very reliable for design decision making." |
|
|
elif relative_unc <= 20: |
|
|
confidence_level = "high" |
|
|
reliability_desc = "The prediction is reliable, we recommend validation through limited testing." |
|
|
elif relative_unc <= 30: |
|
|
confidence_level = "moderate" |
|
|
reliability_desc = "The prediction provides a useful estimate, but additional testing is recommended." |
|
|
else: |
|
|
confidence_level = "limited" |
|
|
reliability_desc = "The prediction is indicative, extensive testing is recommended for validation." |
|
|
|
|
|
interpretation = { |
|
|
"central_prediction": f"The most probable UCS value is {central_prediction:.0f} kPa, based on the input parameters.", |
|
|
|
|
|
"uncertainty": f"The estimated uncertainty is Β±{uncertainty_estimate:.0f} kPa ({relative_unc:.1f}%), " |
|
|
f"indicating {confidence_level} confidence in the prediction.", |
|
|
|
|
|
"confidence_95": f"We have 95% confidence that the actual UCS value is between " |
|
|
f"{interval_95.lower:.0f} and {interval_95.upper:.0f} kPa." if interval_95 else "", |
|
|
|
|
|
"reliability": reliability_desc, |
|
|
|
|
|
"practical_guidance": f"For applications with UCS requirements > {central_prediction + uncertainty_estimate:.0f} kPa, " |
|
|
f"consider increasing cement content or extending the curing period." |
|
|
} |
|
|
|
|
|
return interpretation |
|
|
|
|
|
async def validate_models_loaded(): |
|
|
"""Dependency function for validating model loading.""" |
|
|
if not system_loaded or primary_model is None or uncertainty_model is None: |
|
|
raise HTTPException( |
|
|
status_code=503, |
|
|
detail="Model system is not loaded correctly. Contact administrator." |
|
|
) |
|
|
return True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse, summary="Main page") |
|
|
async def root(): |
|
|
""" |
|
|
Returns the main page with API information. |
|
|
""" |
|
|
return """ |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<title>UCS Prediction API</title> |
|
|
<style> |
|
|
body { font-family: Arial, sans-serif; margin: 40px; } |
|
|
.header { color: #2c3e50; } |
|
|
.endpoint { background: #f8f9fa; padding: 15px; margin: 10px 0; border-left: 4px solid #007bff; } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<h1 class="header">ποΈ UCS Prediction API with Uncertainty Quantification</h1> |
|
|
<p>Advanced API for predicting unconfined compressive strength of cement-stabilized soils.</p> |
|
|
|
|
|
<h2>π Available endpoints:</h2> |
|
|
<div class="endpoint"> |
|
|
<strong>POST /predict</strong> - UCS prediction with uncertainty quantification |
|
|
</div> |
|
|
<div class="endpoint"> |
|
|
<strong>POST /sensitivity-analysis</strong> - Parameter sensitivity analysis |
|
|
</div> |
|
|
<div class="endpoint"> |
|
|
<strong>GET /status</strong> - System status |
|
|
</div> |
|
|
<div class="endpoint"> |
|
|
<strong>GET /model-info</strong> - Detailed model information |
|
|
</div> |
|
|
|
|
|
<h2>π Documentation:</h2> |
|
|
<p><a href="/docs">Swagger UI - Interactive documentation</a></p> |
|
|
<p><a href="/redoc">ReDoc - Alternative documentation</a></p> |
|
|
|
|
|
<footer style="margin-top: 40px; color: #666;"> |
|
|
<p>Developed by the research team - Technical University Gheorghe Asachi of IaΘi</p> |
|
|
</footer> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
@app.post("/predict", response_model=UncertaintyPredictionResponse, |
|
|
summary="UCS Prediction with Uncertainty Quantification") |
|
|
async def predict_ucs_with_uncertainty( |
|
|
soil_data: SoilInput, |
|
|
include_model_info: bool = Query(False, description="Include detailed model information"), |
|
|
_: bool = Depends(validate_models_loaded) |
|
|
): |
|
|
""" |
|
|
**Performs UCS prediction with complete uncertainty quantification.** |
|
|
|
|
|
This endpoint implements the advanced uncertainty system developed in our research, |
|
|
providing not only the central prediction but also calibrated confidence intervals at multiple levels. |
|
|
|
|
|
**Input parameters:** |
|
|
- **cement_percent**: Cement content (0-15%) |
|
|
- **curing_period**: Curing period (0-90 days) |
|
|
- **compaction_rate**: Compaction rate (0.5-2.0 mm/min) |
|
|
|
|
|
**Results include:** |
|
|
- Central UCS prediction in kPa |
|
|
- Absolute and relative uncertainty estimation |
|
|
- Confidence intervals at 68%, 80%, 90% and 95% |
|
|
- Personalized interpretation guide for results |
|
|
|
|
|
**Typical usage:** |
|
|
```json |
|
|
{ |
|
|
"cement_percent": 7.5, |
|
|
"curing_period": 28, |
|
|
"compaction_rate": 1.0 |
|
|
} |
|
|
``` |
|
|
""" |
|
|
|
|
|
try: |
|
|
start_time = time.time() |
|
|
|
|
|
|
|
|
input_data = soil_data.dict() |
|
|
input_df = pd.DataFrame([input_data]) |
|
|
|
|
|
|
|
|
prediction_df = pd.DataFrame() |
|
|
for feature in FEATURE_ORDER: |
|
|
if feature in input_df.columns: |
|
|
prediction_df[feature] = input_df[feature] |
|
|
else: |
|
|
raise ValueError(f"Feature '{feature}' missing from input data") |
|
|
|
|
|
|
|
|
input_array = prediction_df.values |
|
|
|
|
|
|
|
|
prediction_result = predict_with_uncertainty(input_array) |
|
|
|
|
|
|
|
|
interpretation = generate_interpretation_guide( |
|
|
prediction_result['central_prediction'], |
|
|
prediction_result['uncertainty_estimate'], |
|
|
prediction_result['confidence_intervals'] |
|
|
) |
|
|
|
|
|
|
|
|
model_info = None |
|
|
if include_model_info: |
|
|
model_info = { |
|
|
"primary_model": type(primary_model).__name__, |
|
|
"uncertainty_model": type(uncertainty_model).__name__, |
|
|
"feature_order": FEATURE_ORDER.tolist(), |
|
|
"system_metadata": system_metadata if system_metadata else "Not available" |
|
|
} |
|
|
|
|
|
|
|
|
processing_time = (time.time() - start_time) * 1000 |
|
|
|
|
|
|
|
|
return UncertaintyPredictionResponse( |
|
|
success=True, |
|
|
central_prediction=prediction_result['central_prediction'], |
|
|
units="kPa", |
|
|
uncertainty_estimate=prediction_result['uncertainty_estimate'], |
|
|
relative_uncertainty=prediction_result['relative_uncertainty'], |
|
|
confidence_intervals=prediction_result['confidence_intervals'], |
|
|
interpretation=interpretation, |
|
|
input_parameters=input_data, |
|
|
prediction_time_ms=processing_time, |
|
|
model_info=model_info |
|
|
) |
|
|
|
|
|
except ValueError as ve: |
|
|
raise HTTPException(status_code=400, detail=f"Validation error: {str(ve)}") |
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=f"Processing error: {str(e)}") |
|
|
|
|
|
@app.post("/sensitivity-analysis", summary="Parameter Sensitivity Analysis") |
|
|
async def perform_sensitivity_analysis( |
|
|
request: SensitivityAnalysisRequest, |
|
|
_: bool = Depends(validate_models_loaded) |
|
|
): |
|
|
""" |
|
|
**Performs sensitivity analysis for a specific parameter.** |
|
|
|
|
|
This analysis shows how variation of an input parameter affects |
|
|
both the central prediction and associated uncertainty, providing valuable |
|
|
insights for mix design optimization. |
|
|
""" |
|
|
|
|
|
try: |
|
|
base_params = request.base_parameters.dict() |
|
|
param_to_vary = request.parameter_to_vary |
|
|
variation_range = request.variation_range / 100 |
|
|
num_points = request.num_points |
|
|
|
|
|
|
|
|
base_value = base_params[param_to_vary] |
|
|
|
|
|
|
|
|
min_variation = base_value * (1 - variation_range) |
|
|
max_variation = base_value * (1 + variation_range) |
|
|
|
|
|
|
|
|
if param_to_vary == "cement_percent": |
|
|
min_variation = max(0, min_variation) |
|
|
max_variation = min(15, max_variation) |
|
|
elif param_to_vary == "curing_period": |
|
|
min_variation = max(0 if base_params["cement_percent"] == 0 else 1, min_variation) |
|
|
max_variation = min(90, max_variation) |
|
|
elif param_to_vary == "compaction_rate": |
|
|
min_variation = max(0.5, min_variation) |
|
|
max_variation = min(2.0, max_variation) |
|
|
|
|
|
|
|
|
variation_values = np.linspace(min_variation, max_variation, num_points) |
|
|
|
|
|
results = [] |
|
|
|
|
|
for value in variation_values: |
|
|
|
|
|
modified_params = base_params.copy() |
|
|
modified_params[param_to_vary] = float(value) |
|
|
|
|
|
|
|
|
if modified_params["cement_percent"] == 0: |
|
|
modified_params["curing_period"] = 0 |
|
|
|
|
|
|
|
|
input_df = pd.DataFrame([modified_params]) |
|
|
prediction_df = pd.DataFrame() |
|
|
for feature in FEATURE_ORDER: |
|
|
prediction_df[feature] = input_df[feature] |
|
|
|
|
|
input_array = prediction_df.values |
|
|
prediction_result = predict_with_uncertainty(input_array) |
|
|
|
|
|
results.append({ |
|
|
param_to_vary: float(value), |
|
|
"central_prediction": prediction_result['central_prediction'], |
|
|
"uncertainty_estimate": prediction_result['uncertainty_estimate'], |
|
|
"relative_uncertainty": prediction_result['relative_uncertainty'], |
|
|
"confidence_95_lower": prediction_result['confidence_intervals']['95%'].lower, |
|
|
"confidence_95_upper": prediction_result['confidence_intervals']['95%'].upper |
|
|
}) |
|
|
|
|
|
|
|
|
predictions = [r["central_prediction"] for r in results] |
|
|
uncertainties = [r["uncertainty_estimate"] for r in results] |
|
|
|
|
|
sensitivity_stats = { |
|
|
"parameter_range": { |
|
|
"min": float(min_variation), |
|
|
"max": float(max_variation), |
|
|
"base_value": float(base_value) |
|
|
}, |
|
|
"prediction_sensitivity": { |
|
|
"min_prediction": float(min(predictions)), |
|
|
"max_prediction": float(max(predictions)), |
|
|
"range": float(max(predictions) - min(predictions)), |
|
|
"relative_change": float((max(predictions) - min(predictions)) / base_params.get("central_prediction", predictions[num_points//2]) * 100) |
|
|
}, |
|
|
"uncertainty_sensitivity": { |
|
|
"min_uncertainty": float(min(uncertainties)), |
|
|
"max_uncertainty": float(max(uncertainties)), |
|
|
"range": float(max(uncertainties) - min(uncertainties)) |
|
|
} |
|
|
} |
|
|
|
|
|
return { |
|
|
"success": True, |
|
|
"parameter_analyzed": param_to_vary, |
|
|
"base_parameters": base_params, |
|
|
"sensitivity_data": results, |
|
|
"sensitivity_statistics": sensitivity_stats, |
|
|
"interpretation": { |
|
|
"parameter_impact": f"A {variation_range*100:.1f}% variation in {param_to_vary} " |
|
|
f"produces a change of {sensitivity_stats['prediction_sensitivity']['range']:.1f} kPa in UCS", |
|
|
"recommendation": "The parameter with the greatest impact should be carefully controlled in the field" |
|
|
if sensitivity_stats['prediction_sensitivity']['relative_change'] > 10 |
|
|
else "The parameter has moderate impact, small variations are acceptable" |
|
|
} |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=f"Error in sensitivity analysis: {str(e)}") |
|
|
|
|
|
@app.get("/status", summary="System Status") |
|
|
async def get_system_status(): |
|
|
""" |
|
|
**Returns complete system status for uncertainty quantification.** |
|
|
|
|
|
Useful for monitoring application health and diagnosing problems. |
|
|
""" |
|
|
|
|
|
status_info = { |
|
|
"api_status": "running", |
|
|
"timestamp": datetime.now().isoformat(), |
|
|
"system_loaded": system_loaded, |
|
|
"models_status": { |
|
|
"primary_model": primary_model is not None, |
|
|
"uncertainty_model": uncertainty_model is not None, |
|
|
"metadata_available": system_metadata is not None |
|
|
}, |
|
|
"feature_configuration": { |
|
|
"feature_order": FEATURE_ORDER.tolist() if FEATURE_ORDER is not None else [], |
|
|
"num_features": len(FEATURE_ORDER) if FEATURE_ORDER is not None else 0 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if system_loaded: |
|
|
try: |
|
|
test_result = validate_models_compatibility() |
|
|
status_info["functionality_test"] = "passed" if test_result else "failed" |
|
|
except Exception as e: |
|
|
status_info["functionality_test"] = f"error: {str(e)}" |
|
|
|
|
|
return status_info |
|
|
|
|
|
@app.get("/model-info", summary="Model Information") |
|
|
async def get_model_information(_: bool = Depends(validate_models_loaded)): |
|
|
""" |
|
|
**Returns detailed information about the models used.** |
|
|
|
|
|
Includes model parameters, historical performance and applicability limits. |
|
|
""" |
|
|
|
|
|
try: |
|
|
model_info = { |
|
|
"system_type": "Two-stage Random Forest Uncertainty Quantification", |
|
|
"models": { |
|
|
"primary_model": { |
|
|
"type": type(primary_model).__name__, |
|
|
"parameters": primary_model.get_params(), |
|
|
"purpose": "Central UCS prediction" |
|
|
}, |
|
|
"uncertainty_model": { |
|
|
"type": type(uncertainty_model).__name__, |
|
|
"parameters": uncertainty_model.get_params(), |
|
|
"purpose": "Prediction error magnitude estimation" |
|
|
} |
|
|
}, |
|
|
"features": { |
|
|
"input_features": FEATURE_ORDER.tolist(), |
|
|
"feature_engineering": "Feature augmentation for uncertainty model (original features + central prediction)" |
|
|
}, |
|
|
"valid_ranges": { |
|
|
"cement_percent": {"min": 0, "max": 15, "units": "%", "note": "Based on experimental data"}, |
|
|
"curing_period": {"min": 0, "max": 90, "units": "days", "note": "0 only valid for 0% cement"}, |
|
|
"compaction_rate": {"min": 0.5, "max": 2.0, "units": "mm/min", "note": "Within experimental range"} |
|
|
}, |
|
|
"confidence_levels": ["68%", "80%", "90%", "95%"], |
|
|
"target_variable": { |
|
|
"name": "UCS", |
|
|
"description": "Unconfined Compressive Strength", |
|
|
"units": "kPa", |
|
|
"typical_range": "150-5500 kPa based on experimental data" |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if system_metadata: |
|
|
model_info["training_metadata"] = { |
|
|
"training_samples": system_metadata.get("n_training_samples", "Unknown"), |
|
|
"training_timestamp": system_metadata.get("training_timestamp", "Unknown"), |
|
|
"model_version": "2.0.0" |
|
|
} |
|
|
|
|
|
return model_info |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=f"Error obtaining information: {str(e)}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(ValidationError) |
|
|
async def validation_exception_handler(request: Request, exc: ValidationError): |
|
|
""" |
|
|
Custom handler for Pydantic validation errors. |
|
|
Provides more user-friendly error messages. |
|
|
""" |
|
|
|
|
|
friendly_errors = [] |
|
|
for error in exc.errors(): |
|
|
field = " -> ".join(str(loc) for loc in error.get('loc', [])) |
|
|
message = error.get('msg', '') |
|
|
|
|
|
|
|
|
if "greater than or equal" in message: |
|
|
message = f"Value for {field} is too small" |
|
|
elif "less than or equal" in message: |
|
|
message = f"Value for {field} is too large" |
|
|
elif "string does not match regex" in message: |
|
|
message = f"Value for {field} is not valid" |
|
|
|
|
|
friendly_errors.append({ |
|
|
"field": field, |
|
|
"message": message, |
|
|
"error_type": error.get('type', '') |
|
|
}) |
|
|
|
|
|
return JSONResponse( |
|
|
status_code=422, |
|
|
content={ |
|
|
"success": False, |
|
|
"error": "Input data validation error", |
|
|
"details": friendly_errors, |
|
|
"help": "Check that all values are within specified ranges and try again" |
|
|
} |
|
|
) |
|
|
|
|
|
@app.exception_handler(Exception) |
|
|
async def general_exception_handler(request: Request, exc: Exception): |
|
|
""" |
|
|
General handler for unexpected exceptions. |
|
|
""" |
|
|
return JSONResponse( |
|
|
status_code=500, |
|
|
content={ |
|
|
"success": False, |
|
|
"error": "Internal server error", |
|
|
"message": "An unexpected error occurred. Contact administrator if problem persists.", |
|
|
"request_id": str(time.time()) |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.on_event("startup") |
|
|
async def startup_event(): |
|
|
""" |
|
|
Event executed at application startup. |
|
|
Performs final checks and prepares system for production. |
|
|
""" |
|
|
print("π Starting UCS Prediction API v2.0...") |
|
|
|
|
|
if system_loaded: |
|
|
print("β
Uncertainty system loaded and functional") |
|
|
print(f"π Features configured: {FEATURE_ORDER.tolist()}") |
|
|
else: |
|
|
print("β WARNING: System was not loaded correctly!") |
|
|
print(" Check that model files are present in the models_for_deployment/ directory") |
|
|
|
|
|
print("π API available for requests") |
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
|
import uvicorn |
|
|
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True) |