Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![CI](https://github.com/harsh-pandhe/Autonomous-Ropeway-Alignment/actions/workflows/ci.yml/badge.svg)](https://github.com/harsh-pandhe/Autonomous-Ropeway-Alignment/actions/workflows/ci.yml)
[![Python](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Tests](https://img.shields.io/badge/tests-415%20passing-brightgreen.svg)](#testing)
[![Tests](https://img.shields.io/badge/tests-500%20passing-brightgreen.svg)](#testing)
[![Validation](https://img.shields.io/badge/textbook%20validation-0.00%25%20error-brightgreen.svg)](src/ropeway/validation.py)

Open-source toolchain that ingests a free DEM and two station coordinates, then
Expand Down
186 changes: 185 additions & 1 deletion app/streamlit_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,9 @@ def _load_terrain():
# --------------------------------------------------------------------------
# Tabs
# --------------------------------------------------------------------------
tab_opt, tab_3d, tab_proj = st.tabs(["Optimize", "3-D Digital Twin", "Projects"])
tab_demo, tab_opt, tab_3d, tab_proj = st.tabs(
["Demo", "Optimize", "3-D Digital Twin", "Projects"]
)

profile = st.session_state["profile"]
patch = st.session_state["patch"]
Expand All @@ -249,6 +251,188 @@ def _load_terrain():
history_best, history_avg = st.session_state["history"]


# ---- Tab 0: Demo (civil-engineer-friendly entry point) ----
with tab_demo:
st.markdown(
"## Two coordinates. Sixty seconds. A permit-grade alignment."
)
st.markdown(
"Pick a real ropeway preset to compare against the as-built record, "
"or click two points on the map and run your own corridor."
)

DEMO_PRESETS = {
"Aiguille du Midi (Stage 2) — jig-back, French Alps":
dict(start=(6.8700, 45.8920), end=(6.8870, 45.9160),
system="jigback", note="World-record 2 867 m span; 0.6 % delta."),
"Cablebús Línea 2 (CDMX) — urban MGD":
dict(start=(-99.04320, 19.34600), end=(-99.00240, 19.36450),
system="mgd", note="Longest urban gondola at opening (2021)."),
"Whistler Peak 2 Peak — 3S, Canada":
dict(start=(-122.9530, 50.0670), end=(-122.9170, 50.0590),
system="3s", note="3 024 m unsupported span."),
"Funitel de Péclet (Val Thorens) — funitel":
dict(start=(6.5798, 45.3017), end=(6.5980, 45.3110),
system="funitel", note="World's first funitel, 611 m rise."),
"Roosevelt Island Tramway (NYC) — urban jig-back":
dict(start=(-73.9580, 40.7570), end=(-73.9495, 40.7610),
system="jigback", note="Queensboro Bridge fly-over."),
"Mexico City L2 + Caracas (new) — try a corridor":
dict(start=(-99.04, 19.34), end=(-99.00, 19.37),
system="mgd", note="Click two map points to override."),
}
preset_name = st.selectbox(
"Preset corridor", list(DEMO_PRESETS.keys()), key="demo_preset"
)
preset = DEMO_PRESETS[preset_name]
st.caption(preset["note"])

# Map picker (best-effort; falls back to number inputs if folium fails).
try:
import folium
from streamlit_folium import st_folium
mid_lon = 0.5 * (preset["start"][0] + preset["end"][0])
mid_lat = 0.5 * (preset["start"][1] + preset["end"][1])
m = folium.Map(location=[mid_lat, mid_lon], zoom_start=13, tiles="OpenStreetMap")
folium.Marker(
[preset["start"][1], preset["start"][0]], tooltip="Lower terminal",
icon=folium.Icon(color="green"),
).add_to(m)
folium.Marker(
[preset["end"][1], preset["end"][0]], tooltip="Upper terminal",
icon=folium.Icon(color="red"),
).add_to(m)
folium.PolyLine(
[(preset["start"][1], preset["start"][0]),
(preset["end"][1], preset["end"][0])],
color="#3949ab", weight=4,
).add_to(m)
st_folium(m, height=400, width=None, key="demo_map")
st.caption(
"Map shows the preset corridor. Adjust the lat/lon below to override."
)
except Exception as map_exc:
st.caption(f"Map widget unavailable ({map_exc}); use the number inputs below.")

c1, c2 = st.columns(2)
with c1:
demo_start_lon = st.number_input("Lower lon", value=float(preset["start"][0]),
format="%.5f", key="demo_lo_lon")
demo_start_lat = st.number_input("Lower lat", value=float(preset["start"][1]),
format="%.5f", key="demo_lo_lat")
with c2:
demo_end_lon = st.number_input("Upper lon", value=float(preset["end"][0]),
format="%.5f", key="demo_up_lon")
demo_end_lat = st.number_input("Upper lat", value=float(preset["end"][1]),
format="%.5f", key="demo_up_lat")

demo_system = st.selectbox(
"System type",
["jigback", "mgd", "bgd", "3s", "chair", "funitel"],
index=["jigback", "mgd", "bgd", "3s", "chair", "funitel"].index(preset["system"]),
key="demo_system",
)

demo_run = st.button("Run optimization on this corridor",
type="primary", key="demo_run_btn", width="stretch")

if demo_run:
from ropeway.dem import ensure_dem_tile
from ropeway.multi_rope import RopewaySystemType, system_defaults

st.session_state["console_log"] = []
log_box = st.empty()
progress = st.progress(0.0, text="Fetching DEM tile...")

try:
tile_path = ensure_dem_tile(
demo_start_lon, demo_start_lat,
cache_dir="data/dem",
)
progress.progress(0.25, text=f"DEM ready: {tile_path.name}")
except Exception as exc:
st.error(f"DEM fetch failed: {exc}")
st.stop()

try:
profile = extract_profile_from_dem(
tile_path,
(demo_start_lon, demo_start_lat),
(demo_end_lon, demo_end_lat),
sample_spacing_m=15.0,
)
patch = extract_corridor_patch_from_dem(
tile_path,
(demo_start_lon, demo_start_lat),
(demo_end_lon, demo_end_lat),
cross_half_width_m=200.0,
)
progress.progress(0.45, text=f"Terrain loaded — {profile.total_length:.0f} m corridor")
except Exception as exc:
st.error(f"Profile extraction failed: {exc}")
st.stop()

sys_enum = {
"jigback": RopewaySystemType.JIG_BACK,
"mgd": RopewaySystemType.MGD,
"bgd": RopewaySystemType.BGD,
"3s": RopewaySystemType.TGD_3S,
"chair": RopewaySystemType.CHAIRLIFT,
"funitel": RopewaySystemType.FUNITEL,
}[demo_system]
cfg_demo = system_defaults(sys_enum)
ga_demo = GAConfig(
generations=60, population_size=60, seed=2026,
max_intermediate_towers=12,
)

progress.progress(0.55, text="Running GA optimizer...")
result = optimize(
profile.as_function(), profile.total_length,
cfg=cfg_demo, ga=ga_demo, verbose=False,
)
progress.progress(0.95, text="Rendering results...")

align = result.best_alignment
eval_res_demo = result.best_result
rep = eval_res_demo.report

# Stash in session state so the Optimize / 3-D tabs can pick it up.
st.session_state["profile"] = profile
st.session_state["patch"] = patch
st.session_state["cfg"] = cfg_demo
st.session_state["alignment"] = align
st.session_state["eval"] = eval_res_demo
st.session_state["history"] = (result.history_best, result.history_avg)

progress.progress(1.0, text="Done.")

st.success(
f"Feasible: **{eval_res_demo.feasible}** · "
f"intermediate towers: **{max(0, len(align.towers) - 2)}** · "
f"cable length: **{rep.total_cable_length_m:.0f} m** · "
f"min clearance: **{rep.min_clearance_m:.2f} m** · "
f"max tension: **{rep.max_tension_n/1e3:.1f} kN**"
)

from ropeway.viz import plot_alignment, plot_convergence
c1, c2 = st.columns(2)
with c1:
st.markdown("**Alignment (plan + elevation)**")
fig_a, _ = plot_alignment(profile, align, segments=eval_res_demo.segments,
title=preset_name)
st.pyplot(fig_a, width="stretch")
with c2:
st.markdown("**GA convergence**")
fig_c, _ = plot_convergence(result.history_best, result.history_avg)
st.pyplot(fig_c, width="stretch")

st.caption(
"Open the **3-D Digital Twin** tab above for the interactive view, "
"or the **Optimize** tab for downloads (DXF, LandXML, PDF, BoM, GeoJSON)."
)


# ---- Tab 1: Optimize ----
with tab_opt:
rep = eval_res.report
Expand Down
96 changes: 96 additions & 0 deletions docs/DEMO_RUNBOOK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Demo runbook (internal)

> What to click, what to say, what to do if it crashes. Keep this open
> on a second screen during the demo.

## 30 minutes before

```bash
# 1. Make sure main is current.
git checkout main && git pull --ff-only

# 2. Smoke the test suite — confirms nothing rotted overnight.
make test # ~40 s

# 3. Pre-fetch DEMs for the presets (so the first click is fast).
python -c "
from ropeway.dem import ensure_dem_tile
for lon, lat in [(6.87, 45.89), (-99.04, 19.34), (-122.95, 50.07),
(6.58, 45.30), (-73.96, 40.76)]:
print(ensure_dem_tile(lon, lat, cache_dir='data/dem'))
"

# 4. Pre-regenerate 3D twins (so the case study pages embed them).
python tools/build_case_study_twins.py

# 5. Start the services.
mkdocs serve --dev-addr 0.0.0.0:8000 & # landing + docs
streamlit run app/streamlit_app.py & # interactive UI
ropeway serve & # FastAPI :8000... wait, port clash:
# use ROPEWAY_PORT=8080 or skip if not needed

# 6. Tunnel.
cloudflared tunnel --url http://localhost:8501 # share for Streamlit
# or for docs:
cloudflared tunnel --url http://localhost:8000

# 7. Test the tunnel URL from your phone — confirms it actually reaches.
```

## Script (what to say + click)

| Beat | What the engineer hears | What you click |
|---|---|---|
| **0:00 — pitch** | "Two coordinates in, permit-grade alignment out. Fifteen seconds vs three weeks." | Landing page open, point at the hero. |
| **0:30 — proof** | "We've reproduced twelve real installations against the as-built record." | Scroll to the case-study grid. Click **Aiguille du Midi**. |
| **1:00 — 3D twin** | "This is the actual world-record 2 867 m span. We hit it at 2 850 m, 0.6 % delta, knowing only the two terminals." | The embedded 3-D twin loads. Drag to rotate. Point at the unsupported middle span. |
| **2:00 — process** | "Here's how a civil engineer uses it end-to-end." | Click **See the process** in the hero. |
| **3:00 — live demo** | "Now your turn. Let's run one." | Click **Try the live demo**. Demo tab opens. |
| **3:30 — pick preset** | "I'll start with Cablebús Línea 2 — the longest urban gondola in the world." | Pick that preset. Map updates. |
| **4:00 — Run** | "Click Run. Watch the GA converge." | Click **Run optimization**. ~5-10 s. |
| **4:30 — results** | "Feasible. Eight towers. Convergence curve dropped out of the infeasibility band in 12 generations." | Point at the alignment + convergence plots. |
| **5:00 — own corridor** | "Now let's try a corridor you care about." | Engineer types his own lon/lat. Run. |
| **6:00 — exports** | "Switch tabs — DXF, LandXML, PDF, BoM, capex estimate, 3-D twin. Everything a permit pack needs." | Click the **Optimize** tab. Show downloads. |
| **7:00 — limitations** | "Some things we're honest about." | Scroll to the limitations section in PROCESS.md. |
| **7:30 — Q&A** | — | — |

## Backup plans

| If this breaks | Do this |
|---|---|
| Streamlit refuses to start | `pkill -f streamlit; streamlit run app/streamlit_app.py` |
| Map widget shows blank | Use the lat/lon number inputs underneath; same effect. |
| Optimize spinner stuck | Click the page refresh; the GA is stateless, you re-run. |
| 3-D twin shows blank iframe | `python tools/build_case_study_twins.py --rebuild`. |
| Cloudflare Tunnel disconnects | Restart `cloudflared`; URL changes — DM the new one. |
| DEM fetch hangs | The Copernicus S3 mirror is occasionally slow. Switch to a pre-cached preset. |
| Test suite fails on `make test` | Stay on last green commit; demo with the live UI instead. |
| Everything is on fire | Show the [validation report PDF](case_studies/aiguille_du_midi_outputs/validation_report.pdf) and the 12-installation table — that's the real proof. |

## Things to NOT say

- "It works on every corridor" → it doesn't; tight bends, urban routes
with many obstacles, and over-water spans need declarations.
- "It replaces an engineer" → it accelerates pre-design; engineering
sign-off and Eurocode certification still belong to a human.
- "It's TÜV certified" → it's not (yet). The Eurocode + ISO checks
are *implemented*, not *certified*.

## Things to say if pressed on certification

- The toolchain implements EN 12929-1, ANSI B77.1, EN 1991-1-4, ISO
12494, EN 1993-1-1, and Eurocode 7 foundation checks. Every
violation is named and traceable.
- TÜV / notified-body recognition as a *pre-design aid* is on the
roadmap (Phase 63).
- Outputs are permit-grade in **shape and content** — DXF, LandXML,
PDF with the full check matrix. Stamped permit submission still
needs an engineer's signature.

## After the demo

- Capture his feedback verbatim in a GitHub issue titled
"Engineer trial — <date> — <name>". This is the Phase-17 gate
evidence the whole roadmap pivots on.
- Note: did it save him time? Quantify in his words.
- File any bugs he hit as separate issues with the "demo" label.
Loading
Loading