ropesim 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. ropesim-0.1.0/Cargo.lock +232 -0
  2. ropesim-0.1.0/Cargo.toml +3 -0
  3. ropesim-0.1.0/LICENSE +21 -0
  4. ropesim-0.1.0/PKG-INFO +255 -0
  5. ropesim-0.1.0/README.md +213 -0
  6. ropesim-0.1.0/pyproject.toml +80 -0
  7. ropesim-0.1.0/ropesim/__init__.py +73 -0
  8. ropesim-0.1.0/ropesim/_rustcore/Cargo.toml +15 -0
  9. ropesim-0.1.0/ropesim/_rustcore/src/anchor.rs +352 -0
  10. ropesim-0.1.0/ropesim/_rustcore/src/batch.rs +141 -0
  11. ropesim-0.1.0/ropesim/_rustcore/src/lib.rs +22 -0
  12. ropesim-0.1.0/ropesim/_rustcore/src/physics.rs +406 -0
  13. ropesim-0.1.0/ropesim/anchor.py +347 -0
  14. ropesim-0.1.0/ropesim/cli.py +378 -0
  15. ropesim-0.1.0/ropesim/database/__init__.py +0 -0
  16. ropesim-0.1.0/ropesim/database/anchors.json +88 -0
  17. ropesim-0.1.0/ropesim/database/db.py +28 -0
  18. ropesim-0.1.0/ropesim/database/ropes.json +477 -0
  19. ropesim-0.1.0/ropesim/fall.py +268 -0
  20. ropesim-0.1.0/ropesim/gui/__init__.py +11 -0
  21. ropesim-0.1.0/ropesim/gui/canvas.py +442 -0
  22. ropesim-0.1.0/ropesim/gui/dialogs/__init__.py +10 -0
  23. ropesim-0.1.0/ropesim/gui/dialogs/gear_editor.py +312 -0
  24. ropesim-0.1.0/ropesim/gui/dialogs/rope_editor.py +224 -0
  25. ropesim-0.1.0/ropesim/gui/main.py +30 -0
  26. ropesim-0.1.0/ropesim/gui/main_window.py +495 -0
  27. ropesim-0.1.0/ropesim/gui/models.py +212 -0
  28. ropesim-0.1.0/ropesim/gui/properties_panel.py +195 -0
  29. ropesim-0.1.0/ropesim/gui/results_panel.py +221 -0
  30. ropesim-0.1.0/ropesim/gui/style.py +277 -0
  31. ropesim-0.1.0/ropesim/gui/workers.py +77 -0
  32. ropesim-0.1.0/ropesim/rope.py +336 -0
  33. ropesim-0.1.0/ropesim/simulate.py +361 -0
  34. ropesim-0.1.0/ropesim/standards.py +194 -0
  35. ropesim-0.1.0/ropesim/units.py +105 -0
  36. ropesim-0.1.0/ropesim/viz.py +478 -0
@@ -0,0 +1,232 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.5.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
10
+
11
+ [[package]]
12
+ name = "cfg-if"
13
+ version = "1.0.4"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
16
+
17
+ [[package]]
18
+ name = "crossbeam-deque"
19
+ version = "0.8.6"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
22
+ dependencies = [
23
+ "crossbeam-epoch",
24
+ "crossbeam-utils",
25
+ ]
26
+
27
+ [[package]]
28
+ name = "crossbeam-epoch"
29
+ version = "0.9.18"
30
+ source = "registry+https://github.com/rust-lang/crates.io-index"
31
+ checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
32
+ dependencies = [
33
+ "crossbeam-utils",
34
+ ]
35
+
36
+ [[package]]
37
+ name = "crossbeam-utils"
38
+ version = "0.8.21"
39
+ source = "registry+https://github.com/rust-lang/crates.io-index"
40
+ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
41
+
42
+ [[package]]
43
+ name = "either"
44
+ version = "1.15.0"
45
+ source = "registry+https://github.com/rust-lang/crates.io-index"
46
+ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
47
+
48
+ [[package]]
49
+ name = "heck"
50
+ version = "0.5.0"
51
+ source = "registry+https://github.com/rust-lang/crates.io-index"
52
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
53
+
54
+ [[package]]
55
+ name = "indoc"
56
+ version = "2.0.7"
57
+ source = "registry+https://github.com/rust-lang/crates.io-index"
58
+ checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
59
+ dependencies = [
60
+ "rustversion",
61
+ ]
62
+
63
+ [[package]]
64
+ name = "libc"
65
+ version = "0.2.186"
66
+ source = "registry+https://github.com/rust-lang/crates.io-index"
67
+ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
68
+
69
+ [[package]]
70
+ name = "memoffset"
71
+ version = "0.9.1"
72
+ source = "registry+https://github.com/rust-lang/crates.io-index"
73
+ checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
74
+ dependencies = [
75
+ "autocfg",
76
+ ]
77
+
78
+ [[package]]
79
+ name = "once_cell"
80
+ version = "1.21.4"
81
+ source = "registry+https://github.com/rust-lang/crates.io-index"
82
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
83
+
84
+ [[package]]
85
+ name = "portable-atomic"
86
+ version = "1.13.1"
87
+ source = "registry+https://github.com/rust-lang/crates.io-index"
88
+ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
89
+
90
+ [[package]]
91
+ name = "proc-macro2"
92
+ version = "1.0.106"
93
+ source = "registry+https://github.com/rust-lang/crates.io-index"
94
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
95
+ dependencies = [
96
+ "unicode-ident",
97
+ ]
98
+
99
+ [[package]]
100
+ name = "pyo3"
101
+ version = "0.22.6"
102
+ source = "registry+https://github.com/rust-lang/crates.io-index"
103
+ checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884"
104
+ dependencies = [
105
+ "cfg-if",
106
+ "indoc",
107
+ "libc",
108
+ "memoffset",
109
+ "once_cell",
110
+ "portable-atomic",
111
+ "pyo3-build-config",
112
+ "pyo3-ffi",
113
+ "pyo3-macros",
114
+ "unindent",
115
+ ]
116
+
117
+ [[package]]
118
+ name = "pyo3-build-config"
119
+ version = "0.22.6"
120
+ source = "registry+https://github.com/rust-lang/crates.io-index"
121
+ checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38"
122
+ dependencies = [
123
+ "once_cell",
124
+ "target-lexicon",
125
+ ]
126
+
127
+ [[package]]
128
+ name = "pyo3-ffi"
129
+ version = "0.22.6"
130
+ source = "registry+https://github.com/rust-lang/crates.io-index"
131
+ checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636"
132
+ dependencies = [
133
+ "libc",
134
+ "pyo3-build-config",
135
+ ]
136
+
137
+ [[package]]
138
+ name = "pyo3-macros"
139
+ version = "0.22.6"
140
+ source = "registry+https://github.com/rust-lang/crates.io-index"
141
+ checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453"
142
+ dependencies = [
143
+ "proc-macro2",
144
+ "pyo3-macros-backend",
145
+ "quote",
146
+ "syn",
147
+ ]
148
+
149
+ [[package]]
150
+ name = "pyo3-macros-backend"
151
+ version = "0.22.6"
152
+ source = "registry+https://github.com/rust-lang/crates.io-index"
153
+ checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe"
154
+ dependencies = [
155
+ "heck",
156
+ "proc-macro2",
157
+ "pyo3-build-config",
158
+ "quote",
159
+ "syn",
160
+ ]
161
+
162
+ [[package]]
163
+ name = "quote"
164
+ version = "1.0.45"
165
+ source = "registry+https://github.com/rust-lang/crates.io-index"
166
+ checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
167
+ dependencies = [
168
+ "proc-macro2",
169
+ ]
170
+
171
+ [[package]]
172
+ name = "rayon"
173
+ version = "1.12.0"
174
+ source = "registry+https://github.com/rust-lang/crates.io-index"
175
+ checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
176
+ dependencies = [
177
+ "either",
178
+ "rayon-core",
179
+ ]
180
+
181
+ [[package]]
182
+ name = "rayon-core"
183
+ version = "1.13.0"
184
+ source = "registry+https://github.com/rust-lang/crates.io-index"
185
+ checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
186
+ dependencies = [
187
+ "crossbeam-deque",
188
+ "crossbeam-utils",
189
+ ]
190
+
191
+ [[package]]
192
+ name = "ropesim-rustcore"
193
+ version = "0.1.0"
194
+ dependencies = [
195
+ "pyo3",
196
+ "rayon",
197
+ ]
198
+
199
+ [[package]]
200
+ name = "rustversion"
201
+ version = "1.0.22"
202
+ source = "registry+https://github.com/rust-lang/crates.io-index"
203
+ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
204
+
205
+ [[package]]
206
+ name = "syn"
207
+ version = "2.0.117"
208
+ source = "registry+https://github.com/rust-lang/crates.io-index"
209
+ checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
210
+ dependencies = [
211
+ "proc-macro2",
212
+ "quote",
213
+ "unicode-ident",
214
+ ]
215
+
216
+ [[package]]
217
+ name = "target-lexicon"
218
+ version = "0.12.16"
219
+ source = "registry+https://github.com/rust-lang/crates.io-index"
220
+ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
221
+
222
+ [[package]]
223
+ name = "unicode-ident"
224
+ version = "1.0.24"
225
+ source = "registry+https://github.com/rust-lang/crates.io-index"
226
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
227
+
228
+ [[package]]
229
+ name = "unindent"
230
+ version = "0.2.4"
231
+ source = "registry+https://github.com/rust-lang/crates.io-index"
232
+ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
@@ -0,0 +1,3 @@
1
+ [workspace]
2
+ members = ["ropesim/_rustcore"]
3
+ resolver = "2"
ropesim-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ropesim contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ropesim-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,255 @@
1
+ Metadata-Version: 2.4
2
+ Name: ropesim
3
+ Version: 0.1.0
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Intended Audience :: Science/Research
6
+ Classifier: Intended Audience :: Education
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Rust
14
+ Classifier: Topic :: Scientific/Engineering :: Physics
15
+ Classifier: Topic :: Scientific/Engineering :: Visualization
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: numpy>=1.26
18
+ Requires-Dist: pydantic>=2.0
19
+ Requires-Dist: matplotlib>=3.8
20
+ Requires-Dist: pyside6>=6.6 ; extra == 'all'
21
+ Requires-Dist: pytest>=7.4 ; extra == 'all'
22
+ Requires-Dist: pytest-benchmark>=4.0 ; extra == 'all'
23
+ Requires-Dist: maturin>=1.5 ; extra == 'all'
24
+ Requires-Dist: pytest>=7.4 ; extra == 'dev'
25
+ Requires-Dist: pytest-benchmark>=4.0 ; extra == 'dev'
26
+ Requires-Dist: maturin>=1.5 ; extra == 'dev'
27
+ Requires-Dist: pyside6>=6.6 ; extra == 'gui'
28
+ Provides-Extra: all
29
+ Provides-Extra: dev
30
+ Provides-Extra: gui
31
+ License-File: LICENSE
32
+ Summary: Climbing rope physics engine and GUI simulator
33
+ Keywords: climbing,physics,rope,simulation,UIAA,EN892,safety
34
+ Author-email: Londopy <ropesim@pxdmail.com>
35
+ Requires-Python: >=3.10
36
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
37
+ Project-URL: Changelog, https://github.com/Londopy/ropesim/blob/main/CHANGELOG.md
38
+ Project-URL: Homepage, https://github.com/Londopy/ropesim
39
+ Project-URL: Issues, https://github.com/Londopy/ropesim/issues
40
+ Project-URL: Repository, https://github.com/Londopy/ropesim
41
+
42
+ # RopeSim
43
+
44
+ **Climbing rope physics engine and GUI simulator — UIAA 101 / EN 892**
45
+
46
+ RopeSim models lead-fall dynamics using a damped spring / RK4 integration in
47
+ Rust, exposed to Python via PyO3/Maturin. It ships a full Python API, a
48
+ five-command CLI, and a PySide6 desktop GUI.
49
+
50
+ [![CI](https://github.com/Londopy/ropesim/actions/workflows/ci.yml/badge.svg)](https://github.com/Londopy/ropesim/actions/workflows/ci.yml)
51
+ [![PyPI](https://img.shields.io/pypi/v/ropesim)](https://pypi.org/project/ropesim/)
52
+ [![Python 3.10+](https://img.shields.io/pypi/pyversions/ropesim)](https://pypi.org/project/ropesim/)
53
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
54
+
55
+ ---
56
+
57
+ ## Features
58
+
59
+ - **UIAA 101 / EN 892** impact-force model with belay-device friction, wet-rope modifier, and temperature correction
60
+ - **RK4 force-time curve** — full damped spring integration in Rust for accurate energy modelling
61
+ - **Parallel batch sweeps** via Rayon — sweep 200 fall positions in milliseconds
62
+ - **Anchor system physics** — sliding-X, quad, 2-bolt cordelette with load distribution
63
+ - **25-rope database** covering Beal, Mammut, Sterling, Petzl, Edelrid, Black Diamond, and more
64
+ - **PySide6 GUI** — drag-and-drop route builder, live simulation, fall animation, matplotlib plots, PDF/CSV export
65
+ - **CLI** — five commands: `simulate`, `anchor`, `list-ropes`, `validate-rope`, `sweep`
66
+
67
+ ---
68
+
69
+ ## Installation
70
+
71
+ ### Pre-built wheel (recommended)
72
+
73
+ ```bash
74
+ pip install ropesim # physics library + CLI only
75
+ pip install "ropesim[gui]" # + PySide6 GUI
76
+ ```
77
+
78
+ ### From source (requires Rust toolchain)
79
+
80
+ ```bash
81
+ git clone https://github.com/Londopy/ropesim.git
82
+ cd ropesim
83
+ pip install maturin
84
+ maturin develop --release # compiles Rust, installs in editable mode
85
+ pip install ".[gui]" # optional GUI deps
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Quick start — Python API
91
+
92
+ ```python
93
+ from ropesim.rope import RopeDatabase
94
+ from ropesim.fall import FallConditions, Fall, BelayDevice
95
+
96
+ # Load a rope from the bundled database
97
+ spec = RopeDatabase().get("Mammut Crag Classic 10.2")
98
+
99
+ # Simulate a factor-0.5 fall on 20 m of rope
100
+ conditions = FallConditions(
101
+ climber_mass_kg=80.0,
102
+ fall_distance_m=10.0, # fell 2 × 5 m above last pro
103
+ rope_out_m=20.0,
104
+ belay_device=BelayDevice.GRIGRI,
105
+ rope=spec,
106
+ )
107
+ result = Fall(conditions).simulate()
108
+
109
+ print(f"Peak impact force : {result.peak_force_kn:.2f} kN")
110
+ print(f"Fall factor : {result.fall_factor:.3f}")
111
+ print(f"Energy absorbed : {result.energy_budget.rope_absorption_j:.0f} J")
112
+ print(f"Warnings : {result.warnings or 'none'}")
113
+ ```
114
+
115
+ ### Scenario builder (multi-pitch / gear placement)
116
+
117
+ ```python
118
+ from ropesim.rope import Rope, RopeDatabase
119
+ from ropesim.anchor import AnchorSystem, AnchorType, Bolt
120
+ from ropesim.simulate import Scenario
121
+
122
+ rope = Rope(RopeDatabase().get("Beal Opera 8.5 Dry"))
123
+ scenario = Scenario(rope=rope, climber_mass_kg=75.0)
124
+
125
+ # Place three bolts
126
+ for height in [3.0, 7.0, 12.0]:
127
+ anchor = AnchorSystem(AnchorType.SINGLE_POINT, [Bolt(rated_mbs_kn=25.0)])
128
+ scenario.add_protection(height, anchor, label=f"B{int(height)}")
129
+
130
+ # Simulate a fall from 15 m
131
+ result = scenario.simulate_fall(climber_height_m=15.0)
132
+ print(f"Peak: {result.peak_force_kn:.2f} kN FF: {result.fall_factor:.3f}")
133
+
134
+ # Sweep all positions
135
+ sweep = scenario.sweep_fall_positions(steps=60)
136
+ print(f"Worst position: {sweep.worst_height_m:.1f} m → {sweep.worst_peak_kn:.2f} kN")
137
+ ```
138
+
139
+ ### Batch parallel sweep (Rust/Rayon)
140
+
141
+ ```python
142
+ from ropesim._rustcore import batch_sweep_fall_factors
143
+ import numpy as np
144
+
145
+ fall_factors = np.linspace(0.1, 2.0, 200).tolist()
146
+ peak_forces = batch_sweep_fall_factors(
147
+ mass_kg=80.0,
148
+ ff_values=fall_factors,
149
+ stiffness_kn=20.0,
150
+ belay_friction=0.35,
151
+ )
152
+ print(f"Max peak: {max(peak_forces):.2f} kN at FF {fall_factors[peak_forces.index(max(peak_forces))]:.2f}")
153
+ ```
154
+
155
+ ---
156
+
157
+ ## CLI
158
+
159
+ ```bash
160
+ # List all ropes in the database
161
+ ropesim-cli list-ropes
162
+
163
+ # Simulate a single fall
164
+ ropesim-cli simulate --mass 80 --fall-distance 8 --rope-out 20 --device grigri
165
+
166
+ # Anchor force distribution
167
+ ropesim-cli anchor --type sliding_x --load-kn 9.5 --angle 60
168
+
169
+ # Sweep fall positions
170
+ ropesim-cli sweep --mass 80 --rope "Beal Opera 8.5 Dry" --steps 50
171
+
172
+ # Validate rope data
173
+ ropesim-cli validate-rope --name "Mammut Crag Classic 10.2"
174
+ ```
175
+
176
+ Add `--json` to any command for machine-readable output.
177
+
178
+ ---
179
+
180
+ ## GUI
181
+
182
+ ```bash
183
+ ropesim # launch the desktop GUI (requires pip install "ropesim[gui]")
184
+ ```
185
+
186
+ <!-- Screenshot placeholder -->
187
+ <!-- ![RopeSim GUI](docs/screenshot.png) -->
188
+
189
+ **Workflow:**
190
+ 1. Select a rope from the left panel
191
+ 2. Double-click the canvas or use the toolbar to place bolts, cams, or nuts
192
+ 3. Set climber mass and height
193
+ 4. Press **F5** (or ▶ Run) to simulate a fall — watch the animation
194
+ 5. Press **F6** to sweep all positions and see the force-vs-height plot
195
+ 6. Export results as PDF or CSV from the File menu
196
+
197
+ **Keyboard shortcuts:**
198
+
199
+ | Key | Action |
200
+ |-----|--------|
201
+ | `F5` | Run fall simulation |
202
+ | `F6` | Sweep all positions |
203
+ | `F7` | Zipper analysis |
204
+ | `B` / `C` / `N` | Add bolt / cam / nut |
205
+ | `F` | Fit canvas to view |
206
+ | `Ctrl+Scroll` | Zoom canvas |
207
+ | `Middle-drag` | Pan canvas |
208
+ | `Delete` | Remove selected gear |
209
+
210
+ ---
211
+
212
+ ## Physics model
213
+
214
+ The impact force is computed using the UIAA 101 analytic formula:
215
+
216
+ ```
217
+ F = mg + √((mg)² + 2·mg·ff·k_eff)
218
+ ```
219
+
220
+ where `k_eff` is the length-normalised rope stiffness back-calculated from the
221
+ EN 892 test-mass drop (80 kg, fall factor 1.77). The full force-time curve is
222
+ obtained by integrating the damped spring equation with a 4th-order
223
+ Runge-Kutta solver at 1 ms resolution.
224
+
225
+ Modifiers applied:
226
+ - **Belay device friction** (Grigri: 55%, ATC: 35%, Munter: 45% …)
227
+ - **Wet rope** +12 % impact force (EN 892 §6.1.3)
228
+ - **Temperature** — stiffness increases ~2 % per 10 °C below 20 °C
229
+ - **Rope age / degradation** — elongation and stiffness drift modelled from
230
+ published UIAA fatigue data
231
+
232
+ ---
233
+
234
+ ## Development
235
+
236
+ ```bash
237
+ # Install dev dependencies
238
+ pip install -e ".[dev,gui]"
239
+
240
+ # Run tests (no Rust required)
241
+ pytest -m "not requires_rust"
242
+
243
+ # Run full suite (after maturin develop)
244
+ pytest
245
+
246
+ # Benchmarks
247
+ pytest -m benchmark --benchmark-only
248
+ ```
249
+
250
+ ---
251
+
252
+ ## License
253
+
254
+ MIT — see [LICENSE](LICENSE).
255
+