Update app.py
Browse files
app.py
CHANGED
|
@@ -1,1095 +1,102 @@
|
|
| 1 |
-
|
| 2 |
-
"""
|
| 3 |
-
GeoMate - Final integrated Gradio application
|
| 4 |
-
Features:
|
| 5 |
-
- USCS & AASHTO classification using your exact CLI logic
|
| 6 |
-
- Soil recognition (PyTorch model 'soil_model.pth' if present)
|
| 7 |
-
- Sieve Analysis (plot PSD on semi-log, find D60,D30,D10 via interpolation)
|
| 8 |
-
- Problem solvers (bearing capacity, slope stability, consolidation, seepage, compaction)
|
| 9 |
-
- LLM Geotech expert conclusion (uses GROQ_API_l_KEY env var if provided)
|
| 10 |
-
- Styled PDF export with GeoMate logo, orange/black/red borders, decision path in PDF
|
| 11 |
-
- Multi-tab futuristic GUI (icons, martian background option)
|
| 12 |
-
"""
|
| 13 |
|
| 14 |
-
|
| 15 |
-
import os
|
| 16 |
-
import math
|
| 17 |
-
from math import floor
|
| 18 |
-
import datetime
|
| 19 |
-
import tempfile
|
| 20 |
-
from io import BytesIO
|
| 21 |
-
import base64
|
| 22 |
|
| 23 |
-
|
| 24 |
-
import matplotlib
|
| 25 |
-
matplotlib.use('Agg')
|
| 26 |
-
import matplotlib.pyplot as plt
|
| 27 |
|
| 28 |
-
|
| 29 |
-
import torch.nn as nn
|
| 30 |
-
import torchvision.transforms as transforms
|
| 31 |
-
import torchvision.models as models
|
| 32 |
-
from PIL import Image
|
| 33 |
|
| 34 |
-
|
| 35 |
-
from reportlab.pdfgen import canvas
|
| 36 |
-
from reportlab.lib.pagesizes import A4
|
| 37 |
-
from reportlab.lib.colors import HexColor
|
| 38 |
-
from reportlab.lib.utils import ImageReader
|
| 39 |
|
| 40 |
-
|
| 41 |
-
import os
|
| 42 |
-
import torch
|
| 43 |
-
import torch.nn as nn
|
| 44 |
-
import torchvision.models as models
|
| 45 |
-
import torchvision.transforms as transforms
|
| 46 |
|
| 47 |
-
|
| 48 |
-
GROQ_API_KEY = os.environ.get("Shah")
|
| 49 |
-
GROQ_MODEL_NAME = "llama-3.3-70b-versatile"
|
| 50 |
|
| 51 |
-
|
| 52 |
-
if GROQ_API_KEY:
|
| 53 |
-
try:
|
| 54 |
-
from groq import Groq
|
| 55 |
-
client = Groq(api_key=GROQ_API_KEY)
|
| 56 |
-
print("Groq LLM client initialized successfully.")
|
| 57 |
-
except Exception as e:
|
| 58 |
-
client = None
|
| 59 |
-
print("Groq init failed:", e)
|
| 60 |
-
else:
|
| 61 |
-
print("No API key found for 'Shah'. LLM features will be disabled.")
|
| 62 |
|
| 63 |
-
# ---- SECTION: Soil Image Model setup ----
|
| 64 |
-
class SoilClassifierNet(nn.Module):
|
| 65 |
-
def __init__(self):
|
| 66 |
-
super().__init__()
|
| 67 |
-
self.base_model = models.resnet18(weights=None)
|
| 68 |
-
num_f = self.base_model.fc.in_features
|
| 69 |
-
self.base_model.fc = nn.Linear(num_f, 1)
|
| 70 |
|
| 71 |
-
|
| 72 |
-
return self.base_model(x)
|
| 73 |
|
| 74 |
-
|
| 75 |
-
transform = transforms.Compose([
|
| 76 |
-
transforms.Resize((224, 224)),
|
| 77 |
-
transforms.ToTensor(),
|
| 78 |
-
transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])
|
| 79 |
-
])
|
| 80 |
|
| 81 |
-
|
| 82 |
-
if os.path.exists("soil_model.pth"):
|
| 83 |
-
soil_model = SoilClassifierNet()
|
| 84 |
-
soil_model.load_state_dict(torch.load("soil_model.pth", map_location="cpu"))
|
| 85 |
-
soil_model.eval()
|
| 86 |
-
print("Loaded soil_model.pth successfully.")
|
| 87 |
-
else:
|
| 88 |
-
print("soil_model.pth not found — image recognition disabled.")
|
| 89 |
-
except Exception as e:
|
| 90 |
-
soil_model = None
|
| 91 |
-
print("Error loading soil model:", e)
|
| 92 |
-
# ---- SECTION: Global SESSION storage ----
|
| 93 |
-
SESSION = {
|
| 94 |
-
"choice": None,
|
| 95 |
-
"uscs": {
|
| 96 |
-
"opt_organic": None, "P2": None, "P4": None, "enter_Dvals": None,
|
| 97 |
-
"D60": None, "D30": None, "D10": None,
|
| 98 |
-
"LL": None, "PL": None, "nDS": None, "nDIL": None, "nTG": None,
|
| 99 |
-
"result": None, "path": []
|
| 100 |
-
},
|
| 101 |
-
"aashto": {
|
| 102 |
-
"P2": None, "P4": None, "LL": None, "PL": None, "P1": None,
|
| 103 |
-
"result": None, "GI": None, "path": []
|
| 104 |
-
},
|
| 105 |
-
"solver_outputs": {},
|
| 106 |
-
"chat_history": [],
|
| 107 |
-
"sieve": {
|
| 108 |
-
"sieve_sizes": None, # mm
|
| 109 |
-
"percent_finer": None,
|
| 110 |
-
"D10": None, "D30": None, "D60": None,
|
| 111 |
-
"Cu": None, "Cc": None
|
| 112 |
-
}
|
| 113 |
-
}
|
| 114 |
|
| 115 |
-
|
| 116 |
-
def parse_float_label(label, v, allow_empty=False, minimum=None, maximum=None):
|
| 117 |
-
if v is None or (isinstance(v,str) and v.strip()==""):
|
| 118 |
-
if allow_empty:
|
| 119 |
-
return None, None
|
| 120 |
-
return None, f"{label} required."
|
| 121 |
-
try:
|
| 122 |
-
num = float(v)
|
| 123 |
-
except Exception:
|
| 124 |
-
return None, f"{label} must be numeric."
|
| 125 |
-
if minimum is not None and num < minimum:
|
| 126 |
-
return None, f"{label} must be >= {minimum}."
|
| 127 |
-
if maximum is not None and num > maximum:
|
| 128 |
-
return None, f"{label} must be <= {maximum}."
|
| 129 |
-
return num, None
|
| 130 |
|
| 131 |
-
|
| 132 |
-
words = str(text).split()
|
| 133 |
-
lines = []
|
| 134 |
-
cur = ""
|
| 135 |
-
for w in words:
|
| 136 |
-
if len(cur) + len(w) + 1 <= chars:
|
| 137 |
-
cur = (cur + " " + w).strip()
|
| 138 |
-
else:
|
| 139 |
-
lines.append(cur)
|
| 140 |
-
cur = w
|
| 141 |
-
if cur:
|
| 142 |
-
lines.append(cur)
|
| 143 |
-
return lines
|
| 144 |
|
| 145 |
-
# ---- SECTION: SOIL DESCRIPTIONS (relevant items only) ----
|
| 146 |
-
SOIL_DESCRIPTIONS = {
|
| 147 |
-
"GW": {"name":"GW — Well-graded gravel","desc":"Well-graded gravel. Excellent drainage, high shear strength.","notes":"High bearing capacity; minimal settlement; good for subbase/drainage."},
|
| 148 |
-
"GP": {"name":"GP — Poorly-graded gravel","desc":"Uniform gravel with poorer gradation.","notes":"Good drainage but lower interlock; careful compaction required."},
|
| 149 |
-
"GM": {"name":"GM — Silty gravel","desc":"Gravel with silt-sized fines.","notes":"Lower permeability; may need stabilization."},
|
| 150 |
-
"GC": {"name":"GC — Clayey gravel","desc":"Gravel with clay fines.","notes":"Poor drainage; plasticity concerns."},
|
| 151 |
-
"SW": {"name":"SW — Well-graded sand","desc":"Well-graded sand.","notes":"Good drainage and bearing; possible liquefaction if loose & saturated."},
|
| 152 |
-
"SP": {"name":"SP — Poorly-graded sand","desc":"Uniform sand.","notes":"Lower interlock; erosion concerns."},
|
| 153 |
-
"SM": {"name":"SM — Silty sand","desc":"Sand with silt.","notes":"Lower drainage; frost-susceptible."},
|
| 154 |
-
"SC": {"name":"SC — Clayey sand","desc":"Sand with clay fines.","notes":"May show plastic behavior; treat before use."},
|
| 155 |
-
"ML": {"name":"ML — Inorganic silt","desc":"Fine silt low plasticity.","notes":"Low strength; frost-susceptible."},
|
| 156 |
-
"OL": {"name":"OL — Organic silt","desc":"Organic fines.","notes":"Highly compressible; avoid for foundations."},
|
| 157 |
-
"CL": {"name":"CL — Lean clay","desc":"Low to medium plasticity clay.","notes":"Moderate compressibility; some swell-shrink."},
|
| 158 |
-
"CH": {"name":"CH — Fat clay","desc":"High plasticity clay.","notes":"Low bearing capacity; high settlement."},
|
| 159 |
-
"MH": {"name":"MH — Organic silts (high PI)","desc":"High-plasticity organic silts.","notes":"Problematic for foundations."},
|
| 160 |
-
"OH": {"name":"OH — Organic clays (high PI)","desc":"Organic clays.","notes":"Very compressible."},
|
| 161 |
-
"Pt": {"name":"Pt — Peat","desc":"Highly organic peat.","notes":"Very compressible; extensive ground improvement needed."},
|
| 162 |
-
"A-1-a": {"name":"A-1-a","desc":"Clean sands & gravels.","notes":"Excellent subgrade material."},
|
| 163 |
-
"A-1-b": {"name":"A-1-b","desc":"Sands & gravels with some fines.","notes":"Generally acceptable with compaction control."},
|
| 164 |
-
"A-2-4": {"name":"A-2-4","desc":"Sands & gravels with some fines","notes":"Acceptable with care."},
|
| 165 |
-
"A-2-5": {"name":"A-2-5","desc":"Sands & gravels with higher LL fines","notes":"May need attention."},
|
| 166 |
-
"A-2-6": {"name":"A-2-6","desc":"Sands with PI≥11","notes":"Variable behaviour."},
|
| 167 |
-
"A-2-7": {"name":"A-2-7","desc":"Sands with high PI fines","notes":"Less desirable."},
|
| 168 |
-
"A-3": {"name":"A-3","desc":"Fine sands","notes":"Compaction required."},
|
| 169 |
-
"A-4": {"name":"A-4","desc":"Silts - low plasticity","notes":"Frost-susceptible."},
|
| 170 |
-
"A-5": {"name":"A-5","desc":"Silts - higher LL","notes":"Moisture sensitive."},
|
| 171 |
-
"A-6": {"name":"A-6","desc":"Clays low plasticity","notes":"Moderate strength."},
|
| 172 |
-
"A-7-5": {"name":"A-7-5","desc":"High plasticity clays, GI condition","notes":"Poor subgrade unless treated."},
|
| 173 |
-
"A-7-6": {"name":"A-7-6","desc":"High plasticity clays, worse behaviour","notes":"Often requires stabilization."}
|
| 174 |
-
}
|
| 175 |
|
| 176 |
-
|
| 177 |
-
def classify_uscs_from_session(uscs):
|
| 178 |
-
path = []
|
| 179 |
-
opt = uscs["opt_organic"]
|
| 180 |
-
if opt is None:
|
| 181 |
-
path.append("Organic check not provided.")
|
| 182 |
-
return None, path
|
| 183 |
-
if str(opt).lower() == 'y':
|
| 184 |
-
path.append("Organic contents detected → Pt")
|
| 185 |
-
return "Pt", path
|
| 186 |
|
| 187 |
-
|
| 188 |
-
if P2 is None:
|
| 189 |
-
path.append("P2 missing.")
|
| 190 |
-
return None, path
|
| 191 |
-
path.append(f"P2 = {P2}%")
|
| 192 |
|
| 193 |
-
if P2 <= 50:
|
| 194 |
-
P4 = uscs["P4"]
|
| 195 |
-
if P4 is None:
|
| 196 |
-
path.append("P4 missing.")
|
| 197 |
-
return None, path
|
| 198 |
-
path.append(f"P4 = {P4}%")
|
| 199 |
|
| 200 |
-
|
| 201 |
-
if op and str(op).lower() == 'y':
|
| 202 |
-
if uscs["D60"] is None or uscs["D30"] is None or uscs["D10"] is None:
|
| 203 |
-
path.append("D60/D30/D10 required but missing.")
|
| 204 |
-
return None, path
|
| 205 |
-
D60 = uscs["D60"]; D30 = uscs["D30"]; D10 = uscs["D10"]
|
| 206 |
-
path.append(f"D60={D60}, D30={D30}, D10={D10}")
|
| 207 |
-
else:
|
| 208 |
-
D60 = uscs["D60"] or 0
|
| 209 |
-
D30 = uscs["D30"] or 0
|
| 210 |
-
D10 = uscs["D10"] or 0
|
| 211 |
-
path.append("D values not entered by user (treated as 0).")
|
| 212 |
|
| 213 |
-
LL = uscs["LL"]; PL = uscs["PL"]
|
| 214 |
-
if LL is None or PL is None:
|
| 215 |
-
path.append("LL/PL missing.")
|
| 216 |
-
return None, path
|
| 217 |
-
PI = LL - PL
|
| 218 |
-
path.append(f"LL={LL}, PL={PL}, PI={PI}")
|
| 219 |
|
| 220 |
-
|
| 221 |
-
Cu = D60 / D10 if D10!=0 else float('inf')
|
| 222 |
-
Cc = (D30 ** 2) / (D10 * D60) if (D10*D60)!=0 else float('inf')
|
| 223 |
-
else:
|
| 224 |
-
Cu = 0; Cc = 0
|
| 225 |
-
path.append(f"Cu={Cu}, Cc={Cc}")
|
| 226 |
|
| 227 |
-
|
| 228 |
-
path.append("Gravel branch (P4 <= 50).")
|
| 229 |
-
if Cu != 0 and Cc != 0:
|
| 230 |
-
if Cu >= 4 and 1 <= Cc <= 3:
|
| 231 |
-
path.append("Cu >= 4 and 1 <= Cc <= 3 → GW")
|
| 232 |
-
return "GW", path
|
| 233 |
-
elif (Cu < 4 and 1 <= Cc <= 3) == False:
|
| 234 |
-
path.append("(Cu < 4 and 1 <= Cc <= 3) == False → GP")
|
| 235 |
-
return "GP", path
|
| 236 |
-
if PI < 4 or PI < 0.73 * (LL - 20):
|
| 237 |
-
path.append("PI < 4 or PI < 0.73*(LL-20) → GM")
|
| 238 |
-
return "GM", path
|
| 239 |
-
elif PI > 7 and PI > 0.73 * (LL - 20):
|
| 240 |
-
path.append("PI > 7 and PI > 0.73*(LL-20) → GC")
|
| 241 |
-
return "GC", path
|
| 242 |
-
else:
|
| 243 |
-
path.append("Else → GM-GC")
|
| 244 |
-
return "GM-GC", path
|
| 245 |
-
else:
|
| 246 |
-
path.append("Sand branch (P4 > 50).")
|
| 247 |
-
if Cu != 0 and Cc != 0:
|
| 248 |
-
if Cu >= 6 and 1 <= Cc <= 3:
|
| 249 |
-
path.append("Cu >= 6 and 1 <= Cc <= 3 → SW")
|
| 250 |
-
return "SW", path
|
| 251 |
-
elif (Cu < 6 and 1 <= Cc <= 3) == False:
|
| 252 |
-
path.append("(Cu < 6 and 1 <= Cc <= 3) == False → SP")
|
| 253 |
-
return "SP", path
|
| 254 |
-
if PI < 4 or PI <= 0.73 * (LL - 20):
|
| 255 |
-
path.append("PI < 4 or PI <= 0.73*(LL-20) → SM")
|
| 256 |
-
return "SM", path
|
| 257 |
-
elif PI > 7 and PI > 0.73 * (LL - 20):
|
| 258 |
-
path.append("PI > 7 and PI > 0.73*(LL-20) → SC")
|
| 259 |
-
return "SC", path
|
| 260 |
-
else:
|
| 261 |
-
path.append("Else → SM-SC")
|
| 262 |
-
return "SM-SC", path
|
| 263 |
-
else:
|
| 264 |
-
LL = uscs["LL"]; PL = uscs["PL"]
|
| 265 |
-
if LL is None or PL is None:
|
| 266 |
-
path.append("LL/PL missing for fine-grained branch.")
|
| 267 |
-
return None, path
|
| 268 |
-
PI = LL - PL
|
| 269 |
-
path.append(f"Fine branch: LL={LL}, PL={PL}, PI={PI}")
|
| 270 |
-
nDS = uscs["nDS"]; nDIL = uscs["nDIL"]; nTG = uscs["nTG"]
|
| 271 |
-
if nDS is None or nDIL is None or nTG is None:
|
| 272 |
-
path.append("nDS/nDIL/nTG missing.")
|
| 273 |
-
return None, path
|
| 274 |
-
path.append(f"nDS={nDS}, nDIL={nDIL}, nTG={nTG}")
|
| 275 |
|
| 276 |
-
|
| 277 |
-
path.append("LL < 50 → Low plasticity fines.")
|
| 278 |
-
if 20 <= LL < 50 and PI <= 0.73 * (LL - 20):
|
| 279 |
-
path.append("20 ≤ LL < 50 and PI ≤ 0.73*(LL-20)")
|
| 280 |
-
if nDS == 1 or nDIL == 3 or nTG == 3:
|
| 281 |
-
path.append("→ ML"); return "ML", path
|
| 282 |
-
elif nDS == 3 or nDIL == 3 or nTG == 3:
|
| 283 |
-
path.append("→ OL"); return "OL", path
|
| 284 |
-
else:
|
| 285 |
-
path.append("→ ML-OL"); return "ML-OL", path
|
| 286 |
-
elif 10 <= LL <= 30 and 4 <= PI <= 7 and PI > 0.72 * (LL - 20):
|
| 287 |
-
path.append("10 ≤ LL ≤ 30 and 4 ≤ PI ≤ 7 and PI > 0.72*(LL-20)")
|
| 288 |
-
if nDS == 1 or nDIL == 1 or nTG == 1:
|
| 289 |
-
path.append("→ ML"); return "ML", path
|
| 290 |
-
elif nDS == 2 or nDIL == 2 or nTG == 2:
|
| 291 |
-
path.append("→ CL"); return "CL", path
|
| 292 |
-
else:
|
| 293 |
-
path.append("→ ML-CL"); return "ML-CL", path
|
| 294 |
-
else:
|
| 295 |
-
path.append("Else → CL"); return "CL", path
|
| 296 |
-
else:
|
| 297 |
-
path.append("LL ≥ 50 → High plasticity fines.")
|
| 298 |
-
if PI < 0.73 * (LL - 20):
|
| 299 |
-
path.append("PI < 0.73*(LL-20)")
|
| 300 |
-
if nDS == 3 or nDIL == 4 or nTG == 4:
|
| 301 |
-
path.append("→ MH"); return "MH", path
|
| 302 |
-
elif nDS == 2 or nDIL == 2 or nTG == 4:
|
| 303 |
-
path.append("→ OH"); return "OH", path
|
| 304 |
-
else:
|
| 305 |
-
path.append("→ MH-OH"); return "MH-OH", path
|
| 306 |
-
else:
|
| 307 |
-
path.append("Else → CH"); return "CH", path
|
| 308 |
|
| 309 |
-
|
| 310 |
-
path = []
|
| 311 |
-
PI = LL - PL
|
| 312 |
-
path.append(f"P200={P2}%, P40={P4}%, LL={LL}, PL={PL} → PI={PI}")
|
| 313 |
|
| 314 |
-
|
| 315 |
-
if P2 <= 35:
|
| 316 |
-
path.append("P200 ≤ 35 → Granular materials.")
|
| 317 |
-
if P2 <= 15 and P4 <= 30 and PI <= 6:
|
| 318 |
-
path.append("P200 ≤15 and P40 ≤30 and PI ≤6 → Candidate A-1-a.")
|
| 319 |
-
if P1 is None:
|
| 320 |
-
path.append("P1 necessary for A-1-a decision (missing).")
|
| 321 |
-
return None, path
|
| 322 |
-
if P1 <= 50:
|
| 323 |
-
Result = "A-1-a"; path.append(f"P1={P1} ≤ 50 → A-1-a")
|
| 324 |
-
else:
|
| 325 |
-
path.append("P1 > 50 → inconsistent for A-1-a."); return None, path
|
| 326 |
-
elif P2 <= 25 and P4 <= 50 and PI <= 6:
|
| 327 |
-
Result = "A-1-b"; path.append("P200 ≤25 and P40 ≤50 and PI ≤6 → A-1-b")
|
| 328 |
-
elif P2 <= 35 and P4 > 0:
|
| 329 |
-
path.append("P200 ≤35 and P40 > 0 → A-2 subgroup.")
|
| 330 |
-
if LL <= 40 and PI <= 10:
|
| 331 |
-
Result = "A-2-4"; path.append("LL ≤ 40 and PI ≤10 → A-2-4")
|
| 332 |
-
elif LL >= 41 and PI <= 10:
|
| 333 |
-
Result = "A-2-5"; path.append("LL ≥41 and PI ≤10 → A-2-5")
|
| 334 |
-
elif LL <= 40 and PI >= 11:
|
| 335 |
-
Result = "A-2-6"; path.append("LL ≤40 and PI ≥11 → A-2-6")
|
| 336 |
-
elif LL >= 41 and PI >= 11:
|
| 337 |
-
Result = "A-2-7"; path.append("LL ≥41 and PI ≥11 → A-2-7")
|
| 338 |
-
else:
|
| 339 |
-
Result = "A-3"; path.append("Else → A-3")
|
| 340 |
-
else:
|
| 341 |
-
path.append("P200 > 35 → Silt-Clay branch.")
|
| 342 |
-
if LL <= 40 and PI <= 10:
|
| 343 |
-
Result = "A-4"; path.append("LL ≤40 and PI ≤10 → A-4")
|
| 344 |
-
elif LL >= 41 and PI <= 10:
|
| 345 |
-
Result = "A-5"; path.append("LL ≥41 and PI ≤10 → A-5")
|
| 346 |
-
elif LL <= 40 and PI >= 11:
|
| 347 |
-
Result = "A-6"; path.append("LL ≤40 and PI ≥11 → A-6")
|
| 348 |
-
else:
|
| 349 |
-
if PI <= (LL - 30):
|
| 350 |
-
Result = "A-7-5"; path.append("PI ≤ LL-30 → A-7-5")
|
| 351 |
-
elif PI > (LL - 30):
|
| 352 |
-
Result = "A-7-6"; path.append("PI > LL-30 → A-7-6")
|
| 353 |
-
else:
|
| 354 |
-
path.append("Error in inputs."); return None, path
|
| 355 |
|
| 356 |
-
# Group Index as provided
|
| 357 |
-
a = P2 - 35
|
| 358 |
-
if a <= 40 and a >= 0:
|
| 359 |
-
a = a
|
| 360 |
-
elif a < 0:
|
| 361 |
-
a = 0
|
| 362 |
-
else:
|
| 363 |
-
a = 40
|
| 364 |
|
| 365 |
-
b = P2 - 15
|
| 366 |
-
if b <= 40 and b >= 0:
|
| 367 |
-
b = b
|
| 368 |
-
elif b < 0:
|
| 369 |
-
b = 0
|
| 370 |
-
else:
|
| 371 |
-
b = 40
|
| 372 |
|
| 373 |
-
|
| 374 |
-
if c <= 20 and c >= 0:
|
| 375 |
-
c = c
|
| 376 |
-
elif c < 0:
|
| 377 |
-
c = 0
|
| 378 |
-
else:
|
| 379 |
-
c = 20
|
| 380 |
|
| 381 |
-
d = PI - 10
|
| 382 |
-
if d <= 20 and d >= 0:
|
| 383 |
-
d = d
|
| 384 |
-
elif d < 0:
|
| 385 |
-
d = 0
|
| 386 |
-
else:
|
| 387 |
-
d = 20
|
| 388 |
|
| 389 |
-
GI = floor(0.2*a + 0.005*a*c + 0.01*b*d)
|
| 390 |
-
path.append(f"Group Index calculation: a={a},b={b},c={c},d={d} → GI={GI}")
|
| 391 |
-
Result_with_GI = f"{Result} ({GI})"
|
| 392 |
-
path.append(f"Final AASHTO: {Result_with_GI}")
|
| 393 |
-
return Result_with_GI, path
|
| 394 |
|
| 395 |
-
|
| 396 |
-
def get_geotech_expert_conclusion(context_text):
|
| 397 |
-
if client is None:
|
| 398 |
-
return "LLM unavailable (set environment variable GROQ_API_l_KEY to enable expert conclusions)."
|
| 399 |
-
prompt = (
|
| 400 |
-
"You are GeoMate, a professional geotechnical engineer. "
|
| 401 |
-
"Given the following soil data, classification results and solver outputs, "
|
| 402 |
-
"write a concise geotechnical conclusion (no more than 150 words) with practical recommendations and critical cautions.\n\n"
|
| 403 |
-
f"Context:\n{context_text}\n\nConclusion:"
|
| 404 |
-
)
|
| 405 |
-
try:
|
| 406 |
-
resp = client.chat.completions.create(
|
| 407 |
-
model=GROQ_MODEL_NAME,
|
| 408 |
-
messages=[{"role":"user","content":prompt}]
|
| 409 |
-
)
|
| 410 |
-
ans = resp.choices[0].message.content.strip()
|
| 411 |
-
# limit to ~150 words
|
| 412 |
-
words = ans.split()
|
| 413 |
-
if len(words) > 160:
|
| 414 |
-
ans = " ".join(words[:160]) + " ..."
|
| 415 |
-
return ans
|
| 416 |
-
except Exception as e:
|
| 417 |
-
return f"LLM call failed: {e}"
|
| 418 |
|
| 419 |
-
|
| 420 |
-
def bearing_capacity_solver(q, unit, Nq, B):
|
| 421 |
-
try:
|
| 422 |
-
qv = float(q)
|
| 423 |
-
if unit == "psf":
|
| 424 |
-
q_conv = qv * 0.04788
|
| 425 |
-
else:
|
| 426 |
-
q_conv = qv
|
| 427 |
-
res = q_conv * float(Nq) * float(B)
|
| 428 |
-
out = f"Ultimate bearing capacity (approx): {round(res,2)} kN/m² (q converted: {round(q_conv,3)} kN/m²)"
|
| 429 |
-
SESSION["solver_outputs"]["bearing_capacity"] = out
|
| 430 |
-
return out
|
| 431 |
-
except Exception as e:
|
| 432 |
-
return f"Error: {e}"
|
| 433 |
|
| 434 |
-
|
| 435 |
-
try:
|
| 436 |
-
phi_r = math.radians(float(phi))
|
| 437 |
-
fs = (float(c) + float(gamma)*float(height)*math.tan(phi_r)) / (float(gamma)*float(height))
|
| 438 |
-
out = f"Factor of Safety (approx): {round(fs,3)}"
|
| 439 |
-
SESSION["solver_outputs"]["slope_stability"] = out
|
| 440 |
-
return out
|
| 441 |
-
except Exception as e:
|
| 442 |
-
return f"Error: {e}"
|
| 443 |
|
| 444 |
-
|
| 445 |
-
try:
|
| 446 |
-
settlement = float(mv) * float(delta_sigma) * float(H)
|
| 447 |
-
out = f"Estimated consolidation settlement: {round(settlement,3)} m"
|
| 448 |
-
SESSION["solver_outputs"]["consolidation"] = out
|
| 449 |
-
return out
|
| 450 |
-
except Exception as e:
|
| 451 |
-
return f"Error: {e}"
|
| 452 |
|
| 453 |
-
|
| 454 |
-
try:
|
| 455 |
-
q = float(k)*float(i)*float(A)
|
| 456 |
-
out = f"Seepage discharge: {round(q,6)} m³/s"
|
| 457 |
-
SESSION["solver_outputs"]["seepage"] = out
|
| 458 |
-
return out
|
| 459 |
-
except Exception as e:
|
| 460 |
-
return f"Error: {e}"
|
| 461 |
|
| 462 |
-
|
| 463 |
-
try:
|
| 464 |
-
dry_density = float(W)/float(V)
|
| 465 |
-
out = f"Dry density: {round(dry_density,3)} kN/m³"
|
| 466 |
-
SESSION["solver_outputs"]["compaction"] = out
|
| 467 |
-
return out
|
| 468 |
-
except Exception as e:
|
| 469 |
-
return f"Error: {e}"
|
| 470 |
|
| 471 |
-
|
| 472 |
-
def parse_sieve_input(text_or_csv):
|
| 473 |
-
"""
|
| 474 |
-
Accepts CSV-like text with two columns: sieve_size_mm, percent_finer
|
| 475 |
-
Returns numpy arrays sorted by size descending (for plotting)
|
| 476 |
-
"""
|
| 477 |
-
lines = [ln.strip() for ln in text_or_csv.strip().splitlines() if ln.strip()]
|
| 478 |
-
sizes = []
|
| 479 |
-
perc = []
|
| 480 |
-
for ln in lines:
|
| 481 |
-
parts = [p for p in ln.replace(",", " ").split() if p]
|
| 482 |
-
if len(parts) < 2: continue
|
| 483 |
-
try:
|
| 484 |
-
s = float(parts[0])
|
| 485 |
-
p = float(parts[1])
|
| 486 |
-
except:
|
| 487 |
-
continue
|
| 488 |
-
sizes.append(s)
|
| 489 |
-
perc.append(p)
|
| 490 |
-
if not sizes:
|
| 491 |
-
return None, None
|
| 492 |
-
arr = np.array(sizes), np.array(perc)
|
| 493 |
-
# sort by sieve size descending
|
| 494 |
-
idx = np.argsort(arr[0])[::-1]
|
| 495 |
-
return arr[0][idx], arr[1][idx]
|
| 496 |
|
| 497 |
-
|
| 498 |
-
"""
|
| 499 |
-
Sizes: descending array (mm), perc_finer: same shape
|
| 500 |
-
Interpolate to find D10,D30,D60 (phi vs log size)
|
| 501 |
-
We'll interpolate percent finer vs log(size) using linear interpolation.
|
| 502 |
-
"""
|
| 503 |
-
# avoid zeros or duplicates, ensure positive sizes
|
| 504 |
-
mask = sizes_mm > 0
|
| 505 |
-
sizes = sizes_mm[mask]
|
| 506 |
-
perc = perc_finer[mask]
|
| 507 |
-
if len(sizes) < 2:
|
| 508 |
-
return None, None, None
|
| 509 |
-
# transform to log10(size)
|
| 510 |
-
logd = np.log10(sizes)
|
| 511 |
-
# interpolation function perc -> logd (we need size at certain percent finer)
|
| 512 |
-
# But perc is % finer (0-100), not monotonic necessarily — ensure monotonic by interpolation over perc
|
| 513 |
-
# For interpolation, perc should be increasing with decreasing size (since sizes descending)
|
| 514 |
-
# We'll create arrays sorted by perc ascending
|
| 515 |
-
order = np.argsort(perc)
|
| 516 |
-
perc_sort = perc[order]
|
| 517 |
-
logd_sort = logd[order]
|
| 518 |
-
# if duplicates in perc_sort, use unique with average logd
|
| 519 |
-
perc_unique, idx_first = np.unique(perc_sort, return_index=True)
|
| 520 |
-
# If perc_unique too small, fallback to linear interp on original
|
| 521 |
-
try:
|
| 522 |
-
D10_log = np.interp(10.0, perc_sort, logd_sort)
|
| 523 |
-
D30_log = np.interp(30.0, perc_sort, logd_sort)
|
| 524 |
-
D60_log = np.interp(60.0, perc_sort, logd_sort)
|
| 525 |
-
except Exception:
|
| 526 |
-
return None, None, None
|
| 527 |
-
D10 = 10 ** D10_log
|
| 528 |
-
D30 = 10 ** D30_log
|
| 529 |
-
D60 = 10 ** D60_log
|
| 530 |
-
return D10, D30, D60
|
| 531 |
|
| 532 |
-
|
| 533 |
-
"""
|
| 534 |
-
Create PSD plot (percent finer vs particle size) with x axis log scale.
|
| 535 |
-
Return PNG bytes.
|
| 536 |
-
"""
|
| 537 |
-
fig, ax = plt.subplots(figsize=(6,3.5))
|
| 538 |
-
ax.semilogx(sizes_mm, perc_finer, marker='o', linestyle='-', linewidth=1.5)
|
| 539 |
-
ax.set_xlabel("Particle size (mm) - log scale")
|
| 540 |
-
ax.set_ylabel("% Passing (finer)")
|
| 541 |
-
ax.set_title("Particle Size Distribution (PSD)")
|
| 542 |
-
ax.grid(which='both', linestyle='--', linewidth=0.5)
|
| 543 |
-
ax.set_xlim(max(sizes_mm)*1.1, min(sizes_mm)*0.001) # show descending
|
| 544 |
-
ax.set_ylim(-5,105)
|
| 545 |
-
# invert x for conventional left-to-right large->small
|
| 546 |
-
ax.invert_xaxis()
|
| 547 |
-
buf = BytesIO()
|
| 548 |
-
plt.tight_layout()
|
| 549 |
-
fig.savefig(buf, format='png', dpi=150)
|
| 550 |
-
plt.close(fig)
|
| 551 |
-
buf.seek(0)
|
| 552 |
-
return buf.getvalue()
|
| 553 |
|
| 554 |
-
|
| 555 |
-
def generate_report_pdf(logo_bytes=None):
|
| 556 |
-
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 557 |
-
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=f"_GeoMate_Report_{ts}.pdf")
|
| 558 |
-
path = tmp.name
|
| 559 |
-
tmp.close()
|
| 560 |
|
| 561 |
-
|
| 562 |
-
w, h = A4
|
| 563 |
-
orange = HexColor("#ff6600")
|
| 564 |
-
black = HexColor("#000000")
|
| 565 |
-
red = HexColor("#cc0000")
|
| 566 |
-
margin = 20
|
| 567 |
|
| 568 |
-
|
| 569 |
-
c.setStrokeColor(orange); c.setLineWidth(3)
|
| 570 |
-
c.rect(margin/2, margin/2, w - margin, h - margin, stroke=1, fill=0)
|
| 571 |
-
# inner black border
|
| 572 |
-
c.setStrokeColor(black); c.setLineWidth(1.2)
|
| 573 |
-
c.rect(margin+6, margin+6, w - 2*(margin+6), h - 2*(margin+6), stroke=1, fill=0)
|
| 574 |
|
| 575 |
-
|
| 576 |
-
# logo if provided
|
| 577 |
-
if logo_bytes:
|
| 578 |
-
try:
|
| 579 |
-
logo = Image.open(BytesIO(logo_bytes))
|
| 580 |
-
lw = 70
|
| 581 |
-
lh = int((logo.size[1]/logo.size[0]) * lw)
|
| 582 |
-
logo = logo.resize((lw, lh))
|
| 583 |
-
buf = BytesIO(); logo.save(buf, format="PNG"); buf.seek(0)
|
| 584 |
-
c.drawImage(ImageReader(buf), margin + 10, y - lh, width=lw, height=lh)
|
| 585 |
-
except Exception:
|
| 586 |
-
pass
|
| 587 |
|
| 588 |
-
|
| 589 |
-
c.drawString(margin + 100, y - 8, "GeoMate - Soil Classification Report")
|
| 590 |
-
c.setFont("Helvetica", 9); c.drawString(margin + 100, y - 26, f"Generated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 591 |
-
y -= 70
|
| 592 |
|
| 593 |
-
|
| 594 |
-
c.setFont("Helvetica-Bold", 12); c.drawString(margin + 10, y, "Selected System(s):")
|
| 595 |
-
c.setFont("Helvetica", 11); c.drawString(margin + 150, y, str(SESSION.get("choice")))
|
| 596 |
-
y -= 22
|
| 597 |
|
| 598 |
-
|
| 599 |
-
if SESSION["uscs"]["result"]:
|
| 600 |
-
res = SESSION["uscs"]["result"]
|
| 601 |
-
c.setFont("Helvetica-Bold", 12); c.drawString(margin + 10, y, "USCS Classification:")
|
| 602 |
-
c.setFont("Helvetica", 11); c.drawString(margin + 150, y, res); y -= 18
|
| 603 |
-
info = SOIL_DESCRIPTIONS.get(res)
|
| 604 |
-
if info:
|
| 605 |
-
c.setFont("Helvetica-Bold", 11); c.drawString(margin + 10, y, "Relevant USCS Notes:")
|
| 606 |
-
y -= 14
|
| 607 |
-
c.setFont("Helvetica", 10)
|
| 608 |
-
txt = info.get("desc","") + " " + info.get("notes","")
|
| 609 |
-
for line in split_text(txt, 95):
|
| 610 |
-
c.drawString(margin + 12, y, line); y -= 12
|
| 611 |
-
y -= 6
|
| 612 |
|
| 613 |
-
|
| 614 |
-
if SESSION["aashto"]["result"]:
|
| 615 |
-
ares = SESSION["aashto"]["result"]
|
| 616 |
-
c.setFont("Helvetica-Bold", 12); c.drawString(margin + 10, y, "AASHTO Classification:")
|
| 617 |
-
c.setFont("Helvetica", 11); c.drawString(margin + 150, y, ares); y -= 18
|
| 618 |
-
code = ares.split()[0]
|
| 619 |
-
info = SOIL_DESCRIPTIONS.get(code)
|
| 620 |
-
if info:
|
| 621 |
-
c.setFont("Helvetica-Bold", 11); c.drawString(margin + 10, y, "Relevant AASHTO Notes:")
|
| 622 |
-
y -= 14
|
| 623 |
-
c.setFont("Helvetica", 10)
|
| 624 |
-
txt = info.get("desc","") + " " + info.get("notes","")
|
| 625 |
-
for line in split_text(txt, 95):
|
| 626 |
-
c.drawString(margin + 12, y, line); y -= 12
|
| 627 |
-
y -= 6
|
| 628 |
|
| 629 |
-
|
| 630 |
-
s = SESSION["sieve"]
|
| 631 |
-
if s.get("D10") is not None:
|
| 632 |
-
c.setFont("Helvetica-Bold", 12); c.drawString(margin + 10, y, "Sieve Analysis:")
|
| 633 |
-
y -= 16
|
| 634 |
-
c.setFont("Helvetica", 10)
|
| 635 |
-
c.drawString(margin + 12, y, f"D10={round(s['D10'],5)} mm, D30={round(s['D30'],5)} mm, D60={round(s['D60'],5)} mm")
|
| 636 |
-
y -= 14
|
| 637 |
-
c.drawString(margin + 12, y, f"Cu={round(s['Cu'],3)}, Cc={round(s['Cc'],3)}")
|
| 638 |
-
y -= 18
|
| 639 |
|
| 640 |
-
|
| 641 |
-
if SESSION["solver_outputs"]:
|
| 642 |
-
c.setFont("Helvetica-Bold", 12); c.drawString(margin + 10, y, "Problem Solver Outputs:")
|
| 643 |
-
y -= 16
|
| 644 |
-
c.setFont("Helvetica", 10)
|
| 645 |
-
for k, v in SESSION["solver_outputs"].items():
|
| 646 |
-
for line in split_text(f"{k.replace('_',' ').title()}: {v}", 95):
|
| 647 |
-
c.drawString(margin + 12, y, line); y -= 12
|
| 648 |
-
y -= 6
|
| 649 |
|
| 650 |
-
|
| 651 |
-
ctx = []
|
| 652 |
-
ctx.append(f"Selected systems: {SESSION.get('choice')}")
|
| 653 |
-
if SESSION["uscs"]["result"]:
|
| 654 |
-
ctx.append(f"USCS: {SESSION['uscs']['result']}")
|
| 655 |
-
if SESSION["aashto"]["result"]:
|
| 656 |
-
ctx.append(f"AASHTO: {SESSION['aashto']['result']}")
|
| 657 |
-
us = SESSION["uscs"]; aa = SESSION["aashto"]
|
| 658 |
-
ctx.append(f"USCS inputs: P2={us.get('P2')}, P4={us.get('P4')}, LL={us.get('LL')}, PL={us.get('PL')}, D60={us.get('D60')}, D30={us.get('D30')}, D10={us.get('D10')}, nDS={us.get('nDS')}, nDIL={us.get('nDIL')}, nTG={us.get('nTG')}")
|
| 659 |
-
ctx.append(f"AASHTO inputs: P2={aa.get('P2')}, P4={aa.get('P4')}, LL={aa.get('LL')}, PL={aa.get('PL')}, P1={aa.get('P1')}")
|
| 660 |
-
if SESSION["solver_outputs"]:
|
| 661 |
-
ctx.append("Solver outputs:")
|
| 662 |
-
for k,v in SESSION["solver_outputs"].items():
|
| 663 |
-
ctx.append(f"{k}: {v}")
|
| 664 |
-
context_text = "\n".join(ctx)
|
| 665 |
|
| 666 |
-
|
| 667 |
-
c.setFont("Helvetica-Bold", 12); c.drawString(margin + 10, y, "Geotech Expert Conclusion:")
|
| 668 |
-
y -= 16
|
| 669 |
-
conclusion = get_geotech_expert_conclusion(context_text) if client is not None else "LLM unavailable. Set env var GROQ_API_l_KEY to enable."
|
| 670 |
-
c.setFont("Helvetica", 10)
|
| 671 |
-
for line in split_text(conclusion, 95):
|
| 672 |
-
c.drawString(margin + 12, y, line); y -= 12
|
| 673 |
-
y -= 8
|
| 674 |
|
| 675 |
-
|
| 676 |
-
c.setFont("Helvetica-Bold", 10); c.drawString(margin + 10, 110, "Decision Path (internal):")
|
| 677 |
-
ypos = 96
|
| 678 |
-
c.setFont("Helvetica", 8)
|
| 679 |
-
for step in SESSION["aashto"]["path"]:
|
| 680 |
-
for line in split_text(step, 120):
|
| 681 |
-
c.drawString(margin + 12, ypos, "- " + line); ypos -= 8
|
| 682 |
-
if ypos < 20:
|
| 683 |
-
c.showPage(); ypos = h - 40
|
| 684 |
-
for step in SESSION["uscs"]["path"]:
|
| 685 |
-
for line in split_text(step, 120):
|
| 686 |
-
c.drawString(margin + 12, ypos, "- " + line); ypos -= 8
|
| 687 |
-
if ypos < 20:
|
| 688 |
-
c.showPage(); ypos = h - 40
|
| 689 |
|
| 690 |
-
|
| 691 |
-
return path
|
| 692 |
|
| 693 |
-
|
| 694 |
-
DRY_STRENGTH_OPTIONS = ["1. Non-Slight","2. Med-high","3. Slight-Med","4. High-V.high","5. Null"]
|
| 695 |
-
DILATANCY_OPTIONS = ["1. Quick-slow","2. None-V.slow","3. Slow","4. Slow-none","5. None","6. Null"]
|
| 696 |
-
TOUGHNESS_OPTIONS = ["1. None","2. Med","3. Slight","4. Slight-Med","5. High","6. Null"]
|
| 697 |
|
| 698 |
-
|
| 699 |
-
SESSION["choice"] = choice
|
| 700 |
-
SESSION["uscs"]["path"] = []
|
| 701 |
-
SESSION["uscs"]["result"] = None
|
| 702 |
-
SESSION["aashto"]["path"] = []
|
| 703 |
-
SESSION["aashto"]["result"] = None
|
| 704 |
-
SESSION["solver_outputs"] = {}
|
| 705 |
-
return "uscs_org", "Does the soil contain organic contents, feels spongy or has odour? (enter 'y' or 'n')", ""
|
| 706 |
|
| 707 |
-
def wizard_next(prompt_id, value):
|
| 708 |
-
us = SESSION["uscs"]; aa = SESSION["aashto"]
|
| 709 |
-
# replicate the CLI flow; return (next_prompt_id, prompt_text, status_message, finished_bool)
|
| 710 |
-
if prompt_id == "uscs_org":
|
| 711 |
-
ans = str(value).strip().lower() if value is not None else ""
|
| 712 |
-
if ans not in ("y","n"): return "uscs_org", "Enter 'y' or 'n' please.", "Invalid input.", False
|
| 713 |
-
us["opt_organic"] = ans; us["path"].append(f"Organic: {ans}")
|
| 714 |
-
if ans == 'y':
|
| 715 |
-
us["result"] = "Pt"; us["path"].append("→ Pt")
|
| 716 |
-
if SESSION["choice"] == "Both":
|
| 717 |
-
return "aashto_P2", "% Passing sieve no. 200 (P2) for AASHTO:", "USCS Pt done; continue AASHTO.", False
|
| 718 |
-
return None, None, "USCS classification: Pt (Peat).", True
|
| 719 |
-
else:
|
| 720 |
-
return "uscs_P2", "% Passing sieve no. 200 (P2):", "", False
|
| 721 |
-
|
| 722 |
-
if prompt_id == "uscs_P2":
|
| 723 |
-
num, err = parse_float_label("P2", value, False, 0, 100)
|
| 724 |
-
if err: return "uscs_P2", "% Passing sieve no. 200 (P2):", err, False
|
| 725 |
-
us["P2"] = num; us["path"].append(f"P2={num}%")
|
| 726 |
-
if num <= 50:
|
| 727 |
-
return "uscs_P4", "% Passing sieve no. 4 (P4):", "", False
|
| 728 |
-
else:
|
| 729 |
-
return "uscs_fine_LL", "Liquid limit (LL):", "", False
|
| 730 |
-
|
| 731 |
-
if prompt_id == "uscs_P4":
|
| 732 |
-
num, err = parse_float_label("P4", value, False, 0, 100)
|
| 733 |
-
if err: return "uscs_P4", "% Passing sieve no. 4 (P4):", err, False
|
| 734 |
-
us["P4"] = num; us["path"].append(f"P4={num}%"); return "uscs_op_D", "Enter D? (y/n):", "", False
|
| 735 |
-
|
| 736 |
-
if prompt_id == "uscs_op_D":
|
| 737 |
-
ans = str(value).strip().lower() if value is not None else ""
|
| 738 |
-
if ans not in ("y","n"): return "uscs_op_D", "Enter 'y' or 'n':", "Invalid", False
|
| 739 |
-
us["enter_Dvals"] = ans; us["path"].append(f"enter_Dvals={ans}")
|
| 740 |
-
if ans == 'y':
|
| 741 |
-
return "uscs_D60", "Enter D60 (mm):", "", False
|
| 742 |
-
else:
|
| 743 |
-
us["D60"] = 0; us["D30"] = 0; us["D10"] = 0; us["path"].append("D-values set to 0")
|
| 744 |
-
return "uscs_LL", "Enter liquid limit (LL):", "", False
|
| 745 |
-
|
| 746 |
-
if prompt_id == "uscs_D60":
|
| 747 |
-
num, err = parse_float_label("D60", value, False, 0.0, None)
|
| 748 |
-
if err: return "uscs_D60", "D60 (mm):", err, False
|
| 749 |
-
us["D60"] = num; us["path"].append(f"D60={num}mm"); return "uscs_D30", "Enter D30 (mm):", "", False
|
| 750 |
-
|
| 751 |
-
if prompt_id == "uscs_D30":
|
| 752 |
-
num, err = parse_float_label("D30", value, False, 0.0, None)
|
| 753 |
-
if err: return "uscs_D30", "D30 (mm):", err, False
|
| 754 |
-
us["D30"] = num; us["path"].append(f"D30={num}mm"); return "uscs_D10", "Enter D10 (mm):", "", False
|
| 755 |
-
|
| 756 |
-
if prompt_id == "uscs_D10":
|
| 757 |
-
num, err = parse_float_label("D10", value, False, 0.0, None)
|
| 758 |
-
if err: return "uscs_D10", "D10 (mm):", err, False
|
| 759 |
-
us["D10"] = num; us["path"].append(f"D10={num}mm"); return "uscs_LL", "Enter liquid limit (LL):", "", False
|
| 760 |
-
|
| 761 |
-
if prompt_id == "uscs_LL":
|
| 762 |
-
num, err = parse_float_label("LL", value, False, 0.0, None)
|
| 763 |
-
if err: return "uscs_LL", "Liquid limit (LL):", err, False
|
| 764 |
-
us["LL"] = num; us["path"].append(f"LL={num}"); return "uscs_PL", "Enter plastic limit (PL):", "", False
|
| 765 |
-
|
| 766 |
-
if prompt_id == "uscs_PL":
|
| 767 |
-
num, err = parse_float_label("PL", value, False, 0.0, None)
|
| 768 |
-
if err: return "uscs_PL", "Plastic limit (PL):", err, False
|
| 769 |
-
us["PL"] = num; us["path"].append(f"PL={num}")
|
| 770 |
-
# classify
|
| 771 |
-
res, path = classify_uscs_from_session(us)
|
| 772 |
-
if res is None:
|
| 773 |
-
us["path"].extend(path)
|
| 774 |
-
return None, None, "USCS classification incomplete: " + "; ".join(path), False
|
| 775 |
-
us["result"] = res; us["path"].extend(path)
|
| 776 |
-
if SESSION["choice"] == "Both":
|
| 777 |
-
return "aashto_P2", "% Passing sieve no. 200 (P2) for AASHTO:", f"USCS done: {res}. Continue AASHTO.", False
|
| 778 |
-
return None, None, f"USCS classification completed: {res}", True
|
| 779 |
-
|
| 780 |
-
# fine branch prompts
|
| 781 |
-
if prompt_id == "uscs_fine_LL":
|
| 782 |
-
num, err = parse_float_label("LL", value, False, 0.0, None)
|
| 783 |
-
if err: return "uscs_fine_LL", "LL:", err, False
|
| 784 |
-
us["LL"] = num; us["path"].append(f"LL={num}"); return "uscs_fine_PL", "PL:", "", False
|
| 785 |
-
|
| 786 |
-
if prompt_id == "uscs_fine_PL":
|
| 787 |
-
num, err = parse_float_label("PL", value, False, 0.0, None)
|
| 788 |
-
if err: return "uscs_fine_PL", "PL:", err, False
|
| 789 |
-
us["PL"] = num; us["path"].append(f"PL={num}"); return "uscs_fine_nDS", "Select Dry Strength (nDS):", "", False
|
| 790 |
-
|
| 791 |
-
if prompt_id == "uscs_fine_nDS":
|
| 792 |
-
try:
|
| 793 |
-
n = int(str(value).split(".")[0]) if isinstance(value, str) else int(value)
|
| 794 |
-
except:
|
| 795 |
-
return "uscs_fine_nDS", "Select nDS:", "Invalid selection", False
|
| 796 |
-
if not (1 <= n <= 5): return "uscs_fine_nDS", "Select nDS:", "Invalid", False
|
| 797 |
-
us["nDS"] = n; us["path"].append(f"nDS={n}"); return "uscs_fine_nDIL", "Select Dilatancy (nDIL):", "", False
|
| 798 |
-
|
| 799 |
-
if prompt_id == "uscs_fine_nDIL":
|
| 800 |
-
try:
|
| 801 |
-
n = int(str(value).split(".")[0]) if isinstance(value, str) else int(value)
|
| 802 |
-
except:
|
| 803 |
-
return "uscs_fine_nDIL", "Select nDIL:", "Invalid selection", False
|
| 804 |
-
if not (1 <= n <= 6): return "uscs_fine_nDIL", "Select nDIL:", "Invalid", False
|
| 805 |
-
us["nDIL"] = n; us["path"].append(f"nDIL={n}"); return "uscs_fine_nTG", "Select Toughness (nTG):", "", False
|
| 806 |
-
|
| 807 |
-
if prompt_id == "uscs_fine_nTG":
|
| 808 |
-
try:
|
| 809 |
-
n = int(str(value).split(".")[0]) if isinstance(value, str) else int(value)
|
| 810 |
-
except:
|
| 811 |
-
return "uscs_fine_nTG", "Select nTG:", "Invalid selection", False
|
| 812 |
-
if not (1 <= n <= 6): return "uscs_fine_nTG", "Select nTG:", "Invalid", False
|
| 813 |
-
us["nTG"] = n; us["path"].append(f"nTG={n}")
|
| 814 |
-
res, path = classify_uscs_from_session(us)
|
| 815 |
-
if res is None:
|
| 816 |
-
us["path"].extend(path)
|
| 817 |
-
return None, None, "USCS classification incomplete: " + "; ".join(path), False
|
| 818 |
-
us["result"] = res; us["path"].extend(path)
|
| 819 |
-
if SESSION["choice"] == "Both":
|
| 820 |
-
return "aashto_P2", "% Passing sieve no. 200 (P2) for AASHTO:", f"USCS done: {res}. Continue AASHTO.", False
|
| 821 |
-
return None, None, f"USCS classification completed: {res}", True
|
| 822 |
-
|
| 823 |
-
# AASHTO branch
|
| 824 |
-
if prompt_id == "aashto_P2":
|
| 825 |
-
num, err = parse_float_label("P2", value, False, 0, 100)
|
| 826 |
-
if err: return "aashto_P2", "% Passing sieve no. 200 (P2):", err, False
|
| 827 |
-
aa["P2"] = num; aa["path"] = [f"P2={num}%"]; return "aashto_P4", "% Passing sieve no. 40 (P4) (0 if none):", "", False
|
| 828 |
-
|
| 829 |
-
if prompt_id == "aashto_P4":
|
| 830 |
-
num, err = parse_float_label("P4", value, True, 0, 100)
|
| 831 |
-
if err: return "aashto_P4", "% Passing sieve no. 40 (P4):", err, False
|
| 832 |
-
aa["P4"] = 0 if num is None else num; aa["path"].append(f"P4={aa['P4']}%"); return "aashto_LL", "LL:", "", False
|
| 833 |
-
|
| 834 |
-
if prompt_id == "aashto_LL":
|
| 835 |
-
num, err = parse_float_label("LL", value, False, 0, None)
|
| 836 |
-
if err: return "aashto_LL", "LL:", err, False
|
| 837 |
-
aa["LL"] = num; aa["path"].append(f"LL={num}"); return "aashto_PL", "PL:", "", False
|
| 838 |
-
|
| 839 |
-
if prompt_id == "aashto_PL":
|
| 840 |
-
num, err = parse_float_label("PL", value, False, 0, None)
|
| 841 |
-
if err: return "aashto_PL", "PL:", err, False
|
| 842 |
-
aa["PL"] = num; aa["path"].append(f"PL={num}")
|
| 843 |
-
result, path = classify_aashto_from_inputs(aa["P2"], aa["P4"], aa["LL"], aa["PL"], aa.get("P1"))
|
| 844 |
-
if result is None:
|
| 845 |
-
aa["path"].extend(path)
|
| 846 |
-
if any("A-1-a" in p or "Candidate A-1-a" in p for p in path):
|
| 847 |
-
return "aashto_P1", "% Passing sieve no. 10 (P1) required:", "", False
|
| 848 |
-
return None, None, "AASHTO classification incomplete: " + "; ".join(path), False
|
| 849 |
-
aa["result"] = result; aa["path"].extend(path)
|
| 850 |
-
try:
|
| 851 |
-
aa["GI"] = int(result.split("(")[-1].split(")")[0])
|
| 852 |
-
except:
|
| 853 |
-
aa["GI"] = None
|
| 854 |
-
return None, None, f"AASHTO classification completed: {result}", True
|
| 855 |
-
|
| 856 |
-
if prompt_id == "aashto_P1":
|
| 857 |
-
num, err = parse_float_label("P1", value, False, 0, 100)
|
| 858 |
-
if err: return "aashto_P1", "% Passing sieve no. 10 (P1):", err, False
|
| 859 |
-
aa["P1"] = num; aa["path"].append(f"P1={num}%")
|
| 860 |
-
result, path = classify_aashto_from_inputs(aa["P2"], aa["P4"], aa["LL"], aa["PL"], aa["P1"])
|
| 861 |
-
if result is None:
|
| 862 |
-
aa["path"].extend(path); return None, None, "AASHTO classification incomplete: " + "; ".join(path), False
|
| 863 |
-
aa["result"] = result; aa["path"].extend(path)
|
| 864 |
-
try: aa["GI"] = int(result.split("(")[-1].split(")")[0])
|
| 865 |
-
except: aa["GI"] = None
|
| 866 |
-
return None, None, f"AASHTO classification completed: {result}", True
|
| 867 |
-
|
| 868 |
-
return None, None, "Unhandled state; restart wizard.", False
|
| 869 |
-
|
| 870 |
-
# ---- SECTION: Gradio UI ----
|
| 871 |
-
# We will create a multi-tab futuristic interface with icons.
|
| 872 |
-
with gr.Blocks(title="GeoMate 🌍 - Soil Engineering Toolkit") as demo:
|
| 873 |
-
# style: black background with accent colors via HTML/CSS
|
| 874 |
-
css = """
|
| 875 |
-
body { background-color: #0b0b0b; color: #ffffff; }
|
| 876 |
-
.gm-title { text-align:center; font-size:26px; font-weight:700; color:#ff6600; margin-bottom:6px; }
|
| 877 |
-
.gm-sub { text-align:center; color:#ffb199; margin-bottom:18px; }
|
| 878 |
-
.tab-btn { background: linear-gradient(90deg, rgba(255,102,0,0.12), rgba(204,0,0,0.06)); border-radius:8px; padding:8px; }
|
| 879 |
-
"""
|
| 880 |
-
gr.HTML(f"<style>{css}</style>")
|
| 881 |
-
gr.Markdown("<div class='gm-title'>🌍 GeoMate - Soil Engineering Toolkit</div>", elem_id="title")
|
| 882 |
-
gr.Markdown("<div class='gm-sub'>Futuristic interface • Black – Orange – Red theme • Martian background optional</div>")
|
| 883 |
-
|
| 884 |
-
# Tabs
|
| 885 |
-
with gr.Tab("🏠 Home"):
|
| 886 |
-
gr.Markdown("### Welcome to GeoMate")
|
| 887 |
-
gr.Markdown("Use the tabs to navigate: Soil Recognition, Classification, Sieve Analysis, Problem Solvers, LLM Expert, Report.")
|
| 888 |
-
gr.Markdown("Tip: Set environment variable `GROQ_API_l_KEY` to enable LLM expert (Report will include a concise expert conclusion).")
|
| 889 |
-
|
| 890 |
-
with gr.Tab("📷 Soil Recognition"):
|
| 891 |
-
gr.Markdown("Upload soil image; model (soil_model.pth) will predict. If model not present, this is disabled.")
|
| 892 |
-
img_in = gr.Image(type="pil", label="Upload Soil Image")
|
| 893 |
-
img_pred = gr.Textbox(label="Image Model Output", interactive=False)
|
| 894 |
-
def do_image_pred(img):
|
| 895 |
-
if img is None:
|
| 896 |
-
return "No image."
|
| 897 |
-
if soil_model is None:
|
| 898 |
-
return "Image model not configured (soil_model.pth missing)."
|
| 899 |
-
try:
|
| 900 |
-
im = img.convert("RGB")
|
| 901 |
-
t = transform(im).unsqueeze(0)
|
| 902 |
-
with torch.no_grad():
|
| 903 |
-
out = soil_model(t)
|
| 904 |
-
raw = float(out.item()); prob = float(torch.sigmoid(out).item())
|
| 905 |
-
SESSION["chat_history"].append(("Image prediction", f"raw={raw:.4f}, prob={prob:.4f}"))
|
| 906 |
-
return f"Raw={raw:.4f}, Sigmoid={prob:.4f}"
|
| 907 |
-
except Exception as e:
|
| 908 |
-
return f"Image prediction error: {e}"
|
| 909 |
-
img_in.change(do_image_pred, inputs=[img_in], outputs=[img_pred])
|
| 910 |
-
|
| 911 |
-
with gr.Tab("🧪 Soil Classification"):
|
| 912 |
-
gr.Markdown("Step-by-step wizard for USCS & AASHTO. Use Next to proceed; previous answers are retained.")
|
| 913 |
-
choice = gr.Dropdown(["USCS","AASHTO","Both"], label="Choose classification system", value="USCS")
|
| 914 |
-
start = gr.Button("Start / Reset Wizard")
|
| 915 |
-
prompt_text = gr.Markdown("")
|
| 916 |
-
answer = gr.Textbox(label="Answer (or numeric input)", placeholder="Type answer here (or select dropdown below if shown)")
|
| 917 |
-
ds_dd = gr.Dropdown(choices=DRY_STRENGTH_OPTIONS, label="Dry Strength (nDS)", visible=False)
|
| 918 |
-
dil_dd = gr.Dropdown(choices=DILATANCY_OPTIONS, label="Dilatancy (nDIL)", visible=False)
|
| 919 |
-
tg_dd = gr.Dropdown(choices=TOUGHNESS_OPTIONS, label="Toughness (nTG)", visible=False)
|
| 920 |
-
next_btn = gr.Button("Next")
|
| 921 |
-
status = gr.Textbox(label="Status / Messages", interactive=False)
|
| 922 |
-
final_result = gr.Textbox(label="Final classification(s)", interactive=False)
|
| 923 |
-
|
| 924 |
-
state = gr.State(value=None)
|
| 925 |
-
|
| 926 |
-
def on_start_system(ch):
|
| 927 |
-
pid, ptext, _ = wizard_start(ch)
|
| 928 |
-
# hide all dropdowns initially
|
| 929 |
-
return ptext, "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), "", pid
|
| 930 |
-
|
| 931 |
-
start.click(on_start_system, inputs=[choice], outputs=[prompt_text, status, ds_dd, dil_dd, tg_dd, final_result, state])
|
| 932 |
-
|
| 933 |
-
def on_next(pid, txt, ds_val, dil_val, tg_val, ch):
|
| 934 |
-
# decide which input to use based on pid
|
| 935 |
-
if pid is None:
|
| 936 |
-
return gr.update(value=""), "No active prompt: click Start", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), "", None
|
| 937 |
-
use_val = None
|
| 938 |
-
if pid == "uscs_fine_nDS": use_val = ds_val
|
| 939 |
-
elif pid == "uscs_fine_nDIL": use_val = dil_val
|
| 940 |
-
elif pid == "uscs_fine_nTG": use_val = tg_val
|
| 941 |
-
else: use_val = txt
|
| 942 |
-
next_pid, next_prompt, msg, finished = wizard_next(pid, use_val)
|
| 943 |
-
# show appropriate dropdowns
|
| 944 |
-
show_ds = next_pid in ("uscs_fine_nDS",)
|
| 945 |
-
show_dil = next_pid in ("uscs_fine_nDIL",)
|
| 946 |
-
show_tg = next_pid in ("uscs_fine_nTG",)
|
| 947 |
-
# update final result display
|
| 948 |
-
res_display = []
|
| 949 |
-
if SESSION["uscs"]["result"]:
|
| 950 |
-
res_display.append(f"USCS: {SESSION['uscs']['result']}")
|
| 951 |
-
if SESSION["aashto"]["result"]:
|
| 952 |
-
res_display.append(f"AASHTO: {SESSION['aashto']['result']}")
|
| 953 |
-
return (next_prompt or ""), msg, gr.update(visible=show_ds), gr.update(visible=show_dil), gr.update(visible=show_tg), " | ".join(res_display), next_pid
|
| 954 |
-
|
| 955 |
-
next_btn.click(on_next, inputs=[state, answer, ds_dd, dil_dd, tg_dd, choice],
|
| 956 |
-
outputs=[prompt_text, status, ds_dd, dil_dd, tg_dd, final_result, state])
|
| 957 |
-
|
| 958 |
-
with gr.Tab("🧾 Sieve Analysis"):
|
| 959 |
-
gr.Markdown("Upload CSV or paste two-column data (sieve_size_mm, percent_finer). Example:\n`19 100\n9.5 95\n4.75 85\n2.36 65\n0.425 10`")
|
| 960 |
-
sieve_input = gr.Textbox(label="Paste sieve data (size_mm percent_finer) or upload CSV", lines=6, placeholder="size_mm percent_finer")
|
| 961 |
-
sieve_upload = gr.File(label="Upload CSV file (optional)", file_types=['.csv'])
|
| 962 |
-
run_sieve = gr.Button("Run Sieve Analysis")
|
| 963 |
-
sieve_plot = gr.Image(label="PSD Plot")
|
| 964 |
-
dvals_box = gr.Textbox(label="D10, D30, D60 and Cu, Cc", interactive=False)
|
| 965 |
-
|
| 966 |
-
def on_sieve_run(text, csv_file):
|
| 967 |
-
data_text = text
|
| 968 |
-
if csv_file:
|
| 969 |
-
try:
|
| 970 |
-
with open(csv_file.name, "r") as f:
|
| 971 |
-
data_text = f.read()
|
| 972 |
-
except Exception:
|
| 973 |
-
pass
|
| 974 |
-
if not data_text or data_text.strip()=="":
|
| 975 |
-
return None, "No data provided."
|
| 976 |
-
sizes, perc = parse_sieve_input(data_text)
|
| 977 |
-
if sizes is None:
|
| 978 |
-
return None, "No valid numeric data found. Ensure two columns: size_mm percent_finer."
|
| 979 |
-
# compute D-values
|
| 980 |
-
D10, D30, D60 = compute_D_values(sizes, perc)
|
| 981 |
-
if D10 is None:
|
| 982 |
-
return None, "Could not compute D10/D30/D60 from given data."
|
| 983 |
-
Cu = D60 / D10 if D10 != 0 else None
|
| 984 |
-
Cc = (D30 ** 2) / (D10 * D60) if (D10*D60) != 0 else None
|
| 985 |
-
# store in SESSION
|
| 986 |
-
SESSION["sieve"]["sieve_sizes"] = sizes.tolist()
|
| 987 |
-
SESSION["sieve"]["percent_finer"] = perc.tolist()
|
| 988 |
-
SESSION["sieve"]["D10"] = D10; SESSION["sieve"]["D30"] = D30; SESSION["sieve"]["D60"] = D60
|
| 989 |
-
SESSION["sieve"]["Cu"] = Cu; SESSION["sieve"]["Cc"] = Cc
|
| 990 |
-
# generate plot
|
| 991 |
-
img_bytes = plot_sieve_curve(sizes, perc)
|
| 992 |
-
info = f"D10={D10:.6f} mm, D30={D30:.6f} mm, D60={D60:.6f} mm\nCu={Cu:.3f}, Cc={Cc:.3f}"
|
| 993 |
-
return (img_bytes, info)
|
| 994 |
-
|
| 995 |
-
run_sieve.click(on_sieve_run, inputs=[sieve_input, sieve_upload], outputs=[sieve_plot, dvals_box])
|
| 996 |
-
|
| 997 |
-
with gr.Tab("🧮 Problem Solvers"):
|
| 998 |
-
gr.Markdown("Use these to compute engineering values; results are appended to the report context.")
|
| 999 |
-
with gr.Group():
|
| 1000 |
-
gr.Markdown("#### Bearing Capacity")
|
| 1001 |
-
bc_q = gr.Number(label="Overburden q")
|
| 1002 |
-
bc_unit = gr.Dropdown(["kN/m²","psf","kPa"], value="kN/m²")
|
| 1003 |
-
bc_Nq = gr.Number(label="Nq", value=10)
|
| 1004 |
-
bc_B = gr.Number(label="Width B (m)", value=1.0)
|
| 1005 |
-
bc_btn = gr.Button("Compute")
|
| 1006 |
-
bc_out = gr.Textbox(interactive=False)
|
| 1007 |
-
bc_btn.click(bearing_capacity_solver, inputs=[bc_q, bc_unit, bc_Nq, bc_B], outputs=[bc_out])
|
| 1008 |
-
|
| 1009 |
-
gr.Markdown("#### Slope Stability")
|
| 1010 |
-
s_c = gr.Number(label="C (kN/m²)")
|
| 1011 |
-
s_phi = gr.Number(label="phi (deg)")
|
| 1012 |
-
s_gamma = gr.Number(label="gamma (kN/m³)")
|
| 1013 |
-
s_h = gr.Number(label="height (m)")
|
| 1014 |
-
s_btn = gr.Button("Compute")
|
| 1015 |
-
s_out = gr.Textbox(interactive=False)
|
| 1016 |
-
s_btn.click(slope_stability_solver, inputs=[s_c, s_phi, s_gamma, s_h], outputs=[s_out])
|
| 1017 |
-
|
| 1018 |
-
gr.Markdown("#### Consolidation")
|
| 1019 |
-
ds_ds = gr.Number(label="Δσ (kN/m²)")
|
| 1020 |
-
ds_mv = gr.Number(label="mv (m²/kN)")
|
| 1021 |
-
ds_H = gr.Number(label="H (m)")
|
| 1022 |
-
ds_btn = gr.Button("Compute")
|
| 1023 |
-
ds_out = gr.Textbox(interactive=False)
|
| 1024 |
-
ds_btn.click(consolidation_solver, inputs=[ds_ds, ds_mv, ds_H], outputs=[ds_out])
|
| 1025 |
-
|
| 1026 |
-
gr.Markdown("#### Seepage")
|
| 1027 |
-
sp_k = gr.Number(label="k (m/s)")
|
| 1028 |
-
sp_i = gr.Number(label="i")
|
| 1029 |
-
sp_A = gr.Number(label="A (m²)")
|
| 1030 |
-
sp_btn = gr.Button("Compute")
|
| 1031 |
-
sp_out = gr.Textbox(interactive=False)
|
| 1032 |
-
sp_btn.click(seepage_solver, inputs=[sp_k, sp_i, sp_A], outputs=[sp_out])
|
| 1033 |
-
|
| 1034 |
-
gr.Markdown("#### Compaction")
|
| 1035 |
-
cp_W = gr.Number(label="W (kN)")
|
| 1036 |
-
cp_V = gr.Number(label="V (m³)")
|
| 1037 |
-
cp_btn = gr.Button("Compute")
|
| 1038 |
-
cp_out = gr.Textbox(interactive=False)
|
| 1039 |
-
cp_btn.click(compaction_solver, inputs=[cp_W, cp_V], outputs=[cp_out])
|
| 1040 |
-
|
| 1041 |
-
with gr.Tab("🤖 GeoMate LLM Expert"):
|
| 1042 |
-
gr.Markdown("LLM assistant. Requires env var `GROQ_API_l_KEY`. If not set, LLM features are disabled.")
|
| 1043 |
-
qbox = gr.Textbox(label="Ask GeoMate", lines=4, placeholder="e.g., Recommend foundation type for CL soil under water table")
|
| 1044 |
-
ask_btn = gr.Button("Ask")
|
| 1045 |
-
ans_box = gr.Textbox(label="Geotech Expert (LLM) answer", lines=8, interactive=False)
|
| 1046 |
-
|
| 1047 |
-
def ask_llm(q):
|
| 1048 |
-
if client is None:
|
| 1049 |
-
return "LLM unavailable. Set environment variable GROQ_API_l_KEY to enable."
|
| 1050 |
-
prompt = f"You are GeoMate, an expert geotechnical engineer. Answer concisely.\nQuery: {q}"
|
| 1051 |
-
try:
|
| 1052 |
-
r = client.chat.completions.create(model=GROQ_MODEL_NAME, messages=[{"role":"user","content":prompt}])
|
| 1053 |
-
ans = r.choices[0].message.content
|
| 1054 |
-
SESSION["chat_history"].append((q, ans))
|
| 1055 |
-
return ans
|
| 1056 |
-
except Exception as e:
|
| 1057 |
-
return f"LLM call failed: {e}"
|
| 1058 |
-
|
| 1059 |
-
ask_btn.click(ask_llm, inputs=[qbox], outputs=[ans_box])
|
| 1060 |
-
|
| 1061 |
-
with gr.Tab("📄 Report & Export"):
|
| 1062 |
-
gr.Markdown("Generate final PDF report. The decision path is only included in the PDF (not the UI).")
|
| 1063 |
-
logo_file = gr.File(label="Upload GeoMate logo (PNG/JPG) - optional", file_types=[".png",".jpg",".jpeg"])
|
| 1064 |
-
gen_btn = gr.Button("Generate PDF Report")
|
| 1065 |
-
pdf_out = gr.File(label="Download PDF")
|
| 1066 |
-
def on_generate(logo):
|
| 1067 |
-
logo_bytes = None
|
| 1068 |
-
if logo:
|
| 1069 |
-
try:
|
| 1070 |
-
with open(logo.name, "rb") as f:
|
| 1071 |
-
logo_bytes = f.read()
|
| 1072 |
-
except:
|
| 1073 |
-
logo_bytes = None
|
| 1074 |
-
p = generate_report_pdf(logo_bytes)
|
| 1075 |
-
return p
|
| 1076 |
-
gen_btn.click(on_generate, inputs=[logo_file], outputs=[pdf_out])
|
| 1077 |
-
|
| 1078 |
-
with gr.Tab("✉️ Feedback"):
|
| 1079 |
-
feedback = gr.Textbox(label="Feedback / Bug report", lines=4)
|
| 1080 |
-
fb_btn = gr.Button("Send Feedback (saves locally)")
|
| 1081 |
-
fb_out = gr.Textbox(interactive=False)
|
| 1082 |
-
def save_feedback(txt):
|
| 1083 |
-
try:
|
| 1084 |
-
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 1085 |
-
fname = f"feedback_{ts}.txt"
|
| 1086 |
-
with open(fname, "w") as f:
|
| 1087 |
-
f.write(txt)
|
| 1088 |
-
return f"Saved feedback to {fname}"
|
| 1089 |
-
except Exception as e:
|
| 1090 |
-
return f"Error saving feedback: {e}"
|
| 1091 |
-
fb_btn.click(save_feedback, inputs=[feedback], outputs=[fb_out])
|
| 1092 |
-
|
| 1093 |
-
# ---- SECTION: Launch app ----
|
| 1094 |
-
if __name__ == "__main__":
|
| 1095 |
-
demo.launch(share=False)
|
|
|
|
| 1 |
+
""" GeoMate: Soil Engineering Toolkit 🌍
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
A complete geotechnical engineering assistant for soil classification, analysis, and design.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
===================== 🚀 Features =====================
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
+
📷 Soil Type Classifier — Upload image to identify soil type via PyTorch model
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
🤖 Ask GeoMate — AI-based chat powered by Groq (LLaMA 70B)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
🧪 Soil Classification — USCS and AASHTO classification systems
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
🏗️ Engineering Calculators: • Bearing Capacity • Slope Stability • Consolidation Settlement • Seepage Flow • Compaction Density
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
📄 PDF Generator — Download summary reports
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
===================== 🔧 Tech Stack =====================
|
|
|
|
| 19 |
|
| 20 |
+
Gradio (UI)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
PyTorch (Image Classifier)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
Groq API (AI Chat)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
ReportLab (PDF Export)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
===================== ✅ Hugging Face Deployment =====================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
1. Create a new Space on https://huggingface.co/spaces
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
2. Choose Gradio SDK
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
+
3. Upload:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
app.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
+
README.md
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
+
soil_model.pth
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
4. Add Secret Key: GROQ_API_KEY
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
+
Use AI for smarter soil engineering 💡 """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
import os import gradio as gr import torch import torchvision.transforms as transforms from PIL import Image from groq import Groq from reportlab.lib.pagesizes import A4 from reportlab.pdfgen import canvas import tempfile
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
Load model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = torch.load("soil_model.pth", map_location=device) model.eval()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
+
transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), ])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
classes = ['Clay', 'Silt', 'Sand', 'Gravel']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
+
def classify_image(img): image = transform(img).unsqueeze(0).to(device) with torch.no_grad(): output = model(image) _, predicted = torch.max(output, 1) return classes[predicted.item()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
+
Groq AI Chat
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
+
groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
+
def ask_geomate(prompt): chat_completion = groq_client.chat.completions.create( messages=[{"role": "user", "content": prompt}], model="llama3-70b-8192" ) return chat_completion.choices[0].message.content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
+
Soil classification
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
+
def classify_uscs(symbol, fines_percent): if symbol.startswith("G"): if fines_percent < 5: return "Well-graded Gravel (GW)" if "W" in symbol else "Poorly-graded Gravel (GP)" elif 5 <= fines_percent <= 12: return "Gravel with Fines (GW-GM or GW-GC)" else: return "Silty or Clayey Gravel (GM/GC)" elif symbol.startswith("S"): if fines_percent < 5: return "Well-graded Sand (SW)" if "W" in symbol else "Poorly-graded Sand (SP)" elif 5 <= fines_percent <= 12: return "Sand with Fines (SW-SM or SW-SC)" else: return "Silty or Clayey Sand (SM/SC)" elif symbol.startswith("M"): return "Silt (ML)" elif symbol.startswith("C"): return "Clay (CL)" return "Check symbol"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
def classify_aashto(percent_passing_no200): if percent_passing_no200 < 35: return "Granular Soil (A-1 to A-3)" else: return "Silty-Clay Soil (A-4 to A-7)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
+
Calculators
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
+
def bearing_capacity(q, Nq, sc, dc, ic): return q * Nq * sc * dc * ic
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
+
def slope_stability(factor_safety, slope_angle): return "Stable" if factor_safety > 1.5 else "Unstable"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
def consolidation_settlement(H, Cv, t): return (Cv * t) / (H**2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
+
def seepage_flow(K, H, L): return K * H / L
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
+
def compaction_density(W, V): return W / V
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
+
PDF Generator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
+
def generate_pdf(text): with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp: c = canvas.Canvas(tmp.name, pagesize=A4) width, height = A4 text_obj = c.beginText(40, height - 50) text_obj.setFont("Helvetica", 12) for line in text.split("\n"): text_obj.textLine(line) c.drawText(text_obj) c.showPage() c.save() return tmp.name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
+
Gradio Interface
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
+
tab1 = gr.Interface(fn=classify_image, inputs=gr.Image(type="pil"), outputs="text", title="Soil Image Classifier") tab2 = gr.Interface(fn=ask_geomate, inputs=gr.Textbox(), outputs="text", title="Ask GeoMate") tab3 = gr.Interface(fn=classify_uscs, inputs=[gr.Textbox(label="USCS Symbol"), gr.Number(label="% Fines")], outputs="text", title="USCS Classification") tab4 = gr.Interface(fn=classify_aashto, inputs=gr.Number(label="% Passing No. 200"), outputs="text", title="AASHTO Classification") tab5 = gr.Interface(fn=bearing_capacity, inputs=[gr.Number(), gr.Number(), gr.Number(), gr.Number(), gr.Number()], outputs="number", title="Bearing Capacity") tab6 = gr.Interface(fn=slope_stability, inputs=[gr.Number(), gr.Number()], outputs="text", title="Slope Stability") tab7 = gr.Interface(fn=consolidation_settlement, inputs=[gr.Number(), gr.Number(), gr.Number()], outputs="number", title="Consolidation Settlement") tab8 = gr.Interface(fn=seepage_flow, inputs=[gr.Number(), gr.Number(), gr.Number()], outputs="number", title="Seepage Flow") tab9 = gr.Interface(fn=compaction_density, inputs=[gr.Number(), gr.Number()], outputs="number", title="Compaction Density") tab10 = gr.Interface(fn=generate_pdf, inputs=gr.Textbox(lines=10, placeholder="Paste your summary here"), outputs="file", title="📄 Generate PDF Report")
|
|
|
|
| 98 |
|
| 99 |
+
demo = gr.TabbedInterface([tab1, tab2, tab3, tab4, tab5, tab6, tab7, tab8, tab9, tab10], tab_names=["Image Classifier", "Ask GeoMate", "USCS", "AASHTO", "Bearing", "Slope", "Consolidation", "Seepage", "Compaction", "📄 Generate PDF"])
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
+
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|