MSU576 commited on
Commit
58a9939
·
verified ·
1 Parent(s): 9c1214d

Delete app.kgjycbu

Browse files
Files changed (1) hide show
  1. app.kgjycbu +0 -958
app.kgjycbu DELETED
@@ -1,958 +0,0 @@
1
- # =========================
2
- # GeoMate V2 - Full App
3
- # =========================
4
- import os
5
- import re
6
- import json
7
- import datetime
8
- from math import floor
9
- from typing import Dict, Any, Tuple, List
10
-
11
- import streamlit as st
12
- from streamlit_option_menu import option_menu
13
- from fpdf import FPDF
14
- # ===============================
15
- # Earth Engine Initialization
16
- # ===============================
17
- import ee, os, json
18
- from google.oauth2 import service_account
19
-
20
- def init_earth_engine():
21
- try:
22
- key_json = os.getenv("EARTH_ENGINE_KEY")
23
- if not key_json:
24
- st.error("❌ EARTH_ENGINE_KEY secret is missing. Please configure it under Settings → Secrets.")
25
- return False
26
- key_dict = json.loads(key_json)
27
- creds = service_account.Credentials.from_service_account_info(key_dict)
28
- ee.Initialize(creds, project=key_dict.get("project_id"))
29
- st.success("✅ Earth Engine initialized successfully!")
30
- return True
31
- except Exception as e:
32
- st.error(f"Earth Engine init failed: {e}")
33
- return False
34
-
35
- EE_READY = init_earth_engine()
36
- # Optional libs (RAG, embeddings, FAISS, Groq, EE)
37
- # They may not be available on first load; we guard usage at runtime.
38
- try:
39
- from sentence_transformers import SentenceTransformer
40
- import faiss # noqa: F401
41
- HAVE_EMBED = True
42
- except Exception:
43
- HAVE_EMBED = False
44
-
45
- try:
46
- from langchain.memory import ConversationBufferMemory
47
- from langchain.chains import ConversationChain
48
- from langchain_community.chat_models import ChatGroq
49
- HAVE_LANGCHAIN = True
50
- except Exception:
51
- HAVE_LANGCHAIN = False
52
-
53
- try:
54
- import ee
55
- import geemap
56
- HAVE_EE = True
57
- except Exception:
58
- HAVE_EE = False
59
-
60
-
61
- # ==============================================================
62
- # GLOBALS & SESSION BOOT
63
- # ==============================================================
64
- st.set_page_config(page_title="GeoMate V2", page_icon="🌍", layout="wide")
65
-
66
- # Global/session stores
67
- ss = st.session_state
68
- if "soil_description_site" not in ss:
69
- # multi-site dict
70
- ss.soil_description_site: Dict[str, Dict[str, Any]] = {}
71
- if "sites" not in ss:
72
- ss.sites: List[str] = ["site1"]
73
- if "current_site" not in ss:
74
- ss.current_site = "site1"
75
- if "MODEL_NAME" not in ss:
76
- ss.MODEL_NAME = "llama-3.1-70b-versatile" # <- change freely
77
- if "secrets_status" not in ss:
78
- ss.secrets_status = {"groq_ok": False, "ee_ok": False}
79
- if "rag_ready" not in ss:
80
- ss.rag_ready = False
81
- if "rag_memory" not in ss:
82
- ss.rag_memory = None
83
- if "rag_chain" not in ss:
84
- ss.rag_chain = None
85
- if "emb_model" not in ss:
86
- ss.emb_model = None
87
-
88
- # Step trackers for chatbot-style flows (per page, per site)
89
- if "steps" not in ss:
90
- ss.steps = {}
91
- # ensure per-page steppers exist
92
- for key in ["classifier", "reports", "locator"]:
93
- if key not in ss.steps:
94
- ss.steps[key] = {}
95
- if ss.current_site not in ss.steps[key]:
96
- ss.steps[key][ss.current_site] = {"step": 0}
97
-
98
- # Store classifier working inputs per site
99
- if "cls_inputs" not in ss:
100
- ss.cls_inputs = {}
101
- if ss.current_site not in ss.cls_inputs:
102
- ss.cls_inputs[ss.current_site] = {}
103
-
104
- # Store reports Q&A per site
105
- if "reports_inputs" not in ss:
106
- ss.reports_inputs = {}
107
- if ss.current_site not in ss.reports_inputs:
108
- ss.reports_inputs[ss.current_site] = {}
109
-
110
-
111
- # ==============================================================
112
- # STARTUP SECRET CHECKS (top-level popups, no crash)
113
- # ==============================================================
114
- def check_secrets_banner():
115
- groq_key = os.getenv("GROQ_API_KEY", "")
116
- ee_key = os.getenv("EARTH_ENGINE_KEY", "")
117
-
118
- groq_ok = bool(groq_key)
119
- ee_ok = bool(ee_key)
120
-
121
- ss.secrets_status["groq_ok"] = groq_ok
122
- ss.secrets_status["ee_ok"] = ee_ok
123
-
124
- cols = st.columns(2)
125
- with cols[0]:
126
- if groq_ok:
127
- st.success("✅ Groq API key detected.")
128
- else:
129
- st.error("❌ Groq API key missing (set `GROQ_API_KEY`). LLM chat will be disabled.")
130
- with cols[1]:
131
- if ee_ok:
132
- st.success("✅ Earth Engine key detected.")
133
- else:
134
- st.error("❌ Earth Engine key missing (set `EARTH_ENGINE_KEY`). Locator map will be limited.")
135
-
136
-
137
- # ==============================================================
138
- # RAG MEMORY (session-scoped, “like ChatGPT” during this run)
139
- # ==============================================================
140
- def init_rag():
141
- if not ss.secrets_status["groq_ok"] or not HAVE_LANGCHAIN:
142
- ss.rag_ready = False
143
- return
144
-
145
- if ss.rag_memory is None:
146
- ss.rag_memory = ConversationBufferMemory()
147
- if ss.rag_chain is None:
148
- try:
149
- llm = ChatGroq(model_name=ss.MODEL_NAME, temperature=0.2)
150
- ss.rag_chain = ConversationChain(llm=llm, memory=ss.rag_memory, verbose=False)
151
- ss.rag_ready = True
152
- except Exception:
153
- ss.rag_ready = False
154
-
155
- # Embeddings for future RAG vectorization if needed
156
- if HAVE_EMBED and ss.emb_model is None:
157
- try:
158
- ss.emb_model = SentenceTransformer("all-MiniLM-L6-v2")
159
- except Exception:
160
- ss.emb_model = None
161
-
162
-
163
- def rag_ask(query: str) -> str:
164
- """
165
- Converse naturally; memory retained within this session.
166
- """
167
- if not ss.rag_ready or ss.rag_chain is None:
168
- return "LLM is unavailable (Groq key missing or initialization failed)."
169
- try:
170
- return ss.rag_chain.predict(input=query)
171
- except Exception as e:
172
- return f"LLM error: {e}"
173
-
174
-
175
- # ==============================================================
176
- # SITE STORE
177
- # ==============================================================
178
- def save_site_info(site: str, key: str, value: Any):
179
- if site not in ss.soil_description_site:
180
- ss.soil_description_site[site] = {}
181
- ss.soil_description_site[site][key] = value
182
-
183
-
184
- # ==============================================================
185
- # USCS & AASHTO VERBATIM LOGIC
186
- # ==============================================================
187
- ENGINEERING_CHARACTERISTICS = {
188
- "Gravel": {
189
- "Settlement": "None",
190
- "Quicksand": "Impossible",
191
- "Frost-heaving": "None",
192
- "Groundwater_lowering": "Possible",
193
- "Cement_grouting": "Possible",
194
- "Silicate_bitumen_injections": "Unsuitable",
195
- "Compressed_air": "Possible (see notes)"
196
- },
197
- "Coarse sand": {
198
- "Settlement": "None",
199
- "Quicksand": "Impossible",
200
- "Frost-heaving": "None",
201
- "Groundwater_lowering": "Possible",
202
- "Cement_grouting": "Possible only if very coarse",
203
- "Silicate_bitumen_injections": "Suitable",
204
- "Compressed_air": "Suitable"
205
- },
206
- "Medium sand": {
207
- "Settlement": "None",
208
- "Quicksand": "Unlikely",
209
- "Frost-heaving": "None",
210
- "Groundwater_lowering": "Suitable",
211
- "Cement_grouting": "Impossible",
212
- "Silicate_bitumen_injections": "Suitable",
213
- "Compressed_air": "Suitable"
214
- },
215
- "Fine sand": {
216
- "Settlement": "None",
217
- "Quicksand": "Liable",
218
- "Frost-heaving": "None",
219
- "Groundwater_lowering": "Suitable",
220
- "Cement_grouting": "Impossible",
221
- "Silicate_bitumen_injections": "Not possible in very fine sands",
222
- "Compressed_air": "Suitable"
223
- },
224
- "Silt": {
225
- "Settlement": "Occurs",
226
- "Quicksand": "Liable (very coarse silts may behave differently)",
227
- "Frost-heaving": "Occurs",
228
- "Groundwater_lowering": "Generally not suitable (electro-osmosis possible)",
229
- "Cement_grouting": "Impossible",
230
- "Silicate_bitumen_injections": "Impossible",
231
- "Compressed_air": "Suitable"
232
- },
233
- "Clay": {
234
- "Settlement": "Occurs",
235
- "Quicksand": "Impossible",
236
- "Frost-heaving": "None",
237
- "Groundwater_lowering": "Impossible (generally)",
238
- "Cement_grouting": "Only in stiff fissured clay",
239
- "Silicate_bitumen_injections": "Impossible",
240
- "Compressed_air": "Used for support only in special cases"
241
- }
242
- }
243
-
244
- def uscs_aashto_verbatim(inputs: Dict[str, Any]) -> Tuple[str, str, str, int, Dict[str, str]]:
245
- """
246
- Verbatim USCS & AASHTO classifier.
247
- Returns: (result_text, uscs, aashto, GI, char_summary)
248
- """
249
- opt = str(inputs.get("opt","n")).lower()
250
- if opt == 'y':
251
- uscs = "Pt"
252
- uscs_expl = "Peat / organic soil — compressible, high organic content; poor engineering properties."
253
- aashto = "Organic (special handling)"
254
- characteristics = {"summary":"Highly organic peat — large settlement, low strength, not suitable for foundations."}
255
- return f"USCS: **{uscs}** — {uscs_expl}\n\nAASHTO: **{aashto}**", uscs, aashto, 0, characteristics
256
-
257
- P2 = float(inputs.get("P2", 0.0))
258
- P4 = float(inputs.get("P4", 0.0))
259
- D60 = float(inputs.get("D60", 0.0))
260
- D30 = float(inputs.get("D30", 0.0))
261
- D10 = float(inputs.get("D10", 0.0))
262
- LL = float(inputs.get("LL", 0.0))
263
- PL = float(inputs.get("PL", 0.0))
264
- PI = LL - PL
265
-
266
- Cu = (D60 / D10) if (D10 > 0 and D60 > 0) else 0
267
- Cc = ((D30 ** 2) / (D10 * D60)) if (D10 > 0 and D30 > 0 and D60 > 0) else 0
268
-
269
- uscs, uscs_expl = "Unknown", ""
270
-
271
- if P2 <= 50:
272
- # Coarse soils
273
- if P4 <= 50:
274
- # Gravels
275
- if Cu and Cc:
276
- if Cu >= 4 and 1 <= Cc <= 3:
277
- uscs, uscs_expl = "GW", "Well-graded gravel (good engineering properties)."
278
- else:
279
- uscs, uscs_expl = "GP", "Poorly-graded gravel."
280
- else:
281
- if PI < 4 or PI < 0.73 * (LL - 20):
282
- uscs, uscs_expl = "GM", "Silty gravel."
283
- elif PI > 7 and PI > 0.73 * (LL - 20):
284
- uscs, uscs_expl = "GC", "Clayey gravel."
285
- else:
286
- uscs, uscs_expl = "GM-GC", "Gravel with mixed fines."
287
- else:
288
- # Sands
289
- if Cu and Cc:
290
- if Cu >= 6 and 1 <= Cc <= 3:
291
- uscs, uscs_expl = "SW", "Well-graded sand."
292
- else:
293
- uscs, uscs_expl = "SP", "Poorly-graded sand."
294
- else:
295
- if PI < 4 or PI <= 0.73 * (LL - 20):
296
- uscs, uscs_expl = "SM", "Silty sand."
297
- elif PI > 7 and PI > 0.73 * (LL - 20):
298
- uscs, uscs_expl = "SC", "Clayey sand."
299
- else:
300
- uscs, uscs_expl = "SM-SC", "Transition silty/clayey sand."
301
- else:
302
- # Fine soils (P2 > 50)
303
- nDS = int(inputs.get("nDS", 5))
304
- nDIL = int(inputs.get("nDIL", 6))
305
- nTG = int(inputs.get("nTG", 6))
306
- if LL < 50:
307
- if 20 <= LL < 50 and PI <= 0.73 * (LL - 20):
308
- if nDS == 1 or nDIL == 3 or nTG == 3:
309
- uscs, uscs_expl = "ML", "Silt (low plasticity)."
310
- elif nDS == 3 or nDIL == 3 or nTG == 3:
311
- uscs, uscs_expl = "OL", "Organic silt."
312
- else:
313
- uscs, uscs_expl = "ML-OL", "Mixed silt/organic."
314
- elif 10 <= LL <= 30 and 4 <= PI <= 7 and PI > 0.72 * (LL - 20):
315
- if nDS == 1 or nDIL == 1 or nTG == 1:
316
- uscs, uscs_expl = "ML", "Silt."
317
- elif nDS == 2 or nDIL == 2 or nTG == 2:
318
- uscs, uscs_expl = "CL", "Clay (low plasticity)."
319
- else:
320
- uscs, uscs_expl = "ML-CL", "Mixed silt/clay."
321
- else:
322
- uscs, uscs_expl = "CL", "Clay (low plasticity)."
323
- else:
324
- if PI < 0.73 * (LL - 20):
325
- if nDS == 3 or nDIL == 4 or nTG == 4:
326
- uscs, uscs_expl = "MH", "Silt (high plasticity)."
327
- elif nDS == 2 or nDIL == 2 or nTG == 4:
328
- uscs, uscs_expl = "OH", "Organic clay/silt (high plasticity)."
329
- else:
330
- uscs, uscs_expl = "MH-OH", "Mixed high-plasticity."
331
- else:
332
- uscs, uscs_expl = "CH", "Clay (high plasticity)."
333
-
334
- # AASHTO
335
- if P2 <= 35:
336
- if P2 <= 15 and P4 <= 30 and PI <= 6:
337
- aashto = "A-1-a"
338
- elif P2 <= 25 and P4 <= 50 and PI <= 6:
339
- aashto = "A-1-b"
340
- elif P2 <= 35 and P4 > 0:
341
- if LL <= 40 and PI <= 10: aashto = "A-2-4"
342
- elif LL >= 41 and PI <= 10: aashto = "A-2-5"
343
- elif LL <= 40 and PI >= 11: aashto = "A-2-6"
344
- elif LL >= 41 and PI >= 11: aashto = "A-2-7"
345
- else: aashto = "A-2"
346
- else: aashto = "A-3"
347
- else:
348
- if LL <= 40 and PI <= 10: aashto = "A-4"
349
- elif LL >= 41 and PI <= 10: aashto = "A-5"
350
- elif LL <= 40 and PI >= 11: aashto = "A-6"
351
- else:
352
- aashto = "A-7-5" if PI <= (LL - 30) else "A-7-6"
353
-
354
- a, b = max(P2-35,0), max(P2-15,0)
355
- c, d = max(LL-40,0), max(PI-10,0)
356
- GI = floor(0.2*a + 0.005*a*c + 0.01*b*d)
357
-
358
- aashto_expl = f"{aashto} (GI = {GI})"
359
- char_summary = ENGINEERING_CHARACTERISTICS.get("Silt", {})
360
- if uscs.startswith(("G","S")): char_summary = ENGINEERING_CHARACTERISTICS.get("Coarse sand", {})
361
- if uscs.startswith(("M","C","O","H")): char_summary = ENGINEERING_CHARACTERISTICS.get("Silt", {})
362
-
363
- result_text = f"USCS: **{uscs}** — {uscs_expl}\n\nAASHTO: **{aashto_expl}**"
364
- return result_text, uscs, aashto, GI, char_summary
365
-
366
-
367
- # ==============================================================
368
- # CLASSIFIER CHATBOT (stepwise with retention)
369
- # ==============================================================
370
- # Exact dropdowns (text shown), backend mapping to integers
371
- DILATANCY_OPTS = [
372
- "Quick slow", "None-Very slow", "Slow", "Slow-none", "None", "Null?"
373
- ]
374
- TOUGHNESS_OPTS = [
375
- "None", "Medium", "Slight?", "Slight-Medium?", "High", "Null"
376
- ]
377
- DRY_STRENGTH_OPTS = [
378
- "None - slight", "Medium - high", "Slight - Medium", "High - Very high", "Null?"
379
- ]
380
-
381
- DIL_MAP = {txt: i+1 for i, txt in enumerate(DILATANCY_OPTS)} # 1..6
382
- TOUGH_MAP = {txt: i+1 for i, txt in enumerate(TOUGHNESS_OPTS)} # 1..6
383
- DRY_MAP = {txt: i+1 for i, txt in enumerate(DRY_STRENGTH_OPTS)} # 1..5
384
-
385
- def classifier_chatbot(site: str):
386
- st.markdown("🤖 **GeoMate:** Hello there! I am the soil classifier. Ready to start!")
387
-
388
- # Current step
389
- state = ss.steps["classifier"][site]
390
- step = state["step"]
391
- inputs = ss.cls_inputs[site]
392
-
393
- # Helper renderers for “Back/Next”
394
- def nav_buttons(next_enabled=True, back_enabled=True):
395
- cols = st.columns(2)
396
- with cols[0]:
397
- if back_enabled and step > 0 and st.button("⬅️ Back"):
398
- state["step"] = max(0, step - 1)
399
- st.rerun()
400
- with cols[1]:
401
- if next_enabled and st.button("➡️ Next"):
402
- state["step"] = step + 1
403
- st.rerun()
404
-
405
- # Step 0: Organic
406
- if step == 0:
407
- val = st.radio("Is the soil organic?", ["No", "Yes"], index=0 if inputs.get("opt","n")=="n" else 1, key=f"{site}_opt_radio")
408
- inputs["opt"] = "y" if val == "Yes" else "n"
409
- save_site_info(site, "Organic", val)
410
- nav_buttons(next_enabled=True, back_enabled=False)
411
- return
412
-
413
- # Step 1: % passing #200
414
- if step == 1:
415
- p2 = st.number_input("% Passing #200 (0–100)", min_value=0.0, max_value=100.0, value=float(inputs.get("P2", 0.0)), key=f"{site}_P2")
416
- inputs["P2"] = p2
417
- save_site_info(site, "% Passing #200", p2)
418
- nav_buttons()
419
- return
420
-
421
- # Decision fork by P2:
422
- P2 = float(inputs.get("P2", 0.0))
423
-
424
- # If organic -> we can classify right away, but still show a summary step
425
- if inputs.get("opt","n") == "y":
426
- st.info("Organic soil path selected → USCS: Pt")
427
- if st.button("🔍 Classify Now"):
428
- res_text, uscs, aashto, GI, chars = uscs_aashto_verbatim(inputs)
429
- st.success(res_text)
430
- save_site_info(site, "USCS", uscs)
431
- save_site_info(site, "AASHTO", aashto)
432
- save_site_info(site, "Group Index", GI)
433
- nav_buttons()
434
- return
435
-
436
- # Step 2: % passing #4
437
- if step == 2:
438
- p4 = st.number_input("% Passing #4 (0–100)", min_value=0.0, max_value=100.0, value=float(inputs.get("P4", 0.0)), key=f"{site}_P4")
439
- inputs["P4"] = p4
440
- save_site_info(site, "% Passing #4", p4)
441
- nav_buttons()
442
- return
443
-
444
- P4 = float(inputs.get("P4", 0.0))
445
-
446
- # Step 3+: by coarse vs fine
447
- if P2 <= 50:
448
- # Coarse soils path
449
- st.caption("Coarse-grained path (P2 ≤ 50)")
450
-
451
- # Step 3: D-sizes + Atterberg (for fines cases)
452
- if step == 3:
453
- c1, c2, c3 = st.columns(3)
454
- d60 = c1.number_input("D60 (mm)", min_value=0.0, value=float(inputs.get("D60", 0.0)), key=f"{site}_D60")
455
- d30 = c2.number_input("D30 (mm)", min_value=0.0, value=float(inputs.get("D30", 0.0)), key=f"{site}_D30")
456
- d10 = c3.number_input("D10 (mm)", min_value=0.0, value=float(inputs.get("D10", 0.0)), key=f"{site}_D10")
457
- inputs["D60"], inputs["D30"], inputs["D10"] = d60, d30, d10
458
- save_site_info(site, "D-values (mm)", {"D60": d60, "D30": d30, "D10": d10})
459
- nav_buttons()
460
- return
461
-
462
- # Step 4: Atterberg limits
463
- if step == 4:
464
- LL = st.number_input("Liquid Limit (LL)", min_value=0.0, max_value=200.0, value=float(inputs.get("LL", 0.0)), key=f"{site}_LL")
465
- PL = st.number_input("Plastic Limit (PL)", min_value=0.0, max_value=200.0, value=float(inputs.get("PL", 0.0)), key=f"{site}_PL")
466
- inputs["LL"], inputs["PL"] = LL, PL
467
- save_site_info(site, "Atterberg Limits", {"LL": LL, "PL": PL})
468
- nav_buttons()
469
- return
470
-
471
- # Step 5: Classify
472
- if step == 5:
473
- if st.button("🔍 Run Classification"):
474
- res_text, uscs, aashto, GI, chars = uscs_aashto_verbatim(inputs)
475
- st.success(res_text)
476
- save_site_info(site, "USCS", uscs)
477
- save_site_info(site, "AASHTO", aashto)
478
- save_site_info(site, "Group Index", GI)
479
- save_site_info(site, "Engineering Characteristics", chars)
480
- nav_buttons(next_enabled=False)
481
- return
482
-
483
- else:
484
- # Fine soils path (P2 > 50)
485
- st.caption("Fine-grained path (P2 > 50)")
486
-
487
- # Step 3: Atterberg limits (needed now)
488
- if step == 3:
489
- LL = st.number_input("Liquid Limit (LL)", min_value=0.0, max_value=200.0, value=float(inputs.get("LL", 0.0)), key=f"{site}_LL")
490
- PL = st.number_input("Plastic Limit (PL)", min_value=0.0, max_value=200.0, value=float(inputs.get("PL", 0.0)), key=f"{site}_PL")
491
- inputs["LL"], inputs["PL"] = LL, PL
492
- save_site_info(site, "Atterberg Limits", {"LL": LL, "PL": PL})
493
- nav_buttons()
494
- return
495
-
496
- # Step 4: Descriptors — text dropdowns mapped to integers
497
- if step == 4:
498
- dil_txt = st.selectbox("Dilatancy", DILATANCY_OPTS, index=(list(DIL_MAP).index(inputs.get("dil_txt", DILATANCY_OPTS[1])) if "dil_txt" in inputs else 1), key=f"{site}_DIL")
499
- tou_txt = st.selectbox("Toughness", TOUGHNESS_OPTS, index=(list(TOUGH_MAP).index(inputs.get("tou_txt", TOUGHNESS_OPTS[0])) if "tou_txt" in inputs else 0), key=f"{site}_TOU")
500
- dry_txt = st.selectbox("Dry Strength", DRY_STRENGTH_OPTS, index=(list(DRY_MAP).index(inputs.get("dry_txt", DRY_STRENGTH_OPTS[0])) if "dry_txt" in inputs else 0), key=f"{site}_DRY")
501
-
502
- # store both text & numeric codes
503
- inputs["dil_txt"], inputs["nDIL"] = dil_txt, DIL_MAP[dil_txt]
504
- inputs["tou_txt"], inputs["nTG"] = tou_txt, TOUGH_MAP[tou_txt]
505
- inputs["dry_txt"], inputs["nDS"] = dry_txt, DRY_MAP[dry_txt]
506
-
507
- save_site_info(site, "Dilatancy", dil_txt)
508
- save_site_info(site, "Toughness", tou_txt)
509
- save_site_info(site, "Dry Strength", dry_txt)
510
- nav_buttons()
511
- return
512
-
513
- # Step 5: Classify
514
- if step == 5:
515
- if st.button("🔍 Run Classification"):
516
- res_text, uscs, aashto, GI, chars = uscs_aashto_verbatim(inputs)
517
- st.success(res_text)
518
- save_site_info(site, "USCS", uscs)
519
- save_site_info(site, "AASHTO", aashto)
520
- save_site_info(site, "Group Index", GI)
521
- save_site_info(site, "Engineering Characteristics", chars)
522
- nav_buttons(next_enabled=False)
523
- return
524
-
525
-
526
- # ==============================================================
527
- # LOCATOR (chatty minimal)
528
- # ==============================================================
529
- import ee, geemap, json, os, streamlit as st
530
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image
531
- from reportlab.lib.styles import getSampleStyleSheet
532
- from reportlab.lib.pagesizes import A4
533
-
534
- # ----------------- EARTH ENGINE AUTH -----------------
535
- EE_READY = False
536
- try:
537
- service_json = st.secrets["EARTH_ENGINE_KEY"]
538
- if isinstance(service_json, str):
539
- service_json = json.loads(service_json)
540
- credentials = ee.ServiceAccountCredentials(
541
- email=service_json["client_email"],
542
- key_data=json.dumps(service_json)
543
- )
544
- ee.Initialize(credentials)
545
- EE_READY = True
546
- except Exception as e:
547
- st.error(f"⚠️ Earth Engine init failed: {e}")
548
- EE_READY = False
549
-
550
-
551
- # ----------------- SAVE SITE INFO -----------------
552
- def save_site_info(site: str, key: str, value):
553
- if "soil_description_site" not in st.session_state:
554
- st.session_state.soil_description_site = {}
555
- if site not in st.session_state.soil_description_site:
556
- st.session_state.soil_description_site[site] = {}
557
- st.session_state.soil_description_site[site][key] = value
558
-
559
-
560
- # ----------------- ADD EARTH ENGINE LAYERS -----------------
561
- def add_datasets_to_map(m, site, lat, lon):
562
- try:
563
- # Soil dataset (OpenLandMap USDA texture class)
564
- soil = ee.Image("OpenLandMap/SOL/SOL_TEXTURE-CLASS_USDA-TT_M/v02")
565
- soil_val = soil.sample(ee.Geometry.Point([lon, lat]), scale=250).first().getInfo()
566
- save_site_info(site, "soil_texture_value", soil_val)
567
-
568
- # Flood dataset (JRC Global Surface Water)
569
- flood = ee.ImageCollection("JRC/GSW1_4/YearlyHistory").mosaic()
570
- flood_val = flood.sample(ee.Geometry.Point([lon, lat]), scale=30).first().getInfo()
571
- save_site_info(site, "flood_risk_value", flood_val)
572
-
573
- # Elevation (SRTM)
574
- elevation = ee.Image("USGS/SRTMGL1_003")
575
- elev_val = elevation.sample(ee.Geometry.Point([lon, lat]), scale=30).first().getInfo()
576
- save_site_info(site, "elevation", elev_val)
577
-
578
- # Seismic/Environmental dataset (SEDAC NDGain placeholder)
579
- seismic = ee.Image("SEDAC/ND-GAIN/2015")
580
- seismic_val = seismic.sample(ee.Geometry.Point([lon, lat]), scale=1000).first().getInfo()
581
- save_site_info(site, "seismic_risk_value", seismic_val)
582
-
583
- # Visualization styles
584
- soil_vis = {"min": 1, "max": 12, "palette": ["ffffb2","fd8d3c","f03b20","bd0026"]}
585
- flood_vis = {"palette": ["0000ff"]}
586
- elev_vis = {"min": 0, "max": 3000, "palette": ["006633","E5FFCC","662A00","DDBB99","FFFFFF"]}
587
- seismic_vis = {"min": 0, "max": 100, "palette": ["green", "yellow", "red"]}
588
-
589
- # Add layers to map
590
- m.addLayer(soil, soil_vis, "Soil Texture")
591
- m.addLayer(flood, flood_vis, "Flood Risk")
592
- m.addLayer(elevation, elev_vis, "Elevation / Topography")
593
- m.addLayer(seismic, seismic_vis, "Seismic/Environmental Risk")
594
-
595
- # Center map
596
- m.setCenter(lon, lat, 8)
597
-
598
- except Exception as e:
599
- st.error(f"⚠️ Adding datasets failed: {e}")
600
-
601
-
602
- # ----------------- LOCATOR CHATBOT -----------------
603
- def locator_chat(site: str):
604
- st.markdown("🤖 **GeoMate:** Share your Area of Interest coordinates.")
605
-
606
- lat = st.number_input("Latitude", value=float(st.session_state.soil_description_site.get(site, {}).get("lat", 0.0)))
607
- lon = st.number_input("Longitude", value=float(st.session_state.soil_description_site.get(site, {}).get("lon", 0.0)))
608
-
609
- save_site_info(site, "lat", lat)
610
- save_site_info(site, "lon", lon)
611
-
612
- if EE_READY:
613
- try:
614
- m = geemap.Map(center=[lat or 0.0, lon or 0.0], zoom=6)
615
- add_datasets_to_map(m, site, lat, lon)
616
-
617
- # Save map snapshot for report
618
- map_path = f"map_{site}.png"
619
- m.to_image(out_path=map_path, zoom=6, dimensions=(600, 400))
620
- save_site_info(site, "map_snapshot", map_path)
621
-
622
- # Display map
623
- m.to_streamlit(height=500)
624
- st.success("✅ Earth Engine map loaded with soil, flood, seismic, and topography layers.")
625
- except Exception as e:
626
- st.error(f"🌍 Earth Engine map failed: {e}")
627
- else:
628
- st.warning("⚠️ Earth Engine not available — map disabled.")
629
-
630
-
631
- # ----------------- PDF REPORT -----------------
632
- from reportlab.lib import colors
633
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, Table, TableStyle
634
- from reportlab.lib.pagesizes import A4
635
- from reportlab.lib.styles import getSampleStyleSheet
636
- import os
637
-
638
- def generate_geotech_report(site: str, filename="geotech_report.pdf"):
639
- if "soil_description_site" not in st.session_state or site not in st.session_state.soil_description_site:
640
- st.error("❌ No site data available.")
641
- return
642
-
643
- data = st.session_state.soil_description_site[site]
644
- doc = SimpleDocTemplate(filename, pagesize=A4)
645
- styles = getSampleStyleSheet()
646
- content = []
647
-
648
- # Title
649
- content.append(Paragraph(f"<b>Geotechnical Report for {site}</b>", styles["Title"]))
650
- content.append(Spacer(1, 12))
651
-
652
- # General site data
653
- for key, value in data.items():
654
- if key not in ["map_snapshot"]:
655
- content.append(Paragraph(f"<b>{key}:</b> {value}", styles["Normal"]))
656
- content.append(Spacer(1, 6))
657
-
658
- # Add site map snapshot if available
659
- if "map_snapshot" in data and os.path.exists(data["map_snapshot"]):
660
- content.append(Spacer(1, 12))
661
- content.append(Paragraph("<b>Site Map:</b>", styles["Heading2"]))
662
- content.append(Image(data["map_snapshot"], width=400, height=300))
663
-
664
- # Add legends section
665
- content.append(Spacer(1, 12))
666
- content.append(Paragraph("<b>Legend</b>", styles["Heading2"]))
667
-
668
- legend_data = [
669
- ["Layer", "Description", "Color Representation"],
670
- ["Soil Texture", "USDA Texture Classes", "🟨 → 🟧 → 🟥 → 🟥 Dark"],
671
- ["Flood Risk", "Water extent (JRC GSW)", "🟦"],
672
- ["Elevation / Topography", "SRTM Elevation", "🟩 → 🟨 → 🟫 → ⬜"],
673
- ["Seismic Risk", "NDGain Risk Index", "🟩 → 🟨 → 🟥"]
674
- ]
675
-
676
- table = Table(legend_data, colWidths=[120, 240, 140])
677
- table.setStyle(TableStyle([
678
- ("BACKGROUND", (0, 0), (-1, 0), colors.grey),
679
- ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
680
- ("ALIGN", (0, 0), (-1, -1), "LEFT"),
681
- ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
682
- ("BOTTOMPADDING", (0, 0), (-1, 0), 8),
683
- ("BACKGROUND", (0, 1), (-1, -1), colors.beige),
684
- ("GRID", (0, 0), (-1, -1), 0.5, colors.black),
685
- ]))
686
-
687
- content.append(table)
688
-
689
- # Build PDF
690
- doc.build(content)
691
- st.success(f"📄 Full Geotechnical Report generated: {filename}")
692
- return filename
693
- # ==============================================================
694
- # GEOmate Ask (LLM + memory + fact capture)
695
- # ==============================================================
696
- FACT_PATTERNS = [
697
- ("Load Bearing Capacity", r"(?:bearing\s*capacity|q_?ult|qult)\s*[:=]?\s*([\d\.]+)\s*(k?pa|tsf|ksf|psi|mpa)?"),
698
- ("% Compaction", r"(?:%?\s*compaction|relative\s*compaction)\s*[:=]?\s*([\d\.]+)\s*%"),
699
- ("Skin Shear Strength", r"(?:skin\s*shear\s*strength|adhesion|α\s*su)\s*[:=]?\s*([\d\.]+)\s*(k?pa|psf|kpa)"),
700
- ]
701
-
702
- def extract_and_save_facts(site: str, text: str, answer: str):
703
- lower = f"{text}\n{answer}".lower()
704
- for key, pattern in FACT_PATTERNS:
705
- m = re.search(pattern, lower)
706
- if m:
707
- val = m.group(1)
708
- unit = (m.group(2) or "").strip()
709
- save_site_info(site, key, f"{val} {unit}".strip())
710
-
711
- def geomate_ask(site: str):
712
- st.markdown("🤖 **GeoMate:** Ask anything about your site’s soils or design.")
713
- if ss.secrets_status["groq_ok"] and HAVE_LANGCHAIN:
714
- init_rag()
715
-
716
- q = st.text_input("Your question (press Enter to send)", key=f"{site}_ask")
717
- if q:
718
- resp = rag_ask(q)
719
- st.markdown(f"**GeoMate:** {resp}")
720
- extract_and_save_facts(site, q, resp)
721
-
722
-
723
- # ==============================================================
724
- # REPORTS CHATBOT
725
- # ==============================================================
726
- REPORT_QUESTIONS = [
727
- ("Load Bearing Capacity", "What is the soil bearing capacity? (e.g., 150 kPa)"),
728
- ("Skin Shear Strength", "What is the skin shear strength? (e.g., 25 kPa)"),
729
- ("% Compaction", "What is the required % relative compaction? (e.g., 95 %)"),
730
- ("Rate of Consolidation", "What is the rate of consolidation / settlement time?"),
731
- ("Nature of Construction", "What is the nature of construction? (e.g., G+1 Residential, Tank, Retaining wall)"),
732
- ]
733
-
734
- def reports_chatbot(site: str):
735
- st.markdown("🤖 **GeoMate:** I’ll collect details for a full geotechnical report. You can type **skip** to move on.")
736
-
737
- s = ss.steps["reports"][site]["step"]
738
- answers = ss.reports_inputs[site]
739
-
740
- # ask current question
741
- if s < len(REPORT_QUESTIONS):
742
- key, prompt = REPORT_QUESTIONS[s]
743
- st.write(f"**Q{s+1}. {prompt}**")
744
- default_val = str(ss.soil_description_site.get(site, {}).get(key, answers.get(key, "")))
745
- ans = st.text_input("Your answer", value=default_val, key=f"{site}_rep_{s}")
746
- cols = st.columns(2)
747
- with cols[0]:
748
- if st.button("➡️ Next", key=f"{site}_rep_next_{s}"):
749
- if ans and ans.strip().lower() != "skip":
750
- answers[key] = ans.strip()
751
- save_site_info(site, key, ans.strip())
752
- ss.steps["reports"][site]["step"] = s + 1
753
- st.rerun()
754
- with cols[1]:
755
- if s > 0 and st.button("⬅️ Back", key=f"{site}_rep_back_{s}"):
756
- ss.steps["reports"][site]["step"] = s - 1
757
- st.rerun()
758
- else:
759
- st.success("All questions answered (or skipped). Generate your report when ready.")
760
- if st.button("📄 Generate Full Geotechnical Report", key=f"{site}_gen_pdf"):
761
- fname = generate_report_pdf(site)
762
- with open(fname, "rb") as f:
763
- st.download_button("⬇️ Download Report", f, file_name=fname, mime="application/pdf")
764
-
765
-
766
- # ==============================================================
767
- # PDF EXPORT
768
- # ==============================================================
769
- def generate_report_pdf(site: str) -> str:
770
- data = ss.soil_description_site.get(site, {})
771
- fname = f"{site}_geotechnical_report.pdf"
772
- pdf = FPDF()
773
- pdf.add_page()
774
- pdf.set_font("Arial", "B", 16)
775
- pdf.cell(0, 10, "GeoMate Geotechnical Report", ln=True, align="C")
776
- pdf.ln(6)
777
- pdf.set_font("Arial", "", 12)
778
- pdf.cell(0, 8, f"Site: {site}", ln=True)
779
- pdf.cell(0, 8, f"Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}", ln=True)
780
- pdf.ln(4)
781
-
782
- # Summary sections
783
- pdf.set_font("Arial", "B", 13)
784
- pdf.cell(0, 8, "Collected Parameters", ln=True)
785
- pdf.set_font("Arial", "", 11)
786
- for k, v in data.items():
787
- pdf.multi_cell(0, 6, f"{k}: {json.dumps(v) if isinstance(v, (dict, list)) else v}")
788
- pdf.ln(2)
789
-
790
- # If classification stored, show clearly
791
- uscs = data.get("USCS", None)
792
- aashto = data.get("AASHTO", None)
793
- gi = data.get("Group Index", None)
794
- if uscs or aashto:
795
- pdf.set_font("Arial", "B", 13)
796
- pdf.cell(0, 8, "Classification", ln=True)
797
- pdf.set_font("Arial", "", 11)
798
- if uscs: pdf.cell(0, 6, f"USCS: {uscs}", ln=True)
799
- if aashto: pdf.cell(0, 6, f"AASHTO: {aashto}", ln=True)
800
- if gi is not None: pdf.cell(0, 6, f"Group Index (GI): {gi}", ln=True)
801
- pdf.ln(2)
802
-
803
- pdf.output(fname)
804
- return fname
805
-
806
-
807
- # ==============================================================
808
- # PAGE SECTIONS
809
- # ==============================================================
810
- def landing_page():
811
- st.markdown(
812
- """
813
- <style>
814
- .stApp { background: linear-gradient(180deg, #060606 0%, #0f0f0f 100%); color: #e9eef6; }
815
- .landing-card { background: linear-gradient(180deg, rgba(255,122,0,0.06), rgba(255,122,0,0.02));
816
- border-radius: 12px; padding: 18px; margin-bottom: 14px; border: 1px solid rgba(255,122,0,0.08); }
817
- </style>
818
- """,
819
- unsafe_allow_html=True,
820
- )
821
- st.markdown("<h1 style='color:#FF8C00;margin-bottom:0'>GeoMate V2</h1>", unsafe_allow_html=True)
822
- st.caption("AI geotechnical copilot — soil recognition, classification, locator, RAG, and reports")
823
-
824
- st.markdown("### Startup Status")
825
- check_secrets_banner()
826
-
827
- col1, col2 = st.columns([2, 1])
828
- with col1:
829
- st.markdown("<div class='landing-card'>", unsafe_allow_html=True)
830
- st.subheader("What GeoMate does")
831
- st.markdown(
832
- """
833
- - **Soil Classifier:** USCS & AASHTO (verbatim logic) with guided, chatbot-style inputs.
834
- - **Locator:** (EE) Set AOIs and preview layers (if EE credentials available).
835
- - **GeoMate Ask:** Session-memory LLM with fact capture into site variables.
836
- - **Reports:** Chatbot gathers all remaining design data and generates a PDF.
837
- """
838
- )
839
- st.markdown("</div>", unsafe_allow_html=True)
840
-
841
- with col2:
842
- st.markdown("<div class='landing-card'>", unsafe_allow_html=True)
843
- st.subheader("Live Project")
844
- n_sites = len(ss.sites)
845
- n_cls = sum(1 for s in ss.soil_description_site.values() if "USCS" in s or "AASHTO" in s)
846
- st.metric("Sites", n_sites)
847
- st.metric("Classified Sites", n_cls)
848
- st.markdown("</div>", unsafe_allow_html=True)
849
-
850
-
851
- def sidebar_controls():
852
- with st.sidebar:
853
- st.markdown("<div style='text-align:center; padding:6px 0;'><h3 style='color:#FF8C00; margin:0;'>GeoMate V2</h3></div>", unsafe_allow_html=True)
854
-
855
- # Model selector (variable model name)
856
- st.subheader("LLM Model")
857
- ss.MODEL_NAME = st.selectbox(
858
- "Model",
859
- [
860
- "llama-3.1-70b-versatile",
861
- "llama-3.1-8b-instant",
862
- "mixtral-8x7b-32768",
863
- "gemma-7b-it"
864
- ],
865
- index=["llama-3.1-70b-versatile","llama-3.1-8b-instant","mixtral-8x7b-32768","gemma-7b-it"].index(ss.MODEL_NAME)
866
- if ss.MODEL_NAME in ["llama-3.1-70b-versatile","llama-3.1-8b-instant","mixtral-8x7b-32768","gemma-7b-it"] else 0
867
- )
868
-
869
- # Site selector / creator
870
- st.subheader("Sites")
871
- site_choice = st.selectbox("Current site", ss.sites, index=ss.sites.index(ss.current_site))
872
- if site_choice != ss.current_site:
873
- ss.current_site = site_choice
874
- # ensure step entries for this site
875
- for key in ss.steps:
876
- if ss.current_site not in ss.steps[key]:
877
- ss.steps[key][ss.current_site] = {"step": 0}
878
- if ss.current_site not in ss.cls_inputs:
879
- ss.cls_inputs[ss.current_site] = {}
880
- if ss.current_site not in ss.reports_inputs:
881
- ss.reports_inputs[ss.current_site] = {}
882
- st.rerun()
883
-
884
- new_site = st.text_input("New site name")
885
- if st.button("➕ Add site"):
886
- ns = new_site.strip() or f"site{len(ss.sites)+1}"
887
- if ns not in ss.sites:
888
- ss.sites.append(ns)
889
- ss.current_site = ns
890
- ss.soil_description_site.setdefault(ns, {})
891
- for key in ss.steps:
892
- ss.steps[key][ns] = {"step": 0}
893
- ss.cls_inputs.setdefault(ns, {})
894
- ss.reports_inputs.setdefault(ns, {})
895
- st.success(f"Created {ns}")
896
- st.rerun()
897
-
898
- st.markdown("---")
899
- pages = ["Landing", "Locator", "Soil Classifier", "GeoMate Ask", "Reports"]
900
- choice = option_menu(
901
- menu_title="",
902
- options=pages,
903
- icons=["house", "geo-alt", "flask", "robot", "file-earmark-text"],
904
- default_index=0
905
- )
906
- return choice
907
-
908
-
909
- def locator_page():
910
- st.header("🌍 Locator")
911
- locator_chat(ss.current_site)
912
-
913
-
914
- def classifier_page():
915
- st.header("🧪 Soil Classifier — USCS & AASHTO")
916
- classifier_chatbot(ss.current_site)
917
-
918
- # Save classification snapshot (if present)
919
- data = ss.soil_description_site.get(ss.current_site, {})
920
- if any(k in data for k in ["USCS", "AASHTO"]):
921
- st.markdown("### Saved Classification")
922
- st.json({k: data[k] for k in data if k in ["USCS", "AASHTO", "Group Index", "Engineering Characteristics"]})
923
-
924
-
925
- def ask_page():
926
- st.header("🤖 GeoMate Ask — RAG with Session Memory")
927
- if not ss.secrets_status["groq_ok"] or not HAVE_LANGCHAIN:
928
- st.warning("LLM not available (Groq key missing or LangChain not installed).")
929
- geomate_ask(ss.current_site)
930
-
931
- st.markdown("### Current Site Facts")
932
- st.json(ss.soil_description_site.get(ss.current_site, {}))
933
-
934
-
935
- def reports_page():
936
- st.header("📑 Reports")
937
- reports_chatbot(ss.current_site)
938
-
939
-
940
- # ==============================================================
941
- # MAIN
942
- # ==============================================================
943
- def main():
944
- choice = sidebar_controls()
945
- if choice == "Landing":
946
- landing_page()
947
- elif choice == "Locator":
948
- locator_page()
949
- elif choice == "Soil Classifier":
950
- classifier_page()
951
- elif choice == "GeoMate Ask":
952
- ask_page()
953
- elif choice == "Reports":
954
- reports_page()
955
-
956
-
957
- if __name__ == "__main__":
958
- main()