MSU576 commited on
Commit
ee779b9
Β·
verified Β·
1 Parent(s): 3a05c7a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +349 -1
app.py CHANGED
@@ -522,4 +522,352 @@ def gsd_curve_ui():
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.columns([1,1])
526
+ uploaded = None
527
+ sieve = None; passing = None
528
+ with col_up:
529
+ uploaded = st.file_uploader("Upload CSV (diameter_mm, percent_passing)", type=["csv","txt"], key=f"gsd_upload_{site['Site Name']}")
530
+ if uploaded:
531
+ try:
532
+ df = pd.read_csv(uploaded)
533
+ # heuristic: first numeric column = diameter, second numeric = percent passing
534
+ numeric_cols = [c for c in df.columns if np.issubdtype(df[c].dtype, np.number)]
535
+ if len(numeric_cols) >= 2:
536
+ sieve = df[numeric_cols[0]].values.astype(float)
537
+ passing = df[numeric_cols[1]].values.astype(float)
538
+ else:
539
+ # try to coerce
540
+ sieve = df.iloc[:,0].astype(float).values
541
+ passing = df.iloc[:,1].astype(float).values
542
+ st.success("CSV loaded.")
543
+ except Exception as e:
544
+ st.error(f"Error reading CSV: {e}")
545
+ sieve = passing = None
546
+
547
+ with col_manual:
548
+ diam_text = st.text_area("Diameters (mm) comma-separated (e.g. 75,50,37.5,...)", key=f"gsd_diams_{site['Site Name']}")
549
+ pass_text = st.text_area("% Passing comma-separated (same order)", key=f"gsd_pass_{site['Site Name']}")
550
+ if diam_text.strip() and pass_text.strip():
551
+ try:
552
+ sieve = np.array([float(x.strip()) for x in diam_text.split(",") if x.strip()])
553
+ passing = np.array([float(x.strip()) for x in pass_text.split(",") if x.strip()])
554
+ except Exception as e:
555
+ st.error(f"Invalid manual input: {e}")
556
+ sieve = passing = None
557
+
558
+ if sieve is None or passing is None:
559
+ st.info("Provide GSD data above to compute D-values.")
560
+ return
561
+
562
+ # Sort descending by sieve
563
+ order = np.argsort(-sieve)
564
+ sieve = sieve[order]; passing = passing[order]
565
+ # Ensure percent is in 0-100
566
+ if np.any(passing < 0) or np.any(passing > 100):
567
+ st.warning("Some % passing values are outside 0-100. Please verify.")
568
+ # Interpolate percent -> diameter (need percent increasing)
569
+ percent = passing.copy()
570
+ if not np.all(np.diff(percent) >= 0):
571
+ percent = percent[::-1]; sieve = sieve[::-1]
572
+ # Ensure arrays are floats
573
+ percent = percent.astype(float); sieve = sieve.astype(float)
574
+ # interpolation function (percent -> diameter)
575
+ def interp_d(pct: float) -> Optional[float]:
576
+ try:
577
+ # xp must be increasing
578
+ return float(np.interp(pct, percent, sieve))
579
+ except Exception:
580
+ return None
581
+ D10 = interp_d(10.0); D30 = interp_d(30.0); D60 = interp_d(60.0)
582
+ Cu = (D60 / D10) if (D10 and D60 and D10>0) else None
583
+ Cc = (D30**2)/(D10*D60) if (D10 and D30 and D60 and D10>0) else None
584
+
585
+ # Plot
586
+ fig, ax = plt.subplots(figsize=(7,4))
587
+ ax.plot(sieve, passing, marker='o', label="% Passing")
588
+ ax.set_xscale('log')
589
+ ax.invert_xaxis()
590
+ ax.set_xlabel("Particle diameter (mm) β€” log scale")
591
+ ax.set_ylabel("% Passing")
592
+ if D10: ax.axvline(D10, color='orange', linestyle='--', label=f"D10={D10:.4g} mm")
593
+ if D30: ax.axvline(D30, color='red', linestyle='--', label=f"D30={D30:.4g} mm")
594
+ if D60: ax.axvline(D60, color='blue', linestyle='--', label=f"D60={D60:.4g} mm")
595
+ ax.grid(True, which='both', linestyle='--', linewidth=0.4)
596
+ ax.legend()
597
+ st.pyplot(fig)
598
+
599
+ # Save to active site
600
+ idx, sdict = active_site()
601
+ sdict["GSD"] = {
602
+ "sieve_mm": sieve.tolist(),
603
+ "percent_passing": passing.tolist(),
604
+ "D10": float(D10) if D10 is not None else None,
605
+ "D30": float(D30) if D30 is not None else None,
606
+ "D60": float(D60) if D60 is not None else None,
607
+ "Cu": float(Cu) if Cu is not None else None,
608
+ "Cc": float(Cc) if Cc is not None else None
609
+ }
610
+ ss["site_descriptions"][idx] = sdict
611
+ st.success(f"Saved GSD to site: D10={D10}, D30={D30}, D60={D60}")
612
+ if st.button("Copy D-values to Soil Classifier inputs (for this site)", key=f"copy_d_to_cls_{sdict['Site Name']}"):
613
+ ss.setdefault("classifier_states", {})
614
+ ss["classifier_states"].setdefault(sdict["Site Name"], {"step":0, "inputs":{}})
615
+ ss["classifier_states"][sdict["Site Name"]]["inputs"]["D10"] = float(D10) if D10 is not None else 0.0
616
+ ss["classifier_states"][sdict["Site Name"]]["inputs"]["D30"] = float(D30) if D30 is not None else 0.0
617
+ ss["classifier_states"][sdict["Site Name"]]["inputs"]["D60"] = float(D60) if D60 is not None else 0.0
618
+ st.info("Copied. Go to Soil Classifier page and continue.")
619
+ st.markdown("---")
620
+ st.caption("Tip: If your classifier asks for D-values and you don't have them, compute them here and copy them back.")
621
+
622
+ # -------------------------
623
+ # Soil Classifier (chatbot-style; smart input flow)
624
+ # -------------------------
625
+ def soil_classifier_ui():
626
+ st.header("πŸ§ͺ Soil Classifier β€” Chatbot-style (smart inputs)")
627
+ idx, sdict = active_site()
628
+ site_name = sdict["Site Name"]
629
+
630
+ # Prepare classifier state container per-site
631
+ ss.setdefault("classifier_states", {})
632
+ state = ss["classifier_states"].setdefault(site_name, {"step":0, "inputs":{}})
633
+
634
+ # Pre-populate inputs from site GSD if present
635
+ if sdict.get("GSD"):
636
+ g = sdict["GSD"]
637
+ state["inputs"].setdefault("D10", g.get("D10", 0.0))
638
+ state["inputs"].setdefault("D30", g.get("D30", 0.0))
639
+ state["inputs"].setdefault("D60", g.get("D60", 0.0))
640
+
641
+ # Dropdown exact strings mapping per your requested table
642
+ dil_options = [
643
+ "1. Quick to slow",
644
+ "2. None to very slow",
645
+ "3. Slow",
646
+ "4. Slow to none",
647
+ "5. None",
648
+ "6. Null?"
649
+ ]
650
+ tough_options = [
651
+ "1. None",
652
+ "2. Medium",
653
+ "3. Slight?",
654
+ "4. Slight to Medium?",
655
+ "5. High",
656
+ "6. Null?"
657
+ ]
658
+ dry_options = [
659
+ "1. None to slight",
660
+ "2. Medium to high",
661
+ "3. Slight to Medium",
662
+ "4. High to very high",
663
+ "5. Null?"
664
+ ]
665
+
666
+ # Mode selector: Both / USCS only / AASHTO only
667
+ if "classifier_mode" not in ss:
668
+ ss["classifier_mode"] = {}
669
+ ss["classifier_mode"].setdefault(site_name, "Both")
670
+ mode = st.selectbox("Classification mode", ["Both", "USCS only", "AASHTO only"], index=["Both","USCS only","AASHTO only"].index(ss["classifier_mode"][site_name]), key=f"mode_{site_name}")
671
+ ss["classifier_mode"][site_name] = mode
672
+
673
+ # Step machine:
674
+ step = state.get("step", 0)
675
+ inputs = state.setdefault("inputs", {})
676
+
677
+ def goto(n):
678
+ state["step"] = n
679
+ ss["classifier_states"][site_name] = state
680
+ safe_rerun()
681
+
682
+ def save_input(key, value):
683
+ inputs[key] = value
684
+ state["inputs"] = inputs
685
+ ss["classifier_states"][site_name] = state
686
+
687
+ # Chat style UI: show recent conversation history as simple list
688
+ st.markdown(f"**Active site:** {site_name}")
689
+ st.markdown("---")
690
+
691
+ if step == 0:
692
+ st.markdown("**GeoMate:** Hello! I'm the Soil Classifier bot. Shall we begin classification for this site?")
693
+ c1, c2 = st.columns(2)
694
+ if c1.button("Yes β€” Start", key=f"cls_start_{site_name}"):
695
+ goto(1)
696
+ if c2.button("Cancel", key=f"cls_cancel_{site_name}"):
697
+ st.info("Classifier cancelled.")
698
+
699
+ elif step == 1:
700
+ st.markdown("**GeoMate:** Is the soil organic (contains high organic matter, spongy, odour)?")
701
+ c1, c2 = st.columns(2)
702
+ if c1.button("No (inorganic)", key=f"org_no_{site_name}"):
703
+ save_input("opt", "n"); goto(2)
704
+ if c2.button("Yes (organic)", key=f"org_yes_{site_name}"):
705
+ save_input("opt", "y"); goto(12) # skip to final classification for organic
706
+
707
+ elif step == 2:
708
+ st.markdown("**GeoMate:** What is the percentage passing the #200 sieve (0.075 mm)?")
709
+ val = st.number_input("Percentage passing #200 (P2)", value=float(inputs.get("P2",0.0)), min_value=0.0, max_value=100.0, key=f"P2_input_{site_name}", format="%.2f")
710
+ c1, c2, c3 = st.columns([1,1,1])
711
+ if c1.button("Next", key=f"P2_next_{site_name}"):
712
+ save_input("P2", float(val)); goto(3)
713
+ if c2.button("Skip", key=f"P2_skip_{site_name}"):
714
+ save_input("P2", 0.0); goto(3)
715
+ if c3.button("Back", key=f"P2_back_{site_name}"):
716
+ goto(0)
717
+
718
+ elif step == 3:
719
+ # decide path based on P2
720
+ P2 = float(inputs.get("P2", 0.0))
721
+ if P2 > 50:
722
+ st.markdown("**GeoMate:** P2 > 50 β€” fine-grained soil path selected.")
723
+ if st.button("Continue (fine-grained)", key=f"cont_fine_{site_name}"):
724
+ goto(4)
725
+ else:
726
+ st.markdown("**GeoMate:** P2 <= 50 β€” coarse-grained soil path selected.")
727
+ if st.button("Continue (coarse-grained)", key=f"cont_coarse_{site_name}"):
728
+ goto(6)
729
+
730
+ # Fine-grained branch
731
+ elif step == 4:
732
+ st.markdown("**GeoMate:** Enter Liquid Limit (LL).")
733
+ val = st.number_input("Liquid Limit (LL)", value=float(inputs.get("LL",0.0)), min_value=0.0, max_value=200.0, key=f"LL_{site_name}", format="%.2f")
734
+ col1, col2 = st.columns(2)
735
+ if col1.button("Next", key=f"LL_next_{site_name}"):
736
+ save_input("LL", float(val)); goto(5)
737
+ if col2.button("Back", key=f"LL_back_{site_name}"):
738
+ goto(3)
739
+
740
+ elif step == 5:
741
+ st.markdown("**GeoMate:** Enter Plastic Limit (PL).")
742
+ val = st.number_input("Plastic Limit (PL)", value=float(inputs.get("PL",0.0)), min_value=0.0, max_value=200.0, key=f"PL_{site_name}", format="%.2f")
743
+ col1, col2 = st.columns(2)
744
+ if col1.button("Next", key=f"PL_next_{site_name}"):
745
+ save_input("PL", float(val)); goto(11) # go to descriptors step
746
+ if col2.button("Back", key=f"PL_back_{site_name}"):
747
+ goto(4)
748
+
749
+ # Coarse branch: ask % passing #4 and D-values option
750
+ elif step == 6:
751
+ st.markdown("**GeoMate:** What is the % passing sieve no. 4 (4.75 mm)?")
752
+ val = st.number_input("% passing #4 (P4)", value=float(inputs.get("P4",0.0)), min_value=0.0, max_value=100.0, key=f"P4_{site_name}", format="%.2f")
753
+ c1, c2, c3 = st.columns([1,1,1])
754
+ if c1.button("Next", key=f"P4_next_{site_name}"):
755
+ save_input("P4", float(val)); goto(7)
756
+ if c2.button("Compute D-values (GSD page)", key=f"P4_gsd_{site_name}"):
757
+ st.info("Please use GSD Curve page to compute D-values and then copy them back to classifier.")
758
+ if c3.button("Back", key=f"P4_back_{site_name}"):
759
+ goto(3)
760
+
761
+ elif step == 7:
762
+ st.markdown("**GeoMate:** Do you know D60, D30, D10 diameters (mm)?")
763
+ c1, c2, c3 = st.columns([1,1,1])
764
+ if c1.button("Yes β€” enter values", key=f"dvals_yes_{site_name}"):
765
+ goto(8)
766
+ if c2.button("No β€” compute from GSD", key=f"dvals_no_{site_name}"):
767
+ st.info("Use the GSD Curve page and then click 'Copy D-values to Soil Classifier' there.")
768
+ if c3.button("Skip", key=f"dvals_skip_{site_name}"):
769
+ save_input("D60", 0.0); save_input("D30", 0.0); save_input("D10", 0.0); goto(11)
770
+
771
+ elif step == 8:
772
+ st.markdown("**GeoMate:** Enter D60 (mm)")
773
+ val = st.number_input("D60 (mm)", value=float(inputs.get("D60",0.0)), min_value=0.0, key=f"D60_{site_name}")
774
+ if st.button("Next", key=f"D60_next_{site_name}"):
775
+ save_input("D60", float(val)); goto(9)
776
+ if st.button("Back", key=f"D60_back_{site_name}"):
777
+ goto(7)
778
+
779
+ elif step == 9:
780
+ st.markdown("**GeoMate:** Enter D30 (mm)")
781
+ val = st.number_input("D30 (mm)", value=float(inputs.get("D30",0.0)), min_value=0.0, key=f"D30_{site_name}")
782
+ if st.button("Next", key=f"D30_next_{site_name}"):
783
+ save_input("D30", float(val)); goto(10)
784
+ if st.button("Back", key=f"D30_back_{site_name}"):
785
+ goto(8)
786
+
787
+ elif step == 10:
788
+ st.markdown("**GeoMate:** Enter D10 (mm)")
789
+ val = st.number_input("D10 (mm)", value=float(inputs.get("D10",0.0)), min_value=0.0, key=f"D10_{site_name}")
790
+ if st.button("Next", key=f"D10_next_{site_name}"):
791
+ save_input("D10", float(val)); goto(11)
792
+ if st.button("Back", key=f"D10_back_{site_name}"):
793
+ goto(9)
794
+
795
+ # descriptors (for fine soils) and finishing
796
+ elif step == 11:
797
+ st.markdown("**GeoMate:** Fine soil descriptors (if applicable). You can skip.")
798
+ sel_dry = st.selectbox("Dry strength", dry_options, index=min(2, len(dry_options)-1), key=f"dry_{site_name}")
799
+ sel_dil = st.selectbox("Dilatancy", dil_options, index=0, key=f"dil_{site_name}")
800
+ sel_tg = st.selectbox("Toughness", tough_options, index=0, key=f"tough_{site_name}")
801
+ col1, col2, col3 = st.columns([1,1,1])
802
+ # map to numeric - map strings to numbers i+1
803
+ dry_map = {dry_options[i]: i+1 for i in range(len(dry_options))}
804
+ dil_map = {dil_options[i]: i+1 for i in range(len(dil_options))}
805
+ tough_map = {tough_options[i]: i+1 for i in range(len(tough_options))}
806
+ if col1.button("Save & Continue", key=f"desc_save_{site_name}"):
807
+ save_input("nDS", dry_map.get(sel_dry, 5))
808
+ save_input("nDIL", dil_map.get(sel_dil, 6))
809
+ save_input("nTG", tough_map.get(sel_tg, 6))
810
+ goto(12)
811
+ if col2.button("Skip descriptors", key=f"desc_skip_{site_name}"):
812
+ save_input("nDS", 5); save_input("nDIL", 6); save_input("nTG", 6); goto(12)
813
+ if col3.button("Back", key=f"desc_back_{site_name}"):
814
+ # go to previous appropriate step
815
+ # if LL in inputs, go to LL/PL steps else go to coarse branch
816
+ if inputs.get("LL") is not None and inputs.get("PL") is not None:
817
+ goto(5)
818
+ else:
819
+ goto(6)
820
+
821
+ elif step == 12:
822
+ st.markdown("**GeoMate:** Ready to classify. Review inputs and press **Classify**.")
823
+ st.json(inputs)
824
+ col1, col2 = st.columns([1,1])
825
+ if col1.button("Classify", key=f"classify_now_{site_name}"):
826
+ try:
827
+ res_text, uscs_sym, aashto_sym, GI, char_summary = uscs_aashto_from_inputs(inputs)
828
+ # Save to site dict
829
+ sdict["USCS"] = uscs_sym
830
+ sdict["AASHTO"] = aashto_sym
831
+ sdict["GI"] = GI
832
+ sdict["classifier_inputs"] = inputs
833
+ sdict["classifier_decision_path"] = res_text
834
+ # persist
835
+ ss["site_descriptions"][ss["active_site_index"]] = sdict
836
+ st.success("Classification complete and saved.")
837
+ st.markdown("### Result")
838
+ if mode == "USCS only":
839
+ st.markdown(f"**USCS:** {uscs_sym}")
840
+ elif mode == "AASHTO only":
841
+ st.markdown(f"**AASHTO:** {aashto_sym} (GI={GI})")
842
+ else:
843
+ st.markdown(res_text)
844
+ # offer PDF export
845
+ if FPDF_OK and st.button("Export Classification PDF", key=f"exp_cls_pdf_{site_name}"):
846
+ pdf = FPDF()
847
+ pdf.add_page()
848
+ pdf.set_font("Arial", "B", 14)
849
+ pdf.cell(0, 8, f"GeoMate Classification β€” {site_name}", ln=1)
850
+ pdf.ln(4)
851
+ pdf.set_font("Arial", "", 11)
852
+ pdf.multi_cell(0, 7, res_text)
853
+ pdf.ln(4)
854
+ pdf.set_font("Arial", "B", 12)
855
+ pdf.cell(0, 6, "Inputs:", ln=1)
856
+ pdf.set_font("Arial", "", 10)
857
+ for k, v in inputs.items():
858
+ pdf.cell(0,5, f"{k}: {v}", ln=1)
859
+ fn = f"{site_name.replace(' ','_')}_classification.pdf"
860
+ pdf.output(fn)
861
+ with open(fn,"rb") as f:
862
+ st.download_button("Download PDF", f, file_name=fn, mime="application/pdf")
863
+ # done
864
+ except Exception as e:
865
+ st.error(f"Classification failed: {e}\n{traceback.format_exc()}")
866
+ if col2.button("Back to edit", key=f"back_edit_{site_name}"):
867
+ goto(11)
868
+
869
+ else:
870
+ st.warning("Classifier state reset.")
871
+ state["step"] = 0
872
+ ss["classifier_states"][site_name] = state
873
+