Delete app.kgjycbu
Browse files- 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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|