MSU576 commited on
Commit
b7998e7
·
verified ·
1 Parent(s): 7b8a754

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +47 -1040
app.py CHANGED
@@ -1,1095 +1,102 @@
1
- # app.py
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
- # ---- SECTION: Imports ----
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
- import numpy as np
24
- import matplotlib
25
- matplotlib.use('Agg')
26
- import matplotlib.pyplot as plt
27
 
28
- import torch
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
- import gradio as gr
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
- # ---- SECTION: Environment / LLM config ----
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
- # Using HF secret key stored under name "Shah"
48
- GROQ_API_KEY = os.environ.get("Shah")
49
- GROQ_MODEL_NAME = "llama-3.3-70b-versatile"
50
 
51
- client = None
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
- def forward(self, x):
72
- return self.base_model(x)
73
 
74
- soil_model = None
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
- try:
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
- # ---- SECTION: Utility helpers ----
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
- def split_text(text, chars=95):
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
- # ---- SECTION: Exact CLI logic (USCS & AASHTO) - kept identical to your corrected logic ----
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
- P2 = uscs["P2"]
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
- op = uscs.get("enter_Dvals", "n")
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
- if D60 != 0 and D30 != 0 and D10 != 0:
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
- if P4 <= 50:
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
- if LL < 50:
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
- def classify_aashto_from_inputs(P2, P4, LL, PL, P1=None):
310
- path = []
311
- PI = LL - PL
312
- path.append(f"P200={P2}%, P40={P4}%, LL={LL}, PL={PL} → PI={PI}")
313
 
314
- Result = None
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
- c = LL - 40
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
- # ---- SECTION: LLM wrapper for short geotech conclusion (≤150 words) ----
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
- # ---- SECTION: Problem solvers (same as earlier) ----
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
- def slope_stability_solver(c, phi, gamma, height):
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
- def consolidation_solver(delta_sigma, mv, H):
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
- def seepage_solver(k, i, A):
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
- def compaction_solver(W, V):
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
- # ---- SECTION: Sieve analysis utilities (plotting & interpolation) ----
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
- def compute_D_values(sizes_mm, perc_finer):
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
- def plot_sieve_curve(sizes_mm, perc_finer):
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
- # ---- SECTION: PDF generator ----
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
- c = canvas.Canvas(path, pagesize=A4)
562
- w, h = A4
563
- orange = HexColor("#ff6600")
564
- black = HexColor("#000000")
565
- red = HexColor("#cc0000")
566
- margin = 20
567
 
568
- # outer orange border
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
- y = h - 50
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
- c.setFont("Helvetica-Bold", 18); c.setFillColor(black)
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
- # Selected systems
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
- # USCS result
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
- # AASHTO result
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
- # Sieve outputs
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
- # Problem solver outputs
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
- # Build LLM context
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
- # LLM conclusion
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
- # Decision path (small font) at bottom
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
- c.save()
691
- return path
692
 
693
- # ---- SECTION: Wizard flow helpers for Gradio classification tab ----
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
- def wizard_start(choice):
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