diff --git a/README.md b/README.md index f8dfb1d..070cd50 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/streamlit_app.py b/app/streamlit_app.py index e4ed6bb..890f5d7 100644 --- a/app/streamlit_app.py +++ b/app/streamlit_app.py @@ -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"] @@ -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 diff --git a/docs/DEMO_RUNBOOK.md b/docs/DEMO_RUNBOOK.md new file mode 100644 index 0000000..b23a100 --- /dev/null +++ b/docs/DEMO_RUNBOOK.md @@ -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 — ". 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. diff --git a/docs/DEMO_TOMORROW.md b/docs/DEMO_TOMORROW.md new file mode 100644 index 0000000..38edc33 --- /dev/null +++ b/docs/DEMO_TOMORROW.md @@ -0,0 +1,187 @@ +# Demo plan — civil engineer trial (tomorrow) + +> **Audience:** non-technical civil engineer. He needs to **see** the process, +> **run** it himself on a real corridor, and **see** the result in 3D — all +> without touching a terminal. + +Two-track plan: a public-facing **landing + walkthrough** that frames the +story, and a polished **Streamlit run surface** he actually clicks. Plus +the 3D twin embedded on every case study page so each is "a big deal." + +--- + +## Stage 0 — clear the deck (merge queue) + +Eight PRs are open and green. Merge in this order (no conflicts; all +branched independently off main): + +1. **#56 P47 auto-DEM cache** — landing needs it (any coord just works) +2. **#57 P49 case-study scaffold** — independent +3. **#55 P32 swath mask** — engine +4. **#54 P30 GA weights + warm-start** — engine +5. **#52 P29 RSM-in-loop** — engine (uses optimizer changes) +6. **#53 P26 OAuth callback** — server (needs db migration) +7. **#50 P28b CORS** — server (touches CORS in api.py) +8. **#51 TAGLINE benchmark** — uses CLI + +After each merge: `git checkout main && git pull && pytest -q` (smoke). +If any PR conflicts with main (it shouldn't, but P28b + P26 both touch +`api.py`), rebase the smaller diff onto the bigger one. + +**Estimated time:** 30-45 min including local smoke after each. + +--- + +## Stage 1 — landing page (the "what + how" story) + +Single-page site at `/` rendered by mkdocs-material (already configured). +**No new framework, no Next.js for tomorrow.** Goal: a civil guy gets the +pitch in 30 seconds, sees the process in 90. + +Build into `docs/index.md`: + +### Hero +- One sentence: "Permit-grade aerial-ropeway alignment from two + coordinates and a free DEM. **15 seconds vs 3-4 weeks.**" +- Big "Try it" button → `/demo` (Streamlit URL we host locally tomorrow) +- Tiny "See the engine" link → `/case_studies/aiguille_du_midi` + +### How it works (4 panels, left-to-right) +1. **Drop two coordinates** (or click a map — Streamlit map input) +2. **Auto-DEM** (P47 cache fetches Copernicus tile) +3. **GA / NSGA-II solves** (animation: convergence plot) +4. **Permit pack out** (DXF + LandXML + PDF + 3D twin) + +Each panel: 1 sentence + a thumbnail. PNG screenshots from existing +case-study outputs. No JS animation needed — static images that look +like a comic strip. + +### Proof strip +- "12 real installations reproduced" — six logos / station names in a row +- TAGLINE numbers — "6 corridors, 100% feasible, ~4 s, **~626 000× + speedup**" (auto-generated by `ropeway tagline`) +- Validation badge — "0.00% textbook catenary error" + +### Case-study grid +- 12 cards, 3 columns × 4 rows +- Each card = thumbnail (`alignment.png`) + name + system + headline + metric ("2 850 m vs 2 867 m as-built, 0.6% Δ") +- Card links to that case study's page with embedded 3D twin + +**Time:** 1.5 hours. + +--- + +## Stage 2 — 3D twin on every case study (the "big deal" visual) + +Most case studies already produce `validation_report.pdf` and +`alignment.png`. The interactive `viz3d` HTML exists but isn't bundled +into the docs build. Make it so: + +1. For each case study with `*_outputs/`, regenerate the twin: + `python -c "from ropeway.viz3d import export_scene_html; ..."` + Need a tiny helper script `tools/build_case_study_twins.py` that + loops over `examples/case_*.py` outputs and emits `twin.html` per + case. +2. In `docs/case_studies/.md`, embed an ` + diff --git a/docs/case_studies/aiguille_du_midi_outputs/twin.html b/docs/case_studies/aiguille_du_midi_outputs/twin.html new file mode 100644 index 0000000..2dd8722 --- /dev/null +++ b/docs/case_studies/aiguille_du_midi_outputs/twin.html @@ -0,0 +1,26 @@ + + + + + + VTK.js | Example - OfflineLocalView + + + +
+ + + + + diff --git a/docs/case_studies/cablebus_linea2.md b/docs/case_studies/cablebus_linea2.md index 1f9bf5a..016f778 100644 --- a/docs/case_studies/cablebus_linea2.md +++ b/docs/case_studies/cablebus_linea2.md @@ -136,3 +136,13 @@ calibrated for this archetype. * [Mi Teleférico Línea Roja](mi_teleferico_linea_roja.md) — La Paz urban MGD. * [Metrocable Línea K](medellin_linea_k.md) — Medellín, first urban gondola. * [IFS Cloud Cable Car](london_ifs_cloud.md) — London Thames crossing MGD. + +--- + +## Interactive 3-D twin + +Drag to rotate, scroll to zoom. Click the *Reset Camera* button (top-right +inside the viewer) to recentre. + + + diff --git a/docs/case_studies/cablebus_linea2_outputs/twin.html b/docs/case_studies/cablebus_linea2_outputs/twin.html new file mode 100644 index 0000000..7edb025 --- /dev/null +++ b/docs/case_studies/cablebus_linea2_outputs/twin.html @@ -0,0 +1,26 @@ + + + + + + VTK.js | Example - OfflineLocalView + + + +
+ + + + + diff --git a/docs/case_studies/funitel_peclet.md b/docs/case_studies/funitel_peclet.md index 5be783d..2d80198 100644 --- a/docs/case_studies/funitel_peclet.md +++ b/docs/case_studies/funitel_peclet.md @@ -122,3 +122,13 @@ to be validated against a real installation. * [Aiguille du Midi](aiguille_du_midi.md) — jig-back, same DEM tile. * [Whistler Peak Express](whistler_peak_chair.md) — chairlift archetype. * [Ngong Ping 360](ngong_ping_360.md) — BGD bi-cable archetype. + +--- + +## Interactive 3-D twin + +Drag to rotate, scroll to zoom. Click the *Reset Camera* button (top-right +inside the viewer) to recentre. + + + diff --git a/docs/case_studies/funitel_peclet_outputs/twin.html b/docs/case_studies/funitel_peclet_outputs/twin.html new file mode 100644 index 0000000..0c96d93 --- /dev/null +++ b/docs/case_studies/funitel_peclet_outputs/twin.html @@ -0,0 +1,26 @@ + + + + + + VTK.js | Example - OfflineLocalView + + + +
+ + + + + diff --git a/docs/case_studies/london_ifs_cloud.md b/docs/case_studies/london_ifs_cloud.md index 724ad4a..1129655 100644 --- a/docs/case_studies/london_ifs_cloud.md +++ b/docs/case_studies/london_ifs_cloud.md @@ -127,3 +127,13 @@ behaviour a real PLA-approved design must exhibit. * [Roosevelt Island Tramway](roosevelt_island.md) — NYC East River, jig-back. * [Ngong Ping 360](ngong_ping_360.md) — Hong Kong bay crossing, BGD. * [Mi Teleférico Línea Roja](mi_teleferico_linea_roja.md) — La Paz urban MGD. + +--- + +## Interactive 3-D twin + +Drag to rotate, scroll to zoom. Click the *Reset Camera* button (top-right +inside the viewer) to recentre. + + + diff --git a/docs/case_studies/london_ifs_cloud_outputs/twin.html b/docs/case_studies/london_ifs_cloud_outputs/twin.html new file mode 100644 index 0000000..b07e6a0 --- /dev/null +++ b/docs/case_studies/london_ifs_cloud_outputs/twin.html @@ -0,0 +1,26 @@ + + + + + + VTK.js | Example - OfflineLocalView + + + +
+ + + + + diff --git a/docs/case_studies/medellin_linea_k.md b/docs/case_studies/medellin_linea_k.md index 8b52010..d478877 100644 --- a/docs/case_studies/medellin_linea_k.md +++ b/docs/case_studies/medellin_linea_k.md @@ -135,3 +135,13 @@ feature — multi-waypoint urban routing works. 1. **Mexico City Cablebús Línea 2** (longest urban gondola at opening) 2. **Caracas Metrocable San Agustín** (urban, complex terrain) 3. Curved-corridor routing to close the horizontal-length gap + +--- + +## Interactive 3-D twin + +Drag to rotate, scroll to zoom. Click the *Reset Camera* button (top-right +inside the viewer) to recentre. + + + diff --git a/docs/case_studies/medellin_linea_k_outputs/twin.html b/docs/case_studies/medellin_linea_k_outputs/twin.html new file mode 100644 index 0000000..830bb1b --- /dev/null +++ b/docs/case_studies/medellin_linea_k_outputs/twin.html @@ -0,0 +1,26 @@ + + + + + + VTK.js | Example - OfflineLocalView + + + +
+ + + + + diff --git a/docs/case_studies/mi_teleferico_linea_roja.md b/docs/case_studies/mi_teleferico_linea_roja.md index 76bda78..1ea6225 100644 --- a/docs/case_studies/mi_teleferico_linea_roja.md +++ b/docs/case_studies/mi_teleferico_linea_roja.md @@ -165,3 +165,13 @@ The single hardest physics question — **how much altitude must the cable rise* 1. **Mexico City Cablebús Línea 2** (longest urban gondola at opening) 2. A funitel installation, to exercise the last catalogue archetype + +--- + +## Interactive 3-D twin + +Drag to rotate, scroll to zoom. Click the *Reset Camera* button (top-right +inside the viewer) to recentre. + + + diff --git a/docs/case_studies/mi_teleferico_linea_roja_outputs/twin.html b/docs/case_studies/mi_teleferico_linea_roja_outputs/twin.html new file mode 100644 index 0000000..387de92 --- /dev/null +++ b/docs/case_studies/mi_teleferico_linea_roja_outputs/twin.html @@ -0,0 +1,26 @@ + + + + + + VTK.js | Example - OfflineLocalView + + + +
+ + + + + diff --git a/docs/case_studies/ngong_ping_360.md b/docs/case_studies/ngong_ping_360.md index a692aad..9f60c36 100644 --- a/docs/case_studies/ngong_ping_360.md +++ b/docs/case_studies/ngong_ping_360.md @@ -128,3 +128,13 @@ system's value lies — to cross the bay in as few towers as possible. * [Mi Teleférico Línea Roja](mi_teleferico_linea_roja.md) — monocable MGD. * [Funitel de Péclet](funitel_peclet.md) — funitel archetype. * [Whistler Peak Express](whistler_peak_chair.md) — chairlift archetype. + +--- + +## Interactive 3-D twin + +Drag to rotate, scroll to zoom. Click the *Reset Camera* button (top-right +inside the viewer) to recentre. + + + diff --git a/docs/case_studies/ngong_ping_360_outputs/twin.html b/docs/case_studies/ngong_ping_360_outputs/twin.html new file mode 100644 index 0000000..427cb22 --- /dev/null +++ b/docs/case_studies/ngong_ping_360_outputs/twin.html @@ -0,0 +1,26 @@ + + + + + + VTK.js | Example - OfflineLocalView + + + +
+ + + + + diff --git a/docs/case_studies/portland_ohsu.md b/docs/case_studies/portland_ohsu.md index 8179c86..d501cfa 100644 --- a/docs/case_studies/portland_ohsu.md +++ b/docs/case_studies/portland_ohsu.md @@ -138,3 +138,13 @@ closest cabin-capacity reproduction in the case-study set. * [Roosevelt Island Tramway](roosevelt_island.md) — NYC water crossing, jig-back. * [Aiguille du Midi](aiguille_du_midi.md) — alpine jig-back record span. * [Seilbahn Zugspitze](zugspitze_eibsee.md) — alpine jig-back, record vertical. + +--- + +## Interactive 3-D twin + +Drag to rotate, scroll to zoom. Click the *Reset Camera* button (top-right +inside the viewer) to recentre. + + + diff --git a/docs/case_studies/portland_ohsu_outputs/twin.html b/docs/case_studies/portland_ohsu_outputs/twin.html new file mode 100644 index 0000000..b8a5664 --- /dev/null +++ b/docs/case_studies/portland_ohsu_outputs/twin.html @@ -0,0 +1,26 @@ + + + + + + VTK.js | Example - OfflineLocalView + + + +
+ + + + + diff --git a/docs/case_studies/roosevelt_island.md b/docs/case_studies/roosevelt_island.md index 39020e2..3278a8e 100644 --- a/docs/case_studies/roosevelt_island.md +++ b/docs/case_studies/roosevelt_island.md @@ -139,3 +139,13 @@ discipline a real urban tramway over a public waterway must show. 1. **Portland Aerial Tram** (OHSU, US urban jig-back #2) 2. **London IFS Cloud Cable Car** (Emirates Air Line — Thames crossing) 3. A funitel installation, to exercise the last catalogue archetype + +--- + +## Interactive 3-D twin + +Drag to rotate, scroll to zoom. Click the *Reset Camera* button (top-right +inside the viewer) to recentre. + + + diff --git a/docs/case_studies/roosevelt_island_outputs/twin.html b/docs/case_studies/roosevelt_island_outputs/twin.html new file mode 100644 index 0000000..ee4dad7 --- /dev/null +++ b/docs/case_studies/roosevelt_island_outputs/twin.html @@ -0,0 +1,26 @@ + + + + + + VTK.js | Example - OfflineLocalView + + + +
+ + + + + diff --git a/docs/case_studies/whistler_peak2peak.md b/docs/case_studies/whistler_peak2peak.md index 8948f7c..19e8300 100644 --- a/docs/case_studies/whistler_peak2peak.md +++ b/docs/case_studies/whistler_peak2peak.md @@ -171,3 +171,13 @@ Within the system-spec band. 1. **Vanoise Express** (double-decker, deep-valley jig-back) 2. A funitel installation, to exercise the last catalogue archetype + +--- + +## Interactive 3-D twin + +Drag to rotate, scroll to zoom. Click the *Reset Camera* button (top-right +inside the viewer) to recentre. + + + diff --git a/docs/case_studies/whistler_peak2peak_outputs/twin.html b/docs/case_studies/whistler_peak2peak_outputs/twin.html new file mode 100644 index 0000000..cfac059 --- /dev/null +++ b/docs/case_studies/whistler_peak2peak_outputs/twin.html @@ -0,0 +1,26 @@ + + + + + + VTK.js | Example - OfflineLocalView + + + +
+ + + + + diff --git a/docs/case_studies/whistler_peak_chair.md b/docs/case_studies/whistler_peak_chair.md index 572c4b4..8801630 100644 --- a/docs/case_studies/whistler_peak_chair.md +++ b/docs/case_studies/whistler_peak_chair.md @@ -124,3 +124,13 @@ the sixth and final catalogue archetype. * [Whistler Peak 2 Peak](whistler_peak2peak.md) — 3S, same DEM tile. * [Funitel de Péclet](funitel_peclet.md) — funitel archetype. * [Ngong Ping 360](ngong_ping_360.md) — BGD bi-cable archetype. + +--- + +## Interactive 3-D twin + +Drag to rotate, scroll to zoom. Click the *Reset Camera* button (top-right +inside the viewer) to recentre. + + + diff --git a/docs/case_studies/whistler_peak_chair_outputs/twin.html b/docs/case_studies/whistler_peak_chair_outputs/twin.html new file mode 100644 index 0000000..a4097ce --- /dev/null +++ b/docs/case_studies/whistler_peak_chair_outputs/twin.html @@ -0,0 +1,26 @@ + + + + + + VTK.js | Example - OfflineLocalView + + + +
+ + + + + diff --git a/docs/case_studies/zugspitze_eibsee.md b/docs/case_studies/zugspitze_eibsee.md index 92b8a65..72de41c 100644 --- a/docs/case_studies/zugspitze_eibsee.md +++ b/docs/case_studies/zugspitze_eibsee.md @@ -170,3 +170,13 @@ decision exactly. 1. **Vanoise Express** (double-decker, deep-valley jig-back) 2. **Tibetan / Himalayan high-altitude ropeway** (thermal + altitude) 3. A funitel installation, to exercise the last catalogue archetype + +--- + +## Interactive 3-D twin + +Drag to rotate, scroll to zoom. Click the *Reset Camera* button (top-right +inside the viewer) to recentre. + + + diff --git a/docs/case_studies/zugspitze_eibsee_outputs/twin.html b/docs/case_studies/zugspitze_eibsee_outputs/twin.html new file mode 100644 index 0000000..bc4dc81 --- /dev/null +++ b/docs/case_studies/zugspitze_eibsee_outputs/twin.html @@ -0,0 +1,26 @@ + + + + + + VTK.js | Example - OfflineLocalView + + + +
+ + + + + diff --git a/docs/index.md b/docs/index.md index 8d35182..19e73ab 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,44 +1,232 @@ # Autonomous Ropeway Alignment -Open-source Python toolchain that ingests a free Digital Elevation Model -(DEM) and two station coordinates, then emits a **permit-grade** -aerial-ropeway alignment: + -* Tower placement and tower-height schedule -* Catenary cable with per-span horizontal tension (with optional - elastic + thermal + ice loading) -* Eurocode safety checks (EN 12929-1 clearance · ANSI B77.1 live load · - EN 1991-1-4 wind · ISO 12494 ice · EN 1993-1-1 tower buckling) -* AutoCAD DXF + LandXML 1.2 + interactive 3-D HTML twin -* Bill of materials + regional capex estimate -* All in under fifteen seconds on a consumer laptop. +
+

From two coordinates to a permit-grade ropeway alignment — in 15 seconds.

+

A civil engineer drops two GPS points. The toolchain pulls a free + terrain model, runs a genetic algorithm against Eurocode safety envelopes, and + emits a tower schedule, catenary cable, AutoCAD DXF, LandXML, validation PDF, + and an interactive 3-D twin. The same job manually takes 3-4 weeks.

+ Try the live demo → + See the process +
-> **No cloud. No GPU. No vendor lock-in.** +## How it works -## Validated against twelve real installations +
+
+ 1 + Two coordinates +

Click two points on a map, or paste lon/lat. Optionally choose a + system type (jig-back, MGD, 3S, funitel, chairlift, BGD).

+
+
+ 2 + Auto DEM +

The matching Copernicus GLO-30 tile is fetched and cached + automatically. No manual download. Cached tiles are reused.

+
+
+ 3 + GA + NSGA-II solves +

A genetic algorithm places towers under Eurocode clearance, + foundation safety, ice and wind loads. Pareto front trades cost, + tower count and cable length.

+
+
+ 4 + Permit pack out +

AutoCAD DXF, LandXML 1.2, multi-page PDF, GeoJSON, Bill of + Materials, capex estimate, and an interactive 3-D HTML twin.

+
+
-Across **all six** `RopewaySystemType` archetypes on **five -continents**: +## Proof — twelve real installations, six system archetypes, five continents -| Archetype | Installations | -|---|---| -| Jig-back | Aiguille du Midi · Zugspitze · Roosevelt Island · Portland OHSU | -| MGD | Mi Teleférico Línea Roja · Medellín Línea K · Cablebús Línea 2 · London IFS Cloud | -| 3S | Whistler Peak 2 Peak | -| Funitel | Funitel de Péclet (Val Thorens) | -| BGD | Ngong Ping 360 (Hong Kong) | -| Chairlift | Whistler Mountain | +
+
+ 0.6 % + Aiguille du Midi (Stage 2): optimizer reproduces the world-record + 2 867 m unsupported span at 2 850 m, fed only the two terminal + coordinates. +
+
+ ~626 000× + Wall-clock speedup vs the 3-week / 120 engineer-hour / ~$60 k + feasibility-study baseline (per corridor). Six corridors, + 100 % feasible, ~4 s total — re-run with ropeway tagline. +
+
+ 0.00 % + Error against three textbook catenary identities — every load + branch validated to machine precision. +
+
-Plus **0.00 %** error against three textbook catenary cases. +## Twelve case studies + + + +## What's inside + +* **Catenary mechanics** — inclined catenary kernel; per-span horizontal + tension with sheave-loss propagation; elastic + thermal + ISO 12494 + ice coupling. +* **Safety envelope** — EN 12929-1 piecewise clearance (open / road / + railway / building / water), ANSI B77.1 wind swing buffer, ISO 12494 + ice loading, EN 1993-1-1 tower buckling + bending, EN 1991-1-4 wind, + foundation overturning and sliding safety factors. +* **Optimizers** — DEAP single-objective GA · pymoo NSGA-II Pareto + front · stable-baselines3 PPO RL agent · scikit-learn RSM surrogate + in the GA hot loop. +* **Constraints the GA honours** — no-tower exclusion zones (avalanche, + water, property, military), 2-D swath polygons (lateral sidestep), + forced fly-over zones (bridge decks, freeways, shipping channels), + pinned intermediate stations (with off-centreline offsets). +* **System catalogue** — MGD, BGD, 3S, jig-back, chairlift, funitel + with system-appropriate defaults via `--system`. +* **Outputs** — PNG plots, GeoJSON, CSV tower schedule, AutoCAD DXF, + LandXML 1.2, multi-page validation PDF, interactive 3-D HTML twin, + Bill of Materials, regional capex estimate. ## Where to next -* **[Pitch](PITCH.md)** — the one-pager -* **[Pricing](PRICING.md)** — tier definitions -* **[Whitepaper](WHITEPAPER.md)** — the technical brief -* **[Scorecard](SCORECARD.md)** — where this stands vs RopeCAD / ROPEKON / Cassia -* **Case studies** — every real-installation reproduction with PNG + DXF + LandXML + PDF -* **[Phase plan](PHASE_PLAN.md)** — current roadmap status +* **[Process walkthrough](PROCESS.md)** — narrated tour of how an + engineer uses it. +* **[Pitch](PITCH.md)** — one-pager for stakeholders. +* **[Pricing](PRICING.md)** — tier definitions. +* **[Whitepaper](WHITEPAPER.md)** — technical brief. +* **[Scorecard](SCORECARD.md)** — vs RopeCAD / ROPEKON / Cassia. +* **[Phase plan](PHASE_PLAN.md)** — current roadmap status. +* **[2-year roadmap](ROADMAP_2YEAR.md)** — narrative behind the + sequencing. +* **[15s vs 3-4 weeks](tagline/tagline.md)** — reproducible benchmark. ## Quickstart @@ -46,6 +234,10 @@ Plus **0.00 %** error against three textbook catenary cases. git clone https://github.com/harsh-pandhe/Autonomous-Ropeway-Alignment cd Autonomous-Ropeway-Alignment make install -make demo # synthetic 3 km run, no DEM needed -make ui # Streamlit UI at http://localhost:8501 +make demo # synthetic 3 km run, no DEM needed +make ui # Streamlit UI at http://localhost:8501 +ropeway tagline # regenerate the speedup benchmark page +ropeway serve # FastAPI at :8000 (OpenAPI at /docs) ``` + +> **No cloud. No GPU. No vendor lock-in.** Runs on a consumer PC. diff --git a/mkdocs.yml b/mkdocs.yml index 8a510a6..69c6200 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,6 +55,7 @@ markdown_extensions: nav: - Home: index.md + - Process: PROCESS.md - Pitch: PITCH.md - Pricing: PRICING.md - Whitepaper: WHITEPAPER.md diff --git a/pyproject.toml b/pyproject.toml index 8b92d60..376e62f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ ] [project.optional-dependencies] -ui = ["streamlit>=1.30", "stpyvista>=0.2", "trame>=3.0", "trame-vtk>=2.8", "trame-vuetify>=2.4", "nest_asyncio2>=1.7"] +ui = ["streamlit>=1.30", "stpyvista>=0.2", "trame>=3.0", "trame-vtk>=2.8", "trame-vuetify>=2.4", "nest_asyncio2>=1.7", "streamlit-folium>=0.20", "folium>=0.16"] viz3d = ["pyvista>=0.43", "nest_asyncio2>=1.7"] rl = ["gymnasium>=1.0", "stable-baselines3>=2.0", "torch>=2.0"] server = [ diff --git a/tools/build_case_study_twins.py b/tools/build_case_study_twins.py new file mode 100644 index 0000000..a32a15b --- /dev/null +++ b/tools/build_case_study_twins.py @@ -0,0 +1,182 @@ +"""Build interactive 3-D HTML twins for every case study with outputs. + +Reads each `examples/case_*.py` to find its terminal coordinates + DEM +tile, regenerates a `CorridorPatch`, fits a fresh `Alignment` from the +existing `towers.csv`, and writes `twin.html` next to the existing +artifacts in `docs/case_studies/_outputs/`. + +Idempotent: skips a case if `twin.html` already exists and is newer +than its `towers.csv`. Pass `--rebuild` to force regeneration. + +Usage:: + + python tools/build_case_study_twins.py # incremental + python tools/build_case_study_twins.py --rebuild # all + python tools/build_case_study_twins.py --only aiguille_du_midi +""" + +from __future__ import annotations + +import argparse +import ast +import csv +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + +from ropeway.alignment import Alignment, Tower +from ropeway.dem import ( + CorridorPatch, + extract_corridor_patch_from_dem, + synthetic_corridor_patch, +) +from ropeway.safety import ConstraintConfig +from ropeway.viz3d import export_scene_html + + +def _parse_example(path: Path) -> dict | None: + """Pull START_LON/START_LAT/END_LON/END_LAT/DEM_PATH/OUT_DIR from + a case-study script via static AST parsing — no execution.""" + src = path.read_text() + tree = ast.parse(src) + bag: dict = {} + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + try: + value = ast.literal_eval(node.value) + except (ValueError, SyntaxError): + # DEM_PATH / OUT_DIR use Path(...); reconstruct. + if ( + isinstance(node.value, ast.Call) + and isinstance(node.value.func, ast.Name) + and node.value.func.id == "Path" + and node.value.args + and isinstance(node.value.args[0], ast.Constant) + ): + value = Path(node.value.args[0].value) + else: + continue + for tgt in node.targets: + if isinstance(tgt, ast.Name): + bag[tgt.id] = value + elif isinstance(tgt, ast.Tuple): + # `START_LON, START_LAT = -99.04, 19.34` + if isinstance(value, tuple) and len(value) == len(tgt.elts): + for sub_tgt, sub_val in zip(tgt.elts, value): + if isinstance(sub_tgt, ast.Name): + bag[sub_tgt.id] = sub_val + needed = {"START_LON", "START_LAT", "END_LON", "END_LAT", "DEM_PATH", "OUT_DIR"} + if not needed.issubset(bag): + return None + return bag + + +def _load_alignment_from_csv(towers_csv: Path, cfg: ConstraintConfig) -> list[Tower]: + towers: list[Tower] = [] + with towers_csv.open(newline="") as f: + reader = csv.DictReader(f) + for row in reader: + d = float(row.get("distance_m") or row.get("distance") or row.get("d") or 0.0) + h = float(row.get("height_m") or row.get("height") or row.get("h") or 12.0) + off = float(row.get("offset_m") or row.get("offset") or 0.0) + is_station_raw = row.get("is_station") or row.get("station") or "" + is_station = str(is_station_raw).lower() in ("1", "true", "t", "yes", "station") + towers.append(Tower(distance=d, height=h, is_station=is_station, offset=off)) + if towers: + towers[0].is_station = True + towers[-1].is_station = True + return towers + + +def _patch_for(info: dict) -> CorridorPatch | None: + """Build a CorridorPatch from the case-study's DEM tile. + + Falls back to a synthetic patch if the DEM is missing — better a + smooth-terrain twin than nothing for the demo.""" + dem_path = ROOT / Path(info["DEM_PATH"]) + start = (float(info["START_LON"]), float(info["START_LAT"])) + end = (float(info["END_LON"]), float(info["END_LAT"])) + if dem_path.exists(): + try: + return extract_corridor_patch_from_dem( + dem_path, start, end, + cross_half_width_m=200.0, along_spacing_m=20.0, cross_spacing_m=20.0, + ) + except Exception as exc: + print(f" DEM extract failed ({exc}); falling back to synthetic patch.") + # Synthetic fallback: rough corridor length from haversine. + from ropeway.dem import great_circle_distance_m + length = great_circle_distance_m(start, end) + return synthetic_corridor_patch(length_m=length, seed=hash(info["OUT_DIR"].name) & 0xFFFF) + + +def build_one(example: Path, *, rebuild: bool = False) -> tuple[Path, str]: + """Build the twin for one case-study script. Returns (twin_path, status).""" + info = _parse_example(example) + if info is None: + return (example, "skipped (could not parse case-study script)") + + out_dir = ROOT / Path(info["OUT_DIR"]) + if not out_dir.exists(): + return (out_dir, "skipped (no outputs dir — run the case study first)") + + towers_csv = out_dir / "towers.csv" + if not towers_csv.exists(): + return (out_dir, "skipped (no towers.csv — run the case study first)") + + twin_path = out_dir / "twin.html" + if twin_path.exists() and not rebuild: + if twin_path.stat().st_mtime > towers_csv.stat().st_mtime: + return (twin_path, "up to date") + + cfg = ConstraintConfig() + towers = _load_alignment_from_csv(towers_csv, cfg) + if len(towers) < 2: + return (twin_path, "skipped (towers.csv too small)") + + patch = _patch_for(info) + if patch is None: + return (twin_path, "skipped (no patch — DEM + synthetic both unavailable)") + + alignment = Alignment( + towers=towers, + profile_fn=patch.centerline_profile().as_function(), + cfg=cfg, + ) + export_scene_html(patch, alignment, twin_path) + return (twin_path, "built") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--rebuild", action="store_true", help="Force regeneration.") + parser.add_argument("--only", action="append", default=[], + help="Slug substring to filter (repeatable).") + args = parser.parse_args(argv) + + examples = sorted((ROOT / "examples").glob("case_*.py")) + if args.only: + examples = [ + p for p in examples + if any(sub in p.stem for sub in args.only) + ] + if not examples: + print("No case-study examples matched.") + return 1 + + built = 0 + for ex in examples: + path, status = build_one(ex, rebuild=args.rebuild) + print(f"{ex.stem:<35} → {status:<60} {path}") + if status == "built": + built += 1 + print(f"\n{built} twin(s) (re)generated.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())