Spaces:
Sleeping
Sleeping
File size: 17,155 Bytes
2c200f8 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 |
import numpy as np
import re
import streamlit as st
from typing import Dict, List, Any, Tuple
class SoilCalculations:
def __init__(self):
# Peck correlation coefficients for friction angle calculation
self.peck_coefficients = {
"fine_sand": {"a": 27.1, "b": 0.3},
"medium_sand": {"a": 27.1, "b": 0.3},
"coarse_sand": {"a": 27.1, "b": 0.3},
"silty_sand": {"a": 25.4, "b": 0.3},
"clayey_sand": {"a": 25.4, "b": 0.3}
}
def calculate_su_from_n(self, n_value: float, correlation_factor: float = 5.0) -> float:
"""Calculate undrained shear strength from SPT-N value for clay
Su = correlation_factor * N (typically 5-7 for clay)"""
if n_value is None or n_value <= 0:
return None
return correlation_factor * n_value
def calculate_friction_angle_peck(self, n_value: float, sand_type: str = "medium_sand",
effective_stress: float = 100.0) -> float:
"""Calculate friction angle using Peck correlation
φ = a + b * log10(N60) where N60 is corrected SPT value"""
if n_value is None or n_value <= 0:
return None
# Apply overburden correction (simplified)
n60_corrected = n_value * (effective_stress / 100.0) ** 0.5
n60_corrected = min(n60_corrected, 50) # Cap at reasonable value
coeffs = self.peck_coefficients.get(sand_type, self.peck_coefficients["medium_sand"])
friction_angle = coeffs["a"] + coeffs["b"] * np.log10(max(n60_corrected, 1))
return min(friction_angle, 45) # Cap at reasonable maximum
def classify_soil_consistency(self, soil_type: str, n_value: float = None, su_value: float = None) -> str:
"""Classify soil consistency based on strength parameters"""
if "clay" in soil_type.lower() or "silt" in soil_type.lower():
# Use Su for clay classification
if su_value is not None:
if su_value < 25:
return "very soft"
elif su_value < 50:
return "soft"
elif su_value < 100:
return "medium"
elif su_value < 200:
return "stiff"
elif su_value < 400:
return "very stiff"
else:
return "hard"
# Use N-value for clay if Su not available
elif n_value is not None:
if n_value < 2:
return "very soft"
elif n_value < 4:
return "soft"
elif n_value < 8:
return "medium"
elif n_value < 15:
return "stiff"
elif n_value < 30:
return "very stiff"
else:
return "hard"
elif "sand" in soil_type.lower() or "gravel" in soil_type.lower():
# Use N-value for sand classification
if n_value is not None:
if n_value < 4:
return "very loose"
elif n_value < 10:
return "loose"
elif n_value < 30:
return "medium dense"
elif n_value < 50:
return "dense"
else:
return "very dense"
return "unknown"
def standardize_units(self, text: str) -> Tuple[str, Dict[str, str]]:
"""Standardize units in soil boring log text before LLM processing"""
unit_conversions = {}
standardized_text = text
# Convert feet to meters
feet_pattern = r'(\d+(?:\.\d+)?)\s*(?:ft|feet|\')'
feet_matches = re.findall(feet_pattern, standardized_text, re.IGNORECASE)
for match in feet_matches:
feet_value = float(match)
meters_value = feet_value * 0.3048
old_text = f"{match}ft"
new_text = f"{meters_value:.1f}m"
standardized_text = standardized_text.replace(old_text, new_text)
unit_conversions[old_text] = new_text
# Convert psf to kPa
psf_pattern = r'(\d+(?:\.\d+)?)\s*(?:psf|lbs?/ft²?)'
psf_matches = re.findall(psf_pattern, standardized_text, re.IGNORECASE)
for match in psf_matches:
psf_value = float(match)
kpa_value = psf_value * 0.047880259
old_text = f"{match}psf"
new_text = f"{kpa_value:.1f}kPa"
standardized_text = standardized_text.replace(old_text, new_text)
unit_conversions[old_text] = new_text
# Convert psi to kPa
psi_pattern = r'(\d+(?:\.\d+)?)\s*(?:psi|lbs?/in²?)'
psi_matches = re.findall(psi_pattern, standardized_text, re.IGNORECASE)
for match in psi_matches:
psi_value = float(match)
kpa_value = psi_value * 6.89476
old_text = f"{match}psi"
new_text = f"{kpa_value:.1f}kPa"
standardized_text = standardized_text.replace(old_text, new_text)
unit_conversions[old_text] = new_text
# Convert ksc (kg/cm²) to kPa
ksc_pattern = r'(\d+(?:\.\d+)?)\s*(?:ksc|kg/cm²?|kg/cm2)'
ksc_matches = re.findall(ksc_pattern, standardized_text, re.IGNORECASE)
for match in ksc_matches:
ksc_value = float(match)
kpa_value = ksc_value * 98.0
old_text = f"{match}ksc"
new_text = f"{kpa_value:.1f}kPa"
standardized_text = standardized_text.replace(old_text, new_text)
unit_conversions[old_text] = new_text
# Convert t/m² (tonnes per square meter) to kPa
tonnes_pattern = r'(\d+(?:\.\d+)?)\s*(?:t/m²?|ton/m²?|tonnes?/m²?|tonne/m²?)'
tonnes_matches = re.findall(tonnes_pattern, standardized_text, re.IGNORECASE)
for match in tonnes_matches:
tonnes_value = float(match)
kpa_value = tonnes_value * 9.81
old_text = f"{match}t/m²"
new_text = f"{kpa_value:.1f}kPa"
standardized_text = standardized_text.replace(old_text, new_text)
unit_conversions[old_text] = new_text
# Standardize depth notation
depth_pattern = r'(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*(?:ft|feet|\')'
standardized_text = re.sub(depth_pattern,
lambda m: f"{float(m.group(1))*0.3048:.1f}-{float(m.group(2))*0.3048:.1f}m",
standardized_text, flags=re.IGNORECASE)
return standardized_text, unit_conversions
def enhance_soil_layers(self, soil_layers: List[Dict]) -> List[Dict]:
"""Enhance soil layers with calculated parameters"""
enhanced_layers = []
for layer in soil_layers:
enhanced_layer = layer.copy()
# Extract values
n_value = layer.get("strength_value") if layer.get("strength_parameter") == "SPT-N" else None
su_value = layer.get("strength_value") if layer.get("strength_parameter") == "Su" else None
soil_type = layer.get("soil_type", "").lower()
depth_from = layer.get("depth_from", 0)
depth_to = layer.get("depth_to", 0)
sample_type = layer.get("sample_type", "")
su_source = layer.get("su_source", "")
# CRITICAL RULE: For SS samples, ALWAYS use Su=5*N calculation, IGNORE unconfined compression Su
if sample_type == "SS" and "clay" in soil_type:
# For SS samples, we MUST use N-value to calculate Su, regardless of any other Su data
if n_value is not None:
calculated_su = self.calculate_su_from_n(n_value)
enhanced_layer["strength_parameter"] = "Su"
enhanced_layer["strength_value"] = calculated_su
enhanced_layer["su_source"] = f"SS Sample: Calculated from raw N={n_value} (Su=5*N)"
enhanced_layer["original_spt"] = n_value
# Override any existing Su values for SS samples
if su_value is not None and su_value != calculated_su:
enhanced_layer["ignored_unconfined_su"] = su_value
st.warning(f"⚠️ SS Sample Layer {enhanced_layer.get('layer_id', 'Unknown')}: Ignored unconfined Su={su_value:.0f}, using calculated Su={calculated_su:.0f} kPa from N={n_value}")
st.success(f"✅ SS Sample Layer {enhanced_layer.get('layer_id', 'Unknown')}: Su = 5 × {n_value} = {calculated_su:.0f} kPa")
else:
st.error(f"❌ SS Sample Layer {enhanced_layer.get('layer_id', 'Unknown')}: No N-value found for Su calculation")
# For ST samples, preserve direct Su measurements
elif sample_type == "ST" and su_value is not None:
enhanced_layer["su_source"] = su_source or "ST Sample: Direct measurement from Unconfined Compression Test"
st.success(f"✅ ST Sample Layer {enhanced_layer.get('layer_id', 'Unknown')}: Using direct Su={su_value:.0f} kPa")
# For other cases (no sample type specified), use previous logic but prioritize sample identification
else:
# Try to identify sample type from available data
if n_value is not None and su_value is None and "clay" in soil_type:
# Only calculate Su from N-value if no direct Su available (likely SS sample)
calculated_su = self.calculate_su_from_n(n_value)
enhanced_layer["calculated_su"] = calculated_su
enhanced_layer["su_source"] = f"Calculated from N={n_value} (Su=5*N) - assumed SS sample"
st.info(f"🔬 Layer {enhanced_layer.get('layer_id', 'Unknown')}: Calculated Su={calculated_su:.0f} kPa from N={n_value} (assumed SS)")
elif su_value is not None:
# Preserve direct Su values (likely ST sample)
enhanced_layer["su_source"] = su_source or "Direct measurement - assumed ST sample"
st.success(f"✅ Layer {enhanced_layer.get('layer_id', 'Unknown')}: Using direct Su={su_value:.0f} kPa (assumed ST)")
# Handle sand/silt friction angle calculation
if "sand" in soil_type and n_value is not None:
# Calculate friction angle for sand
mid_depth = (depth_from + depth_to) / 2
effective_stress = 20 * mid_depth # Approximate effective stress (kPa)
sand_type_classification = "medium_sand"
if "fine" in soil_type:
sand_type_classification = "fine_sand"
elif "coarse" in soil_type:
sand_type_classification = "coarse_sand"
elif "silt" in soil_type:
sand_type_classification = "silty_sand"
friction_angle = self.calculate_friction_angle_peck(
n_value, sand_type_classification, effective_stress
)
enhanced_layer["friction_angle"] = friction_angle
enhanced_layer["friction_angle_source"] = f"Peck method from raw N={n_value}"
if sample_type == "SS":
st.success(f"✅ SS Sample Layer {enhanced_layer.get('layer_id', 'Unknown')}: φ = {friction_angle:.1f}° from N={n_value}")
else:
st.info(f"📊 Layer {enhanced_layer.get('layer_id', 'Unknown')}: φ = {friction_angle:.1f}° from N={n_value}")
# Update consistency classification
consistency = self.classify_soil_consistency(soil_type, n_value, su_value)
if consistency != "unknown":
enhanced_layer["consistency"] = consistency
# Keep soil_type as basic type (clay, sand, silt)
base_soil = "clay" if "clay" in soil_type else \
"sand" if "sand" in soil_type else \
"silt" if "silt" in soil_type else \
"gravel" if "gravel" in soil_type else soil_type
# Remove any existing consistency terms from soil_type
for consistency_term in ["very soft", "soft", "medium", "stiff", "very stiff", "hard",
"very loose", "loose", "medium dense", "dense", "very dense"]:
base_soil = base_soil.replace(consistency_term, "").strip()
enhanced_layer["soil_type"] = base_soil
enhanced_layers.append(enhanced_layer)
return enhanced_layers
def validate_soil_classification(self, soil_data: Dict) -> Dict:
"""Validate and improve soil classification"""
if "soil_layers" not in soil_data:
return soil_data
layers = soil_data["soil_layers"]
validated_layers = []
for layer in layers:
validated_layer = layer.copy()
# Check consistency between soil type and strength parameters
soil_type = layer.get("soil_type", "").lower()
strength_param = layer.get("strength_parameter", "")
strength_value = layer.get("strength_value")
# Fix parameter mismatches
if "clay" in soil_type and strength_param == "SPT-N" and strength_value:
# Clay should use Su, but if only N is available, calculate Su
calculated_su = self.calculate_su_from_n(strength_value)
validated_layer["calculated_su"] = calculated_su
validated_layer["su_source"] = f"Calculated from N={strength_value}"
elif "sand" in soil_type and strength_param == "Su":
# Sand should not have Su parameter
validated_layer["strength_parameter"] = "SPT-N"
validated_layer["parameter_note"] = "Corrected from Su to SPT-N for sand"
# Validate depth ranges
if validated_layer.get("depth_from") >= validated_layer.get("depth_to"):
# Fix invalid depth ranges
depth_from = validated_layer.get("depth_from", 0)
validated_layer["depth_to"] = depth_from + 1.0 # Default 1m thickness
validated_layer["depth_note"] = "Corrected invalid depth range"
validated_layers.append(validated_layer)
soil_data["soil_layers"] = validated_layers
return soil_data
def process_with_ss_st_classification(self, soil_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Process soil data with SS/ST sample classification
"""
try:
from soil_classification import SoilClassificationProcessor
if "soil_layers" not in soil_data:
return soil_data
# Initialize the enhanced processor
processor = SoilClassificationProcessor()
# Process layers with SS/ST classification
enhanced_layers = processor.process_soil_layers(soil_data["soil_layers"])
# Update soil data
soil_data["soil_layers"] = enhanced_layers
# Add processing summary
processing_summary = processor.get_processing_summary(enhanced_layers)
soil_data["processing_summary"] = processing_summary
# Display processing summary
st.subheader("📊 SS/ST Processing Summary")
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Total Layers", processing_summary['total_layers'])
st.metric("ST Samples", processing_summary['st_samples'])
with col2:
st.metric("SS Samples", processing_summary['ss_samples'])
st.metric("Clay Layers", processing_summary['clay_layers'])
with col3:
st.metric("Sand/Silt Layers", processing_summary['sand_layers'])
st.metric("Su Calculated", processing_summary['su_calculated'])
with col4:
st.metric("φ Calculated", processing_summary['phi_calculated'])
return soil_data
except ImportError as e:
st.warning(f"⚠️ Enhanced SS/ST classification not available: {str(e)}")
return soil_data
except Exception as e:
st.error(f"❌ Error in SS/ST processing: {str(e)}")
return soil_data |