Metadata-Version: 2.4
Name: ropesim
Version: 3.0.2
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Education
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Rust
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Typing :: Typed
Requires-Dist: numpy>=1.26
Requires-Dist: pydantic>=2.0
Requires-Dist: matplotlib>=3.8
Requires-Dist: textual>=0.80 ; extra == 'all'
Requires-Dist: pytest>=7.4 ; extra == 'all'
Requires-Dist: pytest-benchmark>=4.0 ; extra == 'all'
Requires-Dist: maturin>=1.5 ; extra == 'all'
Requires-Dist: patchnotes>=0.1 ; extra == 'all'
Requires-Dist: pytest>=7.4 ; extra == 'dev'
Requires-Dist: pytest-benchmark>=4.0 ; extra == 'dev'
Requires-Dist: maturin>=1.5 ; extra == 'dev'
Requires-Dist: patchnotes>=0.1 ; extra == 'dev'
Requires-Dist: ruff>=0.6 ; extra == 'dev'
Requires-Dist: black>=24.0 ; extra == 'dev'
Requires-Dist: textual>=0.80 ; extra == 'tui'
Provides-Extra: all
Provides-Extra: dev
Provides-Extra: tui
License-File: LICENSE
Summary: Climbing rope physics engine and GUI simulator 
Keywords: climbing,physics,rope,simulation,UIAA,EN892,safety
Author-email: Londopy <ropesim@pxdmail.com>
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/Londopy/ropesim/blob/main/CHANGELOG.md
Project-URL: Homepage, https://github.com/Londopy/ropesim
Project-URL: Issues, https://github.com/Londopy/ropesim/issues
Project-URL: Repository, https://github.com/Londopy/ropesim

# RopeSim

**Climbing rope physics engine — Python library, CLI, TUI, and native 3D desktop app**

RopeSim models lead-fall dynamics using a damped spring / RK4 integration in
Rust. One physics core, four frontends: a full Python API (PyO3/Maturin), a
CLI with 20+ commands, a Textual terminal UI, and — new in v3 — a **native
C++ Qt6 desktop application** with a 60 fps OpenGL viewport, consuming the
same Rust core over a plain C FFI. Rapier3D full-physics mode, Jupyter
integration, and a hosted documentation site round it out.

[![CI](https://github.com/Londopy/ropesim/actions/workflows/ci.yml/badge.svg)](https://github.com/Londopy/ropesim/actions/workflows/ci.yml)
[![Docs](https://img.shields.io/badge/docs-londopy.github.io%2Fropesim-7ecf45)](https://londopy.github.io/ropesim/)
[![PyPI](https://img.shields.io/pypi/v/ropesim)](https://pypi.org/project/ropesim/)
[![Python 3.10+](https://img.shields.io/pypi/pyversions/ropesim)](https://pypi.org/project/ropesim/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Londopy/ropesim/blob/main/colab/01_basic_fall_simulation.ipynb)

---

## Features

**New in v3**

- **Native desktop app (C++ / Qt6)** — single-binary installers for Windows, macOS, and Linux; 60 fps OpenGL rope renderer with tension heatmap, raycast gear placement, native interactive force plots, simulation playback with scrubber
- **Dual FFI** — the Rust core exports a plain C ABI (cbindgen) alongside PyO3; `gui-cpp/include/ropesim.h` regenerates on every build
- **Twin & half rope physics** — inter-strand friction, load sharing, alternate-clip model
- **Knot strength reduction** — 8 knots with published pull-test factors + knotted cordelette strength
- **Sheath abrasion accumulator** — Archard-style wear from rope-over-rock contact, per rock type, with retirement projection
- **Fall probability & daily exposure** — grade-delta logistic model, cumulative rope-life projection across a multi-pitch day
- **Full dynamic belayer** — mass ratio, stance, device slip, and soft-catch technique (30–45 % force reduction)
- **Terminal UI** — `ropesim tui` (Textual): simulate, browse the database, project risk
- **Docs site** — [londopy.github.io/ropesim](https://londopy.github.io/ropesim/) with generated API reference

- **UIAA 101 / EN 892** impact-force model with belay-device friction, wet-rope modifier, and temperature correction
- **RK4 force-time curve** — full damped spring integration in Rust for accurate energy modelling
- **Rapier3D full-physics mode** — rope modelled as a capsule-chain rigid-body simulation (optional)
- **Parallel batch sweeps** via Rayon — sweep 200 fall positions in milliseconds
- **Anchor system physics** — sliding-X, quad, cordelette, trad gear with load distribution and progressive failure
- **Guide-mode self-locking belay devices** — Reverso Guide, Mega Jul, Giga Jul, Click Up, I-Device, Sum with load-dependent friction model
- **Mechanical advantage / haul systems** — 3:1, 5:1, 6:1, piggyback with friction-corrected MA
- **Top-rope and rappel models** — catch and anchor-load estimation
- **Rope diameter under load** — estimates radial compression at any applied force
- **Rope degradation model** — stiffness and impact-force drift with falls taken
- **25-rope database** covering Beal, Mammut, Sterling, Petzl, Edelrid, Black Diamond, and more
- **PySide6 GUI** — drag-and-drop route builder, live simulation, 2D/3D viewport toggle, fall animation, matplotlib plots, PDF/CSV export
- **3D Vispy viewport** — rope tension heatmap, gear load markers, climber tracker, force arrows, turntable camera, frame-scrubber playback
- **Jupyter notebook integration** — rich HTML/SVG repr for Rope, FallResult, AnchorSystem; five example notebooks
- **CLI** — 20+ commands across `rope`, `scenario`, `validate`, `report`, `toprope`, `rappel`, `haul`, and `interactive` subcommand groups

---

## Installation

### Pre-built wheel (recommended)

```bash
pip install ropesim                  # physics library + CLI
pip install "ropesim[tui]"           # + terminal UI (Textual)
```

### Desktop application (v3 — no Python required)

Download the native app from
[GitHub Releases](https://github.com/Londopy/ropesim/releases):
`.zip` (Windows), `.dmg` (macOS), or `.AppImage` (Linux).

Or grab everything at once:

```bash
pip install "ropesim[all]"
```

### From source (requires Rust toolchain)

```bash
git clone https://github.com/Londopy/ropesim.git
cd ropesim
pip install maturin
maturin develop --release      # compiles Rust, installs in editable mode
pip install -e ".[all]"        # optional GUI + notebook deps
```

> **Python 3.14+** users building from source need one extra step:
> `set PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1` (Windows) or
> `export PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1` (Linux/macOS) before running
> `maturin develop`. PyPI wheel installs are unaffected.

---

## Demo

A self-contained demo script exercises every feature of the library and saves
eight matplotlib plots to your working directory:

```bash
python demo.py
```

This covers: units, standards lookup, rope database search, rope physics
helpers, fall simulation, anchor systems, scenario builder, sweep and zipper
analysis, visualisations, and the low-level Rust core API.

---

## Quick start — Python API

```python
from ropesim.rope import RopeDatabase
from ropesim.fall import FallConditions, Fall, BelayDevice

# Load a rope from the bundled database
spec = RopeDatabase().get("Mammut Crag Classic 10.2")

# Simulate a factor-0.5 fall on 20 m of rope
conditions = FallConditions(
    climber_mass_kg=80.0,
    fall_distance_m=10.0,   # fell 2 x 5 m above last pro
    rope_out_m=20.0,
    belay_device=BelayDevice.GRIGRI,
    rope=spec,
)
result = Fall(conditions).simulate()

print(f"Peak impact force : {result.peak_force_kn:.2f} kN")
print(f"Fall factor       : {result.fall_factor:.3f}")
print(f"Energy absorbed   : {result.energy_budget.rope_absorption_j:.0f} J")
print(f"Warnings          : {result.warnings or 'none'}")
```

### Guide-mode self-locking belay devices

```python
from ropesim.fall import BelayDevice, FallConditions, Fall
from ropesim.simulate import compute_effective_friction, is_guide_mode

device = BelayDevice.REVERSO_GUIDE

# Check if it uses the self-locking model
print(is_guide_mode(device))  # True

# Effective friction increases with load
for kn in [3.0, 6.0, 9.0, 12.0]:
    mu = compute_effective_friction(device, kn)
    print(f"{kn:.0f} kN -> mu_eff = {mu:.3f}")

# Simulate — guide-mode friction solved automatically
conditions = FallConditions(
    climber_mass_kg=80.0,
    fall_distance_m=8.0,
    rope_out_m=18.0,
    belay_device=device,
    rope=RopeDatabase().get("Beal Opera 8.5 Dry"),
)
result = Fall(conditions).simulate()
print(f"Guide-mode active : {result.guide_mode_used}")
print(f"Peak force        : {result.peak_force_kn:.2f} kN")
```

### Rapier3D full-physics mode

```python
from ropesim.simulate import Scenario, PhysicsMode, ScenarioType
from ropesim.rope import Rope, RopeDatabase
from ropesim.anchor import AnchorSystem, AnchorType, Bolt

rope = Rope(RopeDatabase().get("Mammut Crag Classic 10.2"))
scenario = Scenario(rope=rope, climber_mass_kg=80.0)

for h in [4.0, 9.0, 14.0]:
    scenario.add_protection(h, AnchorSystem(AnchorType.SINGLE_POINT, [Bolt(rated_mbs_kn=25.0)]))

# Full Rapier3D rigid-body simulation
result = scenario.simulate_fall(
    climber_height_m=16.0,
    mode=PhysicsMode.RAPIER_3D,
)
print(f"Physics mode : {result.mode}")
print(f"Peak force   : {result.peak_force_kn:.2f} kN")
print(f"Frames       : {len(result.frames)}")

# Frame-by-frame playback
from ropesim.replay import SimulationReplay
replay = SimulationReplay(result.frames)
print(f"Duration     : {replay.total_time_seconds:.2f} s")
print(f"Peak anchor  : {replay.peak_anchor_force_kn():.2f} kN")
peak_frame = replay.peak_force_frame()
print(f"Rope shape at peak: {replay.frame(peak_frame).link_positions[:3]}")
```

### Rope physics helpers

```python
from ropesim.rope import Rope, RopeDatabase

rope = Rope(RopeDatabase().get("Beal Opera 8.5 Dry"))

# Elongation at a given force
print(rope.elongation_at_force(9.0))         # metres

# Estimated rope diameter under load
for kn in [0, 3, 6, 9, 12]:
    print(f"{kn} kN -> {rope.diameter_under_load(kn):.3f} mm")

# Degradation after repeated falls
worn = rope.degrade(falls_taken=10)
print(worn.retirement_warning(falls_taken=10))

# EN 892 / UIAA 101 compliance check
print(rope.validate_standard_compliance())
```

### Scenario builder (multi-pitch / gear placement)

```python
from ropesim.rope import Rope, RopeDatabase
from ropesim.anchor import AnchorSystem, AnchorType, Bolt
from ropesim.simulate import Scenario

rope = Rope(RopeDatabase().get("Beal Opera 8.5 Dry"))
scenario = Scenario(rope=rope, climber_mass_kg=75.0)

for height in [3.0, 7.0, 12.0]:
    anchor = AnchorSystem(AnchorType.SINGLE_POINT, [Bolt(rated_mbs_kn=25.0)])
    scenario.add_protection(height, anchor, label=f"B{int(height)}")

# Simulate a fall from 15 m
result = scenario.simulate_fall(climber_height_m=15.0)
print(f"Peak: {result.peak_force_kn:.2f} kN  FF: {result.fall_factor:.3f}")

# Sweep all positions
sweep = scenario.sweep_fall_positions(steps=60)
print(f"Worst position: {sweep.worst_height_m:.1f} m -> {sweep.worst_peak_kn:.2f} kN")

# Zipper failure cascade
zipper = scenario.simulate_zipper(climber_height_m=15.0)
print(f"Pieces failed: {zipper.total_pieces_failed}  ground fall: {zipper.ground_fall_reached}")
```

### Haul systems and top-rope / rappel models

```python
from ropesim._rustcore import (
    compute_haul_system_force, HaulSystem,
    compute_top_rope_impact,
    compute_rappel_load,
)

# 3:1 Z-pulley haul system
haul = compute_haul_system_force(load_kg=80.0, system=HaulSystem.ThreeToOne, friction_loss=0.12)
print(f"Theoretical MA : {haul.theoretical_ma}")
print(f"Actual MA      : {haul.actual_ma:.2f}  (friction corrected)")
print(f"Hauler effort  : {haul.hauler_effort_n / 1000:.2f} kN")

# Top-rope catch
tr = compute_top_rope_impact(stiffness_kn=18.0, mass_kg=75.0, slack_m=0.5,
                              rope_length_m=25.0, friction=0.35)
print(f"Top-rope peak  : {tr:.2f} kN")

# Rappel anchor load
rappel = compute_rappel_load(mass_kg=80.0, friction=0.25, speed_mps=1.2, sudden_stop=False)
print(f"Rappel load    : {rappel:.2f} kN")
```

### Anchor systems

```python
from ropesim.anchor import AnchorSystem, AnchorType, Bolt, BoltType, RockType

bolt = Bolt(bolt_type=BoltType.GLUE_IN, rated_mbs_kn=25.0,
            age_years=3, rock_type=RockType.GRANITE)
anchor = AnchorSystem(AnchorType.SLIDING_X, [bolt, bolt])

# Force on each component vs load angle
dist = anchor.load_distribution(load_kn=9.0, load_angle_deg=30)
print(dist)

# How much of each bolt's MBS is left
margins = anchor.safety_margins(load_kn=9.0)
print(margins)

# Progressive failure under extreme load
failure = anchor.simulate_failure(load_kn=40.0)
print(f"Cascade: {failure.cascade_occurred}  failed: {failure.failed_indices}")
```

### Batch parallel sweep (Rust/Rayon)

```python
from ropesim._rustcore import batch_sweep_fall_factors
import numpy as np

fall_factors = np.linspace(0.1, 2.0, 200).tolist()
peak_forces  = batch_sweep_fall_factors(
    mass_kg=80.0,
    ff_values=fall_factors,
    stiffness_kn=20.0,
    belay_friction=0.35,
)
print(f"Max peak: {max(peak_forces):.2f} kN at FF {fall_factors[peak_forces.index(max(peak_forces))]:.2f}")
```

### Visualisations

```python
from ropesim import viz
import matplotlib.pyplot as plt

# Force-time curve
fig, ax = viz.plot_force_curve(result, dark=True)

# Energy budget breakdown
fig, ax = viz.plot_energy_budget(result, dark=True)

# Rope elongation vs applied force
fig, ax = viz.plot_rope_elongation(rope, force_range=(0, 15), dark=True)

# Rope diameter under load
fig, ax = viz.plot_diameter_under_load(rope, force_range=(0, 12), dark=True)

# Anchor force distribution vs load angle
fig, ax = viz.plot_anchor_distribution(anchor, load_kn=9.0, dark=True)

# Compare multiple ropes / scenarios on one chart
fig, ax = viz.plot_comparison([result1, result2], ["Rope A", "Rope B"], dark=True)

plt.show()
```

### Jupyter notebook integration

RopeSim ships rich HTML reprs for all major objects — they render automatically
in JupyterLab and VS Code notebooks without any extra calls.

```python
import ropesim.notebook  # activates _repr_html_ patches

rope     # renders as HTML spec card with EN 892 compliance badge
result   # renders as summary table + inline force-time curve PNG
anchor   # renders as inline SVG bolt-and-sling diagram
```

Five example notebooks are included in `notebooks/`:

| Notebook | Contents |
|----------|----------|
| `01_basic_fall_simulation.ipynb` | Fundamentals, device comparison, force curve |
| `02_anchor_comparison.ipynb` | Sliding-X / quad / cordelette angle sweep, heatmaps |
| `03_rope_database_exploration.ipynb` | Scatter plots, bar charts, retirement calculator |
| `04_scenario_builder.ipynb` | Trad pitch, position sweep, zipper analysis |
| `05_rapier_3d_simulation.ipynb` | PyRopeSimWorld, SimulationReplay, 3D link plots |

---

## CLI

RopeSim ships a unified `ropesim-cli` entry point with subcommand groups.

```bash
# ---- Rope database ----
ropesim-cli rope list                              # all ropes
ropesim-cli rope list --type dry_single --diameter 9.5
ropesim-cli rope show "Beal Opera 8.5 Dry"        # full spec card
ropesim-cli rope compare "Beal Opera 8.5 Dry" "Mammut Crag Classic 10.2"
ropesim-cli rope add                               # interactive prompt
ropesim-cli rope import --file my_rope.json
ropesim-cli rope retire "Mammut Crag Classic 10.2" --falls-taken 40

# ---- Scenario runner ----
ropesim-cli scenario run    --file pitch.json --height 15
ropesim-cli scenario sweep  --file pitch.json
ropesim-cli scenario zipper --file pitch.json --height 15
ropesim-cli scenario build                         # interactive builder

# ---- Validation ----
ropesim-cli validate rope     --name "Mammut Crag Classic 10.2"
ropesim-cli validate scenario --file pitch.json
ropesim-cli validate system   --rope "Beal Opera 8.5 Dry" --load 80

# ---- Reports ----
ropesim-cli report --scenario pitch.json           # multi-page PDF

# ---- Specialty calculations ----
ropesim-cli toprope --rope "Beal Opera 8.5 Dry" --slack 0.5
ropesim-cli rappel  --mass 80
ropesim-cli haul    --system 3:1 --load 80

# ---- Classic commands (still available) ----
ropesim-cli simulate --mass 80 --fall-dist 8 --rope-out 20 \
    --rope "Beal Opera 8.5 Dry" --device grigri
ropesim-cli anchor --type sliding_x --load 9.5 --angle 60
ropesim-cli sweep  --rope "Mammut Crag Classic 10.2" --mass 80 --steps 20
ropesim-cli validate-rope --name "Mammut Crag Classic 10.2"
ropesim-cli list-ropes

# ---- REPL ----
ropesim-cli interactive        # Python REPL with all ropesim symbols pre-loaded
```

Add `--format json` (or `--json` on classic commands) to any command for
machine-readable output.

---

## GUI

The v3 desktop app is a **native C++ Qt6 application** (the PySide6 GUI is
retired). Grab it from [Releases](https://github.com/Londopy/ropesim/releases)
or build from source:

```bash
cargo build --release --manifest-path ropesim/_rustcore/Cargo.toml
cd gui-cpp && cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build
./build/ropesim-gui
```

```bash
ropesim tui      # terminal UI
ropesim gui      # where to get the desktop app
```

### Demo mode

The fastest way to see everything in action: **File → Open Scenario** and load
`examples/demo_route.ropesim`, then press `F6` (Rapier 3D run). You get a
realistic mixed sport route, a recorded fall replay in the 3D viewport, force
arrows on every piece, and a full results panel. `F8` sweeps the anchor angle.

**Manual workflow:**
1. Select a rope from the left panel
2. Click **+ Bolt**, **+ Cam**, or **+ Nut** to place protection on the wall
3. Set climber mass and height
4. Press **Run Fall Simulation** — watch the animation, results appear in the right panel
5. Press **Sweep All Positions** to see peak force vs climber height across the whole route
6. **Zipper Analysis** models sequential gear-ripping under high loads
7. Toggle **[2D] / [3D]** in the toolbar to switch between the 2D canvas and the 3D Vispy viewport
8. Toggle **[Analytical] / [Rapier 3D]** to switch physics modes (3D mode mirrors to 3D viewport automatically)
9. Export results as PDF or CSV from the File menu

**Keyboard shortcuts:**

| Key | Action |
|-----|--------|
| `F8` | Demo route (auto-build + simulate) |
| `F5` | Run fall simulation |
| `F6` | Sweep all positions |
| `F7` | Zipper analysis |
| `B` / `C` / `N` | Add bolt / cam / nut |
| `F` | Fit canvas to view |
| `Ctrl+Scroll` | Zoom canvas |
| `Middle-drag` | Pan canvas |
| `Delete` | Remove selected gear |
| `R` (3D view) | Reset camera |

**3D viewport controls (when 3D tab is active):**

| Input | Action |
|-------|--------|
| Left-drag | Orbit / turntable rotate |
| Middle-drag | Pan |
| Scroll | Zoom |
| `R` | Reset to default view |
| Front / Side / Top / Iso buttons | Preset camera angles |
| Play / Pause / Stop bar | Frame-by-frame Rapier playback |
| Speed selector | 0.1x to 2x playback speed |

---

## Physics model

### Analytical mode (default)

The impact force is computed using the UIAA 101 analytic formula:

```
F = mg + sqrt((mg)^2 + 2*mg*ff*k_eff)
```

where `k_eff` is the length-normalised rope stiffness back-calculated from the
EN 892 test-mass drop (80 kg, fall factor 1.77).  The full force-time curve is
obtained by integrating the damped spring equation with a 4th-order
Runge-Kutta solver at 1 ms resolution.

Modifiers applied:

- **Belay device friction** (Grigri: 55 %, ATC: 35 %, Munter: 45 % ...)
- **Guide-mode self-locking friction** for Reverso Guide / Mega Jul / Giga Jul / Click Up / I-Device / Sum: `mu_eff(F) = min(mu_base + k_lock * F_kN, mu_max)`, solved by fixed-point iteration
- **Wet rope** +12 % impact force (EN 892 s.6.1.3)
- **Temperature** — stiffness increases ~2 % per 10 deg C below 20 deg C
- **Rope age / degradation** — elongation and stiffness drift modelled from published UIAA fatigue data
- **Edge friction** — rope running over a ledge reduces effective belay friction

### Rapier 3D mode (optional)

When `PhysicsMode.RAPIER_3D` is requested, the rope is modelled as a chain of
capsule rigid bodies connected by `SphericalJoint` constraints inside a full
Rapier3D 0.21 pipeline (broad phase, narrow phase, CCD, island manager).

Force estimation uses momentum change: `F = m*(dv/dt - g)`.  Results are
returned as a `SimulationResult` carrying per-frame `SimFrame` snapshots that
can be replayed with `SimulationReplay`.

---

## Development

```bash
# Install dev dependencies
pip install -e ".[dev,gui]"

# Run tests (no Rust required)
pytest -m "not requires_rust"

# Run full suite (after maturin develop)
pytest

# Benchmarks
pytest -m benchmark --benchmark-only
```

---

## License

MIT — see [LICENSE](LICENSE).

