MSU576 commited on
Commit
3a401af
·
verified ·
1 Parent(s): 161739f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +407 -1
app.py CHANGED
@@ -515,4 +515,410 @@ def soil_classifier_ui():
515
  sel = st.selectbox("", DILATANCY_OPTIONS, index=0)
516
  if st.button("Submit dilatancy"):
517
  ans = sel
518
- elif key == "tou
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  sel = st.selectbox("", DILATANCY_OPTIONS, index=0)
516
  if st.button("Submit dilatancy"):
517
  ans = sel
518
+ elif key == "toughness":
519
+ sel = st.selectbox("", TOUGHNESS_OPTIONS, index=0)
520
+ if st.button("Submit toughness"):
521
+ ans = sel
522
+ elif key == "confirm":
523
+ if st.button("Classify now"):
524
+ ans = "go"
525
+
526
+ # process answer if provided
527
+ if ans is not None:
528
+ # save chat turn
529
+ site.setdefault("classifier_chat", []).append(("bot", qtext))
530
+ site["classifier_chat"].append(("user", str(ans)))
531
+ # map answers
532
+ if key == "organic_check":
533
+ site["classifier_inputs"]["opt"] = "y" if ans == "y" else "n"
534
+ site["classifier_state"] += 1
535
+ elif key in ("p2","p4","d60","d30","d10","ll","pl"):
536
+ # number input: get current value from widget
537
+ val = float(st.session_state.get(f"num_{key}", ans))
538
+ site["classifier_inputs"][key.upper() if key in ("p2","p4") else key.upper()] = val
539
+ # normalize keys consistent with uscs function: P2, P4, D60, D30, D10, LL, PL
540
+ # store lower-case keys also
541
+ if key == "p2":
542
+ site["classifier_inputs"]["P2"] = val
543
+ elif key == "p4":
544
+ site["classifier_inputs"]["P4"] = val
545
+ elif key == "d60":
546
+ site["classifier_inputs"]["D60"] = val
547
+ elif key == "d30":
548
+ site["classifier_inputs"]["D30"] = val
549
+ elif key == "d10":
550
+ site["classifier_inputs"]["D10"] = val
551
+ elif key == "ll":
552
+ site["classifier_inputs"]["LL"] = val
553
+ elif key == "pl":
554
+ site["classifier_inputs"]["PL"] = val
555
+ site["classifier_state"] += 1
556
+ elif key == "d_values_known":
557
+ if ans == "yes":
558
+ site["classifier_state"] += 1 # will ask D60 next
559
+ else:
560
+ # redirect user to GSD curve page to upload/plot
561
+ st.success("Please go to GSD Curve page to upload or paste your sieve analysis. After saving D10/D30/D60 there, return here and continue.")
562
+ site["classifier_state"] += 3 # skip D60/D30/D10 questions for now
563
+ elif key == "dry_strength":
564
+ site["classifier_inputs"]["nDS"] = DRY_STRENGTH_MAP.get(ans, 5)
565
+ site["classifier_state"] += 1
566
+ elif key == "dilatancy":
567
+ site["classifier_inputs"]["nDIL"] = DILATANCY_MAP.get(ans, 6)
568
+ site["classifier_state"] += 1
569
+ elif key == "toughness":
570
+ site["classifier_inputs"]["nTG"] = TOUGHNESS_MAP.get(ans, 6)
571
+ site["classifier_state"] += 1
572
+ elif key == "confirm":
573
+ # classification step
574
+ # adapt keys to uscs function names
575
+ inps = {
576
+ "opt": site["classifier_inputs"].get("opt","n"),
577
+ "P2": site["classifier_inputs"].get("P2",0.0),
578
+ "P4": site["classifier_inputs"].get("P4",0.0),
579
+ "D60": site["classifier_inputs"].get("D60",0.0),
580
+ "D30": site["classifier_inputs"].get("D30",0.0),
581
+ "D10": site["classifier_inputs"].get("D10",0.0),
582
+ "LL": site["classifier_inputs"].get("LL",0.0),
583
+ "PL": site["classifier_inputs"].get("PL",0.0),
584
+ "nDS": site["classifier_inputs"].get("nDS",5),
585
+ "nDIL": site["classifier_inputs"].get("nDIL",6),
586
+ "nTG": site["classifier_inputs"].get("nTG",6)
587
+ }
588
+ try:
589
+ res_text, uscs, aashto, GI, expl, chars = uscs_aashto_logic(inps)
590
+ site["USCS"] = uscs
591
+ site["AASHTO"] = aashto
592
+ site["GI"] = GI
593
+ site["classifier_decision_path"] = res_text
594
+ site["classifier_inputs"].update(inps)
595
+ # Save classification to shared dict for quick access
596
+ ss.soil_classifications[site.get("Site Name","Site")] = {
597
+ "USCS": uscs, "AASHTO": aashto, "GI": GI, "inputs": inps, "decision_path": res_text
598
+ }
599
+ st.success("Classification complete. Results saved to site.")
600
+ st.markdown("**Results**")
601
+ st.markdown(res_text)
602
+ except Exception as e:
603
+ st.error(f"Classification failed: {e}")
604
+ site["classifier_state"] = len(questions) # finish
605
+ save_active_site(site)
606
+ else:
607
+ st.markdown("🟢 Classification flow completed. See saved results in the Results tab or site JSON.")
608
+
609
+ # -----------------------------
610
+ # --- GSD Curve UI
611
+ # -----------------------------
612
+ def gsd_curve_ui():
613
+ st.header("📊 Grain Size Distribution (GSD) Curve")
614
+ site = get_active_site()
615
+ if not site:
616
+ st.info("Create/select a site first.")
617
+ return
618
+ st.markdown("You can paste a comma-separated list of sieve diameters (mm) and corresponding % passing, or upload a CSV with two columns: diameter_mm, percent_passing.")
619
+ diameters_text = st.text_area("Diameters (mm) comma-separated (descending preferred)", value=site.get("GSD",{}).get("diameters_str","75,50,37.5,25,19,12.5,9.5,4.75,2,0.85,0.425,0.25,0.18,0.15,0.075"))
620
+ passing_text = st.text_area("% Passing comma-separated", value=site.get("GSD",{}).get("passing_str","100,98,96,90,85,78,72,65,55,45,35,25,18,14,8"))
621
+ uploaded = st.file_uploader("Or upload CSV (diameter_mm,percent_passing)", type=["csv"])
622
+ if uploaded:
623
+ try:
624
+ import pandas as pd
625
+ df = pd.read_csv(uploaded)
626
+ d = df.iloc[:,0].astype(float).tolist()
627
+ p = df.iloc[:,1].astype(float).tolist()
628
+ except Exception as e:
629
+ st.error(f"CSV parse failed: {e}")
630
+ return
631
+ else:
632
+ try:
633
+ d = [float(x.strip()) for x in diameters_text.split(",") if x.strip()]
634
+ p = [float(x.strip()) for x in passing_text.split(",") if x.strip()]
635
+ except Exception as e:
636
+ st.error(f"Parse error: {e}")
637
+ return
638
+ if len(d) != len(p):
639
+ st.error("Diameters and passing values must have same length.")
640
+ return
641
+ try:
642
+ # compute D10,D30,D60
643
+ D10 = interpolate_D_from_gsd(d,p,10.0)
644
+ D30 = interpolate_D_from_gsd(d,p,30.0)
645
+ D60 = interpolate_D_from_gsd(d,p,60.0)
646
+ Cu = D60 / D10 if D10>0 else 0
647
+ Cc = (D30**2) / (D10*D60) if (D10>0 and D60>0) else 0
648
+
649
+ # plot
650
+ fig, ax = plt.subplots(figsize=(6,4))
651
+ ax.semilogx(d, p, marker='o')
652
+ ax.set_xlabel("Particle size (mm) - log scale")
653
+ ax.set_ylabel("% Passing")
654
+ ax.set_title("Grain Size Distribution")
655
+ ax.grid(True, which='both', linestyle='--', linewidth=0.5)
656
+ # annotate D-values
657
+ ax.axhline(10, color='gray', linewidth=0.6, linestyle=':')
658
+ ax.axhline(30, color='gray', linewidth=0.6, linestyle=':')
659
+ ax.axhline(60, color='gray', linewidth=0.6, linestyle=':')
660
+ ax.annotate(f"D10={D10:.4g} mm", xy=(D10,10), xytext=(D10,15))
661
+ ax.annotate(f"D30={D30:.4g} mm", xy=(D30,30), xytext=(D30,40))
662
+ ax.annotate(f"D60={D60:.4g} mm", xy=(D60,60), xytext=(D60,70))
663
+ st.pyplot(fig)
664
+
665
+ if st.button("Save D-values to active site"):
666
+ site["GSD"] = {
667
+ "diameters": d, "passing": p,
668
+ "D10": D10, "D30": D30, "D60": D60,
669
+ "Cu": Cu, "Cc": Cc,
670
+ "diameters_str": diameters_text, "passing_str": passing_text
671
+ }
672
+ # also save D values into classifier inputs (if present)
673
+ site.setdefault("classifier_inputs", {})["D10"] = D10
674
+ site["classifier_inputs"]["D30"] = D30
675
+ site["classifier_inputs"]["D60"] = D60
676
+ save_active_site(site)
677
+ st.success("Saved D-values and GSD to site.")
678
+ st.markdown(f"D10 = {D10:.5g} mm, D30 = {D30:.5g} mm, D60 = {D60:.5g} mm, Cu = {Cu:.3g}, Cc = {Cc:.3g}")
679
+ except Exception as e:
680
+ st.error(f"GSD error: {e}")
681
+
682
+ # -----------------------------
683
+ # --- Locator UI (simple, EE optional)
684
+ # -----------------------------
685
+ def try_initialize_ee():
686
+ # attempt to initialize Earth Engine if secrets present
687
+ global EE_READY
688
+ if not EE_AVAILABLE:
689
+ return False
690
+ try:
691
+ import ee, geemap
692
+ key_json = st.secrets["EE_PRIVATE_KEY"]
693
+ service_account = st.secrets["SERVICE_ACCOUNT"]
694
+ # attempt init using ServiceAccountCredentials
695
+ try:
696
+ creds = ee.ServiceAccountCredentials(service_account, key_json)
697
+ ee.Initialize(creds)
698
+ EE_READY = True
699
+ return True
700
+ except Exception as e:
701
+ # try alternate (if key_json is a filepath)
702
+ try:
703
+ ee.Initialize()
704
+ EE_READY = True
705
+ return True
706
+ except Exception:
707
+ EE_READY = False
708
+ return False
709
+ except Exception:
710
+ EE_READY = False
711
+ return False
712
+
713
+ def locator_ui():
714
+ st.header("🌍 Locator (chatbot + map)")
715
+ site = get_active_site()
716
+ if not site:
717
+ st.info("Create/select a site first.")
718
+ return
719
+ # Initialize EE if possible (lazy)
720
+ if ss.secrets_status.get("ee_key_found", False) and not EE_READY:
721
+ try_initialize_ee()
722
+
723
+ st.markdown("GeoMate: Please provide the site's center coordinates or paste a GeoJSON polygon in the box below.")
724
+ cols = st.columns(2)
725
+ lat = cols[0].number_input("Latitude", value=site.get("lat") or 0.0, format="%.6f")
726
+ lon = cols[1].number_input("Longitude", value=site.get("lon") or 0.0, format="%.6f")
727
+ geojson = st.text_area("Optional: Paste GeoJSON polygon for boundary", value="")
728
+ if st.button("Save coordinates & fetch data"):
729
+ # save coords
730
+ site["lat"] = lat
731
+ site["lon"] = lon
732
+ site["Site Coordinates"] = f"{lat:.6f}, {lon:.6f}"
733
+ save_active_site(site)
734
+ st.success("Coordinates saved to site.")
735
+ # fetch EE data if available
736
+ if EE_READY:
737
+ st.info("Fetching Earth Engine data (this may take a moment)...")
738
+ try:
739
+ import ee, geemap
740
+ # placeholder: set site fields
741
+ site["Soil Profile"] = f"EE soil profile placeholder at {lat},{lon}"
742
+ site["Flood Data"] = f"EE flood summary placeholder (20-year history)"
743
+ site["Seismic Data"] = f"EE seismic summary placeholder"
744
+ site["Topography"] = f"Elevation approx. {int(100+lat)%400} m (EE placeholder)"
745
+ # attempt to get a quick map snapshot via geemap (if supported)
746
+ try:
747
+ m = geemap.Map(center=[lat, lon], zoom=10)
748
+ # add a marker (geemap uses ipyleaflet/folium depending); to avoid complex saving, we'll capture a PNG via to_image if available
749
+ try:
750
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
751
+ m.to_image(outfile=tmp.name)
752
+ site["map_snapshot"] = tmp.name
753
+ except Exception:
754
+ site["map_snapshot"] = None
755
+ except Exception:
756
+ site["map_snapshot"] = None
757
+ save_active_site(site)
758
+ st.success("Earth Engine data saved to site.")
759
+ except Exception as e:
760
+ st.error(f"Earth Engine fetch failed: {e}")
761
+ else:
762
+ # fallback: save simple placeholders and create a simple static map image
763
+ site["Soil Profile"] = f"Sample soil summary at {lat},{lon}"
764
+ site["Flood Data"] = "Flood history: not available (EE not configured)."
765
+ site["Seismic Data"] = "Seismic data: not available (EE not configured)."
766
+ # create a simple static map-like image with a marker using matplotlib
767
+ fig, ax = plt.subplots(figsize=(6,3))
768
+ ax.text(0.5, 0.6, f"Map placeholder\n{site['Site Name']}\n{lat:.6f}, {lon:.6f}", ha='center', va='center', fontsize=12)
769
+ ax.axis("off")
770
+ tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
771
+ fig.savefig(tmp.name, bbox_inches='tight', dpi=150)
772
+ plt.close(fig)
773
+ site["map_snapshot"] = tmp.name
774
+ save_active_site(site)
775
+ st.info("Map snapshot saved (placeholder).")
776
+
777
+ # -----------------------------
778
+ # --- Reports UI & PDF builder
779
+ # -----------------------------
780
+ def build_full_report_pdf(site: Dict[str,Any]) -> bytes:
781
+ """
782
+ Build a professional PDF using ReportLab, returns bytes.
783
+ """
784
+ buf = io.BytesIO()
785
+ doc = SimpleDocTemplate(buf, pagesize=A4, leftMargin=20*mm, rightMargin=20*mm, topMargin=20*mm, bottomMargin=15*mm)
786
+ styles = getSampleStyleSheet()
787
+ title_style = ParagraphStyle("title", parent=styles["Title"], fontSize=20, alignment=1, textColor=colors.HexColor("#FF7A00"))
788
+ h1 = ParagraphStyle("h1", parent=styles["Heading1"], fontSize=14, textColor=colors.HexColor("#1F4E79"))
789
+ body = ParagraphStyle("body", parent=styles["BodyText"], fontSize=10, leading=12)
790
+
791
+ elems = []
792
+ # cover
793
+ elems.append(Spacer(1,12))
794
+ elems.append(Paragraph("GEOTECHNICAL INVESTIGATION REPORT", title_style))
795
+ elems.append(Spacer(1,12))
796
+ info_data = [
797
+ ["Project", site.get("Site Name","-")],
798
+ ["Coordinates", site.get("Site Coordinates","-")],
799
+ ["Date", datetime.today().strftime("%Y-%m-%d")],
800
+ ]
801
+ t = Table(info_data, colWidths=[60*mm, 100*mm])
802
+ t.setStyle(TableStyle([("GRID",(0,0),(-1,-1),0.5,colors.grey)]))
803
+ elems.append(t)
804
+ elems.append(Spacer(1,12))
805
+
806
+ # Summary (dummy narrative assembled from site dict)
807
+ elems.append(Paragraph("SUMMARY", h1))
808
+ summary_lines = []
809
+ summary_lines.append(f"The site '{site.get('Site Name')}' was investigated using GeoMate V2 tools and available data.")
810
+ if site.get("USCS"):
811
+ summary_lines.append(f"Classification: USCS = {site.get('USCS')}, AASHTO = {site.get('AASHTO')} (GI = {site.get('GI')}).")
812
+ if site.get("Load Bearing Capacity"):
813
+ summary_lines.append(f"Reported bearing capacity: {site.get('Load Bearing Capacity')}.")
814
+ if site.get("Soil Profile"):
815
+ summary_lines.append(f"Soil profile summary: {site.get('Soil Profile')}")
816
+ for ln in summary_lines:
817
+ elems.append(Paragraph(ln, body))
818
+ elems.append(Spacer(1,8))
819
+
820
+ # Field & Lab table placeholders
821
+ elems.append(Paragraph("FIELD INVESTIGATION AND LABORATORY TESTING", h1))
822
+ elems.append(Paragraph("Field observations and laboratory test results are summarized below.", body))
823
+ lab_rows = []
824
+ # try to extract some lab-like info from site
825
+ if site.get("GSD"):
826
+ g = site["GSD"]
827
+ lab_rows.append(["GSD D10 (mm)", f"{g.get('D10', '-'):.5g}" if g.get('D10') else "-"])
828
+ lab_rows.append(["GSD D30 (mm)", f"{g.get('D30', '-'):.5g}" if g.get('D30') else "-"])
829
+ lab_rows.append(["GSD D60 (mm)", f"{g.get('D60', '-'):.5g}" if g.get('D60') else "-"])
830
+ # create table
831
+ if lab_rows:
832
+ t2 = Table([["Test","Value"]] + lab_rows, colWidths=[70*mm,80*mm])
833
+ t2.setStyle(TableStyle([("GRID",(0,0),(-1,-1),0.5,colors.grey),("BACKGROUND",(0,0),(-1,0),colors.HexColor("#1F4E79")),("TEXTCOLOR",(0,0),(-1,0),colors.white)]))
834
+ elems.append(t2)
835
+ elems.append(Spacer(1,8))
836
+
837
+ # Earth Engine section
838
+ elems.append(Paragraph("EARTH ENGINE / SITE DATA", h1))
839
+ elems.append(Paragraph(f"Soil profile: {site.get('Soil Profile','Not available')}", body))
840
+ elems.append(Paragraph(f"Flood data: {site.get('Flood Data','Not available')}", body))
841
+ elems.append(Paragraph(f"Seismic data: {site.get('Seismic Data','Not available')}", body))
842
+ elems.append(Paragraph(f"Topography: {site.get('Topography','Not available')}", body))
843
+ elems.append(Spacer(1,8))
844
+
845
+ # Insert map snapshot if present
846
+ if site.get("map_snapshot"):
847
+ try:
848
+ elems.append(Paragraph("Site Map", h1))
849
+ from reportlab.platypus import Image as RLImage
850
+ img_path = site["map_snapshot"]
851
+ im = RLImage(img_path, width=160*mm, height=90*mm)
852
+ elems.append(im)
853
+ elems.append(Spacer(1,8))
854
+ except Exception:
855
+ pass
856
+
857
+ # Evaluation & Recommendations (dummy, to be replaced by LLM if available)
858
+ elems.append(Paragraph("EVALUATION OF GEOTECHNICAL PROPERTIES", h1))
859
+ eval_text = site.get("classifier_decision_path","Detailed classification not available.")
860
+ elems.append(Paragraph(eval_text, body))
861
+ elems.append(Paragraph("RECOMMENDATIONS", h1))
862
+ recs = [
863
+ "Remove unconsolidated fill beneath foundation footprints.",
864
+ "Provide positive drainage away from structures.",
865
+ "Consider shallow foundations where competent soil is available at shallow depth; otherwise consider piled solutions."
866
+ ]
867
+ for r in recs:
868
+ elems.append(Paragraph(f"- {r}", body))
869
+
870
+ elems.append(PageBreak())
871
+ # appendices placeholder
872
+ elems.append(Paragraph("APPENDICES", h1))
873
+ elems.append(Paragraph("Borehole logs, laboratory tables and GSD curves are included in the appendices when available.", body))
874
+
875
+ doc.build(elems)
876
+ pdf_bytes = buf.getvalue()
877
+ buf.close()
878
+ return pdf_bytes
879
+
880
+ def reports_ui():
881
+ st.header("📑 Reports")
882
+ site = get_active_site()
883
+ if not site:
884
+ st.info("Create/select a site first.")
885
+ return
886
+
887
+ st.markdown("GeoMate will ask a few final questions (chat-style) before generating the full geotechnical report.")
888
+ # simple chat-driven data collection
889
+ questions = [
890
+ ("Load Bearing Capacity","Please provide the design bearing capacity (e.g., '200 kPa' or '2000 psf')."),
891
+ ("Skin Shear Strength","What is the skin shear strength (if known)?"),
892
+ ("Relative Compaction","What relative % compaction should be used for imported fill? (e.g., 95%)"),
893
+ ("Rate of Consolidation","Provide the approximate consolidation rate (if known)."),
894
+ ("Nature of Construction","Describe nature of construction (Residential, Commercial, Road, Dam, etc.)")
895
+ ]
896
+ state = site.get("report_convo_state",0)
897
+ # display chat bubbles
898
+ if "report_chat" not in site:
899
+ site["report_chat"] = []
900
+ for sender, msg in site["report_chat"]:
901
+ if sender == "bot":
902
+ st.markdown(f"<div style='background:#111;padding:8px;border-radius:8px;color:#FF8C00;'>🤖 {msg}</div>", unsafe_allow_html=True)
903
+ else:
904
+ st.markdown(f"<div style='background:#0b0b0b;padding:8px;border-radius:8px;color:#9fd3ff;text-align:right'>👤 {msg}</div>", unsafe_allow_html=True)
905
+
906
+ if state < len(questions):
907
+ key, qtext = questions[state]
908
+ st.markdown(f"**GeoMate:** {qtext}")
909
+ ans = st.text_input("", key=f"report_input_{state}")
910
+ if st.button("Submit answer", key=f"report_submit_{state}"):
911
+ # save if not skip/dk
912
+ if ans.strip().lower() not in ["", "don't know", "dk", "skip", "n/a"]:
913
+ site[key] = ans.strip()
914
+ site.setdefault("report_chat", []).append(("bot", qtext))
915
+ site["report_chat"].append(("user", ans.strip()))
916
+ site["report_convo_state"] = state + 1
917
+ save_active_site(site)
918
+ st.experimental_rerun()
919
+ else:
920
+ st.success("All questions complete for report generation.")
921
+ if st.button("Generate Full Geotechnical Report (PDF)"):
922
+ with st.spinner("Building PDF..."):
923
+ pdf_bytes = build_full_report_pdf(site)
924
+ st.download_button("Download Report PDF", data=pdf_bytes, file_name=f"GeoMate_FullReport_{site.get('Site Name','site')}.pdf", mime="application/pdf")