Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py — GeoMate V2 (single-file Streamlit app)
|
| 2 |
+
# ------------------------------------------------
|
| 3 |
+
# Paste this entire file into your Hugging Face Space as app.py
|
| 4 |
+
# ------------------------------------------------
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
import os
|
| 8 |
+
import io
|
| 9 |
+
import json
|
| 10 |
+
import math
|
| 11 |
+
from math import floor
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from typing import Any, Dict, List, Tuple, Optional
|
| 14 |
+
|
| 15 |
+
# Streamlit must be imported early and set_page_config must be first Streamlit command
|
| 16 |
+
import streamlit as st
|
| 17 |
+
|
| 18 |
+
# Page config (first Streamlit call)
|
| 19 |
+
st.set_page_config(page_title="GeoMate V2", page_icon="🌍", layout="wide", initial_sidebar_state="expanded")
|
| 20 |
+
|
| 21 |
+
# Standard libs for data & plotting
|
| 22 |
+
import numpy as np
|
| 23 |
+
import pandas as pd
|
| 24 |
+
import matplotlib.pyplot as plt
|
| 25 |
+
import traceback
|
| 26 |
+
|
| 27 |
+
# Optional heavy libs — import safely
|
| 28 |
+
try:
|
| 29 |
+
import faiss # may fail in some environments
|
| 30 |
+
except Exception:
|
| 31 |
+
faiss = None
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
import reportlab
|
| 35 |
+
from reportlab.lib import colors
|
| 36 |
+
from reportlab.lib.pagesizes import A4
|
| 37 |
+
from reportlab.lib.units import mm
|
| 38 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
|
| 39 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 40 |
+
REPORTLAB_OK = True
|
| 41 |
+
except Exception:
|
| 42 |
+
REPORTLAB_OK = False
|
| 43 |
+
|
| 44 |
+
# FPDF fallback for simple PDFs
|
| 45 |
+
try:
|
| 46 |
+
from fpdf import FPDF
|
| 47 |
+
FPDF_OK = True
|
| 48 |
+
except Exception:
|
| 49 |
+
FPDF_OK = False
|
| 50 |
+
|
| 51 |
+
# Groq client optional
|
| 52 |
+
try:
|
| 53 |
+
from groq import Groq
|
| 54 |
+
GROQ_OK = True
|
| 55 |
+
except Exception:
|
| 56 |
+
GROQ_OK = False
|
| 57 |
+
|
| 58 |
+
# Earth Engine & geemap optional
|
| 59 |
+
try:
|
| 60 |
+
import ee
|
| 61 |
+
import geemap
|
| 62 |
+
EE_OK = True
|
| 63 |
+
except Exception:
|
| 64 |
+
ee = None
|
| 65 |
+
geemap = None
|
| 66 |
+
EE_OK = False
|
| 67 |
+
|
| 68 |
+
# OCR optional (pytesseract / easyocr)
|
| 69 |
+
OCR_TESSERACT = False
|
| 70 |
+
try:
|
| 71 |
+
import pytesseract
|
| 72 |
+
from PIL import Image
|
| 73 |
+
OCR_TESSERACT = True
|
| 74 |
+
except Exception:
|
| 75 |
+
OCR_TESSERACT = False
|
| 76 |
+
|
| 77 |
+
# Helpful alias for session state
|
| 78 |
+
ss = st.session_state
|
| 79 |
+
|
| 80 |
+
# -------------------------
|
| 81 |
+
# Helper: safe rerun
|
| 82 |
+
# -------------------------
|
| 83 |
+
def safe_rerun():
|
| 84 |
+
"""Try to rerun app. Prefer st.rerun(); fallback to experimental or simple stop."""
|
| 85 |
+
try:
|
| 86 |
+
st.rerun()
|
| 87 |
+
except Exception:
|
| 88 |
+
try:
|
| 89 |
+
st.experimental_rerun()
|
| 90 |
+
except Exception:
|
| 91 |
+
# last resort: stop execution so user can click/refresh
|
| 92 |
+
st.stop()
|
| 93 |
+
|
| 94 |
+
# -------------------------
|
| 95 |
+
# Secrets: HF-friendly access
|
| 96 |
+
# -------------------------
|
| 97 |
+
def get_secret(name: str) -> Optional[str]:
|
| 98 |
+
"""Read secrets from environment variables first, then streamlit secrets."""
|
| 99 |
+
v = os.environ.get(name)
|
| 100 |
+
if v:
|
| 101 |
+
return v
|
| 102 |
+
try:
|
| 103 |
+
v2 = st.secrets.get(name)
|
| 104 |
+
if v2:
|
| 105 |
+
# st.secrets may contain nested dicts (for JSON keys)
|
| 106 |
+
if isinstance(v2, dict) or isinstance(v2, list):
|
| 107 |
+
# convert to JSON string
|
| 108 |
+
return json.dumps(v2)
|
| 109 |
+
return str(v2)
|
| 110 |
+
except Exception:
|
| 111 |
+
pass
|
| 112 |
+
return None
|
| 113 |
+
|
| 114 |
+
# Required secret names (user asked)
|
| 115 |
+
GROQ_KEY = get_secret("GROQ_API_KEY")
|
| 116 |
+
SERVICE_ACCOUNT = get_secret("SERVICE_ACCOUNT")
|
| 117 |
+
EARTH_ENGINE_KEY = get_secret("EARTH_ENGINE_KEY") # JSON content (string) or path; optional
|
| 118 |
+
# We'll consider them optional; pages will show clear errors if missing
|
| 119 |
+
HAVE_GROQ = bool(GROQ_KEY and GROQ_OK)
|
| 120 |
+
HAVE_SERVICE_ACCOUNT = bool(SERVICE_ACCOUNT)
|
| 121 |
+
HAVE_EE_KEY = bool(EARTH_ENGINE_KEY)
|
| 122 |
+
# EE readiness flag we'll set after attempted init
|
| 123 |
+
EE_READY = False
|
| 124 |
+
|
| 125 |
+
# Attempt to initialize Earth Engine if credentials provided and ee module available
|
| 126 |
+
if EE_OK and (SERVICE_ACCOUNT or EARTH_ENGINE_KEY):
|
| 127 |
+
try:
|
| 128 |
+
# If EARTH_ENGINE_KEY is a JSON string, write to temp file and use service account auth
|
| 129 |
+
key_file = None
|
| 130 |
+
if EARTH_ENGINE_KEY:
|
| 131 |
+
# attempt to parse as json content
|
| 132 |
+
try:
|
| 133 |
+
parsed = json.loads(EARTH_ENGINE_KEY)
|
| 134 |
+
# write to /tmp/geomate_ee_key.json
|
| 135 |
+
key_file = "/tmp/geomate_ee_key.json"
|
| 136 |
+
with open(key_file, "w") as f:
|
| 137 |
+
json.dump(parsed, f)
|
| 138 |
+
except Exception:
|
| 139 |
+
# maybe EARTH_ENGINE_KEY is a path already on disk (unlikely on HF)
|
| 140 |
+
key_file = EARTH_ENGINE_KEY if os.path.exists(EARTH_ENGINE_KEY) else None
|
| 141 |
+
if key_file and SERVICE_ACCOUNT:
|
| 142 |
+
try:
|
| 143 |
+
# Use oauth2client if available
|
| 144 |
+
from oauth2client.service_account import ServiceAccountCredentials
|
| 145 |
+
creds = ServiceAccountCredentials.from_json_keyfile_name(key_file, scopes=['https://www.googleapis.com/auth/earthengine'])
|
| 146 |
+
ee.Initialize(creds)
|
| 147 |
+
EE_READY = True
|
| 148 |
+
except Exception:
|
| 149 |
+
# fallback: try ee.Initialize with service_account
|
| 150 |
+
try:
|
| 151 |
+
# This call may fail depending on ee versions; keep safe
|
| 152 |
+
ee.Initialize()
|
| 153 |
+
EE_READY = True
|
| 154 |
+
except Exception:
|
| 155 |
+
EE_READY = False
|
| 156 |
+
else:
|
| 157 |
+
# try simple ee.Initialize()
|
| 158 |
+
try:
|
| 159 |
+
ee.Initialize()
|
| 160 |
+
EE_READY = True
|
| 161 |
+
except Exception:
|
| 162 |
+
EE_READY = False
|
| 163 |
+
except Exception:
|
| 164 |
+
EE_READY = False
|
| 165 |
+
else:
|
| 166 |
+
EE_READY = False
|
| 167 |
+
|
| 168 |
+
# -------------------------
|
| 169 |
+
# Session state initialization
|
| 170 |
+
# -------------------------
|
| 171 |
+
# site_descriptions: list of dicts (max 4). We'll store as list for site ordering but also index by name.
|
| 172 |
+
if "site_descriptions" not in ss:
|
| 173 |
+
# default single site
|
| 174 |
+
ss["site_descriptions"] = []
|
| 175 |
+
if "active_site_index" not in ss:
|
| 176 |
+
ss["active_site_index"] = 0
|
| 177 |
+
if "llm_model" not in ss:
|
| 178 |
+
ss["llm_model"] = "llama3-8b-8192"
|
| 179 |
+
if "page" not in ss:
|
| 180 |
+
ss["page"] = "Landing"
|
| 181 |
+
if "rag_memory" not in ss:
|
| 182 |
+
ss["rag_memory"] = {} # per-site chat memory
|
| 183 |
+
if "classifier_states" not in ss:
|
| 184 |
+
ss["classifier_states"] = {} # per-site classifier step & inputs
|
| 185 |
+
|
| 186 |
+
# Utility: create default site structure
|
| 187 |
+
def make_empty_site(name: str = "Site 1") -> dict:
|
| 188 |
+
return {
|
| 189 |
+
"Site Name": name,
|
| 190 |
+
"Site Coordinates": "",
|
| 191 |
+
"lat": None,
|
| 192 |
+
"lon": None,
|
| 193 |
+
"Load Bearing Capacity": None,
|
| 194 |
+
"Skin Shear Strength": None,
|
| 195 |
+
"Relative Compaction": None,
|
| 196 |
+
"Rate of Consolidation": None,
|
| 197 |
+
"Nature of Construction": None,
|
| 198 |
+
"Soil Profile": None,
|
| 199 |
+
"Flood Data": None,
|
| 200 |
+
"Seismic Data": None,
|
| 201 |
+
"GSD": None,
|
| 202 |
+
"USCS": None,
|
| 203 |
+
"AASHTO": None,
|
| 204 |
+
"GI": None,
|
| 205 |
+
"classifier_inputs": {},
|
| 206 |
+
"classifier_decision_path": "",
|
| 207 |
+
"chat_history": [],
|
| 208 |
+
"report_convo_state": 0,
|
| 209 |
+
"map_snapshot": None,
|
| 210 |
+
"classifier_state": 0,
|
| 211 |
+
"classifier_chat": []
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
# ensure at least one site exists
|
| 215 |
+
if len(ss["site_descriptions"]) == 0:
|
| 216 |
+
ss["site_descriptions"].append(make_empty_site("Home"))
|
| 217 |
+
|
| 218 |
+
# -------------------------
|
| 219 |
+
# Sidebar: site management & LLM model selection
|
| 220 |
+
# -------------------------
|
| 221 |
+
from streamlit_option_menu import option_menu
|
| 222 |
+
|
| 223 |
+
def sidebar_ui():
|
| 224 |
+
st.sidebar.markdown("<div style='text-align:center'><h2 style='color:#FF8C00;margin:6px 0'>GeoMate V2</h2></div>", unsafe_allow_html=True)
|
| 225 |
+
st.sidebar.markdown("---")
|
| 226 |
+
|
| 227 |
+
# Model selector (persist)
|
| 228 |
+
st.sidebar.subheader("LLM Model")
|
| 229 |
+
model_options = ["llama3-8b-8192", "gemma-7b-it", "mixtral-8x7b-32768"]
|
| 230 |
+
ss["llm_model"] = st.sidebar.selectbox("Select LLM model", model_options, index=model_options.index(ss.get("llm_model","llama3-8b-8192")), key="llm_select")
|
| 231 |
+
|
| 232 |
+
st.sidebar.markdown("---")
|
| 233 |
+
st.sidebar.subheader("Project Sites (max 4)")
|
| 234 |
+
# show sites and allow add/remove, choose active site
|
| 235 |
+
colA, colB = st.sidebar.columns([2,1])
|
| 236 |
+
with colA:
|
| 237 |
+
new_site_name = st.text_input("New site name", value="", key="new_site_name_input")
|
| 238 |
+
with colB:
|
| 239 |
+
if st.button("➕", key="add_site_btn"):
|
| 240 |
+
# add new site up to 4
|
| 241 |
+
if len(ss["site_descriptions"]) >= 4:
|
| 242 |
+
st.sidebar.warning("Maximum 4 sites allowed.")
|
| 243 |
+
else:
|
| 244 |
+
name = new_site_name.strip() or f"Site {len(ss['site_descriptions'])+1}"
|
| 245 |
+
ss["site_descriptions"].append(make_empty_site(name))
|
| 246 |
+
ss["active_site_index"] = len(ss["site_descriptions"]) - 1
|
| 247 |
+
safe_rerun()
|
| 248 |
+
|
| 249 |
+
# list of site names
|
| 250 |
+
site_names = [s["Site Name"] for s in ss["site_descriptions"]]
|
| 251 |
+
idx = st.sidebar.radio("Active Site", options=list(range(len(site_names))), format_func=lambda i: site_names[i], index=ss.get("active_site_index",0), key="active_site_radio")
|
| 252 |
+
ss["active_site_index"] = idx
|
| 253 |
+
|
| 254 |
+
# Remove site
|
| 255 |
+
if st.sidebar.button("🗑️ Remove Active Site", key="remove_site_btn"):
|
| 256 |
+
if len(ss["site_descriptions"]) <= 1:
|
| 257 |
+
st.sidebar.warning("Cannot remove last site.")
|
| 258 |
+
else:
|
| 259 |
+
ss["site_descriptions"].pop(ss["active_site_index"])
|
| 260 |
+
ss["active_site_index"] = max(0, ss["active_site_index"] - 1)
|
| 261 |
+
safe_rerun()
|
| 262 |
+
|
| 263 |
+
st.sidebar.markdown("---")
|
| 264 |
+
st.sidebar.subheader("Active Site JSON")
|
| 265 |
+
with st.sidebar.expander("Show site JSON", expanded=False):
|
| 266 |
+
st.code(json.dumps(ss["site_descriptions"][ss["active_site_index"]], indent=2), language="json")
|
| 267 |
+
|
| 268 |
+
st.sidebar.markdown("---")
|
| 269 |
+
# Secrets indicator (not blocking)
|
| 270 |
+
st.sidebar.subheader("Service Status")
|
| 271 |
+
col1, col2 = st.sidebar.columns(2)
|
| 272 |
+
col1.markdown("LLM:")
|
| 273 |
+
col2.markdown("✅" if HAVE_GROQ else "⚠️ (no Groq)")
|
| 274 |
+
col1.markdown("Earth Engine:")
|
| 275 |
+
col2.markdown("✅" if EE_READY else "⚠️ (not initialized)")
|
| 276 |
+
st.sidebar.markdown("---")
|
| 277 |
+
# Navigation menu
|
| 278 |
+
pages = ["Landing", "Soil Recognizer", "Soil Classifier", "GSD Curve", "Locator", "GeoMate Ask", "Reports"]
|
| 279 |
+
icons = ["house", "image", "flask", "bar-chart", "geo-alt", "robot", "file-earmark-text"]
|
| 280 |
+
choice = option_menu(None, pages, icons=icons, menu_icon="cast", default_index=pages.index(ss.get("page","Landing")), orientation="vertical", styles={
|
| 281 |
+
"container": {"padding": "0px"},
|
| 282 |
+
"nav-link-selected": {"background-color": "#FF7A00"},
|
| 283 |
+
})
|
| 284 |
+
if choice and choice != ss.get("page"):
|
| 285 |
+
ss["page"] = choice
|
| 286 |
+
safe_rerun()
|
| 287 |
+
|
| 288 |
+
# -------------------------
|
| 289 |
+
# Landing UI
|
| 290 |
+
# -------------------------
|
| 291 |
+
def landing_ui():
|
| 292 |
+
st.markdown(
|
| 293 |
+
"""
|
| 294 |
+
<style>
|
| 295 |
+
.hero {
|
| 296 |
+
background: linear-gradient(135deg,#0f0f0f 0%, #060606 100%);
|
| 297 |
+
border-radius: 14px;
|
| 298 |
+
padding: 20px;
|
| 299 |
+
border: 1px solid rgba(255,122,0,0.08);
|
| 300 |
+
}
|
| 301 |
+
.glow-btn {
|
| 302 |
+
background: linear-gradient(90deg,#ff7a00,#ff3a3a);
|
| 303 |
+
color: white;
|
| 304 |
+
padding: 10px 18px;
|
| 305 |
+
border-radius: 10px;
|
| 306 |
+
font-weight:700;
|
| 307 |
+
box-shadow: 0 6px 24px rgba(255,122,0,0.12);
|
| 308 |
+
border: none;
|
| 309 |
+
}
|
| 310 |
+
</style>
|
| 311 |
+
""", unsafe_allow_html=True)
|
| 312 |
+
st.markdown("<div class='hero'>", unsafe_allow_html=True)
|
| 313 |
+
st.markdown("<h1 style='color:#FF8C00;margin:0'>🌍 GeoMate V2</h1>", unsafe_allow_html=True)
|
| 314 |
+
st.markdown("<p style='color:#ddd'>AI copilot for geotechnical engineering — soil recognition, classification, locator, RAG, professional reports.</p>", unsafe_allow_html=True)
|
| 315 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 316 |
+
st.markdown("---")
|
| 317 |
+
st.write("Quick actions")
|
| 318 |
+
c1, c2, c3 = st.columns(3)
|
| 319 |
+
if c1.button("🖼️ Soil Recognizer"):
|
| 320 |
+
ss["page"] = "Soil Recognizer"; safe_rerun()
|
| 321 |
+
if c2.button("🧪 Soil Classifier"):
|
| 322 |
+
ss["page"] = "Soil Classifier"; safe_rerun()
|
| 323 |
+
if c3.button("📊 GSD Curve"):
|
| 324 |
+
ss["page"] = "GSD Curve"; safe_rerun()
|
| 325 |
+
c4, c5, c6 = st.columns(3)
|
| 326 |
+
if c4.button("🌍 Locator"):
|
| 327 |
+
ss["page"] = "Locator"; safe_rerun()
|
| 328 |
+
if c5.button("🤖 GeoMate Ask"):
|
| 329 |
+
ss["page"] = "GeoMate Ask"; safe_rerun()
|
| 330 |
+
if c6.button("📑 Reports"):
|
| 331 |
+
ss["page"] = "Reports"; safe_rerun()
|
| 332 |
+
|
| 333 |
+
# -------------------------
|
| 334 |
+
# Utility: active site helpers
|
| 335 |
+
# -------------------------
|
| 336 |
+
def active_site() -> Tuple[str, dict]:
|
| 337 |
+
idx = ss.get("active_site_index", 0)
|
| 338 |
+
idx = max(0, min(idx, len(ss["site_descriptions"]) - 1))
|
| 339 |
+
ss["active_site_index"] = idx
|
| 340 |
+
site = ss["site_descriptions"][idx]
|
| 341 |
+
return idx, site
|
| 342 |
+
|
| 343 |
+
def save_site_field(field: str, value: Any):
|
| 344 |
+
idx, site = active_site()
|
| 345 |
+
site[field] = value
|
| 346 |
+
ss["site_descriptions"][idx] = site
|
| 347 |
+
|
| 348 |
+
# -------------------------
|
| 349 |
+
# USCS & AASHTO verbatim logic (function)
|
| 350 |
+
# -------------------------
|
| 351 |
+
# Uses exact logic from your script and mapping of descriptor strings to numbers
|
| 352 |
+
def uscs_aashto_from_inputs(inputs: Dict[str,Any]) -> Tuple[str,str,str,int,Dict[str,str]]:
|
| 353 |
+
"""
|
| 354 |
+
Return: (result_text, uscs_symbol, aashto_symbol, GI, char_summary)
|
| 355 |
+
"""
|
| 356 |
+
# Engineering characteristics dictionary (detailed-ish)
|
| 357 |
+
ENGINEERING_CHARACTERISTICS = {
|
| 358 |
+
"Gravel": {
|
| 359 |
+
"Settlement": "None",
|
| 360 |
+
"Quicksand": "Impossible",
|
| 361 |
+
"Frost-heaving": "None",
|
| 362 |
+
"Groundwater_lowering": "Possible",
|
| 363 |
+
"Cement_grouting": "Possible",
|
| 364 |
+
"Silicate_bitumen_injections": "Unsuitable",
|
| 365 |
+
"Compressed_air": "Possible"
|
| 366 |
+
},
|
| 367 |
+
"Coarse sand": {"Settlement":"None","Quicksand":"Impossible","Frost-heaving":"None"},
|
| 368 |
+
"Medium sand": {"Settlement":"None","Quicksand":"Unlikely"},
|
| 369 |
+
"Fine sand": {"Settlement":"None","Quicksand":"Liable"},
|
| 370 |
+
"Silt": {"Settlement":"Occurs","Quicksand":"Liable","Frost-heaving":"Occurs"},
|
| 371 |
+
"Clay": {"Settlement":"Occurs","Quicksand":"Impossible"}
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
opt = str(inputs.get("opt","n")).lower()
|
| 375 |
+
if opt == 'y':
|
| 376 |
+
uscs = "Pt"
|
| 377 |
+
uscs_expl = "Peat / organic soil — compressible, high organic content; poor engineering properties for load-bearing without special treatment."
|
| 378 |
+
aashto = "Organic (special handling)"
|
| 379 |
+
GI = 0
|
| 380 |
+
result_text = f"According to USCS, the soil is {uscs} — {uscs_expl}\nAccording to AASHTO, the soil is {aashto}."
|
| 381 |
+
return result_text, uscs, aashto, GI, {"summary":"Organic peat: large settlement, low strength."}
|
| 382 |
+
|
| 383 |
+
# read numeric inputs safely
|
| 384 |
+
P2 = float(inputs.get("P2", 0.0))
|
| 385 |
+
P4 = float(inputs.get("P4", 0.0))
|
| 386 |
+
D60 = float(inputs.get("D60", 0.0))
|
| 387 |
+
D30 = float(inputs.get("D30", 0.0))
|
| 388 |
+
D10 = float(inputs.get("D10", 0.0))
|
| 389 |
+
LL = float(inputs.get("LL", 0.0))
|
| 390 |
+
PL = float(inputs.get("PL", 0.0))
|
| 391 |
+
PI = LL - PL
|
| 392 |
+
|
| 393 |
+
Cu = (D60 / D10) if (D10 > 0 and D60 > 0) else 0
|
| 394 |
+
Cc = ((D30 ** 2) / (D10 * D60)) if (D10 > 0 and D30 > 0 and D60 > 0) else 0
|
| 395 |
+
|
| 396 |
+
uscs = "Unknown"; uscs_expl = ""
|
| 397 |
+
if P2 <= 50:
|
| 398 |
+
# Coarse-Grained
|
| 399 |
+
if P4 <= 50:
|
| 400 |
+
# Gravels
|
| 401 |
+
if Cu and Cc:
|
| 402 |
+
if Cu >= 4 and 1 <= Cc <= 3:
|
| 403 |
+
uscs, uscs_expl = "GW", "Well-graded gravel (good engineering properties, high strength, good drainage)."
|
| 404 |
+
else:
|
| 405 |
+
uscs, uscs_expl = "GP", "Poorly-graded gravel (less favorable gradation)."
|
| 406 |
+
else:
|
| 407 |
+
if PI < 4 or PI < 0.73 * (LL - 20):
|
| 408 |
+
uscs, uscs_expl = "GM", "Silty gravel (fines may reduce permeability and strength)."
|
| 409 |
+
elif PI > 7 and PI > 0.73 * (LL - 20):
|
| 410 |
+
uscs, uscs_expl = "GC", "Clayey gravel (higher plasticity)."
|
| 411 |
+
else:
|
| 412 |
+
uscs, uscs_expl = "GM-GC", "Gravel with mixed silt/clay fines."
|
| 413 |
+
else:
|
| 414 |
+
# Sands
|
| 415 |
+
if Cu and Cc:
|
| 416 |
+
if Cu >= 6 and 1 <= Cc <= 3:
|
| 417 |
+
uscs, uscs_expl = "SW", "Well-graded sand (good compaction and drainage)."
|
| 418 |
+
else:
|
| 419 |
+
uscs, uscs_expl = "SP", "Poorly-graded sand (uniform or gap-graded)."
|
| 420 |
+
else:
|
| 421 |
+
if PI < 4 or PI <= 0.73 * (LL - 20):
|
| 422 |
+
uscs, uscs_expl = "SM", "Silty sand (low-plasticity fines)."
|
| 423 |
+
elif PI > 7 and PI > 0.73 * (LL - 20):
|
| 424 |
+
uscs, uscs_expl = "SC", "Clayey sand (clayey fines present)."
|
| 425 |
+
else:
|
| 426 |
+
uscs, uscs_expl = "SM-SC", "Transition between silty sand and clayey sand."
|
| 427 |
+
else:
|
| 428 |
+
# Fine-grained soils
|
| 429 |
+
nDS = int(inputs.get("nDS", 5))
|
| 430 |
+
nDIL = int(inputs.get("nDIL", 6))
|
| 431 |
+
nTG = int(inputs.get("nTG", 6))
|
| 432 |
+
if LL < 50:
|
| 433 |
+
if 20 <= LL < 50 and PI <= 0.73 * (LL - 20):
|
| 434 |
+
if nDS == 1 or nDIL == 3 or nTG == 3:
|
| 435 |
+
uscs, uscs_expl = "ML", "Silt (low plasticity)."
|
| 436 |
+
elif nDS == 3 or nDIL == 3 or nTG == 3:
|
| 437 |
+
uscs, uscs_expl = "OL", "Organic silt (low plasticity)."
|
| 438 |
+
else:
|
| 439 |
+
uscs, uscs_expl = "ML-OL", "Mixed silt/organic silt."
|
| 440 |
+
elif 10 <= LL <= 30 and 4 <= PI <= 7 and PI > 0.72 * (LL - 20):
|
| 441 |
+
if nDS == 1 or nDIL == 1 or nTG == 1:
|
| 442 |
+
uscs, uscs_expl = "ML", "Silt."
|
| 443 |
+
elif nDS == 2 or nDIL == 2 or nTG == 2:
|
| 444 |
+
uscs, uscs_expl = "CL", "Clay (low plasticity)."
|
| 445 |
+
else:
|
| 446 |
+
uscs, uscs_expl = "ML-CL", "Mixed silt/clay."
|
| 447 |
+
else:
|
| 448 |
+
uscs, uscs_expl = "CL", "Clay (low plasticity)."
|
| 449 |
+
else:
|
| 450 |
+
if PI < 0.73 * (LL - 20):
|
| 451 |
+
if nDS == 3 or nDIL == 4 or nTG == 4:
|
| 452 |
+
uscs, uscs_expl = "MH", "Silt (high plasticity)"
|
| 453 |
+
elif nDS == 2 or nDIL == 2 or nTG == 4:
|
| 454 |
+
uscs, uscs_expl = "OH", "Organic silt/clay (high plasticity)"
|
| 455 |
+
else:
|
| 456 |
+
uscs, uscs_expl = "MH-OH", "Mixed high-plasticity silt/organic"
|
| 457 |
+
else:
|
| 458 |
+
uscs, uscs_expl = "CH", "Clay (high plasticity)"
|
| 459 |
+
|
| 460 |
+
# AASHTO logic
|
| 461 |
+
if P2 <= 35:
|
| 462 |
+
if P2 <= 15 and P4 <= 30 and PI <= 6:
|
| 463 |
+
aashto = "A-1-a"
|
| 464 |
+
elif P2 <= 25 and P4 <= 50 and PI <= 6:
|
| 465 |
+
aashto = "A-1-b"
|
| 466 |
+
elif P2 <= 35 and P4 > 0:
|
| 467 |
+
if LL <= 40 and PI <= 10:
|
| 468 |
+
aashto = "A-2-4"
|
| 469 |
+
elif LL >= 41 and PI <= 10:
|
| 470 |
+
aashto = "A-2-5"
|
| 471 |
+
elif LL <= 40 and PI >= 11:
|
| 472 |
+
aashto = "A-2-6"
|
| 473 |
+
elif LL >= 41 and PI >= 11:
|
| 474 |
+
aashto = "A-2-7"
|
| 475 |
+
else:
|
| 476 |
+
aashto = "A-2"
|
| 477 |
+
else:
|
| 478 |
+
aashto = "A-3"
|
| 479 |
+
else:
|
| 480 |
+
if LL <= 40 and PI <= 10:
|
| 481 |
+
aashto = "A-4"
|
| 482 |
+
elif LL >= 41 and PI <= 10:
|
| 483 |
+
aashto = "A-5"
|
| 484 |
+
elif LL <= 40 and PI >= 11:
|
| 485 |
+
aashto = "A-6"
|
| 486 |
+
else:
|
| 487 |
+
aashto = "A-7-5" if PI <= (LL - 30) else "A-7-6"
|
| 488 |
+
|
| 489 |
+
# Group Index (GI)
|
| 490 |
+
a = P2 - 35
|
| 491 |
+
a = 0 if a < 0 else (40 if a > 40 else a)
|
| 492 |
+
b = P2 - 15
|
| 493 |
+
b = 0 if b < 0 else (40 if b > 40 else b)
|
| 494 |
+
c = LL - 40
|
| 495 |
+
c = 0 if c < 0 else (20 if c > 20 else c)
|
| 496 |
+
d = PI - 10
|
| 497 |
+
d = 0 if d < 0 else (20 if d > 20 else d)
|
| 498 |
+
GI = floor(0.2 * a + 0.005 * a * c + 0.01 * b * d)
|
| 499 |
+
|
| 500 |
+
aashto_expl = f"{aashto} (GI = {GI})"
|
| 501 |
+
|
| 502 |
+
# Choose characteristic summary based on USCS family
|
| 503 |
+
char_summary = ENGINEERING_CHARACTERISTICS.get("Silt", {})
|
| 504 |
+
if uscs.startswith(("G", "S")):
|
| 505 |
+
char_summary = ENGINEERING_CHARACTERISTICS.get("Coarse sand", ENGINEERING_CHARACTERISTICS.get("Gravel"))
|
| 506 |
+
if uscs.startswith(("M", "C", "O", "H")):
|
| 507 |
+
char_summary = ENGINEERING_CHARACTERISTICS.get("Silt", {})
|
| 508 |
+
|
| 509 |
+
result_text = f"According to USCS, the soil is **{uscs}** — {uscs_expl}\n\nAccording to AASHTO, the soil is **{aashto_expl}**\n\nEngineering characteristics summary:\n"
|
| 510 |
+
for k, v in char_summary.items():
|
| 511 |
+
result_text += f"- {k}: {v}\n"
|
| 512 |
+
|
| 513 |
+
return result_text, uscs, aashto, GI, char_summary
|
| 514 |
+
|
| 515 |
+
# -------------------------
|
| 516 |
+
# GSD Curve page (separate)
|
| 517 |
+
# -------------------------
|
| 518 |
+
def gsd_curve_ui():
|
| 519 |
+
st.header("📊 Grain Size Distribution (GSD) Curve")
|
| 520 |
+
_, site = active_site()
|
| 521 |
+
st.markdown(f"**Active site:** {site['Site Name']}")
|
| 522 |
+
|
| 523 |
+
st.info("Upload a CSV (two columns: diameter_mm, percent_passing) or enter diameters and % passing manually. I will compute D10, D30, D60 and save them for the active site.")
|
| 524 |
+
|
| 525 |
+
col_up, col_manual = st.col
|