Update app.py
Browse files
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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|