ripopt 0.7.1

A memory-safe interior point optimizer in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
# ripopt Release Checklist

A reusable checklist for cutting a new ripopt release. Copy this file when
starting a release and check items off as you go. The order matters: bench
first → docs → versions → verify interfaces → manuscript → tag → publish.

Replace `vX.Y.Z` with the new version number throughout (e.g. `v0.6.2`).

---

## 1. Pre-release verification (clean tree)

- [ ] `git status` — working tree understood (no surprise files)
- [ ] On the correct branch (usually `main`)
- [ ] Pulled latest from origin
- [ ] `cargo check --workspace --all-targets` — no errors
- [ ] `cargo check --examples` — every example compiles (catches stale trait sigs)
- [ ] `cargo build --release` — release build clean, no **new** warnings
  introduced since the previous release (a handful of pre-existing
  `unused_variables`/`unused_imports` warnings in `src/preprocessing.rs` and
  `src/c_api.rs` are tracked separately; the release gate is that the
  warning count has not grown)
- [ ] `cargo test --release --no-fail-fast`**all** test binaries green
  (use `--no-fail-fast` so a single failure doesn't hide later ones)
- [ ] `cargo test --release -p rmumps` — workspace member tests green
- [ ] `cargo doc --no-deps` — rustdoc builds without warnings
- [ ] **Code coverage**: run `cargo llvm-cov test` and update the coverage
  table in `README.md` with current numbers (this is a release gate)
- [ ] Optional `cargo clippy --workspace --all-targets -- -D warnings`
- [ ] Optional `cargo fmt --check`

---

## 2. Run the full benchmark suite

- [ ] `make benchmark` — runs HS + CUTEst + large-scale + domain + report
  (~2 hours; do this on a quiet machine)
- [ ] Inspect `benchmarks/BENCHMARK_REPORT.md` — sanity-check the headline numbers
- [ ] Note any regressions vs. the previous release; investigate before tagging
- [ ] Verify all sub-suites actually ran (HS, CUTEst, Electrolyte, Grid, CHO,
  large-scale, gas, water) — if compilation errors silently skipped a suite, the
  numbers will be stale. Check `benchmarks/large_scale/large_scale_results.txt`
  is real output, not error spew.
- [ ] `make gas-run` — gas pipeline NLPs (4 AMPL `.nl` problems). Does NOT
  feed into `BENCHMARK_REPORT.md`; inspect the per-problem console output and
  `benchmarks/gas/*.sol` files manually.
- [ ] `make water-run` — water distribution network NLPs (6 AMPL `.nl`
  problems from MINLPLib). Does NOT feed into `BENCHMARK_REPORT.md`; inspect
  the per-problem console output and `benchmarks/water/*.sol` files manually.

### Domain benchmarks to re-run individually (for debugging a single suite)

`make benchmark` already runs HS, CUTEst, electrolyte, grid, CHO, and
large-scale. Use the targets below to re-run **one** suite in isolation
when you're debugging a regression without paying the full ~2 hours:

- [ ] `make hs-run` — HS suite
- [ ] `make electrolyte-run` — electrolyte thermodynamics
- [ ] `make grid-run` — Grid (AC OPF)
- [ ] `make cho-run` — CHO parameter estimation (if applicable)
- [ ] `make gas-run` — gas pipeline NLPs (also listed above — not in `make benchmark`)
- [ ] `make water-run` — water distribution network NLPs (also listed above — not in `make benchmark`)
- [ ] CUTEst full sweep: `RESULTS_FILE=benchmarks/cutest/results.json cargo run --bin cutest_suite --features cutest,ipopt-native --release`

---

## 3. Update documentation with fresh numbers

Headline metrics that propagate everywhere: HS solved/total, CUTEst
solved/total, both-solve count, geo-mean speedup, median speedup, ripopt-only
and Ipopt-only counts, electrolyte/grid results.

- [ ] `README.md`
  - HS suite table
  - CUTEst suite table
  - Domain benchmarks table
  - Large-scale benchmark table
  - "Interpreting the speed numbers" prose
  - Inline `RIPOPT_VERSION` comment near the C API example
- [ ] `docs/src/benchmarks.md` — mirror of README benchmark sections
- [ ] `docs/src/introduction.md` and other `docs/src/*.md` if API/CLI changed
- [ ] `docs/src/SUMMARY.md` — add entries for any new mdbook pages introduced
  this release (the mdbook TOC does not auto-discover)
- [ ] `mdbook build docs` — rebuild the book and confirm no broken links or
  missing-page warnings
- [ ] `benchmarks/hs/PERFORMANCE_COMPARISON.md` — full HS-specific report
- [ ] `RIPOPT_VS_IPOPT.md` — strategic comparison narrative
- [ ] `benchmarks/electrolyte/electrolyte_benchmark_report.md` — if domain-specific numbers changed
- [ ] `benchmarks/grid/grid_benchmark_report.md` — same
- [ ] `MEMORY.md` (in `~/.claude/projects/...ripopt/memory/`) —
  HS Benchmark Status section, CUTEst Benchmark Status section
- [ ] `narrative-history.org` — append a new chapter in the same literary
  style covering the work since the last narrative update. Find the prior
  cutoff with `git log -1 --format=%H -- narrative-history.org`, then walk
  `git log --first-parent <prev>..HEAD --format="%ai %h %s"` to gather
  commit messages and dates. Group by theme/issue (not strict chronology),
  cite specific commits and memory-file lessons where they sharpen the
  story, and keep the honesty rule visible. Append after the existing final
  section so prior chapters stay intact.

---

## 4. CHANGELOG

- [ ] Add a new `## [X.Y.Z] - YYYY-MM-DD` section at the top of `CHANGELOG.md`
- [ ] Group changes under `### Added`, `### Changed`, `### Fixed`,
  `### Performance`, `### Notes`
- [ ] Walk `git log <prev-tag>..HEAD --no-merges` to make sure you didn't miss
  anything
- [ ] Mention every test/regression that was fixed during the release prep
- [ ] Include the workspace version bump line (ripopt + rmumps)

---

## 5. Bump version numbers (every place that hard-codes a version)

These must all match. A grep for the **old** version after bumping is the
safest way to confirm nothing was missed. Use two passes: one for quoted
versions (Cargo/pyproject/Markdown prose) and one for raw numeric tokens
(the C header's `#define` macros are unquoted):

```
# Pass 1: quoted "X.Y.Z" occurrences
grep -rn '"X\.Y\.Z"' --include='*.toml' --include='*.h' --include='*.rs' \
     --include='*.md' --include='*.py' . \
  | grep -v target/ | grep -v ref/ | grep -v benchmarks/

# Pass 2: unquoted version tokens in the C header + inline constants
grep -rn -E '\b[0-9]+\.[0-9]+\.[0-9]+\b' ripopt.h
```

- [ ] `Cargo.toml``[package].version`
- [ ] `Cargo.toml``rmumps = { version = "...", path = "rmumps", ... }`
  dependency line (must match `rmumps/Cargo.toml`)
- [ ] `rmumps/Cargo.toml``[package].version` (bump even for ripopt-only
  changes if rmumps is also being released; otherwise leave it)
- [ ] `ripopt.h``RIPOPT_VERSION_MAJOR`, `_MINOR`, `_PATCH`,
  and the `RIPOPT_VERSION "X.Y.Z"` string literal
- [ ] `README.md` — the inline `// "X.Y.Z"` comment in the C API example
- [ ] `pyomo-ripopt/pyproject.toml``[project].version`
- [ ] `pyomo-ripopt/pyomo_ripopt.egg-info/PKG-INFO` — auto-regenerates on
  `pip install`, but verify it after the install step below
- [ ] `ripopt-py/Cargo.toml``[package].version`
- [ ] `ripopt-py/pyproject.toml``[project].version` (must match the
  `ripopt-py/Cargo.toml` version; both are bumped together with the main
  `ripopt` crate so a single `vX.Y.Z` tag publishes consistent binaries to
  PyPI via `.github/workflows/publish-ripopt-py.yml`)
- [ ] `Ripopt.jl/Project.toml`**only if** there are changes that affect
  the Julia binding (FFI signature changes, new C API functions, behavior
  changes Ripopt.jl exposes). Otherwise leave Ripopt.jl on its current
  version. The Julia binding has an independent release cadence.
- [ ] Run `cargo check` after bumping to refresh `Cargo.lock`
- [ ] **Reminder for Section 7b:** any version pin in
  `manuscript/supporting-information.org` (ripopt version, rmumps version,
  Ipopt version, rustc version) must also be bumped. The SI version-pin
  check in Section 7b will catch this, but it's worth noting here so the
  bump is on your radar in one place.

---

## 6. Verify each language interface end-to-end

Each interface should be exercised against the freshly built binary, not a
stale install. Best done in order: native Rust → C → AMPL → Pyomo plugin →
ripopt-py bindings → Julia → GAMS → tutorials, since each later layer
depends (directly or indirectly) on the ones before it.

### 6a. Native Rust library

- [ ] `cargo run --release --example hs071` — known-good Rust example
- [ ] `cargo run --release --example rosenbrock` (or equivalent unconstrained)
- [ ] One large-scale example to exercise sparse path

### 6b. C API + shared library

- [ ] `cargo build --release` produces `target/release/libripopt.{dylib,so}`
- [ ] `ripopt.h` `RIPOPT_VERSION` matches `Cargo.toml`
- [ ] `make test-c` — compiles and runs the bundled C clients
  (`examples/c_api_test.c`, `examples/c_rosenbrock.c`, `examples/c_hs035.c`,
  `examples/c_example_with_options.c`) against the freshly built
  `libripopt.{dylib,so}`
- [ ] `make install` then `ripopt --version` reports the new version

### 6c. AMPL/NL solver binary

- [ ] `cargo build --release --bin ripopt` (the AMPL solver binary)
- [ ] `ripopt --version` and `ripopt --help` work
- [ ] Solve at least one `.nl` file (e.g. an HS problem) and verify it
  reports correct status + objective
- [ ] Confirm `~/.cargo/bin/ripopt` (after `make install`) is on `$PATH` so
  `pyomo-ripopt`'s `SolverFactory('ripopt')` can resolve it in §6d

### 6d. Pyomo solver plugin

- [ ] `pip install -e ./pyomo-ripopt` from a clean Python env
- [ ] `python -c "from pyomo.environ import SolverFactory; s = SolverFactory('ripopt'); print(s.available())"`
- [ ] Run an end-to-end Pyomo model and verify `result.solver.status == 'ok'`
- [ ] Confirm `pyomo_ripopt.egg-info/PKG-INFO` shows the new version
- [ ] If publishing to PyPI: `python -m build` then check the wheel metadata

### 6e. Direct Python bindings (`ripopt-py`, PyPI name `ripopt`)

- [ ] `pip install -e ./ripopt-py` from a clean Python env (builds via maturin)
- [ ] `python -c "from ripopt import minimize; print(minimize)"`
- [ ] Run a small HS/Rosenbrock end-to-end and confirm `success=True`
- [ ] Confirm `ripopt-py/Cargo.toml` and `ripopt-py/pyproject.toml` versions
  match the ripopt `vX.Y.Z` tag — PyPI release is driven by
  `.github/workflows/publish-ripopt-py.yml` on tag push
- [ ] Optional local smoke `cd ripopt-py && maturin build --release` and
  inspect the produced wheel in `target/wheels/`

### 6f. Julia/JuMP interface (`Ripopt.jl`)

Skip this section entirely if the release contains no changes that affect
Ripopt.jl (most patch releases). Otherwise:

- [ ] `RIPOPT_LIBRARY_PATH=target/release julia --project=Ripopt.jl Ripopt.jl/examples/jump_hs071.jl`
- [ ] Run `Ripopt.jl/examples/jump_rosenbrock.jl` and `c_wrapper_hs071.jl`
- [ ] Bump `Ripopt.jl/Project.toml` only if Ripopt.jl itself changed
- [ ] Verify `Ripopt.jl/examples/ripopt_jump_tutorial.ipynb` still runs
- [ ] On Apple Silicon: confirm no closure-cfunction warnings (the
  module-level `@cfunction` rule from 0.6.0 must hold)

### 6g. GAMS solver link

- [ ] `cargo build --release` (gams link links against `libripopt`)
- [ ] `make -C gams` (build the GAMS bridge)
- [ ] `sudo make -C gams install` (only if a GAMS install is present)
- [ ] `sudo make -C gams test` — solves HS071 and checks the result
- [ ] If you don't have GAMS locally, document this as a manual step the
  release manager must run on a GAMS-equipped machine

### 6h. Tutorial notebooks

- [ ] Re-run all 15 notebooks in `tutorials/` **in place** against the newly
  built release, top-to-bottom:

  ```bash
  cd tutorials
  for nb in 0*.ipynb 1*.ipynb; do
      jupyter nbconvert --to notebook --execute --inplace "$nb"
  done
  ```

  Any `pyomo-ripopt` API drift or output-format change surfaces as a cell
  failure. Commit the re-executed notebooks so GitHub renders fresh outputs.
- [ ] Spot-check `15_ripopt_in_practice.ipynb` — this notebook exercises
  the most ripopt-specific surface area (solver options, diagnostics,
  architecture), so any behavioral change is most likely to show up here.
- [ ] Verify `tutorials/README.md` still accurately lists every notebook
  and its topic (update if notebooks were added, removed, or retitled)

---

## 7. Manuscript and supporting information

**Important:** `manuscript/ripopt.tex` is a generated artifact — never edit
it by hand. All content lives in `manuscript/ripopt.org`. The `.tex` (and
`.pdf`) are produced by the scimax export workflow described below. The
same rule applies to `manuscript/supporting-information.org` and its
generated `.tex`/`.pdf`.

### 7a. Update `manuscript/ripopt.org`

- [ ] Abstract — re-state the headline benchmark numbers
- [ ] HS suite section
- [ ] CUTEst suite section
- [ ] Domain-specific benchmarks section — add/update entries for any new
  benchmark suites added since the previous release (gas, water, grid,
  electrolyte, CHO, etc.)
- [ ] Failure analysis section — recompute the failure-mode breakdown
  from the fresh CUTEst run
- [ ] Opportunities for improvement section
- [ ] Conclusions section

### 7b. Update `manuscript/supporting-information.org`

The SI is the implementation/reproducibility companion document. It is
the place where every release-blocking detail about *how* the code works
should live. **Walk the SI top-to-bottom against the actual code state
before tagging.**

- [ ] **File path references** — every `src/...`, `rmumps/...`,
  `benchmarks/...` path cited in the SI must still exist. Reorganizations
  break these silently. Grep the SI for any path-shaped strings and
  re-verify each one. (Common reorganizations to watch for: any rename
  inside `benchmarks/`, anything moved between `src/` and `rmumps/`,
  anything moved between `src/` and a new module split.)
- [ ] **Code line-number references** — line numbers shift any time code
  is added or removed above. For each `file.rs:NNN` citation in the SI,
  open the file at that line and confirm it still points to the function,
  block, or comment the SI claims. Update the line number if it has
  drifted; rewrite the surrounding sentence if the code has been
  refactored beyond just shifting.
- [ ] **Excerpted code snippets** — if the SI quotes Rust source verbatim
  (e.g., a function body, a struct definition, an option list), re-pull
  the current source and diff it against the snippet in the SI. Stale
  snippets are worse than no snippets.
- [ ] **New code coverage** — for every algorithmic change in this
  release (new fallback, new option, new oracle, new convergence rule,
  new linear-algebra path, new problem class, etc.) verify the SI either
  documents it or has a stub pointing to where it lives. Walk the
  CHANGELOG `### Added` and `### Changed` sections and tick each item:
  is it in the SI? If no, decide — should it be? Most algorithmic
  changes should at least get a one-paragraph mention.
- [ ] **Command listings** — every shell command in the SI (`cargo run …`,
  `make …`, `ripopt …`, `python …`) must still work as written. If
  paths, target names, CLI flags, or env vars changed in this release,
  the SI command listings need updating. Run a representative subset
  end-to-end if you can.
- [ ] **Version pins** — bump every literal version string. Common ones:
  ripopt version (`0.6.x`), rmumps version (`0.1.x`), Ipopt version
  (`3.14.19`), Rust toolchain. Cross-reference Section 5 (version bump).
- [ ] **Reproducibility metadata** — re-confirm the listed hardware
  (Apple M3? M4? Mac Mini?), the OS (`Darwin 25.x`?), the Ipopt version,
  the rustc version, and the date stamp. The SI numbers were generated
  on a specific machine; be honest about which one.
- [ ] **CLI option listings** — if the SI documents `ripopt --help`
  output, `SolverOptions` defaults, or AMPL keywords, regenerate from
  the actual current binary (`ripopt --help`) and replace the listing.
- [ ] **Cross-references to the manuscript** — any `Section X` or
  `Equation Y` reference that points back to `ripopt.org` must still
  resolve after manuscript edits in 7a.

### 7c. Build the PDFs

- [ ] Build the manuscript PDF. **Must run from inside `manuscript/`**:
  ```bash
  cd manuscript
  scimax export ripopt.org --format pdf
  ```
  This regenerates both `ripopt.tex` and `ripopt.pdf` in one step.
- [ ] Build the supporting-information PDF, also from `manuscript/`:
  ```bash
  cd manuscript
  scimax export supporting-information.org --format pdf
  ```

### 7d. Verify both PDFs render correctly

- [ ] `manuscript/ripopt.pdf` — page count sane, no `??` undefined
  references, bibliography rendered, figures present, all benchmark
  numbers match the prose
- [ ] `manuscript/supporting-information.pdf` — page count sane, no `??`
  undefined references, every code listing renders without minted
  errors, every cross-reference into the main manuscript resolves

### 7e. Clean up intermediate build artifacts

The scimax/LaTeX pipeline leaves these behind, and **none of them should
ever be committed**. All are gitignored already, but cleaning the working
directory keeps `git status` readable and prevents stale artifacts from
confusing the next build:

```bash
cd manuscript
rm -f ripopt.tex ripopt.html ripopt.pyg ripopt.bbl ripopt.blg
rm -f supporting-information.tex supporting-information.html \
      supporting-information.pyg supporting-information.bbl \
      supporting-information.blg
rm -rf _minted
```

After cleanup the only files in `manuscript/` should be `ripopt.org`,
`ripopt.bib`, `ripopt.pdf`, `supporting-information.org`, and
`supporting-information.pdf`.

### 7f. Commit

- [ ] Stage `manuscript/ripopt.org`, `manuscript/supporting-information.org`,
  `manuscript/ripopt.pdf`, and `manuscript/supporting-information.pdf` in
  the same commit so source and PDF artifact stay in sync. The `.tex`
  files are **not** committed (gitignored, regenerated on every build).

---

## 8. Save tagged benchmark artifacts (per CLAUDE.md)

These let us compare per-problem timing across versions later:

- [ ] `cp benchmarks/hs/hs_ripopt_results.json benchmarks/hs/hs_ripopt_results_vX.Y.Z.json`
- [ ] `cp benchmarks/hs/hs_ipopt_native_results.json benchmarks/hs/hs_ipopt_native_results_vX.Y.Z.json`
- [ ] `cp benchmarks/BENCHMARK_REPORT.json benchmarks/BENCHMARK_REPORT_vX.Y.Z.json`
- [ ] Optional `cp benchmarks/cutest/results.json benchmarks/cutest/results_vX.Y.Z.json`
- [ ] Optional `cp benchmarks/electrolyte/electrolyte_results.json benchmarks/electrolyte/electrolyte_results_vX.Y.Z.json`
- [ ] Optional `cp benchmarks/grid/grid_results.json benchmarks/grid/grid_results_vX.Y.Z.json`
- [ ] Optional `cp benchmarks/cho/cho_results.json benchmarks/cho/cho_results_vX.Y.Z.json`

---

## 9. Final pre-tag verification

- [ ] `cargo test --release --no-fail-fast` one more time after all edits
- [ ] `cargo check --examples` one more time (catches any drift introduced
  by docs/manuscript edits)
- [ ] `cargo package --allow-dirty --list -p ripopt` — verify the file list
  matches your `exclude = [...]` rules in `Cargo.toml`. Look for files that
  shouldn't ship: `adversary/`, `.crucible/`, `benchmarks/` (HS, CUTEst,
  electrolyte, grid, cho, gas, water, large_scale), `docs/`, `manuscript/`,
  `research/`, `tutorials/`, large PDFs, `.ipynb` notebooks, `pyomo-ripopt/build/`
- [ ] `cargo package --allow-dirty -p rmumps` — same for rmumps
- [ ] `cargo publish --dry-run -p rmumps` (rmumps must publish first since
  ripopt depends on it)
- [ ] `cargo publish --dry-run -p ripopt`
- [ ] Read the diff of `Cargo.lock` and make sure no surprise dep updates
  snuck in
- [ ] **PyPI publish readiness** — before pushing the tag, confirm the
  Trusted Publishers are configured (one-time setup, but re-verify every
  release):
  - [ ] PyPI project `pyomo-ripopt` has a Trusted Publisher bound to
    this repo + `publish-pyomo.yml` + environment `pypi`
  - [ ] PyPI project `ripopt` has a Trusted Publisher bound to this repo
    + `publish-ripopt-py.yml` + environment `pypi`
  - [ ] GitHub repo has a `pypi` environment configured (Settings →
    Environments)
  - [ ] Neither workflow file has been renamed/moved since the last
    successful release; if renamed, the Trusted Publisher binding on
    PyPI must be updated to match **before** tagging
  - [ ] See `PYPI_PUBLISHING.md` for full setup details
- [ ] **Python wheel manifests** — quick sanity check that the Python
  packages won't ship stray files:
  - [ ] `cd pyomo-ripopt && python -m build --wheel && unzip -l dist/*.whl | head -40`
    — verify no `tests/`, `build/`, or `__pycache__/` in the wheel
  - [ ] `cd ripopt-py && maturin build --release && unzip -l target/wheels/*.whl | head -40`
    — verify the `_ripopt.abi3.so` is present and no probe/test scripts
    leaked in

---

## 10. Tag and publish

Publishing fans out over three independent channels once the tag is
pushed. **Only the crates.io channel has an internal ordering**:
`cargo publish -p rmumps` must land and be indexed before
`cargo publish -p ripopt`, because the ripopt crate depends on rmumps. The
two PyPI workflows (`publish-pyomo.yml` and `publish-ripopt-py.yml`) build
their own copy of ripopt from the tagged repo source — they do **not**
fetch from crates.io and are independent of the cargo publishes below.

```
                              git push origin vX.Y.Z
                                          |
                ,-------------------------+-------------------------,
                |                         |                         |
         crates.io (manual)        publish-pyomo.yml        publish-ripopt-py.yml
         rmumps → ripopt           (auto, tag trigger)      (auto, tag trigger)
```

### 10a. Push the tag

- [ ] `git add -A` (review carefully — there will be benchmark JSON, the
  manuscript pdf, and many docs)
- [ ] `git commit -m "release: vX.Y.Z"` with full release notes in body
- [ ] `git tag -a vX.Y.Z -m "ripopt vX.Y.Z"`
- [ ] `git push origin main`
- [ ] `git push origin vX.Y.Z`**this starts both PyPI workflows
  immediately**; keep the Actions tab open to monitor them

### 10b. Publish crates.io (manual, ordered)

- [ ] `cargo publish -p rmumps`
- [ ] Wait for crates.io to index rmumps (~30s — `cargo search rmumps`
  should return the new version)
- [ ] `cargo publish -p ripopt`

### 10c. Monitor PyPI workflows (automatic)

- [ ] `.github/workflows/publish-pyomo.yml` — confirm all 5 wheel jobs +
  sdist job + publish job succeeded in the Actions tab
- [ ] `.github/workflows/publish-ripopt-py.yml` — same check (5 wheel
  jobs, publish job; no sdist by design)
- [ ] If either workflow failed during wheel builds (flaky runner, etc.),
  re-run the failed jobs from the Actions UI — the `publish` job will
  wait for them and then upload. Partial uploads are OK: PyPI rejects
  duplicate wheels, so re-running is safe.

### 10d. Other bindings

- [ ] Optional Manual PyPI fallback if a workflow is broken beyond a
  re-run: `cd pyomo-ripopt && python -m build && twine upload dist/*`
  (same for `ripopt-py` via `maturin publish`). Requires an API token,
  since Trusted Publishing only fires from the workflow.
- [ ] Optional Register/tag `Ripopt.jl` release if bumping it

### 10e. Rollback if something goes wrong

If the release blows up after the tag push but before any artifact hits
the public (crates.io, PyPI), rolling back cleanly:

1. **Cancel in-flight workflows** — GitHub Actions UI → any running
   `publish-*` run → "Cancel workflow". This stops wheel builds and
   prevents the `publish` job from firing.
2. **Delete the tag locally and remotely**:
   ```bash
   git tag -d vX.Y.Z
   git push --delete origin vX.Y.Z
   ```
3. **If a GitHub release was auto-created** (e.g. a downstream workflow
   `gh release create`d one), delete it via `gh release delete vX.Y.Z`.
4. **Zenodo** auto-archives on GitHub release creation, not tag push.
   If a Zenodo version was minted, it cannot be deleted — but you can
   publish a `vX.Y.Z+1` immediately and Zenodo will supersede it.
5. **crates.io**: `cargo yank --version X.Y.Z -p rmumps` (and ripopt) if
   a broken crate was published. Yank is reversible but prevents new
   `cargo install` of that version.
6. **PyPI**: broken wheels can be deleted from the project page within
   a few days, but the version number is burned — you cannot re-upload
   `X.Y.Z`. Bump to `X.Y.Z+1` instead.

Prefer catching problems in §9 to avoid any of this.

---

## 11. GitHub release

- [ ] `gh release create vX.Y.Z --notes-file <(awk '/^## \[X.Y.Z\]/,/^## \[/{if(/^## \[/ && !/X.Y.Z/) exit; print}' CHANGELOG.md)`
  (or via the GitHub UI with the CHANGELOG entry pasted in)
- [ ] Attach `manuscript/ripopt.pdf` and
  `manuscript/supporting-information.pdf` if you publish them with the release
- [ ] Verify the release page renders correctly on GitHub

---

## 12. Post-release

- [ ] On crates.io, confirm both `ripopt` and `rmumps` show the new version
- [ ] `cargo install ripopt --version X.Y.Z` from a clean directory and run
  `ripopt --version` to confirm the published binary works
- [ ] **PyPI end-to-end** — from a **fresh** venv (not the development one):
  ```bash
  python -m venv /tmp/ripopt-release-check
  source /tmp/ripopt-release-check/bin/activate
  pip install pyomo-ripopt==X.Y.Z
  pip install ripopt==X.Y.Z
  python -c "from pyomo.environ import SolverFactory; print(SolverFactory('ripopt').available())"
  python -c "from ripopt import minimize; import numpy as np; r = minimize(lambda x: (x[0]-1)**2, np.zeros(1)); print(r['success'], r['x'])"
  ```
  Both packages should resolve a wheel (not fall back to sdist on a
  supported platform). The sanity checks should return `True` and
  `True [1.]` respectively.
- [ ] **Zenodo DOI** — Zenodo is linked to the GitHub repo and auto-archives
  every published release (metadata in `.zenodo.json`). Wait a few minutes
  after the GitHub release in Section 11, then check
  https://doi.org/10.5281/zenodo.19542664 (the concept DOI — the parent
  record for all versions) and confirm `vX.Y.Z` appears in the version
  list. The README badge is a static `img.shields.io/badge/...` SVG that
  hard-codes the concept DOI, so it doesn't need to be regenerated per
  release. (Avoid both the `zenodo.org/badge/<repo_id>.svg` redirect form
  and the `zenodo.org/badge/DOI/...svg` direct form: Zenodo rate-limits
  GitHub Camo, which then caches a 403 and the badge stops rendering.)
- [ ] Update `MEMORY.md` HS/CUTEst status sections with the new release
  numbers if you didn't already in step 3
- [ ] Bump `Cargo.toml` to the next `+dev` working version if you use that
  convention (ripopt currently does not — versions remain on the released
  number until the next release)
- [ ] Close any GitHub issues fixed in this release with a comment pointing
  at the release notes
- [ ] Tweet/announce/etc. as appropriate
- [ ] **Draft a brief LinkedIn post** summarizing the release: one-line hook,
  2–4 headline numbers or changes (HS/CUTEst deltas, notable new features),
  a link to the GitHub release page, and relevant tags (`#Rust`,
  `#Optimization`, `#OpenSource`). Keep it under ~150 words so it renders
  without a "see more" fold on desktop.

---

## Things to double-check that are easy to forget

- [ ] **`cargo check --examples`** — example files often use trait
  signatures that drift; the test suite will not catch this
- [ ] **Compiler warnings** — they don't fail the build but accumulate;
  clean them before tagging
- [ ] **No `--no-verify` git operations** (CLAUDE.md global rule)
- [ ] **Crucible/adversary/research directories** stay excluded from the
  crates.io package (verified via `cargo package --list`)
- [ ] **Large PDFs and notebooks** stay excluded from the package
- [ ] **Benchmark JSON regenerated by `make benchmark`** — these should be
  committed (they're the source of truth for the report numbers) but be
  aware they generate a large diff
- [ ] **License files** — confirm `LICENSE` (EPL-2.0 for ripopt) and
  `rmumps/LICENSE` (CECILL-C) are present and current
- [ ] **rustdoc links** — broken intra-doc links will silently fail; check
  `cargo doc` output for warnings
- [ ] **The C header** (`ripopt.h`) is the public ABI. If any C API
  function signature changed, that's a SemVer concern — major-bump or
  carefully document